Spaces:
Sleeping
Sleeping
Update data/climate_data.py
Browse files- data/climate_data.py +170 -63
data/climate_data.py
CHANGED
|
@@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
|
|
| 30 |
# Define paths at module level
|
| 31 |
AU_CCH_DIR = "au_cch" # Relative path to au_cch folder from climate_data.py in data/ (e.g., au_cch/1/RCP2.6/2070/)
|
| 32 |
|
| 33 |
-
# CSS for consistent formatting
|
| 34 |
STYLE = """
|
| 35 |
<style>
|
| 36 |
.markdown-text {
|
|
@@ -59,7 +59,7 @@ STYLE = """
|
|
| 59 |
</style>
|
| 60 |
"""
|
| 61 |
|
| 62 |
-
# Location mapping from provided list
|
| 63 |
LOCATION_MAPPING = {
|
| 64 |
"24": {"city": "Canberra", "state": "ACT"},
|
| 65 |
"11": {"city": "Coffs Harbour", "state": "NSW"},
|
|
@@ -148,12 +148,12 @@ class ClimateLocation:
|
|
| 148 |
logger.warning(f"High wind speeds detected: {wind_speed[wind_speed > 15].tolist()}")
|
| 149 |
|
| 150 |
# Calculate wet-bulb temperature
|
| 151 |
-
|
| 152 |
|
| 153 |
# Calculate design conditions
|
| 154 |
self.winter_design_temp = round(np.nanpercentile(dry_bulb, 0.4), 1)
|
| 155 |
self.summer_design_temp_db = round(np.nanpercentile(dry_bulb, 99.6), 1)
|
| 156 |
-
self.summer_design_temp_wb = round(np.nanpercentile(
|
| 157 |
|
| 158 |
# Calculate degree days
|
| 159 |
daily_temps = np.nanmean(dry_bulb.reshape(-1, 24), axis=1)
|
|
@@ -420,29 +420,15 @@ class ClimateData:
|
|
| 420 |
|
| 421 |
def load_climate_data(self, country: str, city: str) -> Optional[Dict[str, Any]]:
|
| 422 |
"""Load climate data for a given country and city."""
|
| 423 |
-
# This implementation assumes EPW files are stored in a specific directory structure
|
| 424 |
-
# e.g., data/au_cch/1/RCP2.6/2070/
|
| 425 |
-
|
| 426 |
-
# This is a simplified example, you would need a more robust way to find the EPW file
|
| 427 |
-
# based on country and city, potentially from a manifest or by scanning directories.
|
| 428 |
-
|
| 429 |
-
# For demonstration, let's assume a direct path if a dummy EPW is available
|
| 430 |
-
# In a real scenario, you'd map country/city to a specific EPW file path.
|
| 431 |
-
|
| 432 |
epw_file_path = None
|
| 433 |
-
# Example: If you have a known EPW file for Geelong or other mapped locations
|
| 434 |
found_id = None
|
| 435 |
for loc_id, loc_info in LOCATION_MAPPING.items():
|
| 436 |
-
if loc_info["city"] == city and (loc_info["state"] == "VIC" if city == "Melbourne RO" else True):
|
| 437 |
-
|
| 438 |
-
# You'll need to map LOCATION_MAPPING IDs to actual EPW file names.
|
| 439 |
-
# For now, using a generic example.
|
| 440 |
-
epw_file_name = f"AUS_VIC_Melbourne.AP.720520_TMY3.epw" # Default example
|
| 441 |
if city == "Geelong":
|
| 442 |
-
epw_file_name = "AUS_VIC_Melbourne.AP.720520_TMY3.epw"
|
| 443 |
elif city == "Sydney RO (Observatory Hill)":
|
| 444 |
-
epw_file_name = "AUS_NSW_Sydney.AP.947670_TMY3.epw"
|
| 445 |
-
# Add more specific mappings as needed
|
| 446 |
|
| 447 |
epw_file_path = os_join("data", AU_CCH_DIR, epw_file_name)
|
| 448 |
found_id = loc_id
|
|
@@ -457,37 +443,40 @@ class ClimateData:
|
|
| 457 |
if epw_data_df.empty:
|
| 458 |
return None
|
| 459 |
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
#
|
| 463 |
-
# In a real scenario, you'd parse the EPW header more thoroughly
|
| 464 |
-
location_id = found_id if found_id else f"{country}_{city}".replace(" ", "_").lower()
|
| 465 |
|
| 466 |
# Retrieve typical_extreme_periods and ground_temperatures
|
| 467 |
typical_extreme_periods = self.get_typical_extreme_periods(epw_file_path)
|
| 468 |
ground_temperatures = self.get_ground_temperatures(epw_file_path)
|
| 469 |
|
|
|
|
|
|
|
| 470 |
location = ClimateLocation(
|
| 471 |
-
epw_file=
|
| 472 |
typical_extreme_periods=typical_extreme_periods,
|
| 473 |
ground_temperatures=ground_temperatures,
|
| 474 |
id=location_id,
|
| 475 |
country=country,
|
| 476 |
-
state_province=LOCATION_MAPPING.get(found_id, {}).get("state", "N/A"),
|
| 477 |
city=city,
|
| 478 |
-
latitude=epw_data_df.loc[0, 'latitude'],
|
| 479 |
longitude=epw_data_df.loc[0, 'longitude'],
|
| 480 |
elevation=epw_data_df.loc[0, 'elevation'],
|
| 481 |
time_zone=epw_data_df.loc[0, 'time_zone'],
|
| 482 |
-
climate_zone="Unknown", #
|
| 483 |
-
hourly_data
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
)
|
| 485 |
self.add_location(location)
|
| 486 |
return location.to_dict()
|
| 487 |
|
| 488 |
def get_climate_zone_description(self, climate_zone_code: str) -> str:
|
| 489 |
"""Return a description for a given ASHRAE climate zone code."""
|
| 490 |
-
# This is a placeholder; you'd have a mapping for climate zone codes to descriptions
|
| 491 |
zone_descriptions = {
|
| 492 |
"1A": "Hot-Humid", "1B": "Hot-Dry",
|
| 493 |
"2A": "Warm-Humid", "2B": "Warm-Dry",
|
|
@@ -506,18 +495,14 @@ class ClimateData:
|
|
| 506 |
with open(epw_file_path, 'r') as f:
|
| 507 |
for line in f:
|
| 508 |
if line.startswith('COMMENT1'):
|
| 509 |
-
# Example: COMMENT1 TYPICAL EXTREME PERIODS: Summer Design Day, Winter Design Day
|
| 510 |
match = re.search(r'TYPICAL EXTREME PERIODS: (.+)', line)
|
| 511 |
if match:
|
| 512 |
periods_str = match.group(1)
|
| 513 |
-
# This parsing is highly dependent on the exact format in EPW comments
|
| 514 |
-
# A more robust parser might be needed for varied EPW files.
|
| 515 |
-
# For now, a dummy structure, as in your original file's behavior:
|
| 516 |
typical_extreme_periods = {
|
| 517 |
"summer_design_day": {"start_date": "07/21", "end_date": "07/21"},
|
| 518 |
"winter_design_day": {"start_date": "01/21", "end_date": "01/21"}
|
| 519 |
}
|
| 520 |
-
break
|
| 521 |
except Exception as e:
|
| 522 |
logger.warning(f"Could not extract typical/extreme periods from EPW comments: {e}")
|
| 523 |
return typical_extreme_periods
|
|
@@ -525,7 +510,7 @@ class ClimateData:
|
|
| 525 |
def get_ground_temperatures(self, epw_file_path: str) -> Dict[str, List[float]]:
|
| 526 |
"""Extract ground temperatures from EPW file comments."""
|
| 527 |
ground_temperatures = {
|
| 528 |
-
"depth_0.5m": [15.0] * 12,
|
| 529 |
"depth_1.0m": [14.0] * 12,
|
| 530 |
"depth_2.0m": [13.0] * 12,
|
| 531 |
"depth_4.0m": [12.0] * 12
|
|
@@ -534,11 +519,6 @@ class ClimateData:
|
|
| 534 |
with open(epw_file_path, 'r') as f:
|
| 535 |
for line in f:
|
| 536 |
if line.startswith('COMMENT2'):
|
| 537 |
-
# Example: COMMENT2 GROUND TEMPERATURES (C) 0.5m: 15.0,15.1,...
|
| 538 |
-
# This parsing is highly dependent on the exact format in EPW comments
|
| 539 |
-
# and would need to be robust.
|
| 540 |
-
# For now, we'll stick with the dummy values as parsing this from raw EPW
|
| 541 |
-
# comments can be fragile without specific format knowledge.
|
| 542 |
pass
|
| 543 |
except Exception as e:
|
| 544 |
logger.warning(f"Could not extract ground temperatures from EPW comments: {e}")
|
|
@@ -546,10 +526,7 @@ class ClimateData:
|
|
| 546 |
|
| 547 |
def save_climate_data(self, climate_data_dict: Dict[str, Any]):
|
| 548 |
"""Save climate data to a JSON file."""
|
| 549 |
-
# This is a placeholder for saving logic.
|
| 550 |
-
# In a real app, you might save to a user-specific location or database.
|
| 551 |
logger.info(f"Saving climate data (placeholder): {climate_data_dict['id']}")
|
| 552 |
-
# Example: save to a 'saved_data' directory
|
| 553 |
save_dir = "saved_data"
|
| 554 |
os.makedirs(save_dir, exist_ok=True)
|
| 555 |
file_path = os_join(save_dir, f"{climate_data_dict['id']}.json")
|
|
@@ -563,8 +540,6 @@ class ClimateData:
|
|
| 563 |
|
| 564 |
def load_saved_climate_data(self) -> 'ClimateData':
|
| 565 |
"""Load saved climate data from JSON files."""
|
| 566 |
-
# This is a placeholder for loading logic.
|
| 567 |
-
# It would typically scan a directory or query a database.
|
| 568 |
load_dir = "saved_data"
|
| 569 |
if os.path.exists(load_dir):
|
| 570 |
for filename in os.listdir(load_dir):
|
|
@@ -573,16 +548,11 @@ class ClimateData:
|
|
| 573 |
try:
|
| 574 |
with open(file_path, 'r') as f:
|
| 575 |
loc_dict = json.load(f)
|
| 576 |
-
# Reconstruct ClimateLocation object
|
| 577 |
-
# Ensure all expected keys are present, handle missing gracefully
|
| 578 |
hourly_data = loc_dict.get("hourly_data", [])
|
| 579 |
|
| 580 |
-
#
|
| 581 |
-
#
|
| 582 |
-
#
|
| 583 |
-
# if epw_file is not explicitly loaded, and rely on hourly_data.
|
| 584 |
-
|
| 585 |
-
# Create a dummy DataFrame if not available, as ClimateLocation expects it
|
| 586 |
dummy_epw_df = pd.DataFrame({
|
| 587 |
1: [d['month'] for d in hourly_data],
|
| 588 |
2: [d['day'] for d in hourly_data],
|
|
@@ -595,11 +565,21 @@ class ClimateData:
|
|
| 595 |
15: [d['diffuse_horizontal_radiation'] for d in hourly_data],
|
| 596 |
20: [d['wind_direction'] for d in hourly_data],
|
| 597 |
21: [d['wind_speed'] for d in hourly_data],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 598 |
})
|
| 599 |
-
# Add dummy metadata columns if they don't exist, as ClimateLocation expects them
|
| 600 |
-
for col in ['latitude', 'longitude', 'elevation', 'time_zone']:
|
| 601 |
-
if col not in dummy_epw_df.columns:
|
| 602 |
-
dummy_epw_df[col] = loc_dict.get(col, 0.0) # Use saved value or default
|
| 603 |
|
| 604 |
location = ClimateLocation(
|
| 605 |
epw_file=dummy_epw_df, # Pass dummy DataFrame for __init__
|
|
@@ -988,8 +968,135 @@ class ClimateData:
|
|
| 988 |
|
| 989 |
return fig
|
| 990 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 991 |
# --- Original if __name__ == "__main__": block from your file ---
|
| 992 |
if __name__ == "__main__":
|
| 993 |
climate_data = ClimateData()
|
| 994 |
-
session_state = {"building_info": {"country": "Australia", "city": "Geelong"}, "page": "Climate Data"}
|
| 995 |
-
climate_data.display_climate_input(session_state)
|
|
|
|
| 30 |
# Define paths at module level
|
| 31 |
AU_CCH_DIR = "au_cch" # Relative path to au_cch folder from climate_data.py in data/ (e.g., au_cch/1/RCP2.6/2070/)
|
| 32 |
|
| 33 |
+
# CSS for consistent formatting
|
| 34 |
STYLE = """
|
| 35 |
<style>
|
| 36 |
.markdown-text {
|
|
|
|
| 59 |
</style>
|
| 60 |
"""
|
| 61 |
|
| 62 |
+
# Location mapping from provided list
|
| 63 |
LOCATION_MAPPING = {
|
| 64 |
"24": {"city": "Canberra", "state": "ACT"},
|
| 65 |
"11": {"city": "Coffs Harbour", "state": "NSW"},
|
|
|
|
| 148 |
logger.warning(f"High wind speeds detected: {wind_speed[wind_speed > 15].tolist()}")
|
| 149 |
|
| 150 |
# Calculate wet-bulb temperature
|
| 151 |
+
wet_bulb = ClimateData.calculate_wet_bulb(dry_bulb, humidity)
|
| 152 |
|
| 153 |
# Calculate design conditions
|
| 154 |
self.winter_design_temp = round(np.nanpercentile(dry_bulb, 0.4), 1)
|
| 155 |
self.summer_design_temp_db = round(np.nanpercentile(dry_bulb, 99.6), 1)
|
| 156 |
+
self.summer_design_temp_wb = round(np.nanpercentile(wet_bulb, 99.6), 1)
|
| 157 |
|
| 158 |
# Calculate degree days
|
| 159 |
daily_temps = np.nanmean(dry_bulb.reshape(-1, 24), axis=1)
|
|
|
|
| 420 |
|
| 421 |
def load_climate_data(self, country: str, city: str) -> Optional[Dict[str, Any]]:
|
| 422 |
"""Load climate data for a given country and city."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
epw_file_path = None
|
|
|
|
| 424 |
found_id = None
|
| 425 |
for loc_id, loc_info in LOCATION_MAPPING.items():
|
| 426 |
+
if loc_info["city"] == city and (loc_info["state"] == "VIC" if city == "Melbourne RO" else True):
|
| 427 |
+
epw_file_name = f"AUS_VIC_Melbourne.AP.720520_TMY3.epw"
|
|
|
|
|
|
|
|
|
|
| 428 |
if city == "Geelong":
|
| 429 |
+
epw_file_name = "AUS_VIC_Melbourne.AP.720520_TMY3.epw"
|
| 430 |
elif city == "Sydney RO (Observatory Hill)":
|
| 431 |
+
epw_file_name = "AUS_NSW_Sydney.AP.947670_TMY3.epw"
|
|
|
|
| 432 |
|
| 433 |
epw_file_path = os_join("data", AU_CCH_DIR, epw_file_name)
|
| 434 |
found_id = loc_id
|
|
|
|
| 443 |
if epw_data_df.empty:
|
| 444 |
return None
|
| 445 |
|
| 446 |
+
# Pass the raw epw_data_df to ClimateLocation for its __init__ to process
|
| 447 |
+
# This ensures ClimateLocation's internal calculations (like design temps, degree days)
|
| 448 |
+
# are based on the full EPW data as originally intended.
|
|
|
|
|
|
|
| 449 |
|
| 450 |
# Retrieve typical_extreme_periods and ground_temperatures
|
| 451 |
typical_extreme_periods = self.get_typical_extreme_periods(epw_file_path)
|
| 452 |
ground_temperatures = self.get_ground_temperatures(epw_file_path)
|
| 453 |
|
| 454 |
+
location_id = found_id if found_id else f"{country}_{city}".replace(" ", "_").lower()
|
| 455 |
+
|
| 456 |
location = ClimateLocation(
|
| 457 |
+
epw_file=epw_data_df, # Pass the DataFrame here
|
| 458 |
typical_extreme_periods=typical_extreme_periods,
|
| 459 |
ground_temperatures=ground_temperatures,
|
| 460 |
id=location_id,
|
| 461 |
country=country,
|
| 462 |
+
state_province=LOCATION_MAPPING.get(found_id, {}).get("state", "N/A"),
|
| 463 |
city=city,
|
| 464 |
+
latitude=epw_data_df.loc[0, 'latitude'],
|
| 465 |
longitude=epw_data_df.loc[0, 'longitude'],
|
| 466 |
elevation=epw_data_df.loc[0, 'elevation'],
|
| 467 |
time_zone=epw_data_df.loc[0, 'time_zone'],
|
| 468 |
+
climate_zone="Unknown", # This should ideally be determined by a lookup based on location
|
| 469 |
+
# hourly_data is now handled internally by ClimateLocation's __init__
|
| 470 |
+
# based on the epw_file DataFrame passed above.
|
| 471 |
+
# We explicitly pass it as a keyword argument to satisfy the dataclass init if needed,
|
| 472 |
+
# but the __init__ will re-process it.
|
| 473 |
+
hourly_data=[] # Dummy, will be populated by ClimateLocation's __init__
|
| 474 |
)
|
| 475 |
self.add_location(location)
|
| 476 |
return location.to_dict()
|
| 477 |
|
| 478 |
def get_climate_zone_description(self, climate_zone_code: str) -> str:
|
| 479 |
"""Return a description for a given ASHRAE climate zone code."""
|
|
|
|
| 480 |
zone_descriptions = {
|
| 481 |
"1A": "Hot-Humid", "1B": "Hot-Dry",
|
| 482 |
"2A": "Warm-Humid", "2B": "Warm-Dry",
|
|
|
|
| 495 |
with open(epw_file_path, 'r') as f:
|
| 496 |
for line in f:
|
| 497 |
if line.startswith('COMMENT1'):
|
|
|
|
| 498 |
match = re.search(r'TYPICAL EXTREME PERIODS: (.+)', line)
|
| 499 |
if match:
|
| 500 |
periods_str = match.group(1)
|
|
|
|
|
|
|
|
|
|
| 501 |
typical_extreme_periods = {
|
| 502 |
"summer_design_day": {"start_date": "07/21", "end_date": "07/21"},
|
| 503 |
"winter_design_day": {"start_date": "01/21", "end_date": "01/21"}
|
| 504 |
}
|
| 505 |
+
break
|
| 506 |
except Exception as e:
|
| 507 |
logger.warning(f"Could not extract typical/extreme periods from EPW comments: {e}")
|
| 508 |
return typical_extreme_periods
|
|
|
|
| 510 |
def get_ground_temperatures(self, epw_file_path: str) -> Dict[str, List[float]]:
|
| 511 |
"""Extract ground temperatures from EPW file comments."""
|
| 512 |
ground_temperatures = {
|
| 513 |
+
"depth_0.5m": [15.0] * 12,
|
| 514 |
"depth_1.0m": [14.0] * 12,
|
| 515 |
"depth_2.0m": [13.0] * 12,
|
| 516 |
"depth_4.0m": [12.0] * 12
|
|
|
|
| 519 |
with open(epw_file_path, 'r') as f:
|
| 520 |
for line in f:
|
| 521 |
if line.startswith('COMMENT2'):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
pass
|
| 523 |
except Exception as e:
|
| 524 |
logger.warning(f"Could not extract ground temperatures from EPW comments: {e}")
|
|
|
|
| 526 |
|
| 527 |
def save_climate_data(self, climate_data_dict: Dict[str, Any]):
|
| 528 |
"""Save climate data to a JSON file."""
|
|
|
|
|
|
|
| 529 |
logger.info(f"Saving climate data (placeholder): {climate_data_dict['id']}")
|
|
|
|
| 530 |
save_dir = "saved_data"
|
| 531 |
os.makedirs(save_dir, exist_ok=True)
|
| 532 |
file_path = os_join(save_dir, f"{climate_data_dict['id']}.json")
|
|
|
|
| 540 |
|
| 541 |
def load_saved_climate_data(self) -> 'ClimateData':
|
| 542 |
"""Load saved climate data from JSON files."""
|
|
|
|
|
|
|
| 543 |
load_dir = "saved_data"
|
| 544 |
if os.path.exists(load_dir):
|
| 545 |
for filename in os.listdir(load_dir):
|
|
|
|
| 548 |
try:
|
| 549 |
with open(file_path, 'r') as f:
|
| 550 |
loc_dict = json.load(f)
|
|
|
|
|
|
|
| 551 |
hourly_data = loc_dict.get("hourly_data", [])
|
| 552 |
|
| 553 |
+
# Create a dummy DataFrame with required columns for ClimateLocation __init__
|
| 554 |
+
# This ensures the __init__ can run and re-process the hourly_data
|
| 555 |
+
# and calculate design conditions, etc., as it expects a DataFrame.
|
|
|
|
|
|
|
|
|
|
| 556 |
dummy_epw_df = pd.DataFrame({
|
| 557 |
1: [d['month'] for d in hourly_data],
|
| 558 |
2: [d['day'] for d in hourly_data],
|
|
|
|
| 565 |
15: [d['diffuse_horizontal_radiation'] for d in hourly_data],
|
| 566 |
20: [d['wind_direction'] for d in hourly_data],
|
| 567 |
21: [d['wind_speed'] for d in hourly_data],
|
| 568 |
+
# Add metadata columns if they are expected by ClimateLocation's __init__
|
| 569 |
+
'latitude': [loc_dict.get('latitude', 0.0)] * len(hourly_data),
|
| 570 |
+
'longitude': [loc_dict.get('longitude', 0.0)] * len(hourly_data),
|
| 571 |
+
'elevation': [loc_dict.get('elevation', 0.0)] * len(hourly_data),
|
| 572 |
+
'time_zone': [loc_dict.get('time_zone', 0.0)] * len(hourly_data),
|
| 573 |
+
# Add other pvlib expected columns if they are used in ClimateLocation's __init__
|
| 574 |
+
'temp_dry_bulb': [d['dry_bulb'] for d in hourly_data],
|
| 575 |
+
'relative_humidity': [d['relative_humidity'] for d in hourly_data],
|
| 576 |
+
'station_pressure': [d['atmospheric_pressure'] for d in hourly_data],
|
| 577 |
+
'ghi': [d['global_horizontal_radiation'] for d in hourly_data],
|
| 578 |
+
'dni': [d['direct_normal_radiation'] for d in hourly_data],
|
| 579 |
+
'dhi': [d['diffuse_horizontal_radiation'] for d in hourly_data],
|
| 580 |
+
'wind_direction': [d['wind_direction'] for d in hourly_data],
|
| 581 |
+
'wind_speed': [d['wind_speed'] for d in hourly_data],
|
| 582 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
|
| 584 |
location = ClimateLocation(
|
| 585 |
epw_file=dummy_epw_df, # Pass dummy DataFrame for __init__
|
|
|
|
| 968 |
|
| 969 |
return fig
|
| 970 |
|
| 971 |
+
def display_climate_input(self, session_state: Dict[str, Any]):
|
| 972 |
+
"""Display climate data input and selection in Streamlit."""
|
| 973 |
+
st.markdown(STYLE, unsafe_allow_html=True)
|
| 974 |
+
st.subheader("Climate Data Input")
|
| 975 |
+
|
| 976 |
+
# Country selection
|
| 977 |
+
country_options = sorted(self.countries)
|
| 978 |
+
selected_country = st.selectbox(
|
| 979 |
+
"Select Country",
|
| 980 |
+
country_options,
|
| 981 |
+
index=country_options.index(session_state["building_info"].get("country", "Australia"))
|
| 982 |
+
if session_state["building_info"].get("country", "Australia") in country_options
|
| 983 |
+
else 0,
|
| 984 |
+
key="climate_country_select"
|
| 985 |
+
)
|
| 986 |
+
session_state["building_info"]["country"] = selected_country
|
| 987 |
+
|
| 988 |
+
# State/Province selection
|
| 989 |
+
if selected_country and selected_country in self.country_states:
|
| 990 |
+
state_options = sorted(self.country_states[selected_country].keys())
|
| 991 |
+
selected_state = st.selectbox(
|
| 992 |
+
"Select State/Province",
|
| 993 |
+
state_options,
|
| 994 |
+
index=state_options.index(session_state["building_info"].get("state_province", "Victoria"))
|
| 995 |
+
if session_state["building_info"].get("state_province", "Victoria") in state_options
|
| 996 |
+
else 0,
|
| 997 |
+
key="climate_state_select"
|
| 998 |
+
)
|
| 999 |
+
session_state["building_info"]["state_province"] = selected_state
|
| 1000 |
+
else:
|
| 1001 |
+
selected_state = None
|
| 1002 |
+
st.warning("No states available for the selected country.")
|
| 1003 |
+
|
| 1004 |
+
# City selection
|
| 1005 |
+
if selected_state and selected_country in self.country_states and selected_state in self.country_states[selected_country]:
|
| 1006 |
+
city_options = sorted(self.country_states[selected_country][selected_state])
|
| 1007 |
+
selected_city = st.selectbox(
|
| 1008 |
+
"Select City",
|
| 1009 |
+
city_options,
|
| 1010 |
+
index=city_options.index(session_state["building_info"].get("city", "Geelong"))
|
| 1011 |
+
if session_state["building_info"].get("city", "Geelong") in city_options
|
| 1012 |
+
else 0,
|
| 1013 |
+
key="climate_city_select"
|
| 1014 |
+
)
|
| 1015 |
+
session_state["building_info"]["city"] = selected_city
|
| 1016 |
+
else:
|
| 1017 |
+
selected_city = None
|
| 1018 |
+
st.warning("No cities available for the selected state/province.")
|
| 1019 |
+
|
| 1020 |
+
# Load data button
|
| 1021 |
+
if st.button("Load Climate Data"):
|
| 1022 |
+
if selected_country and selected_city:
|
| 1023 |
+
with st.spinner(f"Loading climate data for {selected_city}, {selected_country}..."):
|
| 1024 |
+
loaded_data = self.load_climate_data(selected_country, selected_city)
|
| 1025 |
+
if loaded_data:
|
| 1026 |
+
if self.validate_climate_data(loaded_data):
|
| 1027 |
+
session_state["climate_data"] = loaded_data
|
| 1028 |
+
st.success(f"Climate data loaded successfully for {selected_city}, {selected_country}!")
|
| 1029 |
+
logger.info(f"Climate data loaded for {selected_city}, {selected_country}.")
|
| 1030 |
+
else:
|
| 1031 |
+
st.error("Loaded climate data failed validation. Please check the data source.")
|
| 1032 |
+
logger.error("Loaded climate data failed validation.")
|
| 1033 |
+
else:
|
| 1034 |
+
st.error(f"Failed to load climate data for {selected_city}, {selected_country}.")
|
| 1035 |
+
logger.error(f"Failed to load climate data for {selected_city}, {selected_country}.")
|
| 1036 |
+
else:
|
| 1037 |
+
st.warning("Please select a country and city to load climate data.")
|
| 1038 |
+
|
| 1039 |
+
# Display loaded data if available
|
| 1040 |
+
if "climate_data" in session_state and session_state["climate_data"]:
|
| 1041 |
+
st.subheader("Loaded Climate Data Summary")
|
| 1042 |
+
climate_info = session_state["climate_data"]
|
| 1043 |
+
|
| 1044 |
+
st.markdown(f"""
|
| 1045 |
+
<div class="markdown-text">
|
| 1046 |
+
<h3>Location Details:</h3>
|
| 1047 |
+
<ul>
|
| 1048 |
+
<li><strong>ID:</strong> {climate_info.get('id', 'N/A')}</li>
|
| 1049 |
+
<li><strong>Country:</strong> {climate_info.get('country', 'N/A')}</li>
|
| 1050 |
+
<li><strong>State/Province:</strong> {climate_info.get('state_province', 'N/A')}</li>
|
| 1051 |
+
<li><strong>City:</strong> {climate_info.get('city', 'N/A')}</li>
|
| 1052 |
+
<li><strong>Latitude:</strong> {climate_info.get('latitude', 'N/A'):.2f}°</li>
|
| 1053 |
+
<li><strong>Longitude:</strong> {climate_info.get('longitude', 'N/A'):.2f}°</li>
|
| 1054 |
+
<li><strong>Elevation:</strong> {climate_info.get('elevation', 'N/A'):.1f} m</li>
|
| 1055 |
+
<li><strong>Time Zone:</strong> UTC{climate_info.get('time_zone', 'N/A'):.1f}</li>
|
| 1056 |
+
<li><strong>Climate Zone:</strong> {climate_info.get('climate_zone', 'N/A')} ({self.get_climate_zone_description(climate_info.get('climate_zone', ''))})</li>
|
| 1057 |
+
</ul>
|
| 1058 |
+
|
| 1059 |
+
<h3>Design Conditions:</h3>
|
| 1060 |
+
<ul>
|
| 1061 |
+
<li><strong>Heating Degree Days (Base 18°C):</strong> {climate_info.get('heating_degree_days', 'N/A')} HDD</li>
|
| 1062 |
+
<li><strong>Cooling Degree Days (Base 18°C):</strong> {climate_info.get('cooling_degree_days', 'N/A')} CDD</li>
|
| 1063 |
+
<li><strong>Winter Design Temp (99.6%):</strong> {climate_info.get('winter_design_temp', 'N/A'):.1f}°C</li>
|
| 1064 |
+
<li><strong>Summer Design Dry-Bulb (0.4%):</strong> {climate_info.get('summer_design_temp_db', 'N/A'):.1f}°C</li>
|
| 1065 |
+
<li><strong>Summer Design Wet-Bulb (0.4%):</strong> {climate_info.get('summer_design_temp_wb', 'N/A'):.1f}°C</li>
|
| 1066 |
+
<li><strong>Summer Daily Range:</strong> {climate_info.get('summer_daily_range', 'N/A'):.1f}°C</li>
|
| 1067 |
+
<li><strong>Mean Wind Speed:</strong> {climate_info.get('wind_speed', 'N/A'):.1f} m/s</li>
|
| 1068 |
+
<li><strong>Mean Atmospheric Pressure:</strong> {climate_info.get('pressure', 'N/A'):.1f} Pa</li>
|
| 1069 |
+
</ul>
|
| 1070 |
+
</div>
|
| 1071 |
+
""", unsafe_allow_html=True)
|
| 1072 |
+
|
| 1073 |
+
# Display Typical/Extreme Periods
|
| 1074 |
+
st.subheader("Typical and Extreme Periods")
|
| 1075 |
+
if climate_info.get("typical_extreme_periods"):
|
| 1076 |
+
for period_name, details in climate_info["typical_extreme_periods"].items():
|
| 1077 |
+
st.write(f"**{period_name.replace('_', ' ').title()}:** Start Date: {details.get('start_date', 'N/A')}, End Date: {details.get('end_date', 'N/A')}")
|
| 1078 |
+
else:
|
| 1079 |
+
st.info("No typical and extreme periods data available for this location.")
|
| 1080 |
+
|
| 1081 |
+
# Display Ground Temperatures
|
| 1082 |
+
st.subheader("Ground Temperatures (Monthly Average)")
|
| 1083 |
+
if climate_info.get("ground_temperatures"):
|
| 1084 |
+
temp_df = pd.DataFrame(climate_info["ground_temperatures"])
|
| 1085 |
+
temp_df.index = [f"Month {i+1}" for i in range(12)]
|
| 1086 |
+
st.dataframe(temp_df)
|
| 1087 |
+
else:
|
| 1088 |
+
st.info("No ground temperature data available for this location.")
|
| 1089 |
+
|
| 1090 |
+
# Option to save data
|
| 1091 |
+
if st.button("Save Climate Data"):
|
| 1092 |
+
self.save_climate_data(climate_info)
|
| 1093 |
+
|
| 1094 |
+
st.subheader("Psychrometric Chart")
|
| 1095 |
+
# Generate and display the psychrometric chart
|
| 1096 |
+
psych_fig = self.plot_psychrometric_chart(climate_info["id"], session_state)
|
| 1097 |
+
st.plotly_chart(psych_fig, use_container_width=True)
|
| 1098 |
+
|
| 1099 |
# --- Original if __name__ == "__main__": block from your file ---
|
| 1100 |
if __name__ == "__main__":
|
| 1101 |
climate_data = ClimateData()
|
| 1102 |
+
session_state = {"building_info": {"country": "Australia", "city": "Geelong"}, "page": "Climate Data"}
|
|
|