Update data/climate_data.py
Browse files- data/climate_data.py +183 -82
data/climate_data.py
CHANGED
|
@@ -4,7 +4,7 @@ Extracts climate data from EPW files and provides visualizations inspired by Cli
|
|
| 4 |
|
| 5 |
Author: Dr Majed Abuseif
|
| 6 |
Date: May 2025
|
| 7 |
-
Version: 2.1.
|
| 8 |
"""
|
| 9 |
|
| 10 |
from typing import Dict, List, Any, Optional
|
|
@@ -23,6 +23,24 @@ import re
|
|
| 23 |
# Define paths
|
| 24 |
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
@dataclass
|
| 27 |
class ClimateLocation:
|
| 28 |
"""Class representing a climate location with ASHRAE 169 data derived from EPW files."""
|
|
@@ -258,8 +276,9 @@ class ClimateData:
|
|
| 258 |
# Validate typical/extreme periods (optional)
|
| 259 |
if "typical_extreme_periods" in data and data["typical_extreme_periods"]:
|
| 260 |
expected_periods = ["summer_extreme", "summer_typical", "winter_extreme", "winter_typical"]
|
| 261 |
-
if not
|
| 262 |
-
|
|
|
|
| 263 |
for period in data["typical_extreme_periods"].values():
|
| 264 |
for date in ["start", "end"]:
|
| 265 |
if not (1 <= period[date]["month"] <= 12 and 1 <= period[date]["day"] <= 31):
|
|
@@ -309,6 +328,9 @@ class ClimateData:
|
|
| 309 |
"""Display Streamlit interface for EPW upload and visualizations."""
|
| 310 |
st.title("Climate Data Analysis")
|
| 311 |
|
|
|
|
|
|
|
|
|
|
| 312 |
# Clear invalid session_state["climate_data"] to prevent validation errors
|
| 313 |
if "climate_data" in session_state and not all(key in session_state["climate_data"] for key in ["id", "country", "city"]):
|
| 314 |
st.warning("Clearing invalid climate data from session state.")
|
|
@@ -342,55 +364,96 @@ class ClimateData:
|
|
| 342 |
if line.startswith("DESIGN CONDITIONS"):
|
| 343 |
parts = line.strip().split(',')
|
| 344 |
if len(parts) < 41:
|
| 345 |
-
st.warning("
|
| 346 |
break
|
| 347 |
try:
|
| 348 |
design_conditions = {
|
| 349 |
-
"heating": {
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
"mean_coincident_db": float(parts[5]),
|
| 353 |
-
"humidification_db": float(parts[6]),
|
| 354 |
-
"mean_coincident_wb": float(parts[7]),
|
| 355 |
-
"wind_speed_1": float(parts[8]),
|
| 356 |
-
"wind_direction_1": float(parts[9]),
|
| 357 |
-
"wind_speed_2": float(parts[10]),
|
| 358 |
-
"wind_direction_2": float(parts[11])
|
| 359 |
-
},
|
| 360 |
-
"cooling": {
|
| 361 |
-
"hottest_month": int(parts[12]),
|
| 362 |
-
"dry_bulb_0_4": float(parts[13]),
|
| 363 |
-
"mean_coincident_wb_0_4": float(parts[14]),
|
| 364 |
-
"wet_bulb_0_4": float(parts[15]),
|
| 365 |
-
"mean_coincident_db_0_4": float(parts[16]),
|
| 366 |
-
"dry_bulb_1_0": float(parts[17]),
|
| 367 |
-
"mean_coincident_wb_1_0": float(parts[18]),
|
| 368 |
-
"wet_bulb_1_0": float(parts[19]),
|
| 369 |
-
"mean_coincident_db_1_0": float(parts[20]),
|
| 370 |
-
"dry_bulb_2_0": float(parts[21]),
|
| 371 |
-
"mean_coincident_wb_2_0": float(parts[22]),
|
| 372 |
-
"wet_bulb_2_0": float(parts[23]),
|
| 373 |
-
"mean_coincident_db_2_0": float(parts[24]),
|
| 374 |
-
"evaporation_wb_0_4": float(parts[25]),
|
| 375 |
-
"mean_coincident_db_wb_0_4": float(parts[26])
|
| 376 |
-
},
|
| 377 |
-
"extremes": {
|
| 378 |
-
"annual_max_db": float(parts[27]),
|
| 379 |
-
"mean_db": float(parts[28]),
|
| 380 |
-
"std_dev_db": float(parts[29]),
|
| 381 |
-
"min_db": float(parts[30]),
|
| 382 |
-
"max_db_1": float(parts[31]),
|
| 383 |
-
"min_db_1": float(parts[32]),
|
| 384 |
-
"max_db_2": float(parts[33]),
|
| 385 |
-
"min_db_2": float(parts[34]),
|
| 386 |
-
"max_db_3": float(parts[35]),
|
| 387 |
-
"min_db_3": float(parts[36]),
|
| 388 |
-
"max_db_4": float(parts[37]),
|
| 389 |
-
"min_db_4": float(parts[38]),
|
| 390 |
-
"max_db_5": float(parts[39]),
|
| 391 |
-
"min_db_5": float(parts[40])
|
| 392 |
-
}
|
| 393 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
except (ValueError, IndexError) as e:
|
| 395 |
st.warning(f"Error parsing DESIGN CONDITIONS: {str(e)}. Using empty design conditions.")
|
| 396 |
design_conditions = {}
|
|
@@ -398,7 +461,7 @@ class ClimateData:
|
|
| 398 |
|
| 399 |
# Parse TYPICAL/EXTREME PERIODS
|
| 400 |
typical_extreme_periods = {}
|
| 401 |
-
date_pattern = r'^\d{1,2}/\d{1,2}$'
|
| 402 |
for line in epw_lines:
|
| 403 |
if line.startswith("TYPICAL/EXTREME PERIODS"):
|
| 404 |
parts = line.strip().split(',')
|
|
@@ -409,10 +472,13 @@ class ClimateData:
|
|
| 409 |
break
|
| 410 |
for i in range(num_periods):
|
| 411 |
try:
|
|
|
|
|
|
|
|
|
|
| 412 |
period_name = parts[2 + i*4]
|
| 413 |
period_type = parts[3 + i*4]
|
| 414 |
-
start_date = parts[4 + i*4]
|
| 415 |
-
end_date = parts[5 + i*4]
|
| 416 |
if period_name in [
|
| 417 |
"Summer - Week Nearest Max Temperature For Period",
|
| 418 |
"Summer - Week Nearest Average Temperature For Period",
|
|
@@ -420,11 +486,15 @@ class ClimateData:
|
|
| 420 |
"Winter - Week Nearest Average Temperature For Period"
|
| 421 |
]:
|
| 422 |
key = f"{'summer' if 'Summer' in period_name else 'winter'}_{'extreme' if 'Max' in period_name else 'typical' if 'Average' in period_name else ''}"
|
| 423 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
st.warning(f"Invalid date format for period {period_name}: {start_date} to {end_date}, skipping.")
|
| 425 |
continue
|
| 426 |
-
start_month, start_day = map(int,
|
| 427 |
-
end_date_clean = re.sub(r'\s+', '', end_date)
|
| 428 |
end_month, end_day = map(int, end_date_clean.split('/'))
|
| 429 |
typical_extreme_periods[key] = {
|
| 430 |
"start": {"month": start_month, "day": start_day},
|
|
@@ -447,8 +517,14 @@ class ClimateData:
|
|
| 447 |
break
|
| 448 |
for i in range(num_depths):
|
| 449 |
try:
|
|
|
|
|
|
|
|
|
|
| 450 |
depth = parts[2 + i*16]
|
| 451 |
-
temps = [float(t) for t in parts[6 + i*16:18 + i*16]]
|
|
|
|
|
|
|
|
|
|
| 452 |
ground_temperatures[depth] = temps
|
| 453 |
except (ValueError, IndexError) as e:
|
| 454 |
st.warning(f"Error parsing ground temperatures for depth {i+1}: {str(e)}, skipping.")
|
|
@@ -563,7 +639,8 @@ class ClimateData:
|
|
| 563 |
st.subheader("Design Conditions")
|
| 564 |
|
| 565 |
# Location Details
|
| 566 |
-
st.markdown("""
|
|
|
|
| 567 |
**Location Details:**
|
| 568 |
- **Country**: {location.country}
|
| 569 |
- **City**: {location.city}
|
|
@@ -571,10 +648,12 @@ class ClimateData:
|
|
| 571 |
- **Latitude**: {location.latitude}°
|
| 572 |
- **Longitude**: {location.longitude}°
|
| 573 |
- **Elevation**: {location.elevation} m
|
| 574 |
-
|
|
|
|
| 575 |
|
| 576 |
# Calculated Climate Parameters
|
| 577 |
-
st.markdown("""
|
|
|
|
| 578 |
**Calculated Climate Parameters:**
|
| 579 |
- **Climate Zone**: {location.climate_zone}
|
| 580 |
- **Heating Degree Days (base 18°C)**: {location.heating_degree_days} HDD
|
|
@@ -585,42 +664,64 @@ class ClimateData:
|
|
| 585 |
- **Summer Daily Temperature Range**: {location.summer_daily_range} °C
|
| 586 |
- **Mean Wind Speed**: {location.wind_speed} m/s
|
| 587 |
- **Mean Atmospheric Pressure**: {location.pressure} Pa
|
| 588 |
-
|
|
|
|
| 589 |
|
| 590 |
# EPW Header Design Conditions
|
| 591 |
if location.design_conditions:
|
| 592 |
-
|
| 593 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
- **Heating:**
|
| 595 |
-
|
| 596 |
-
- Dry-Bulb Temp: {location.design_conditions['heating']['dry_bulb']} °C
|
| 597 |
-
- Mean Coincident Dry-Bulb: {location.design_conditions['heating']['mean_coincident_db']} °C
|
| 598 |
-
- Humidification Dry-Bulb: {location.design_conditions['heating']['humidification_db']} °C
|
| 599 |
-
- Mean Coincident Wet-Bulb: {location.design_conditions['heating']['mean_coincident_wb']} °C
|
| 600 |
- **Cooling:**
|
| 601 |
-
|
| 602 |
-
- Dry-Bulb Temp (0.4%): {location.design_conditions['cooling']['dry_bulb_0_4']} °C
|
| 603 |
-
- Wet-Bulb Temp (0.4%): {location.design_conditions['cooling']['wet_bulb_0_4']} °C
|
| 604 |
-
- Mean Coincident Wet-Bulb (0.4%): {location.design_conditions['cooling']['mean_coincident_wb_0_4']} °C
|
| 605 |
- **Extremes:**
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
"""
|
| 609 |
|
| 610 |
# Typical/Extreme Periods
|
| 611 |
if location.typical_extreme_periods:
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 616 |
|
| 617 |
-
# Ground Temperatures
|
| 618 |
if location.ground_temperatures:
|
| 619 |
-
st.markdown("**Ground Temperatures:**
|
| 620 |
month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
|
|
| 621 |
for depth, temps in location.ground_temperatures.items():
|
| 622 |
-
|
| 623 |
-
|
|
|
|
|
|
|
|
|
|
| 624 |
|
| 625 |
@staticmethod
|
| 626 |
def assign_climate_zone(hdd: float, cdd: float, avg_humidity: float) -> str:
|
|
@@ -660,7 +761,7 @@ class ClimateData:
|
|
| 660 |
vapor_pressure = humidity / 100 * saturation_pressure
|
| 661 |
humidity_ratio = 0.62198 * vapor_pressure / (pressure - vapor_pressure) * 1000 # Convert to g/kg
|
| 662 |
|
| 663 |
-
|
| 664 |
|
| 665 |
# Hourly data points
|
| 666 |
fig.add_trace(go.Scatter(
|
|
|
|
| 4 |
|
| 5 |
Author: Dr Majed Abuseif
|
| 6 |
Date: May 2025
|
| 7 |
+
Version: 2.1.2
|
| 8 |
"""
|
| 9 |
|
| 10 |
from typing import Dict, List, Any, Optional
|
|
|
|
| 23 |
# Define paths
|
| 24 |
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 25 |
|
| 26 |
+
# CSS for consistent formatting
|
| 27 |
+
STYLE = """
|
| 28 |
+
<style>
|
| 29 |
+
.markdown-text {
|
| 30 |
+
font-family: Roboto, sans-serif;
|
| 31 |
+
font-size: 14px;
|
| 32 |
+
line-height: 1.5;
|
| 33 |
+
}
|
| 34 |
+
.markdown-text ul {
|
| 35 |
+
margin-top: 0;
|
| 36 |
+
margin-bottom: 10px;
|
| 37 |
+
}
|
| 38 |
+
.markdown-text li {
|
| 39 |
+
margin-bottom: 5px;
|
| 40 |
+
}
|
| 41 |
+
</style>
|
| 42 |
+
"""
|
| 43 |
+
|
| 44 |
@dataclass
|
| 45 |
class ClimateLocation:
|
| 46 |
"""Class representing a climate location with ASHRAE 169 data derived from EPW files."""
|
|
|
|
| 276 |
# Validate typical/extreme periods (optional)
|
| 277 |
if "typical_extreme_periods" in data and data["typical_extreme_periods"]:
|
| 278 |
expected_periods = ["summer_extreme", "summer_typical", "winter_extreme", "winter_typical"]
|
| 279 |
+
missing_periods = [p for p in expected_periods if p not in data["typical_extreme_periods"]]
|
| 280 |
+
if missing_periods:
|
| 281 |
+
st.warning(f"Validation warning: Missing typical/extreme periods: {', '.join(missing_periods)}")
|
| 282 |
for period in data["typical_extreme_periods"].values():
|
| 283 |
for date in ["start", "end"]:
|
| 284 |
if not (1 <= period[date]["month"] <= 12 and 1 <= period[date]["day"] <= 31):
|
|
|
|
| 328 |
"""Display Streamlit interface for EPW upload and visualizations."""
|
| 329 |
st.title("Climate Data Analysis")
|
| 330 |
|
| 331 |
+
# Apply consistent styling
|
| 332 |
+
st.markdown(STYLE, unsafe_allow_html=True)
|
| 333 |
+
|
| 334 |
# Clear invalid session_state["climate_data"] to prevent validation errors
|
| 335 |
if "climate_data" in session_state and not all(key in session_state["climate_data"] for key in ["id", "country", "city"]):
|
| 336 |
st.warning("Clearing invalid climate data from session state.")
|
|
|
|
| 364 |
if line.startswith("DESIGN CONDITIONS"):
|
| 365 |
parts = line.strip().split(',')
|
| 366 |
if len(parts) < 41:
|
| 367 |
+
st.warning(f"DESIGN CONDITIONS has {len(parts)} fields, expected 41. Skipping parsing.")
|
| 368 |
break
|
| 369 |
try:
|
| 370 |
design_conditions = {
|
| 371 |
+
"heating": {},
|
| 372 |
+
"cooling": {},
|
| 373 |
+
"extremes": {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
}
|
| 375 |
+
# Heating section
|
| 376 |
+
if parts[3] and parts[3].strip():
|
| 377 |
+
design_conditions["heating"]["coldest_month"] = int(parts[3])
|
| 378 |
+
if parts[4] and parts[4].strip():
|
| 379 |
+
design_conditions["heating"]["dry_bulb"] = float(parts[4])
|
| 380 |
+
if parts[5] and parts[5].strip():
|
| 381 |
+
design_conditions["heating"]["mean_coincident_db"] = float(parts[5])
|
| 382 |
+
if parts[6] and parts[6].strip():
|
| 383 |
+
design_conditions["heating"]["humidification_db"] = float(parts[6])
|
| 384 |
+
if parts[7] and parts[7].strip():
|
| 385 |
+
design_conditions["heating"]["mean_coincident_wb"] = float(parts[7])
|
| 386 |
+
if parts[8] and parts[8].strip():
|
| 387 |
+
design_conditions["heating"]["wind_speed_1"] = float(parts[8])
|
| 388 |
+
if parts[9] and parts[9].strip():
|
| 389 |
+
design_conditions["heating"]["wind_direction_1"] = float(parts[9])
|
| 390 |
+
if parts[10] and parts[10].strip():
|
| 391 |
+
design_conditions["heating"]["wind_speed_2"] = float(parts[10])
|
| 392 |
+
if parts[11] and parts[11].strip():
|
| 393 |
+
design_conditions["heating"]["wind_direction_2"] = float(parts[11])
|
| 394 |
+
# Cooling section
|
| 395 |
+
if parts[12] and parts[12].strip():
|
| 396 |
+
design_conditions["cooling"]["hottest_month"] = int(parts[12])
|
| 397 |
+
if parts[13] and parts[13].strip():
|
| 398 |
+
design_conditions["cooling"]["dry_bulb_0_4"] = float(parts[13])
|
| 399 |
+
if parts[14] and parts[14].strip():
|
| 400 |
+
design_conditions["cooling"]["mean_coincident_wb_0_4"] = float(parts[14])
|
| 401 |
+
if parts[15] and parts[15].strip():
|
| 402 |
+
design_conditions["cooling"]["wet_bulb_0_4"] = float(parts[15])
|
| 403 |
+
if parts[16] and parts[16].strip():
|
| 404 |
+
design_conditions["cooling"]["mean_coincident_db_0_4"] = float(parts[16])
|
| 405 |
+
if parts[17] and parts[17].strip():
|
| 406 |
+
design_conditions["cooling"]["dry_bulb_1_0"] = float(parts[17])
|
| 407 |
+
if parts[18] and parts[18].strip():
|
| 408 |
+
design_conditions["cooling"]["mean_coincident_wb_1_0"] = float(parts[18])
|
| 409 |
+
if parts[19] and parts[19].strip():
|
| 410 |
+
design_conditions["cooling"]["wet_bulb_1_0"] = float(parts[19])
|
| 411 |
+
if parts[20] and parts[20].strip():
|
| 412 |
+
design_conditions["cooling"]["mean_coincident_db_1_0"] = float(parts[20])
|
| 413 |
+
if parts[21] and parts[21].strip():
|
| 414 |
+
design_conditions["cooling"]["dry_bulb_2_0"] = float(parts[21])
|
| 415 |
+
if parts[22] and parts[22].strip():
|
| 416 |
+
design_conditions["cooling"]["mean_coincident_wb_2_0"] = float(parts[22])
|
| 417 |
+
if parts[23] and parts[23].strip():
|
| 418 |
+
design_conditions["cooling"]["wet_bulb_2_0"] = float(parts[23])
|
| 419 |
+
if parts[24] and parts[24].strip():
|
| 420 |
+
design_conditions["cooling"]["mean_coincident_db_2_0"] = float(parts[24])
|
| 421 |
+
if parts[25] and parts[25].strip():
|
| 422 |
+
design_conditions["cooling"]["evaporation_wb_0_4"] = float(parts[25])
|
| 423 |
+
if parts[26] and parts[26].strip():
|
| 424 |
+
design_conditions["cooling"]["mean_coincident_db_wb_0_4"] = float(parts[26])
|
| 425 |
+
# Extremes section
|
| 426 |
+
if parts[27] and parts[27].strip():
|
| 427 |
+
design_conditions["extremes"]["annual_max_db"] = float(parts[27])
|
| 428 |
+
if parts[28] and parts[28].strip():
|
| 429 |
+
design_conditions["extremes"]["mean_db"] = float(parts[28])
|
| 430 |
+
if parts[29] and parts[29].strip():
|
| 431 |
+
design_conditions["extremes"]["std_dev_db"] = float(parts[29])
|
| 432 |
+
if parts[30] and parts[30].strip():
|
| 433 |
+
design_conditions["extremes"]["min_db"] = float(parts[30])
|
| 434 |
+
if parts[31] and parts[31].strip():
|
| 435 |
+
design_conditions["extremes"]["max_db_1"] = float(parts[31])
|
| 436 |
+
if parts[32] and parts[32].strip():
|
| 437 |
+
design_conditions["extremes"]["min_db_1"] = float(parts[32])
|
| 438 |
+
if parts[33] and parts[33].strip():
|
| 439 |
+
design_conditions["extremes"]["max_db_2"] = float(parts[33])
|
| 440 |
+
if parts[34] and parts[34].strip():
|
| 441 |
+
design_conditions["extremes"]["min_db_2"] = float(parts[34])
|
| 442 |
+
if parts[35] and parts[35].strip():
|
| 443 |
+
design_conditions["extremes"]["max_db_3"] = float(parts[35])
|
| 444 |
+
if parts[36] and parts[36].strip():
|
| 445 |
+
design_conditions["extremes"]["min_db_3"] = float(parts[36])
|
| 446 |
+
if parts[37] and parts[37].strip():
|
| 447 |
+
design_conditions["extremes"]["max_db_4"] = float(parts[37])
|
| 448 |
+
if parts[38] and parts[38].strip():
|
| 449 |
+
design_conditions["extremes"]["min_db_4"] = float(parts[38])
|
| 450 |
+
if parts[39] and parts[39].strip():
|
| 451 |
+
design_conditions["extremes"]["max_db_5"] = float(parts[39])
|
| 452 |
+
if parts[40] and parts[40].strip():
|
| 453 |
+
design_conditions["extremes"]["min_db_5"] = float(parts[40])
|
| 454 |
+
if not design_conditions["heating"] and not design_conditions["cooling"] and not design_conditions["extremes"]:
|
| 455 |
+
design_conditions = {}
|
| 456 |
+
st.warning("No valid DESIGN CONDITIONS fields parsed.")
|
| 457 |
except (ValueError, IndexError) as e:
|
| 458 |
st.warning(f"Error parsing DESIGN CONDITIONS: {str(e)}. Using empty design conditions.")
|
| 459 |
design_conditions = {}
|
|
|
|
| 461 |
|
| 462 |
# Parse TYPICAL/EXTREME PERIODS
|
| 463 |
typical_extreme_periods = {}
|
| 464 |
+
date_pattern = r'^\d{1,2}\s*/\s*\d{1,2}$'
|
| 465 |
for line in epw_lines:
|
| 466 |
if line.startswith("TYPICAL/EXTREME PERIODS"):
|
| 467 |
parts = line.strip().split(',')
|
|
|
|
| 472 |
break
|
| 473 |
for i in range(num_periods):
|
| 474 |
try:
|
| 475 |
+
if len(parts) < 2 + i*4 + 4:
|
| 476 |
+
st.warning(f"Insufficient fields for period {i+1}, skipping.")
|
| 477 |
+
continue
|
| 478 |
period_name = parts[2 + i*4]
|
| 479 |
period_type = parts[3 + i*4]
|
| 480 |
+
start_date = parts[4 + i*4].strip()
|
| 481 |
+
end_date = parts[5 + i*4].strip()
|
| 482 |
if period_name in [
|
| 483 |
"Summer - Week Nearest Max Temperature For Period",
|
| 484 |
"Summer - Week Nearest Average Temperature For Period",
|
|
|
|
| 486 |
"Winter - Week Nearest Average Temperature For Period"
|
| 487 |
]:
|
| 488 |
key = f"{'summer' if 'Summer' in period_name else 'winter'}_{'extreme' if 'Max' in period_name else 'typical' if 'Average' in period_name else ''}"
|
| 489 |
+
# Clean dates to remove non-standard whitespace
|
| 490 |
+
start_date_clean = re.sub(r'\s+', '', start_date)
|
| 491 |
+
end_date_clean = re.sub(r'\s+', '', end_date)
|
| 492 |
+
# Debug logging
|
| 493 |
+
st.write(f"Parsing period {period_name}: start_date='{start_date_clean}', end_date='{end_date_clean}'")
|
| 494 |
+
if not re.match(date_pattern, start_date) or not re.match(date_pattern, end_date):
|
| 495 |
st.warning(f"Invalid date format for period {period_name}: {start_date} to {end_date}, skipping.")
|
| 496 |
continue
|
| 497 |
+
start_month, start_day = map(int, start_date_clean.split('/'))
|
|
|
|
| 498 |
end_month, end_day = map(int, end_date_clean.split('/'))
|
| 499 |
typical_extreme_periods[key] = {
|
| 500 |
"start": {"month": start_month, "day": start_day},
|
|
|
|
| 517 |
break
|
| 518 |
for i in range(num_depths):
|
| 519 |
try:
|
| 520 |
+
if len(parts) < 2 + i*16 + 16:
|
| 521 |
+
st.warning(f"Insufficient fields for ground temperature depth {i+1}, skipping.")
|
| 522 |
+
continue
|
| 523 |
depth = parts[2 + i*16]
|
| 524 |
+
temps = [float(t) for t in parts[6 + i*16:18 + i*16] if t.strip()]
|
| 525 |
+
if len(temps) != 12:
|
| 526 |
+
st.warning(f"Invalid number of temperatures for depth {depth}m, expected 12, got {len(temps)}, skipping.")
|
| 527 |
+
continue
|
| 528 |
ground_temperatures[depth] = temps
|
| 529 |
except (ValueError, IndexError) as e:
|
| 530 |
st.warning(f"Error parsing ground temperatures for depth {i+1}: {str(e)}, skipping.")
|
|
|
|
| 639 |
st.subheader("Design Conditions")
|
| 640 |
|
| 641 |
# Location Details
|
| 642 |
+
st.markdown(f"""
|
| 643 |
+
<div class="markdown-text">
|
| 644 |
**Location Details:**
|
| 645 |
- **Country**: {location.country}
|
| 646 |
- **City**: {location.city}
|
|
|
|
| 648 |
- **Latitude**: {location.latitude}°
|
| 649 |
- **Longitude**: {location.longitude}°
|
| 650 |
- **Elevation**: {location.elevation} m
|
| 651 |
+
</div>
|
| 652 |
+
""", unsafe_allow_html=True)
|
| 653 |
|
| 654 |
# Calculated Climate Parameters
|
| 655 |
+
st.markdown(f"""
|
| 656 |
+
<div class="markdown-text">
|
| 657 |
**Calculated Climate Parameters:**
|
| 658 |
- **Climate Zone**: {location.climate_zone}
|
| 659 |
- **Heating Degree Days (base 18°C)**: {location.heating_degree_days} HDD
|
|
|
|
| 664 |
- **Summer Daily Temperature Range**: {location.summer_daily_range} °C
|
| 665 |
- **Mean Wind Speed**: {location.wind_speed} m/s
|
| 666 |
- **Mean Atmospheric Pressure**: {location.pressure} Pa
|
| 667 |
+
</div>
|
| 668 |
+
""", unsafe_allow_html=True)
|
| 669 |
|
| 670 |
# EPW Header Design Conditions
|
| 671 |
if location.design_conditions:
|
| 672 |
+
heating_items = [
|
| 673 |
+
f"- Coldest Month: {location.design_conditions['heating'].get('coldest_month', 'N/A')}",
|
| 674 |
+
f"- Dry-Bulb Temp: {location.design_conditions['heating'].get('dry_bulb', 'N/A')} °C",
|
| 675 |
+
f"- Mean Coincident Dry-Bulb: {location.design_conditions['heating'].get('mean_coincident_db', 'N/A')} °C",
|
| 676 |
+
f"- Humidification Dry-Bulb: {location.design_conditions['heating'].get('humidification_db', 'N/A')} °C",
|
| 677 |
+
f"- Mean Coincident Wet-Bulb: {location.design_conditions['heating'].get('mean_coincident_wb', 'N/A')} °C"
|
| 678 |
+
]
|
| 679 |
+
cooling_items = [
|
| 680 |
+
f"- Hottest Month: {location.design_conditions['cooling'].get('hottest_month', 'N/A')}",
|
| 681 |
+
f"- Dry-Bulb Temp (0.4%): {location.design_conditions['cooling'].get('dry_bulb_0_4', 'N/A')} °C",
|
| 682 |
+
f"- Wet-Bulb Temp (0.4%): {location.design_conditions['cooling'].get('wet_bulb_0_4', 'N/A')} °C",
|
| 683 |
+
f"- Mean Coincident Wet-Bulb (0.4%): {location.design_conditions['cooling'].get('mean_coincident_wb_0_4', 'N/A')} °C"
|
| 684 |
+
]
|
| 685 |
+
extremes_items = [
|
| 686 |
+
f"- Annual Max Dry-Bulb: {location.design_conditions['extremes'].get('annual_max_db', 'N/A')} °C",
|
| 687 |
+
f"- Annual Min Dry-Bulb: {location.design_conditions['extremes'].get('min_db', 'N/A')} °C"
|
| 688 |
+
]
|
| 689 |
+
st.markdown(f"""
|
| 690 |
+
<div class="markdown-text">
|
| 691 |
+
**Design Conditions (EPW Header):**
|
| 692 |
- **Heating:**
|
| 693 |
+
{'<br>'.join(heating_items)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
- **Cooling:**
|
| 695 |
+
{'<br>'.join(cooling_items)}
|
|
|
|
|
|
|
|
|
|
| 696 |
- **Extremes:**
|
| 697 |
+
{'<br>'.join(extremes_items)}
|
| 698 |
+
</div>
|
| 699 |
+
""", unsafe_allow_html=True)
|
| 700 |
|
| 701 |
# Typical/Extreme Periods
|
| 702 |
if location.typical_extreme_periods:
|
| 703 |
+
period_items = [
|
| 704 |
+
f"- **{key.replace('_', ' ').title()}**: {period['start']['month']}/{period['start']['day']} to {period['end']['month']}/{period['end']['day']}"
|
| 705 |
+
for key, period in location.typical_extreme_periods.items()
|
| 706 |
+
]
|
| 707 |
+
st.markdown(f"""
|
| 708 |
+
<div class="markdown-text">
|
| 709 |
+
**Typical/Extreme Periods:**
|
| 710 |
+
{'<br>'.join(period_items)}
|
| 711 |
+
</div>
|
| 712 |
+
""", unsafe_allow_html=True)
|
| 713 |
|
| 714 |
+
# Ground Temperatures (Table)
|
| 715 |
if location.ground_temperatures:
|
| 716 |
+
st.markdown('<div class="markdown-text">**Ground Temperatures:**</div>', unsafe_allow_html=True)
|
| 717 |
month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
| 718 |
+
table_data = []
|
| 719 |
for depth, temps in location.ground_temperatures.items():
|
| 720 |
+
row = {"Depth (m)": float(depth)}
|
| 721 |
+
row.update({month: f"{temp:.2f}" for month, temp in zip(month_names, temps)})
|
| 722 |
+
table_data.append(row)
|
| 723 |
+
df = pd.DataFrame(table_data)
|
| 724 |
+
st.dataframe(df, use_container_width=True)
|
| 725 |
|
| 726 |
@staticmethod
|
| 727 |
def assign_climate_zone(hdd: float, cdd: float, avg_humidity: float) -> str:
|
|
|
|
| 761 |
vapor_pressure = humidity / 100 * saturation_pressure
|
| 762 |
humidity_ratio = 0.62198 * vapor_pressure / (pressure - vapor_pressure) * 1000 # Convert to g/kg
|
| 763 |
|
| 764 |
+
figastanza = go.Figure()
|
| 765 |
|
| 766 |
# Hourly data points
|
| 767 |
fig.add_trace(go.Scatter(
|