mabuseif commited on
Commit
526cc85
·
verified ·
1 Parent(s): 6c88e25

Update app/climate_data.py

Browse files
Files changed (1) hide show
  1. app/climate_data.py +351 -70
app/climate_data.py CHANGED
@@ -2,8 +2,8 @@
2
  BuildSustain - Climate Data Module
3
 
4
  This module handles the climate data selection, EPW file processing, and display of climate information
5
- for the BuildSustain application. It allows users to upload EPW weather files and extracts
6
- relevant climate data for use in load calculations.
7
 
8
  Developed by: Dr Majed Abuseif, Deakin University
9
  © 2025
@@ -21,6 +21,8 @@ import plotly.express as px
21
  from datetime import datetime
22
  from typing import Dict, List, Any, Optional, Tuple, Union
23
  import math
 
 
24
 
25
  # Configure logging
26
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -29,6 +31,38 @@ logger = logging.getLogger(__name__)
29
  # Define constants
30
  MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
31
  CLIMATE_ZONES = ["0A", "0B", "1A", "1B", "2A", "2B", "3A", "3B", "3C", "4A", "4B", "4C", "5A", "5B", "5C", "6A", "6B", "7", "8"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  class ClimateDataManager:
34
  """Class for managing climate data from EPW files."""
@@ -37,19 +71,28 @@ class ClimateDataManager:
37
  """Initialize climate data manager."""
38
  pass
39
 
40
- def load_epw(self, uploaded_file) -> Dict[str, Any]:
41
  """
42
  Parse an EPW file and extract climate data.
43
 
44
  Args:
45
- uploaded_file: The uploaded EPW file object
 
 
 
46
 
47
  Returns:
48
  Dict containing parsed climate data
49
  """
50
  try:
51
  # Read the EPW file
52
- content = uploaded_file.getvalue().decode('utf-8')
 
 
 
 
 
 
53
  lines = content.split('\n')
54
 
55
  # Extract header information (first 8 lines)
@@ -71,6 +114,82 @@ class ClimateDataManager:
71
  "elevation": float(location_data[9])
72
  }
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  # Parse data rows (starting from line 9)
75
  data_lines = lines[8:]
76
 
@@ -119,15 +238,17 @@ class ClimateDataManager:
119
 
120
  # Create climate data dictionary
121
  climate_data = {
122
- "id": f"{location['city']}_{location['country']}".replace(" ", "_"),
123
  "location": location,
124
  "design_conditions": design_conditions,
125
  "climate_zone": climate_zone,
126
  "hourly_data": hourly_data,
127
- "epw_filename": uploaded_file.name
 
 
128
  }
129
 
130
- logger.info(f"EPW file processed successfully: {uploaded_file.name}")
131
  return climate_data
132
 
133
  except Exception as e:
@@ -152,7 +273,7 @@ class ClimateDataManager:
152
  winter_design_temp = np.percentile(temp_col, 0.4) # 99.6% heating design temperature
153
  summer_design_temp_db = np.percentile(temp_col, 99.6) # 0.4% cooling design temperature
154
 
155
- # Calculate wet-bulb temperature (simplified)
156
  rh_col = df["relative_humidity"].astype(float)
157
  wet_bulb_temp = self._calculate_wet_bulb(temp_col, rh_col)
158
  summer_design_temp_wb = np.percentile(wet_bulb_temp, 99.6) # 0.4% cooling wet-bulb temperature
@@ -177,8 +298,6 @@ class ClimateDataManager:
177
  monthly_radiation = df.groupby(df["month"])["global_horizontal_radiation"].mean().tolist()
178
 
179
  # Calculate summer daily temperature range
180
- # Summer months: 6-8 for Northern Hemisphere, 12-2 for Southern Hemisphere
181
- # Determine hemisphere based on latitude
182
  latitude = df["latitude"].iloc[0] if "latitude" in df.columns else 0
183
 
184
  if latitude >= 0: # Northern Hemisphere
@@ -213,7 +332,6 @@ class ClimateDataManager:
213
 
214
  except Exception as e:
215
  logger.error(f"Error calculating design conditions: {str(e)}")
