Update data/climate_data.py
Browse files- data/climate_data.py +24 -173
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
|
|
@@ -19,6 +19,11 @@ from io import StringIO
|
|
| 19 |
import pvlib
|
| 20 |
from datetime import datetime, timedelta
|
| 21 |
import re
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
# Define paths
|
| 24 |
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -52,12 +57,6 @@ STYLE = """
|
|
| 52 |
</style>
|
| 53 |
"""
|
| 54 |
|
| 55 |
-
# Country code to full name mapping
|
| 56 |
-
COUNTRY_MAP = {
|
| 57 |
-
"AUS": "Australia",
|
| 58 |
-
# Add other mappings as needed
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
@dataclass
|
| 62 |
class ClimateLocation:
|
| 63 |
"""Class representing a climate location with ASHRAE 169 data derived from EPW files."""
|
|
@@ -81,9 +80,8 @@ class ClimateLocation:
|
|
| 81 |
hourly_data: List[Dict] # Hourly data for integration with main.py
|
| 82 |
typical_extreme_periods: Dict[str, Dict] # Typical/extreme periods (summer/winter)
|
| 83 |
ground_temperatures: Dict[str, List[float]] # Monthly ground temperatures by depth
|
| 84 |
-
design_conditions: Dict[str, Any] # Design conditions from EPW header
|
| 85 |
|
| 86 |
-
def __init__(self, epw_file: pd.DataFrame, typical_extreme_periods: Dict, ground_temperatures: Dict,
|
| 87 |
"""Initialize ClimateLocation with EPW file data and header information."""
|
| 88 |
self.id = kwargs.get("id")
|
| 89 |
self.country = kwargs.get("country")
|
|
@@ -94,7 +92,6 @@ class ClimateLocation:
|
|
| 94 |
self.elevation = kwargs.get("elevation")
|
| 95 |
self.typical_extreme_periods = typical_extreme_periods
|
| 96 |
self.ground_temperatures = ground_temperatures
|
| 97 |
-
self.design_conditions = design_conditions
|
| 98 |
|
| 99 |
# Extract columns from EPW data
|
| 100 |
months = pd.to_numeric(epw_file[1], errors='coerce').values
|
|
@@ -105,7 +102,12 @@ class ClimateLocation:
|
|
| 105 |
pressure = pd.to_numeric(epw_file[9], errors='coerce').values
|
| 106 |
global_radiation = pd.to_numeric(epw_file[13], errors='coerce').values
|
| 107 |
wind_direction = pd.to_numeric(epw_file[20], errors='coerce').values
|
| 108 |
-
wind_speed = pd.to_numeric(epw_file[21], errors='coerce')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
# Calculate wet-bulb temperature
|
| 111 |
wet_bulb = ClimateData.calculate_wet_bulb(dry_bulb, humidity)
|
|
@@ -129,6 +131,9 @@ class ClimateLocation:
|
|
| 129 |
self.wind_speed = round(np.nanmean(wind_speed), 1)
|
| 130 |
self.pressure = round(np.nanmean(pressure), 1)
|
| 131 |
|
|
|
|
|
|
|
|
|
|
| 132 |
# Assign climate zone
|
| 133 |
self.climate_zone = ClimateData.assign_climate_zone(self.heating_degree_days, self.cooling_degree_days, np.nanmean(humidity))
|
| 134 |
|
|
@@ -174,8 +179,7 @@ class ClimateLocation:
|
|
| 174 |
"pressure": self.pressure,
|
| 175 |
"hourly_data": self.hourly_data,
|
| 176 |
"typical_extreme_periods": self.typical_extreme_periods,
|
| 177 |
-
"ground_temperatures": self.ground_temperatures
|
| 178 |
-
"design_conditions": self.design_conditions
|
| 179 |
}
|
| 180 |
|
| 181 |
class ClimateData:
|
|
@@ -251,7 +255,7 @@ class ClimateData:
|
|
| 251 |
if data["summer_daily_range"] < 0:
|
| 252 |
st.error("Validation failed: Negative summer daily range")
|
| 253 |
return False
|
| 254 |
-
if not (0 <= data["wind_speed"] <=
|
| 255 |
st.error(f"Validation failed: Wind speed {data['wind_speed']} outside range")
|
| 256 |
return False
|
| 257 |
if not (80000 <= data["pressure"] <= 110000):
|
|
@@ -283,7 +287,7 @@ class ClimateData:
|
|
| 283 |
if not (0 <= record["global_horizontal_radiation"] <= 1200):
|
| 284 |
st.error(f"Validation failed: Global radiation {record['global_horizontal_radiation']} outside range")
|
| 285 |
return False
|
| 286 |
-
if not (0 <= record["wind_speed"] <=
|
| 287 |
st.error(f"Validation failed: Wind speed {record['wind_speed']} outside range")
|
| 288 |
return False
|
| 289 |
if not (0 <= record["wind_direction"] <= 360):
|
|
@@ -309,17 +313,6 @@ class ClimateData:
|
|
| 309 |
st.error(f"Validation failed: Invalid ground temperatures for depth {depth}")
|
| 310 |
return False
|
| 311 |
|
| 312 |
-
# Validate design conditions (optional)
|
| 313 |
-
if "design_conditions" in data and data["design_conditions"]:
|
| 314 |
-
if not all(key in data["design_conditions"] for key in ["heating", "cooling", "extremes"]):
|
| 315 |
-
st.warning("Validation warning: Incomplete design conditions")
|
| 316 |
-
for section in ["heating", "cooling", "extremes"]:
|
| 317 |
-
if section in data["design_conditions"]:
|
| 318 |
-
for key, value in data["design_conditions"][section].items():
|
| 319 |
-
if isinstance(value, (int, float)) and not (-50 <= value <= 50):
|
| 320 |
-
st.error(f"Validation failed: Invalid {section} design condition {key}: {value}")
|
| 321 |
-
return False
|
| 322 |
-
|
| 323 |
return True
|
| 324 |
|
| 325 |
@staticmethod
|
|
@@ -377,113 +370,16 @@ class ClimateData:
|
|
| 377 |
# Parse header
|
| 378 |
header = next(line for line in epw_lines if line.startswith("LOCATION"))
|
| 379 |
header_parts = header.split(",")
|
| 380 |
-
city = header_parts[1].strip()
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
building_info = session_state.get("building_info", {})
|
| 386 |
-
country = building_info.get("country", COUNTRY_MAP.get(country, country)) or "Unknown"
|
| 387 |
-
city = building_info.get("city", re.sub(r'\..*', '', city)) or "Unknown"
|
| 388 |
|
| 389 |
latitude = float(header_parts[6])
|
| 390 |
longitude = float(header_parts[7])
|
| 391 |
elevation = float(header_parts[8])
|
| 392 |
|
| 393 |
-
# Parse DESIGN CONDITIONS
|
| 394 |
-
design_conditions = {"heating": {}, "cooling": {}, "extremes": {}}
|
| 395 |
-
for line in epw_lines:
|
| 396 |
-
if line.startswith("DESIGN CONDITIONS"):
|
| 397 |
-
parts = line.strip().split(',')
|
| 398 |
-
if len(parts) < 41:
|
| 399 |
-
st.warning(f"DESIGN CONDITIONS has {len(parts)} fields, expected 41. Using partial data.")
|
| 400 |
-
break
|
| 401 |
-
try:
|
| 402 |
-
# Heating section
|
| 403 |
-
if parts[3] and self.is_numeric(parts[3]):
|
| 404 |
-
design_conditions["heating"]["coldest_month"] = int(parts[3])
|
| 405 |
-
if parts[4] and self.is_numeric(parts[4]):
|
| 406 |
-
design_conditions["heating"]["dry_bulb"] = float(parts[4])
|
| 407 |
-
else:
|
| 408 |
-
st.warning(f"Invalid heating dry_bulb: '{parts[4]}'")
|
| 409 |
-
if parts[5] and self.is_numeric(parts[5]):
|
| 410 |
-
design_conditions["heating"]["mean_coincident_db"] = float(parts[5])
|
| 411 |
-
if parts[6] and self.is_numeric(parts[6]):
|
| 412 |
-
design_conditions["heating"]["humidification_db"] = float(parts[6])
|
| 413 |
-
if parts[7] and self.is_numeric(parts[7]):
|
| 414 |
-
design_conditions["heating"]["mean_coincident_wb"] = float(parts[7])
|
| 415 |
-
if parts[8] and self.is_numeric(parts[8]):
|
| 416 |
-
design_conditions["heating"]["wind_speed_1"] = float(parts[8])
|
| 417 |
-
if parts[9] and self.is_numeric(parts[9]):
|
| 418 |
-
design_conditions["heating"]["wind_direction_1"] = float(parts[9])
|
| 419 |
-
if parts[10] and self.is_numeric(parts[10]):
|
| 420 |
-
design_conditions["heating"]["wind_speed_2"] = float(parts[10])
|
| 421 |
-
if parts[11] and self.is_numeric(parts[11]):
|
| 422 |
-
design_conditions["heating"]["wind_direction_2"] = float(parts[11])
|
| 423 |
-
# Cooling section
|
| 424 |
-
if parts[12] and self.is_numeric(parts[12]):
|
| 425 |
-
design_conditions["cooling"]["hottest_month"] = int(parts[12])
|
| 426 |
-
if parts[13] and self.is_numeric(parts[13]):
|
| 427 |
-
design_conditions["cooling"]["dry_bulb_0_4"] = float(parts[13])
|
| 428 |
-
if parts[14] and self.is_numeric(parts[14]):
|
| 429 |
-
design_conditions["cooling"]["mean_coincident_wb_0_4"] = float(parts[14])
|
| 430 |
-
if parts[15] and self.is_numeric(parts[15]):
|
| 431 |
-
design_conditions["cooling"]["wet_bulb_0_4"] = float(parts[15])
|
| 432 |
-
if parts[16] and self.is_numeric(parts[16]):
|
| 433 |
-
design_conditions["cooling"]["mean_coincident_db_0_4"] = float(parts[16])
|
| 434 |
-
if parts[17] and self.is_numeric(parts[17]):
|
| 435 |
-
design_conditions["cooling"]["dry_bulb_1_0"] = float(parts[17])
|
| 436 |
-
if parts[18] and self.is_numeric(parts[18]):
|
| 437 |
-
design_conditions["cooling"]["mean_coincident_wb_1_0"] = float(parts[18])
|
| 438 |
-
if parts[19] and self.is_numeric(parts[19]):
|
| 439 |
-
design_conditions["cooling"]["wet_bulb_1_0"] = float(parts[19])
|
| 440 |
-
if parts[20] and self.is_numeric(parts[20]):
|
| 441 |
-
design_conditions["cooling"]["mean_coincident_db_1_0"] = float(parts[20])
|
| 442 |
-
if parts[21] and self.is_numeric(parts[21]):
|
| 443 |
-
design_conditions["cooling"]["dry_bulb_2_0"] = float(parts[21])
|
| 444 |
-
if parts[22] and self.is_numeric(parts[22]):
|
| 445 |
-
design_conditions["cooling"]["mean_coincident_wb_2_0"] = float(parts[22])
|
| 446 |
-
if parts[23] and self.is_numeric(parts[23]):
|
| 447 |
-
design_conditions["cooling"]["wet_bulb_2_0"] = float(parts[23])
|
| 448 |
-
if parts[24] and self.is_numeric(parts[24]):
|
| 449 |
-
design_conditions["cooling"]["mean_coincident_db_2_0"] = float(parts[24])
|
| 450 |
-
if parts[25] and self.is_numeric(parts[25]):
|
| 451 |
-
design_conditions["cooling"]["evaporation_wb_0_4"] = float(parts[25])
|
| 452 |
-
if parts[26] and self.is_numeric(parts[26]):
|
| 453 |
-
design_conditions["cooling"]["mean_coincident_db_wb_0_4"] = float(parts[26])
|
| 454 |
-
# Extremes section
|
| 455 |
-
if parts[27] and self.is_numeric(parts[27]):
|
| 456 |
-
design_conditions["extremes"]["annual_max_db"] = float(parts[27])
|
| 457 |
-
if parts[28] and self.is_numeric(parts[28]):
|
| 458 |
-
design_conditions["extremes"]["mean_db"] = float(parts[28])
|
| 459 |
-
if parts[29] and self.is_numeric(parts[29]):
|
| 460 |
-
design_conditions["extremes"]["std_dev_db"] = float(parts[29])
|
| 461 |
-
if parts[30] and self.is_numeric(parts[30]):
|
| 462 |
-
design_conditions["extremes"]["min_db"] = float(parts[30])
|
| 463 |
-
if parts[31] and self.is_numeric(parts[31]):
|
| 464 |
-
design_conditions["extremes"]["max_db_1"] = float(parts[31])
|
| 465 |
-
if parts[32] and self.is_numeric(parts[32]):
|
| 466 |
-
design_conditions["extremes"]["min_db_1"] = float(parts[32])
|
| 467 |
-
if parts[33] and self.is_numeric(parts[33]):
|
| 468 |
-
design_conditions["extremes"]["max_db_2"] = float(parts[33])
|
| 469 |
-
if parts[34] and self.is_numeric(parts[34]):
|
| 470 |
-
design_conditions["extremes"]["min_db_2"] = float(parts[34])
|
| 471 |
-
if parts[35] and self.is_numeric(parts[35]):
|
| 472 |
-
design_conditions["extremes"]["max_db_3"] = float(parts[35])
|
| 473 |
-
if parts[36] and self.is_numeric(parts[36]):
|
| 474 |
-
design_conditions["extremes"]["min_db_3"] = float(parts[36])
|
| 475 |
-
if parts[37] and self.is_numeric(parts[37]):
|
| 476 |
-
design_conditions["extremes"]["max_db_4"] = float(parts[37])
|
| 477 |
-
if parts[38] and self.is_numeric(parts[38]):
|
| 478 |
-
design_conditions["extremes"]["min_db_4"] = float(parts[38])
|
| 479 |
-
if parts[39] and self.is_numeric(parts[39]):
|
| 480 |
-
design_conditions["extremes"]["max_db_5"] = float(parts[39])
|
| 481 |
-
if parts[40] and self.is_numeric(parts[40]):
|
| 482 |
-
design_conditions["extremes"]["min_db_5"] = float(parts[40])
|
| 483 |
-
except Exception as e:
|
| 484 |
-
st.warning(f"Error parsing DESIGN CONDITIONS field: {str(e)}. Continuing with partial data.")
|
| 485 |
-
break
|
| 486 |
-
|
| 487 |
# Parse TYPICAL/EXTREME PERIODS
|
| 488 |
typical_extreme_periods = {}
|
| 489 |
date_pattern = r'^\d{1,2}\s*/\s*\d{1,2}$'
|
|
@@ -575,7 +471,6 @@ class ClimateData:
|
|
| 575 |
epw_file=epw_data,
|
| 576 |
typical_extreme_periods=typical_extreme_periods,
|
| 577 |
ground_temperatures=ground_temperatures,
|
| 578 |
-
design_conditions=design_conditions,
|
| 579 |
id=f"{country[:1].upper()}{city[:3].upper()}",
|
| 580 |
country=country,
|
| 581 |
state_province=state_province,
|
|
@@ -617,7 +512,6 @@ class ClimateData:
|
|
| 617 |
epw_file=epw_data,
|
| 618 |
typical_extreme_periods=climate_data_dict["typical_extreme_periods"],
|
| 619 |
ground_temperatures=climate_data_dict["ground_temperatures"],
|
| 620 |
-
design_conditions=climate_data_dict["design_conditions"],
|
| 621 |
id=climate_data_dict["id"],
|
| 622 |
country=climate_data_dict["country"],
|
| 623 |
state_province=climate_data_dict["state_province"],
|
|
@@ -696,48 +590,6 @@ class ClimateData:
|
|
| 696 |
</div>
|
| 697 |
""", unsafe_allow_html=True)
|
| 698 |
|
| 699 |
-
# EPW Header Design Conditions
|
| 700 |
-
if location.design_conditions and any(location.design_conditions[section] for section in ["heating", "cooling", "extremes"]):
|
| 701 |
-
heating_items = [
|
| 702 |
-
f"<li><strong>Coldest Month:</strong> {location.design_conditions['heating'].get('coldest_month', 'N/A')}</li>",
|
| 703 |
-
f"<li><strong>Dry-Bulb Temp:</strong> {location.design_conditions['heating'].get('dry_bulb', 'N/A')} °C</li>",
|
| 704 |
-
f"<li><strong>Mean Coincident Dry-Bulb:</strong> {location.design_conditions['heating'].get('mean_coincident_db', 'N/A')} °C</li>",
|
| 705 |
-
f"<li><strong>Humidification Dry-Bulb:</strong> {location.design_conditions['heating'].get('humidification_db', 'N/A')} °C</li>",
|
| 706 |
-
f"<li><strong>Mean Coincident Wet-Bulb:</strong> {location.design_conditions['heating'].get('mean_coincident_wb', 'N/A')} °C</li>"
|
| 707 |
-
]
|
| 708 |
-
cooling_items = [
|
| 709 |
-
f"<li><strong>Hottest Month:</strong> {location.design_conditions['cooling'].get('hottest_month', 'N/A')}</li>",
|
| 710 |
-
f"<li><strong>Dry-Bulb Temp (0.4%):</strong> {location.design_conditions['cooling'].get('dry_bulb_0_4', 'N/A')} °C</li>",
|
| 711 |
-
f"<li><strong>Wet-Bulb Temp (0.4%):</strong> {location.design_conditions['cooling'].get('wet_bulb_0_4', 'N/A')} °C</li>",
|
| 712 |
-
f"<li><strong>Mean Coincident Wet-Bulb (0.4%):</strong> {location.design_conditions['cooling'].get('mean_coincident_wb_0_4', 'N/A')} °C</li>"
|
| 713 |
-
]
|
| 714 |
-
extremes_items = [
|
| 715 |
-
f"<li><strong>Annual Max Dry-Bulb:</strong> {location.design_conditions['extremes'].get('annual_max_db', 'N/A')} °C</li>",
|
| 716 |
-
f"<li><strong>Annual Min Dry-Bulb:</strong> {location.design_conditions['extremes'].get('min_db', 'N/A')} °C</li>"
|
| 717 |
-
]
|
| 718 |
-
st.markdown(f"""
|
| 719 |
-
<div class="markdown-text">
|
| 720 |
-
<h3>Design Conditions (EPW Header)</h3>
|
| 721 |
-
<ul>
|
| 722 |
-
<li><strong>Heating:</strong>
|
| 723 |
-
<ul>
|
| 724 |
-
{''.join(heating_items)}
|
| 725 |
-
</ul>
|
| 726 |
-
</li>
|
| 727 |
-
<li><strong>Cooling:</strong>
|
| 728 |
-
<ul>
|
| 729 |
-
{''.join(cooling_items)}
|
| 730 |
-
</ul>
|
| 731 |
-
</li>
|
| 732 |
-
<li><strong>Extremes:</strong>
|
| 733 |
-
<ul>
|
| 734 |
-
{''.join(extremes_items)}
|
| 735 |
-
</ul>
|
| 736 |
-
</li>
|
| 737 |
-
</ul>
|
| 738 |
-
</div>
|
| 739 |
-
""", unsafe_allow_html=True)
|
| 740 |
-
|
| 741 |
# Typical/Extreme Periods
|
| 742 |
if location.typical_extreme_periods:
|
| 743 |
period_items = [
|
|
@@ -1080,7 +932,6 @@ class ClimateData:
|
|
| 1080 |
epw_file=epw_data,
|
| 1081 |
typical_extreme_periods=loc_dict["typical_extreme_periods"],
|
| 1082 |
ground_temperatures=loc_dict["ground_temperatures"],
|
| 1083 |
-
design_conditions=loc_dict["design_conditions"],
|
| 1084 |
id=loc_dict["id"],
|
| 1085 |
country=loc_dict["country"],
|
| 1086 |
state_province=loc_dict["state_province"],
|
|
|
|
| 4 |
|
| 5 |
Author: Dr Majed Abuseif
|
| 6 |
Date: May 2025
|
| 7 |
+
Version: 2.1.5
|
| 8 |
"""
|
| 9 |
|
| 10 |
from typing import Dict, List, Any, Optional
|
|
|
|
| 19 |
import pvlib
|
| 20 |
from datetime import datetime, timedelta
|
| 21 |
import re
|
| 22 |
+
import logging
|
| 23 |
+
|
| 24 |
+
# Set up logging
|
| 25 |
+
logging.basicConfig(level=logging.INFO)
|
| 26 |
+
logger = logging.getLogger(__name__)
|
| 27 |
|
| 28 |
# Define paths
|
| 29 |
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
| 57 |
</style>
|
| 58 |
"""
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
@dataclass
|
| 61 |
class ClimateLocation:
|
| 62 |
"""Class representing a climate location with ASHRAE 169 data derived from EPW files."""
|
|
|
|
| 80 |
hourly_data: List[Dict] # Hourly data for integration with main.py
|
| 81 |
typical_extreme_periods: Dict[str, Dict] # Typical/extreme periods (summer/winter)
|
| 82 |
ground_temperatures: Dict[str, List[float]] # Monthly ground temperatures by depth
|
|
|
|
| 83 |
|
| 84 |
+
def __init__(self, epw_file: pd.DataFrame, typical_extreme_periods: Dict, ground_temperatures: Dict, **kwargs):
|
| 85 |
"""Initialize ClimateLocation with EPW file data and header information."""
|
| 86 |
self.id = kwargs.get("id")
|
| 87 |
self.country = kwargs.get("country")
|
|
|
|
| 92 |
self.elevation = kwargs.get("elevation")
|
| 93 |
self.typical_extreme_periods = typical_extreme_periods
|
| 94 |
self.ground_temperatures = ground_temperatures
|
|
|
|
| 95 |
|
| 96 |
# Extract columns from EPW data
|
| 97 |
months = pd.to_numeric(epw_file[1], errors='coerce').values
|
|
|
|
| 102 |
pressure = pd.to_numeric(epw_file[9], errors='coerce').values
|
| 103 |
global_radiation = pd.to_numeric(epw_file[13], errors='coerce').values
|
| 104 |
wind_direction = pd.to_numeric(epw_file[20], errors='coerce').values
|
| 105 |
+
wind_speed = pd.to_numeric(epw_file[21], errors='coerce')
|
| 106 |
+
|
| 107 |
+
# Filter wind speed outliers and log high values
|
| 108 |
+
wind_speed = wind_speed[wind_speed <= 50] # Remove extreme outliers
|
| 109 |
+
if (wind_speed > 15).any():
|
| 110 |
+
logger.warning(f"High wind speeds detected: {wind_speed[wind_speed > 15].tolist()}")
|
| 111 |
|
| 112 |
# Calculate wet-bulb temperature
|
| 113 |
wet_bulb = ClimateData.calculate_wet_bulb(dry_bulb, humidity)
|
|
|
|
| 131 |
self.wind_speed = round(np.nanmean(wind_speed), 1)
|
| 132 |
self.pressure = round(np.nanmean(pressure), 1)
|
| 133 |
|
| 134 |
+
# Log wind speed diagnostics
|
| 135 |
+
logger.info(f"Wind speed stats: min={wind_speed.min():.1f}, max={wind_speed.max():.1f}, mean={self.wind_speed:.1f}")
|
| 136 |
+
|
| 137 |
# Assign climate zone
|
| 138 |
self.climate_zone = ClimateData.assign_climate_zone(self.heating_degree_days, self.cooling_degree_days, np.nanmean(humidity))
|
| 139 |
|
|
|
|
| 179 |
"pressure": self.pressure,
|
| 180 |
"hourly_data": self.hourly_data,
|
| 181 |
"typical_extreme_periods": self.typical_extreme_periods,
|
| 182 |
+
"ground_temperatures": self.ground_temperatures
|
|
|
|
| 183 |
}
|
| 184 |
|
| 185 |
class ClimateData:
|
|
|
|
| 255 |
if data["summer_daily_range"] < 0:
|
| 256 |
st.error("Validation failed: Negative summer daily range")
|
| 257 |
return False
|
| 258 |
+
if not (0 <= data["wind_speed"] <= 30): # Updated range to 0–30 m/s
|
| 259 |
st.error(f"Validation failed: Wind speed {data['wind_speed']} outside range")
|
| 260 |
return False
|
| 261 |
if not (80000 <= data["pressure"] <= 110000):
|
|
|
|
| 287 |
if not (0 <= record["global_horizontal_radiation"] <= 1200):
|
| 288 |
st.error(f"Validation failed: Global radiation {record['global_horizontal_radiation']} outside range")
|
| 289 |
return False
|
| 290 |
+
if not (0 <= record["wind_speed"] <= 30): # Updated range to 0–30 m/s
|
| 291 |
st.error(f"Validation failed: Wind speed {record['wind_speed']} outside range")
|
| 292 |
return False
|
| 293 |
if not (0 <= record["wind_direction"] <= 360):
|
|
|
|
| 313 |
st.error(f"Validation failed: Invalid ground temperatures for depth {depth}")
|
| 314 |
return False
|
| 315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
return True
|
| 317 |
|
| 318 |
@staticmethod
|
|
|
|
| 370 |
# Parse header
|
| 371 |
header = next(line for line in epw_lines if line.startswith("LOCATION"))
|
| 372 |
header_parts = header.split(",")
|
| 373 |
+
city = header_parts[1].strip() or "Unknown"
|
| 374 |
+
# Clean city name by removing suffixes like '.Racecourse'
|
| 375 |
+
city = re.sub(r'\..*', '', city)
|
| 376 |
+
state_province = header_parts[2].strip() or "Unknown"
|
| 377 |
+
country = header_parts[3].strip() or "Unknown"
|
|
|
|
|
|
|
|
|
|
| 378 |
|
| 379 |
latitude = float(header_parts[6])
|
| 380 |
longitude = float(header_parts[7])
|
| 381 |
elevation = float(header_parts[8])
|
| 382 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
# Parse TYPICAL/EXTREME PERIODS
|
| 384 |
typical_extreme_periods = {}
|
| 385 |
date_pattern = r'^\d{1,2}\s*/\s*\d{1,2}$'
|
|
|
|
| 471 |
epw_file=epw_data,
|
| 472 |
typical_extreme_periods=typical_extreme_periods,
|
| 473 |
ground_temperatures=ground_temperatures,
|
|
|
|
| 474 |
id=f"{country[:1].upper()}{city[:3].upper()}",
|
| 475 |
country=country,
|
| 476 |
state_province=state_province,
|
|
|
|
| 512 |
epw_file=epw_data,
|
| 513 |
typical_extreme_periods=climate_data_dict["typical_extreme_periods"],
|
| 514 |
ground_temperatures=climate_data_dict["ground_temperatures"],
|
|
|
|
| 515 |
id=climate_data_dict["id"],
|
| 516 |
country=climate_data_dict["country"],
|
| 517 |
state_province=climate_data_dict["state_province"],
|
|
|
|
| 590 |
</div>
|
| 591 |
""", unsafe_allow_html=True)
|
| 592 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
# Typical/Extreme Periods
|
| 594 |
if location.typical_extreme_periods:
|
| 595 |
period_items = [
|
|
|
|
| 932 |
epw_file=epw_data,
|
| 933 |
typical_extreme_periods=loc_dict["typical_extreme_periods"],
|
| 934 |
ground_temperatures=loc_dict["ground_temperatures"],
|
|
|
|
| 935 |
id=loc_dict["id"],
|
| 936 |
country=loc_dict["country"],
|
| 937 |
state_province=loc_dict["state_province"],
|