Spaces:
Sleeping
Sleeping
Update app/climate_data.py
Browse files- 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
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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":
|
|
|
|
|
128 |
}
|
129 |
|
130 |
-
logger.info(f"EPW file processed successfully: {
|
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
|
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("
|
359 |
|
360 |
-
#
|
361 |
-
|
362 |
-
"
|
363 |
-
|
364 |
-
|
365 |
)
|
366 |
|
367 |
-
if
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
st.
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
396 |
|
397 |
-
|
398 |
-
|
399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
419 |
-
|
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
|
435 |
design = climate_data["design_conditions"]
|
436 |
location = climate_data["location"]
|
437 |
|
438 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
439 |
st.markdown(f"### ASHRAE Climate Zone: {climate_data['climate_zone']}")
|
440 |
|
441 |
-
#
|
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 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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 |
-
#
|
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
|
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 |
+
""")
|