216
- # Return default values if calculation fails
217
  return {
218
  "winter_design_temp": 0.0,
219
  "summer_design_temp_db": 30.0,
@@ -296,7 +414,6 @@ class ClimateDataManager:
296
  Returns:
297
  ASHRAE climate zone designation
298
  """
299
- # Simplified climate zone determination based on ASHRAE 169
300
  if hdd >= 7000:
301
  return "8"
302
  elif hdd >= 5400:
@@ -326,15 +443,21 @@ class ClimateDataManager:
326
  Returns:
327
  Wet-bulb temperature in °C
328
  """
329
- # Simplified formula for wet-bulb temperature
330
  wet_bulb = dry_bulb * np.arctan(0.151977 * np.sqrt(relative_humidity + 8.313659)) + \
331
  np.arctan(dry_bulb + relative_humidity) - np.arctan(relative_humidity - 1.676331) + \
332
  0.00391838 * (relative_humidity)**(3/2) * np.arctan(0.023101 * relative_humidity) - 4.686035
333
 
334
- # Ensure wet-bulb is not higher than dry-bulb
335
  wet_bulb = np.minimum(wet_bulb, dry_bulb)
336
 
337
  return wet_bulb
 
 
 
 
 
 
 
 
338
 
339
  def display_climate_page():
340
  """
@@ -343,6 +466,14 @@ def display_climate_page():
343
  """
344
  st.title("Climate Data and Design Requirements")
345
 
 
 
 
 
 
 
 
 
346
  # Display help information in an expandable section
347
  with st.expander("Help & Information"):
348
  display_climate_help()
@@ -355,55 +486,132 @@ def display_climate_page():
355
 
356
  # EPW Data Input tab
357
  with tab1:
358
- st.subheader("Upload EPW Weather File")
359
 
360
- # File uploader for EPW files
361
- uploaded_file = st.file_uploader(
362
- "Upload EPW File",
363
- type=["epw"],
364
- help="Upload an EnergyPlus Weather (EPW) file for your location."
365
  )
366
 
367
- if uploaded_file is not None:
368
- try:
369
- # Process the uploaded EPW file
370
- with st.spinner("Processing EPW file..."):
371
- climate_data = climate_manager.load_epw(uploaded_file)
372
-
373
- # Store climate data in session state
374
- st.session_state.project_data["climate_data"] = climate_data
375
-
376
- # Show success message
377
- st.success(f"EPW file processed successfully: {uploaded_file.name}")
378
-
379
- # Display basic location information
380
- location = climate_data["location"]
381
- st.subheader("Location Information")
382
-
383
- col1, col2 = st.columns(2)
384
- with col1:
385
- st.write(f"**City:** {location['city']}")
386
- st.write(f"**State/Province:** {location['state_province']}")
387
- st.write(f"**Country:** {location['country']}")
388
-
389
- with col2:
390
- st.write(f"**Latitude:** {location['latitude']}°")
391
- st.write(f"**Longitude:** {location['longitude']}°")
392
- st.write(f"**Elevation:** {location['elevation']} m")
393
-
394
- # Automatically switch to Climate Summary tab
395
- st.button("View Climate Summary", on_click=lambda: st.session_state.update({"climate_tab": "Climate Summary"}))
 
 
 
 
 
 
396
 
397
- except Exception as e:
398
- st.error(f"Error processing EPW file: {str(e)}")
399
- logger.error(f"Error processing EPW file: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
  # Climate Summary tab
402
  with tab2:
403
- if "climate_data" in st.session_state.project_data and st.session_state.project_data["climate_data"]:
404
  display_climate_summary(st.session_state.project_data["climate_data"])
405
  else:
406
- st.info("Please upload an EPW file in the 'EPW Data Input' tab to view climate summary.")
407
 
408
  # Navigation buttons
409
  col1, col2 = st.columns(2)
@@ -415,9 +623,8 @@ def display_climate_page():
415
 
416
  with col2:
417
  if st.button("Continue to Material Library", key="continue_to_material"):
418
- # Check if climate data is available before proceeding
419
- if "climate_data" not in st.session_state.project_data or not st.session_state.project_data["climate_data"]:
420
- st.warning("Please upload an EPW file before continuing.")
421
  else:
422
  st.session_state.current_page = "Material Library"
423
  st.rerun()
@@ -431,17 +638,28 @@ def display_climate_summary(climate_data: Dict[str, Any]):
431
  """
432
  st.subheader("Climate Summary")
433
 
434
- # Extract design conditions
435
  design = climate_data["design_conditions"]
436
  location = climate_data["location"]
437
 
438
- # Display climate zone
 
 
 
 
 
 
 
 
 
 
 
 
439
  st.markdown(f"### ASHRAE Climate Zone: {climate_data['climate_zone']}")
440
 
441
- # Create columns for layout
442
  col1, col2 = st.columns(2)
443
 
444
- # Design temperatures
445
  with col1:
446
  st.subheader("Design Temperatures")
447
  st.write(f"**Winter Design Temperature:** {design['winter_design_temp']}°C")
@@ -449,7 +667,6 @@ def display_climate_summary(climate_data: Dict[str, Any]):
449
  st.write(f"**Summer Design Temperature (WB):** {design['summer_design_temp_wb']}°C")
450
  st.write(f"**Summer Daily Temperature Range:** {design['summer_daily_range']}°C")
451
 
452
- # Degree days
453
  with col2:
454
  st.subheader("Degree Days")
455
  st.write(f"**Heating Degree Days (Base 18°C):** {design['heating_degree_days']}")
@@ -457,7 +674,35 @@ def display_climate_summary(climate_data: Dict[str, Any]):
457
  st.write(f"**Average Wind Speed:** {design['wind_speed']} m/s")
458
  st.write(f"**Average Atmospheric Pressure:** {design['pressure']} Pa")
459
 
460
- # Monthly temperature chart
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  st.subheader("Monthly Average Temperatures")
462
 
463
  fig_temp = go.Figure()
@@ -479,7 +724,7 @@ def display_climate_summary(climate_data: Dict[str, Any]):
479
 
480
  st.plotly_chart(fig_temp, use_container_width=True)
481
 
482
- # Monthly radiation chart
483
  st.subheader("Monthly Average Solar Radiation")
484
 
485
  fig_rad = go.Figure()
@@ -499,7 +744,7 @@ def display_climate_summary(climate_data: Dict[str, Any]):
499
 
500
  st.plotly_chart(fig_rad, use_container_width=True)
501
 
502
- # Display hourly data statistics
503
  st.subheader("Hourly Data Statistics")
504
 
505
  if "hourly_data" in climate_data and climate_data["hourly_data"]:
@@ -508,6 +753,35 @@ def display_climate_summary(climate_data: Dict[str, Any]):
508
 
509
  if hourly_count < 8760:
510
  st.warning(f"Expected 8760 hourly records for a full year, but found {hourly_count}. Some data may be missing.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  else:
512
  st.warning("No hourly data available.")
513
 
@@ -516,7 +790,7 @@ def display_climate_help():
516
  st.markdown("""
517
  ### Climate Data Help
518
 
519
- This section allows you to upload and process weather data for your location, which is essential for accurate calculations.
520
 
521
  **EPW Files:**
522
 
@@ -534,15 +808,22 @@ def display_climate_help():
534
  * [Climate.OneBuilding.Org](https://climate.onebuilding.org/)
535
  * [ASHRAE International Weather for Energy Calculations (IWEC)](https://www.ashrae.org/technical-resources/bookstore/ashrae-international-weather-files-for-energy-calculations-2-0-iwec2)
536
 
 
 
 
 
537
  **Climate Summary:**
538
 
539
- After uploading an EPW file, the Climate Summary tab will display:
540
 
 
541
  * ASHRAE Climate Zone
542
  * Design temperatures for heating and cooling
543
  * Heating and cooling degree days
 
 
544
  * Monthly average temperatures and solar radiation
545
- * Hourly data statistics
546
 
547
  This information will be used throughout the calculation process.
548
- """)
 
2
  BuildSustain - Climate Data Module
3
 
4
  This module handles the climate data selection, EPW file processing, and display of climate information
5
+ for the BuildSustain application. It allows users to upload EPW weather files or select climate projection
6
+ data and extracts relevant climate data for use in load calculations.
7
 
8
  Developed by: Dr Majed Abuseif, Deakin University
9
  © 2025
 
21
  from datetime import datetime
22
  from typing import Dict, List, Any, Optional, Tuple, Union
23
  import math
24
+ import re
25
+ from os.path import join as os_join
26
 
27
  # Configure logging
28
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
31
  # Define constants
32
  MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
33
  CLIMATE_ZONES = ["0A", "0B", "1A", "1B", "2A", "2B", "3A", "3B", "3C", "4A", "4B", "4C", "5A", "5B", "5C", "6A", "6B", "7", "8"]
34
+ AU_CCH_DIR = "au_cch" # Relative path to au_cch folder
35
+
36
+ # Location mapping for Australian climate projections
37
+ LOCATION_MAPPING = {
38
+ "24": {"city": "Canberra", "state": "ACT"},
39
+ "11": {"city": "Coffs Harbour", "state": "NSW"},
40
+ "17": {"city": "Sydney RO (Observatory Hill)", "state": "NSW"},
41
+ "56": {"city": "Mascot (Sydney Airport)", "state": "NSW"},
42
+ "77": {"city": "Parramatta", "state": "NSW"},
43
+ "78": {"city": "Sub-Alpine (Cooma Airport)", "state": "NSW"},
44
+ "79": {"city": "Blue Mountains", "state": "NSW"},
45
+ "1": {"city": "Darwin", "state": "NT"},
46
+ "6": {"city": "Alice Springs", "state": "NT"},
47
+ "5": {"city": "Townsville", "state": "QLD"},
48
+ "7": {"city": "Rockhampton", "state": "QLD"},
49
+ "10": {"city": "Brisbane", "state": "QLD"},
50
+ "19": {"city": "Charleville", "state": "QLD"},
51
+ "32": {"city": "Cairns", "state": "QLD"},
52
+ "70": {"city": "Toowoomba", "state": "QLD"},
53
+ "16": {"city": "Adelaide", "state": "SA"},
54
+ "75": {"city": "Adelaide Coastal (AMO)", "state": "SA"},
55
+ "26": {"city": "Hobart", "state": "TAS"},
56
+ "21": {"city": "Melbourne RO", "state": "VIC"},
57
+ "27": {"city": "Mildura", "state": "VIC"},
58
+ "60": {"city": "Tullamarine (Melbourne Airport)", "state": "VIC"},
59
+ "63": {"city": "Warrnambool", "state": "VIC"},
60
+ "66": {"city": "Ballarat", "state": "VIC"},
61
+ "30": {"city": "Wyndham", "state": "WA"},
62
+ "52": {"city": "Swanbourne", "state": "WA"},
63
+ "58": {"city": "Albany", "state": "WA"},
64
+ "83": {"city": "Christmas Island", "state": "WA"}
65
+ }
66
 
67
  class ClimateDataManager:
68
  """Class for managing climate data from EPW files."""
 
71
  """Initialize climate data manager."""
72
  pass
73
 
74
+ def load_epw(self, uploaded_file, location_num: str = None, rcp: str = None, year: str = None) -> Dict[str, Any]:
75
  """
76
  Parse an EPW file and extract climate data.
77
 
78
  Args:
79
+ uploaded_file: The uploaded EPW file object or file content as string
80
+ location_num: Location number for climate projection (optional)
81
+ rcp: RCP scenario for climate projection (optional)
82
+ year: Year for climate projection (optional)
83
 
84
  Returns:
85
  Dict containing parsed climate data
86
  """
87
  try:
88
  # Read the EPW file
89
+ if isinstance(uploaded_file, str):
90
+ content = uploaded_file
91
+ epw_filename = f"{location_num}_{rcp}_{year}.epw"
92
+ else:
93
+ content = uploaded_file.getvalue().decode('utf-8')
94
+ epw_filename = uploaded_file.name
95
+
96
  lines = content.split('\n')
97
 
98
  # Extract header information (first 8 lines)
 
114
  "elevation": float(location_data[9])
115
  }
116
 
117
+ # Override city and state from LOCATION_MAPPING if provided
118
+ if location_num in LOCATION_MAPPING:
119
+ location["city"] = LOCATION_MAPPING[location_num]["city"]
120
+ location["state_province"] = LOCATION_MAPPING[location_num]["state"]
121
+
122
+ # Parse TYPICAL/EXTREME PERIODS
123
+ typical_extreme_periods = {}
124
+ date_pattern = r'^\d{1,2}\s*/\s*\d{1,2}$'
125
+ for line in lines:
126
+ if line.startswith("TYPICAL/EXTREME PERIODS"):
127
+ parts = line.strip().split(',')
128
+ try:
129
+ num_periods = int(parts[1])
130
+ except ValueError:
131
+ logger.warning("Invalid number of periods in TYPICAL/EXTREME PERIODS, skipping parsing.")
132
+ break
133
+ for i in range(num_periods):
134
+ try:
135
+ if len(parts) < 2 + i*4 + 4:
136
+ logger.warning(f"Insufficient fields for period {i+1}, skipping.")
137
+ continue
138
+ period_name = parts[2 + i*4]
139
+ period_type = parts[3 + i*4]
140
+ start_date = parts[4 + i*4].strip()
141
+ end_date = parts[5 + i*4].strip()
142
+ if period_name in [
143
+ "Summer - Week Nearest Max Temperature For Period",
144
+ "Summer - Week Nearest Average Temperature For Period",
145
+ "Winter - Week Nearest Min Temperature For Period",
146
+ "Winter - Week Nearest Average Temperature For Period"
147
+ ]:
148
+ season = 'summer' if 'Summer' in period_name else 'winter'
149
+ period_type = 'extreme' if 'Max' in period_name or 'Min' in period_name else 'typical'
150
+ key = f"{season}_{period_type}"
151
+ start_date_clean = re.sub(r'\s+', '', start_date)
152
+ end_date_clean = re.sub(r'\s+', '', end_date)
153
+ if not re.match(date_pattern, start_date) or not re.match(date_pattern, end_date):
154
+ logger.warning(f"Invalid date format for period {period_name}: {start_date} to {end_date}, skipping.")
155
+ continue
156
+ start_month, start_day = map(int, start_date_clean.split('/'))
157
+ end_month, end_day = map(int, end_date_clean.split('/'))
158
+ typical_extreme_periods[key] = {
159
+ "start": {"month": start_month, "day": start_day},
160
+ "end": {"month": end_month, "day": end_day}
161
+ }
162
+ except (IndexError, ValueError) as e:
163
+ logger.warning(f"Error parsing period {i+1}: {str(e)}, skipping.")
164
+ continue
165
+ break
166
+
167
+ # Parse GROUND TEMPERATURES
168
+ ground_temperatures = {}
169
+ for line in lines:
170
+ if line.startswith("GROUND TEMPERATURES"):
171
+ parts = line.strip().split(',')
172
+ try:
173
+ num_depths = int(parts[1])
174
+ except ValueError:
175
+ logger.warning("Invalid number of depths in GROUND TEMPERATURES, skipping parsing.")
176
+ break
177
+ for i in range(num_depths):
178
+ try:
179
+ if len(parts) < 2 + i*16 + 16:
180
+ logger.warning(f"Insufficient fields for ground temperature depth {i+1}, skipping.")
181
+ continue
182
+ depth = parts[2 + i*16]
183
+ temps = [float(t) for t in parts[6 + i*16:18 + i*16] if t.strip()]
184
+ if len(temps) != 12:
185
+ logger.warning(f"Invalid number of temperatures for depth {depth}m, expected 12, got {len(temps)}, skipping.")
186
+ continue
187
+ ground_temperatures[depth] = temps
188
+ except (ValueError, IndexError) as e:
189
+ logger.warning(f"Error parsing ground temperatures for depth {i+1}: {str(e)}, skipping.")
190
+ continue
191
+ break
192
+
193
  # Parse data rows (starting from line 9)
194
  data_lines = lines[8:]
195
 
 
238
 
239
  # Create climate data dictionary
240
  climate_data = {
241
+ "id": f"{location['city']}_{location['country']}_{rcp}_{year}".replace(" ", "_") if rcp and year else f"{location['city']}_{location['country']}".replace(" ", "_"),
242
  "location": location,
243
  "design_conditions": design_conditions,
244
  "climate_zone": climate_zone,
245
  "hourly_data": hourly_data,
246
+ "epw_filename": epw_filename,
247
+ "typical_extreme_periods": typical_extreme_periods,
248
+ "ground_temperatures": ground_temperatures
249
  }
250
 
251
+ logger.info(f"EPW file processed successfully: {epw_filename}")
252
  return climate_data
253
 
254
  except Exception as e:
 
273
  winter_design_temp = np.percentile(temp_col, 0.4) # 99.6% heating design temperature
274
  summer_design_temp_db = np.percentile(temp_col, 99.6) # 0.4% cooling design temperature
275
 
276
+ # Calculate wet-bulb temperature
277
  rh_col = df["relative_humidity"].astype(float)
278
  wet_bulb_temp = self._calculate_wet_bulb(temp_col, rh_col)
279
  summer_design_temp_wb = np.percentile(wet_bulb_temp, 99.6) # 0.4% cooling wet-bulb temperature
 
298
  monthly_radiation = df.groupby(df["month"])["global_horizontal_radiation"].mean().tolist()
299
 
300
  # Calculate summer daily temperature range
 
 
301
  latitude = df["latitude"].iloc[0] if "latitude" in df.columns else 0
302
 
303
  if latitude >= 0: # Northern Hemisphere
 
332
 
333
  except Exception as e:
334
  logger.error(f"Error calculating design conditions: {str(e)}")
 
335
  return {
336
  "winter_design_temp": 0.0,
337
  "summer_design_temp_db": 30.0,
 
414
  Returns:
415
  ASHRAE climate zone designation
416
  """
 
417
  if hdd >= 7000:
418
  return "8"
419
  elif hdd >= 5400:
 
443
  Returns:
444
  Wet-bulb temperature in °C
445
  """
 
446
  wet_bulb = dry_bulb * np.arctan(0.151977 * np.sqrt(relative_humidity + 8.313659)) + \
447
  np.arctan(dry_bulb + relative_humidity) - np.arctan(relative_humidity - 1.676331) + \
448
  0.00391838 * (relative_humidity)**(3/2) * np.arctan(0.023101 * relative_humidity) - 4.686035
449
 
 
450
  wet_bulb = np.minimum(wet_bulb, dry_bulb)
451
 
452
  return wet_bulb
453
+
454
+ def get_locations_by_state(self, state: str) -> List[Dict[str, str]]:
455
+ """Get list of locations for a given state from LOCATION_MAPPING."""
456
+ return [
457
+ {"number": loc_num, "city": loc_info["city"]}
458
+ for loc_num, loc_info in LOCATION_MAPPING.items()
459
+ if loc_info["state"] == state
460
+ ]
461
 
462
  def display_climate_page():
463
  """
 
466
  """
467
  st.title("Climate Data and Design Requirements")
468
 
469
+ # Notify if climate data exists in session state
470
+ if "project_data" in st.session_state and "climate_data" in st.session_state.project_data and st.session_state.project_data["climate_data"]:
471
+ climate_data = st.session_state.project_data["climate_data"]
472
+ st.info(
473
+ f"Climate data already extracted for {climate_data['location']['city']}, {climate_data['location']['country']}. "
474
+ f"View details in the 'Climate Summary' tab or upload/select new data below."
475
+ )
476
+
477
  # Display help information in an expandable section
478
  with st.expander("Help & Information"):
479
  display_climate_help()
 
486
 
487
  # EPW Data Input tab
488
  with tab1:
489
+ st.subheader("Select Climate Data Source")
490
 
491
+ # Option to choose data source
492
+ data_source = st.radio(
493
+ "Choose data source:",
494
+ ["Upload EPW File", "Select Climate Projection"],
495
+ key="data_source"
496
  )
497
 
498
+ if data_source == "Upload EPW File":
499
+ # File uploader for EPW files
500
+ uploaded_file = st.file_uploader(
501
+ "Upload EPW File",
502
+ type=["epw"],
503
+ help="Upload an EnergyPlus Weather (EPW) file for your location."
504
+ )
505
+
506
+ if uploaded_file is not None:
507
+ try:
508
+ with st.spinner("Processing EPW file..."):
509
+ climate_data = climate_manager.load_epw(uploaded_file)
510
+
511
+ # Store climate data in session state
512
+ st.session_state.project_data["climate_data"] = climate_data
513
+
514
+ st.success(f"EPW file processed successfully: {uploaded_file.name}")
515
+
516
+ # Display basic location information
517
+ location = climate_data["location"]
518
+ st.subheader("Location Information")
519
+
520
+ col1, col2 = st.columns(2)
521
+ with col1:
522
+ st.write(f"**City:** {location['city']}")
523
+ st.write(f"**State/Province:** {location['state_province']}")
524
+ st.write(f"**Country:** {location['country']}")
525
+
526
+ with col2:
527
+ st.write(f"**Latitude:** {location['latitude']}°")
528
+ st.write(f"**Longitude:** {location['longitude']}°")
529
+ st.write(f"**Elevation:** {location['elevation']} m")
530
+ st.write(f"**Time Zone:** {location['timezone']} hours (UTC)")
531
+
532
+ st.button("View Climate Summary", on_click=lambda: st.session_state.update({"climate_tab": "Climate Summary"}))
533
 
534
+ except Exception as e:
535
+ st.error(f"Error processing EPW file: {str(e)}")
536
+ logger.error(f"Error processing EPW file: {str(e)}")
537
+
538
+ else: # Select Climate Projection
539
+ st.markdown("""
540
+ ### Climate Projection
541
+ Select from available Australian climate projection data based on CSIRO 2022 projections.
542
+ """)
543
+
544
+ # Dropdown menus for climate projection
545
+ country = st.selectbox("Country", ["Australia"], key="projection_country")
546
+ states = ["ACT", "NSW", "NT", "QLD", "SA", "TAS", "VIC", "WA"]
547
+ state = st.selectbox("State", states, key="projection_state")
548
+
549
+ locations = climate_manager.get_locations_by_state(state)
550
+ location_options = [f"{loc['city']} ({loc['number']})" for loc in locations]
551
+ location_display = st.selectbox("Location", location_options, key="location")
552
+
553
+ location_num = ""
554
+ if location_display:
555
+ location_num = next(loc["number"] for loc in locations if f"{loc['city']} ({loc['number']})" == location_display)
556
+
557
+ rcp_options = ["RCP2.6", "RCP4.5", "RCP8.5"]
558
+ rcp = st.selectbox("RCP Scenario", rcp_options, key="rcp")
559
+
560
+ year_options = ["2030", "2050", "2070", "2090"]
561
+ year = st.selectbox("Year", year_options, key="year")
562
+
563
+ if st.button("Extract Projection Data"):
564
+ with st.spinner("Extracting climate projection data..."):
565
+ file_path = os_join(AU_CCH_DIR, location_num, rcp, year)
566
+ logger.debug(f"Attempting to access directory: {os.path.abspath(file_path)}")
567
+
568
+ if not os.path.exists(file_path):
569
+ st.error(
570
+ f"No directory found at au_cch/{location_num}/{rcp}/{year}/. "
571
+ f"Ensure the 'au_cch' folder is in the repository root with structure "
572
+ f"au_cch/{location_num}/{rcp}/{year} (e.g., au_cch/1/RCP2.6/2070/) "
573
+ f"containing a single .epw file."
574
+ )
575
+ logger.error(f"Directory does not exist: {file_path}")
576
+ else:
577
+ try:
578
+ epw_files = [f for f in os.listdir(file_path) if f.endswith(".epw")]
579
+ if not epw_files:
580
+ st.error(
581
+ f"No EPW file found in au_cch/{location_num}/{rcp}/{year}/. "
582
+ f"Ensure the directory contains a single .epw file."
583
+ )
584
+ logger.error(f"No EPW file found in {file_path}")
585
+ elif len(epw_files) > 1:
586
+ st.error(
587
+ f"Multiple EPW files found in au_cch/{location_num}/{rcp}/{year}/: {epw_files}. "
588
+ f"Ensure exactly one .epw file per directory."
589
+ )
590
+ logger.error(f"Multiple EPW files found: {epw_files}")
591
+ else:
592
+ epw_file_path = os_join(file_path, epw_files[0])
593
+ with open(epw_file_path, 'r') as f:
594
+ epw_content = f.read()
595
+
596
+ climate_data = climate_manager.load_epw(epw_content, location_num, rcp, year)
597
+ st.session_state.project_data["climate_data"] = climate_data
598
+ st.success(
599
+ f"Successfully extracted climate projection data for "
600
+ f"{climate_data['location']['city']}, {climate_data['location']['country']}, "
601
+ f"{rcp}, {year}!"
602
+ )
603
+ logger.info(f"Successfully processed projection: {climate_data['id']}")
604
+ st.button("View Climate Summary", on_click=lambda: st.session_state.update({"climate_tab": "Climate Summary"}))
605
+ except Exception as e:
606
+ st.error(f"Error reading {epw_file_path}: {str(e)}")
607
+ logger.error(f"Error reading {epw_file_path}: {str(e)}")
608
 
609
  # Climate Summary tab
610
  with tab2:
611
+ if "project_data" in st.session_state and "climate_data" in st.session_state.project_data and st.session_state.project_data["climate_data"]:
612
  display_climate_summary(st.session_state.project_data["climate_data"])
613
  else:
614
+ st.info("Please upload an EPW file or select a climate projection in the 'EPW Data Input' tab to view climate summary.")
615
 
616
  # Navigation buttons
617
  col1, col2 = st.columns(2)
 
623
 
624
  with col2:
625
  if st.button("Continue to Material Library", key="continue_to_material"):
626
+ if "project_data" not in st.session_state or "climate_data" not in st.session_state.project_data or not st.session_state.project_data["climate_data"]:
627
+ st.warning("Please upload an EPW file or select a climate projection before continuing.")
 
628
  else:
629
  st.session_state.current_page = "Material Library"
630
  st.rerun()
 
638
  """
639
  st.subheader("Climate Summary")
640
 
641
+ # Extract data
642
  design = climate_data["design_conditions"]
643
  location = climate_data["location"]
644
 
645
+ # Location Details
646
+ st.markdown(f"""
647
+ ### Location Details
648
+ - **Country:** {location['country']}
649
+ - **City:** {location['city']}
650
+ - **State/Province:** {location['state_province']}
651
+ - **Latitude:** {location['latitude']}°
652
+ - **Longitude:** {location['longitude']}°
653
+ - **Elevation:** {location['elevation']} m
654
+ - **Time Zone:** {location['timezone']} hours (UTC)
655
+ """)
656
+
657
+ # Climate Zone
658
  st.markdown(f"### ASHRAE Climate Zone: {climate_data['climate_zone']}")
659
 
660
+ # Design Conditions
661
  col1, col2 = st.columns(2)
662
 
 
663
  with col1:
664
  st.subheader("Design Temperatures")
665
  st.write(f"**Winter Design Temperature:** {design['winter_design_temp']}°C")
 
667
  st.write(f"**Summer Design Temperature (WB):** {design['summer_design_temp_wb']}°C")
668
  st.write(f"**Summer Daily Temperature Range:** {design['summer_daily_range']}°C")
669
 
 
670
  with col2:
671
  st.subheader("Degree Days")
672
  st.write(f"**Heating Degree Days (Base 18°C):** {design['heating_degree_days']}")
 
674
  st.write(f"**Average Wind Speed:** {design['wind_speed']} m/s")
675
  st.write(f"**Average Atmospheric Pressure:** {design['pressure']} Pa")
676
 
677
+ # Typical/Extreme Periods
678
+ if climate_data.get("typical_extreme_periods"):
679
+ st.subheader("Typical/Extreme Periods")
680
+ period_items = [
681
+ f"- **{key.replace('_', ' ').title()}:** {period['start']['month']}/{period['start']['day']} to {period['end']['month']}/{period['end']['day']}"
682
+ for key, period in climate_data["typical_extreme_periods"].items()
683
+ ]
684
+ st.markdown("\n".join(period_items))
685
+
686
+ # Ground Temperatures
687
+ if climate_data.get("ground_temperatures"):
688
+ st.subheader("Ground Temperatures")
689
+ table_data = []
690
+ for depth, temps in climate_data["ground_temperatures"].items():
691
+ row = {"Depth (m)": float(depth)}
692
+ row.update({month: f"{temp:.2f}" for month, temp in zip(MONTHS, temps)})
693
+ table_data.append(row)
694
+ df = pd.DataFrame(table_data)
695
+ st.dataframe(df, use_container_width=True)
696
+ csv = df.to_csv(index=False)
697
+ st.download_button(
698
+ label="Download Ground Temperatures as CSV",
699
+ data=csv,
700
+ file_name=f"ground_temperatures_{location['city']}_{location['country']}.csv",
701
+ mime="text/csv",
702
+ key=f"download_ground_temperatures_{climate_data['id']}"
703
+ )
704
+
705
+ # Monthly Temperature Chart
706
  st.subheader("Monthly Average Temperatures")
707
 
708
  fig_temp = go.Figure()
 
724
 
725
  st.plotly_chart(fig_temp, use_container_width=True)
726
 
727
+ # Monthly Radiation Chart
728
  st.subheader("Monthly Average Solar Radiation")
729
 
730
  fig_rad = go.Figure()
 
744
 
745
  st.plotly_chart(fig_rad, use_container_width=True)
746
 
747
+ # Hourly Data Statistics
748
  st.subheader("Hourly Data Statistics")
749
 
750
  if "hourly_data" in climate_data and climate_data["hourly_data"]:
 
753
 
754
  if hourly_count < 8760:
755
  st.warning(f"Expected 8760 hourly records for a full year, but found {hourly_count}. Some data may be missing.")
756
+
757
+ # Hourly Climate Data Table
758
+ st.subheader("Hourly Climate Data")
759
+ hourly_table_data = [
760
+ {
761
+ "Month": record["month"],
762
+ "Day": record["day"],
763
+ "Hour": record["hour"],
764
+ "Dry Bulb Temp (°C)": f"{record['dry_bulb']:.1f}",
765
+ "Relative Humidity (%)": f"{record['relative_humidity']:.1f}",
766
+ "Atmospheric Pressure (Pa)": f"{record['atmospheric_pressure']:.1f}",
767
+ "Global Horizontal Radiation (W/m²)": f"{record['global_horizontal_radiation']:.1f}",
768
+ "Direct Normal Radiation (W/m²)": f"{record['direct_normal_radiation']:.1f}",
769
+ "Diffuse Horizontal Radiation (W/m²)": f"{record['diffuse_horizontal_radiation']:.1f}",
770
+ "Wind Speed (m/s)": f"{record['wind_speed']:.1f}",
771
+ "Wind Direction (°)": f"{record['wind_direction']:.1f}"
772
+ }
773
+ for record in climate_data["hourly_data"]
774
+ ]
775
+ hourly_df = pd.DataFrame(hourly_table_data)
776
+ st.dataframe(hourly_df, use_container_width=True)
777
+ csv = hourly_df.to_csv(index=False)
778
+ st.download_button(
779
+ label="Download Hourly Climate Data as CSV",
780
+ data=csv,
781
+ file_name=f"hourly_climate_data_{location['city']}_{location['country']}.csv",
782
+ mime="text/csv",
783
+ key=f"download_hourly_climate_{climate_data['id']}"
784
+ )
785
  else:
786
  st.warning("No hourly data available.")
787
 
 
790
  st.markdown("""
791
  ### Climate Data Help
792
 
793
+ This section allows you to upload or select weather data for your location, which is essential for accurate calculations.
794
 
795
  **EPW Files:**
796
 
 
808
  * [Climate.OneBuilding.Org](https://climate.onebuilding.org/)
809
  * [ASHRAE International Weather for Energy Calculations (IWEC)](https://www.ashrae.org/technical-resources/bookstore/ashrae-international-weather-files-for-energy-calculations-2-0-iwec2)
810
 
811
+ **Climate Projections:**
812
+
813
+ Select from predefined Australian climate projection data (CSIRO 2022) by choosing a location, RCP scenario, and future year.
814
+
815
  **Climate Summary:**
816
 
817
+ After uploading an EPW file or selecting a climate projection, the Climate Summary tab will display:
818
 
819
+ * Location details (including Time Zone)
820
  * ASHRAE Climate Zone
821
  * Design temperatures for heating and cooling
822
  * Heating and cooling degree days
823
+ * Typical/Extreme periods
824
+ * Ground temperatures by depth
825
  * Monthly average temperatures and solar radiation
826
+ * Hourly data statistics with downloadable tables
827
 
828
  This information will be used throughout the calculation process.
829
+ """)