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 |
+
""")
|