Building-science/HVAC-03
1
1"""2ASHRAE 169 climate data module for HVAC Load Calculator.3This module provides access to climate data for various locations based on ASHRAE 169 standard.4 5Author: Dr Majed Abuseif6Date: March 20257Version: 1.0.08"""9 10from typing import Dict, List, Any, Optional11import pandas as pd12import numpy as np13import os14import json15from dataclasses import dataclass16import streamlit as st17import plotly.graph_objects as go18from io import StringIO19 20# Define paths21DATA_DIR = os.path.dirname(os.path.abspath(__file__))22 23@dataclass24class ClimateLocation:25 """Class representing a climate location with ASHRAE 169 data."""26 27 id: str28 country: str29 state_province: str30 city: str31 latitude: float32 longitude: float33 elevation: float # meters34 climate_zone: str35 heating_degree_days: float # base 18°C36 cooling_degree_days: float # base 18°C37 winter_design_temp: float # 99.6% heating design temperature (°C)38 summer_design_temp_db: float # 0.4% cooling design dry-bulb temperature (°C)39 summer_design_temp_wb: float # 0.4% cooling design wet-bulb temperature (°C)40 summer_daily_range: float # Mean daily temperature range in summer (°C)41 monthly_temps: Dict[str, float] # Average monthly temperatures (°C)42 monthly_humidity: Dict[str, float] # Average monthly relative humidity (%)43 wind_speed: float # Mean wind speed (m/s)44 pressure: float # Atmospheric pressure (Pa)45 46 def __init__(self, epw_file=None, manual_data=None, **kwargs):47 """Initialize ClimateLocation with EPW file or manual data."""48 if epw_file is not None and isinstance(epw_file, pd.DataFrame):49 # Extract from EPW (epw_data[6] for dry-bulb temperature)50 temps = np.array(epw_file[6], dtype=float)51 self.winter_design_temp = np.percentile(temps[~np.isnan(temps)], 0.4) # 99.6% percentile52 self.wind_speed = round(np.nanmean(epw_file[21]), 1) # Wind speed (m/s, index 21)53 self.pressure = round(np.nanmean(epw_file[9]), 1) # Atmospheric pressure (Pa, index 9)54 # Populate other fields from EPW processing55 self.id = kwargs.get("id")56 self.country = kwargs.get("country")57 self.state_province = kwargs.get("state_province")58 self.city = kwargs.get("city")59 self.latitude = kwargs.get("latitude")60 self.longitude = kwargs.get("longitude")61 self.elevation = kwargs.get("elevation")62 self.climate_zone = kwargs.get("climate_zone")63 self.heating_degree_days = kwargs.get("heating_degree_days")64 self.cooling_degree_days = kwargs.get("cooling_degree_days")65 self.summer_design_temp_db = kwargs.get("summer_design_temp_db")66 self.summer_design_temp_wb = kwargs.get("summer_design_temp_wb")67 self.summer_daily_range = kwargs.get("summer_daily_range")68 self.monthly_temps = kwargs.get("monthly_temps")69 self.monthly_humidity = kwargs.get("monthly_humidity")70 elif manual_data:71 self.winter_design_temp = manual_data.get("winter_temp", -10.0)72 self.wind_speed = manual_data.get("wind_speed", 5.0)73 self.pressure = manual_data.get("pressure", 101325.0) # Use provided pressure74 # Populate other fields from manual data75 for key, value in kwargs.items():76 setattr(self, key, value)77 else:78 # Default initialization with kwargs79 for key, value in kwargs.items():80 setattr(self, key, value)81 self.winter_design_temp = kwargs.get("winter_design_temp", -10.0)82 self.wind_speed = kwargs.get("wind_speed", 5.0)83 self.pressure = self.adjust_pressure_for_altitude(kwargs.get("elevation", 0.0))84 85 def adjust_pressure_for_altitude(self, elevation: float) -> float:86 """Calculate atmospheric pressure based on elevation."""87 if elevation is None:88 return 101325.0 # Default sea-level pressure if elevation is None89 return 101325 * (1 - 2.25577e-5 * elevation)**5.2558890 91 def to_dict(self) -> Dict[str, Any]:92 """Convert the climate location to a dictionary."""93 return {94 "id": self.id,95 "country": self.country,96 "state_province": self.state_province,97 "city": self.city,98 "latitude": self.latitude,99 "longitude": self.longitude,100 "elevation": self.elevation,101 "climate_zone": self.climate_zone,102 "heating_degree_days": self.heating_degree_days,103 "cooling_degree_days": self.cooling_degree_days,104 "winter_design_temp": self.winter_design_temp,105 "summer_design_temp_db": self.summer_design_temp_db,106 "summer_design_temp_wb": self.summer_design_temp_wb,107 "summer_daily_range": self.summer_daily_range,108 "monthly_temps": self.monthly_temps,109 "monthly_humidity": self.monthly_humidity,110 "wind_speed": self.wind_speed,111 "pressure": self.pressure112 }113 114class ClimateData:115 """Class for managing ASHRAE 169 climate data."""116 117 def __init__(self):118 """Initialize climate data."""119 self.locations = {}120 self.countries = []121 self.country_states = {}122 123 def _group_locations_by_country_state(self) -> Dict[str, Dict[str, List[str]]]:124 """Group locations by country and state/province."""125 result = {}126 for loc in self.locations.values():127 if loc.country not in result:128 result[loc.country] = {}129 if loc.state_province not in result[loc.country]:130 result[loc.country][loc.state_province] = []131 result[loc.country][loc.state_province].append(loc.city)132 for country in result:133 for state in result[country]:134 result[country][state] = sorted(result[country][state])135 return result136 137 def add_location(self, location: ClimateLocation):138 """Add a new location to the dictionary."""139 self.locations[location.id] = location140 self.countries = sorted(list(set(loc.country for loc in self.locations.values())))141 self.country_states = self._group_locations_by_country_state()142 143 def get_location_by_id(self, location_id: str, session_state: Dict[str, Any]) -> Optional[Dict[str, Any]]:144 """Retrieve climate data by ID from session state or locations."""145 if "climate_data" in session_state and session_state["climate_data"].get("id") == location_id:146 return session_state["climate_data"]147 if location_id in self.locations:148 return self.locations[location_id].to_dict()149 return None150 151 @staticmethod152 def validate_climate_data(data: Dict[str, Any]) -> bool:153 """Validate climate data for required fields and ranges."""154 required_fields = [155 "id", "country", "city", "latitude", "longitude", "elevation",156 "climate_zone", "heating_degree_days", "cooling_degree_days",157 "winter_design_temp", "summer_design_temp_db", "summer_design_temp_wb",158 "summer_daily_range", "monthly_temps", "monthly_humidity",159 "wind_speed", "pressure"160 ]161 month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]162 163 for field in required_fields:164 if field not in data:165 return False166 167 if not (-90 <= data["latitude"] <= 90 and -180 <= data["longitude"] <= 180):168 return False169 if data["elevation"] < 0:170 return False171 if data["climate_zone"] not in ["0A", "0B", "1A", "1B", "2A", "2B", "3A", "3B", "3C", "4A", "4B", "4C", "5A", "5B", "5C", "6A", "6B", "7", "8"]:172 return False173 if not (data["heating_degree_days"] >= 0 and data["cooling_degree_days"] >= 0):174 return False175 if not (-50 <= data["winter_design_temp"] <= 20):176 return False177 if not (0 <= data["summer_design_temp_db"] <= 50 and 0 <= data["summer_design_temp_wb"] <= 40):178 return False179 if data["summer_daily_range"] < 0:180 return False181 if not (0 <= data["wind_speed"] <= 20):182 return False183 if not (50000 <= data["pressure"] <= 120000):184 return False185 186 for month in month_names:187 if month not in data["monthly_temps"] or month not in data["monthly_humidity"]:188 return False189 if not (-50 <= data["monthly_temps"][month] <= 50):190 return False191 if not (0 <= data["monthly_humidity"][month] <= 100):192 return False193 194 return True195 196 @staticmethod197 def calculate_wet_bulb(dry_bulb: np.ndarray, relative_humidity: np.ndarray) -> np.ndarray:198 """Calculate Wet Bulb Temperature using Stull (2011) approximation."""199 db = np.array(dry_bulb, dtype=float)200 rh = np.array(relative_humidity, dtype=float)201 202 term1 = db * np.arctan(0.151977 * (rh + 8.313659)**0.5)203 term2 = np.arctan(db + rh)204 term3 = np.arctan(rh - 1.676331)205 term4 = 0.00391838 * rh**1.5 * np.arctan(0.023101 * rh)206 term5 = -4.686035207 208 wet_bulb = term1 + term2 - term3 + term4 + term5209 210 invalid_mask = (rh < 5) | (rh > 99) | (db < -20) | (db > 50) | np.isnan(db) | np.isnan(rh)211 wet_bulb[invalid_mask] = np.nan212 213 return wet_bulb214 215 def display_climate_input(self, session_state: Dict[str, Any]):216 """Display form for EPW upload or manual input in Streamlit."""217 st.title("Climate Data")218 219 if not session_state.building_info.get("country") or not session_state.building_info.get("city"):220 st.warning("Please enter country and city in Building Information first.")221 st.button("Go to Building Information", on_click=lambda: setattr(session_state, "page", "Building Information"))222 return223 224 st.subheader(f"Location: {session_state.building_info['country']}, {session_state.building_info['city']}")225 tab1, tab2 = st.tabs(["Upload EPW File", "Manual Input"])226 227 # EPW Upload Tab228 with tab1:229 uploaded_file = st.file_uploader("Upload EPW File", type=["epw"])230 if uploaded_file:231 try:232 epw_content = uploaded_file.read().decode("utf-8")233 epw_lines = epw_content.splitlines()234 header = next(line for line in epw_lines if line.startswith("LOCATION"))235 header_parts = header.split(",")236 latitude = float(header_parts[6])237 longitude = float(header_parts[7])238 elevation = float(header_parts[8])239 240 data_start_idx = next(i for i, line in enumerate(epw_lines) if line.startswith("DATA PERIODS")) + 1241 epw_data = pd.read_csv(StringIO("\n".join(epw_lines[data_start_idx:])), header=None, dtype=str)242 243 # Validate row and column counts244 if len(epw_data) != 8760:245 raise ValueError(f"EPW file has {len(epw_data)} records, expected 8760.")246 if len(epw_data.columns) != 35:247 raise ValueError(f"EPW file has {len(epw_data.columns)} columns, expected 35.")248 249 # Convert relevant columns to numeric250 for col in [1, 6, 8, 9, 21]:251 epw_data[col] = pd.to_numeric(epw_data[col], errors='coerce')252 if epw_data[col].isna().all():253 raise ValueError(f"Column {col} (e.g., {'wind speed' if col == 21 else 'pressure' if col == 9 else 'other'}) contains only non-numeric or missing data.")254 255 months = epw_data[1].values # Month256 dry_bulb = epw_data[6].values # Dry-bulb temperature (°C)257 humidity = epw_data[8].values # Relative humidity (%)258 pressure = epw_data[9].values # Atmospheric pressure (Pa)259 wind_speed = epw_data[21].values # Wind speed (m/s)260 261 wet_bulb = self.calculate_wet_bulb(dry_bulb, humidity)262 263 if np.all(np.isnan(dry_bulb)) or np.all(np.isnan(humidity)) or np.all(np.isnan(wet_bulb)):264 raise ValueError("Dry bulb, humidity, or calculated wet bulb data is entirely NaN.")265 266 daily_temps = np.nanmean(dry_bulb.reshape(-1, 24), axis=1)267 hdd = round(np.nansum(np.maximum(18 - daily_temps, 0)))268 cdd = round(np.nansum(np.maximum(daily_temps - 18, 0)))269 270 winter_design_temp = round(np.nanpercentile(dry_bulb, 0.4), 1)271 summer_design_temp_db = round(np.nanpercentile(dry_bulb, 99.6), 1)272 summer_design_temp_wb = round(np.nanpercentile(wet_bulb, 99.6), 1)273 summer_mask = (months >= 6) & (months <= 8)274 summer_temps = dry_bulb[summer_mask].reshape(-1, 24)275 summer_daily_range = round(np.nanmean(np.nanmax(summer_temps, axis=1) - np.nanmin(summer_temps, axis=1)), 1)276 277 monthly_temps = {}278 monthly_humidity = {}279 month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]280 for i in range(1, 13):281 month_mask = (months == i)282 monthly_temps[month_names[i-1]] = round(np.nanmean(dry_bulb[month_mask]), 1)283 monthly_humidity[month_names[i-1]] = round(np.nanmean(humidity[month_mask]), 1)284 285 avg_humidity = np.nanmean(humidity)286 climate_zone = self.assign_climate_zone(hdd, cdd, avg_humidity)287 288 location = ClimateLocation(289 epw_file=epw_data,290 id=f"{session_state.building_info['country'][:2].upper()}-{session_state.building_info['city'][:3].upper()}",291 country=session_state.building_info["country"],292 state_province="N/A",293 city=session_state.building_info["city"],294 latitude=latitude,295 longitude=longitude,296 elevation=elevation,297 climate_zone=climate_zone,298 heating_degree_days=hdd,299 cooling_degree_days=cdd,300 summer_design_temp_db=summer_design_temp_db,301 summer_design_temp_wb=summer_design_temp_wb,302 summer_daily_range=summer_daily_range,303 monthly_temps=monthly_temps,304 monthly_humidity=monthly_humidity305 )306 self.add_location(location)307 climate_data_dict = location.to_dict()308 if not self.validate_climate_data(climate_data_dict):309 raise ValueError("Invalid climate data extracted from EPW file.")310 session_state["climate_data"] = climate_data_dict # Save to session state311 st.success("Climate data extracted from EPW file with calculated Wet Bulb Temperature!")312 st.write(f"Debug: Saved climate data for {location.city} (ID: {location.id}): {climate_data_dict}") # Debug313 self.display_design_conditions(location)314 self.visualize_data(location, epw_data=epw_data)315 except Exception as e:316 st.error(f"Error processing EPW file: {str(e)}. Ensure it has 8760 hourly records and correct format.")317 318 # Manual Input Tab319 with tab2:320 with st.form("manual_climate_form"):321 col1, col2 = st.columns(2)322 with col1:323 latitude = st.number_input(324 "Latitude",325 min_value=-90.0,326 max_value=90.0,327 value=0.0,328 step=0.1,329 help="Enter the latitude of the location in degrees (e.g., 64.1 for Reykjavik)"330 )331 longitude = st.number_input(332 "Longitude",333 min_value=-180.0,334 max_value=180.0,335 value=0.0,336 step=0.1,337 help="Enter the longitude of the location in degrees (e.g., -21.9 for Reykjavik)"338 )339 elevation = st.number_input(340 "Elevation (m)",341 min_value=0.0,342 value=0.0,343 step=10.0,344 help="Enter the elevation of the location above sea level in meters"345 )346 climate_zone = st.selectbox(347 "Climate Zone",348 ["0A", "0B", "1A", "1B", "2A", "2B", "3A", "3B", "3C", "4A", "4B", "4C", "5A", "5B", "5C", "6A", "6B", "7", "8"],349 help="Select the ASHRAE climate zone for the location (e.g., 6A for cold, humid climates)"350 )351 352 with col2:353 hdd = st.number_input(354 "Heating Degree Days (base 18°C)",355 min_value=0.0,356 value=0.0,357 step=100.0,358 help="Enter the annual heating degree days using an 18°C base temperature"359 )360 cdd = st.number_input(361 "Cooling Degree Days (base 18°C)",362 min_value=0.0,363 value=0.0,364 step=100.0,365 help="Enter the annual cooling degree days using an 18°C base temperature"366 )367 winter_design_temp = st.number_input(368 "Winter Design Temp (99.6%) (°C)",369 min_value=-50.0,370 max_value=10.0,371 value=0.0,372 step=0.5,373 help="Enter the winter design temperature in °C"374 )375 summer_design_temp_db = st.number_input(376 "Summer Design Temp DB (0.4%) (°C)",377 min_value=0.0,378 max_value=50.0,379 value=35.0,380 step=0.5,381 help="Enter the 0.4% summer design dry-bulb temperature in °C (extreme hot condition)"382 )383 summer_design_temp_wb = st.number_input(384 "Summer Design Temp WB (0.4%) (°C)",385 min_value=0.0,386 max_value=40.0,387 value=25.0,388 step=0.5,389 help="Enter the 0.4% summer design wet-bulb temperature in °C (for humidity consideration)"390 )391 summer_daily_range = st.number_input(392 "Summer Daily Range (°C)",393 min_value=0.0,394 value=5.0,395 step=0.5,396 help="Enter the average daily temperature range in summer in °C"397 )398 wind_speed = st.number_input(399 "Wind Speed (m/s)",400 min_value=0.0,401 max_value=20.0,402 value=5.0,403 step=0.1,404 help="Enter the average wind speed in meters per second"405 )406 pressure = st.number_input(407 "Atmospheric Pressure (Pa)",408 min_value=50000.0,409 max_value=120000.0,410 value=101325.0,411 step=100.0,412 help="Enter the average atmospheric pressure in Pascals (e.g., 101325 Pa for sea level)"413 )414 415 # Monthly Data with clear titles (no help added here)416 monthly_temps = {}417 monthly_humidity = {}418 month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]419 420 st.subheader("Monthly Temperatures")421 col1, col2 = st.columns(2)422 with col1:423 for month in month_names[:6]:424 monthly_temps[month] = st.number_input(f"{month} Temp (°C)", min_value=-50.0, max_value=50.0, value=20.0, step=0.5, key=f"temp_{month}")425 with col2:426 for month in month_names[6:]:427 monthly_temps[month] = st.number_input(f"{month} Temp (°C)", min_value=-50.0, max_value=50.0, value=20.0, step=0.5, key=f"temp_{month}")428 429 st.subheader("Monthly Humidity")430 col1, col2 = st.columns(2)431 with col1:432 for month in month_names[:6]:433 monthly_humidity[month] = st.number_input(f"{month} Humidity (%)", min_value=0.0, max_value=100.0, value=50.0, step=5.0, key=f"hum_{month}")434 with col2:435 for month in month_names[6:]:436 monthly_humidity[month] = st.number_input(f"{month} Humidity (%)", min_value=0.0, max_value=100.0, value=50.0, step=5.0, key=f"hum_{month}")437 438 if st.form_submit_button("Save Climate Data"):439 try:440 # Generate ID internally using country and city from session_state441 generated_id = f"{session_state.building_info['country'][:2].upper()}-{session_state.building_info['city'][:3].upper()}"442 manual_data = {443 "winter_temp": winter_design_temp,444 "wind_speed": wind_speed,445 "pressure": pressure # Use user-provided pressure446 }447 location = ClimateLocation(448 manual_data=manual_data,449 id=generated_id,450 country=session_state.building_info["country"],451 state_province="N/A",452 city=session_state.building_info["city"],453 latitude=latitude,454 longitude=longitude,455 elevation=elevation,456 climate_zone=climate_zone,457 heating_degree_days=hdd,458 cooling_degree_days=cdd,459 summer_design_temp_db=summer_design_temp_db,460 summer_design_temp_wb=summer_design_temp_wb,461 summer_daily_range=summer_daily_range,462 monthly_temps=monthly_temps,463 monthly_humidity=monthly_humidity464 )465 self.add_location(location)466 climate_data_dict = location.to_dict()467 if not self.validate_climate_data(climate_data_dict):468 raise ValueError("Invalid climate data. Please check all inputs.")469 session_state["climate_data"] = climate_data_dict # Save to session state470 st.success("Climate data saved manually!")471 st.write(f"Debug: Saved climate data for {location.city} (ID: {location.id}): {climate_data_dict}") # Debug472 self.display_design_conditions(location)473 self.visualize_data(location, epw_data=None)474 except Exception as e:475 st.error(f"Error saving climate data: {str(e)}. Please check inputs and try again.")476 477 col1, col2 = st.columns(2)478 with col1:479 st.button("Back to Building Information", on_click=lambda: setattr(session_state, "page", "Building Information"))480 with col2:481 if self.locations:482 st.button("Continue to Building Components", on_click=lambda: setattr(session_state, "page", "Building Components"))483 else:484 st.button("Continue to Building Components", disabled=True)485 486 # Display saved session state data (if any)487 if "climate_data" in session_state and session_state["climate_data"]:488 st.subheader("Saved Climate Data")489 st.json(session_state["climate_data"]) # Display as JSON for clarity490 491 def display_design_conditions(self, location: ClimateLocation):492 """Display a table of design conditions including additional parameters for HVAC calculations."""493 st.subheader("Design Conditions for HVAC Calculations")494 495 design_data = pd.DataFrame({496 "Parameter": [497 "Latitude",498 "Longitude",499 "Elevation (m)",500 "Climate Zone",501 "Heating Degree Days (base 18°C)",502 "Cooling Degree Days (base 18°C)",503 "Winter Design Temperature (99.6%)",504 "Summer Design Dry-Bulb Temp (0.4%)",505 "Summer Design Wet-Bulb Temp (0.4%)",506 "Summer Daily Temperature Range",507 "Wind Speed (m/s)",508 "Atmospheric Pressure (Pa)"509 ],510 "Value": [511 f"{location.latitude}°",512 f"{location.longitude}°",513 f"{location.elevation} m",514 location.climate_zone,515 f"{location.heating_degree_days} HDD",516 f"{location.cooling_degree_days} CDD",517 f"{location.winter_design_temp} °C",518 f"{location.summer_design_temp_db} °C",519 f"{location.summer_design_temp_wb} °C",520 f"{location.summer_daily_range} °C",521 f"{location.wind_speed} m/s",522 f"{location.pressure} Pa"523 ]524 })525 526 month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]527 monthly_temp_data = pd.DataFrame({528 "Parameter": [f"{month} Avg Temp" for month in month_names],529 "Value": [f"{location.monthly_temps[month]} °C" for month in month_names]530 })531 532 monthly_humidity_data = pd.DataFrame({533 "Parameter": [f"{month} Avg Humidity" for month in month_names],534 "Value": [f"{location.monthly_humidity[month]} %" for month in month_names]535 })536 537 full_design_data = pd.concat([design_data, monthly_temp_data, monthly_humidity_data], ignore_index=True)538 st.table(full_design_data)539 540 @staticmethod541 def assign_climate_zone(hdd: float, cdd: float, avg_humidity: float) -> str:542 """Assign ASHRAE 169 climate zone based on HDD, CDD, and humidity."""543 if cdd > 10000:544 return "0A" if avg_humidity > 60 else "0B"545 elif cdd > 5000:546 return "1A" if avg_humidity > 60 else "1B"547 elif cdd > 2500:548 return "2A" if avg_humidity > 60 else "2B"549 elif hdd < 2000 and cdd > 1000:550 return "3A" if avg_humidity > 60 else "3B" if avg_humidity < 40 else "3C"551 elif hdd < 3000:552 return "4A" if avg_humidity > 60 else "4B" if avg_humidity < 40 else "4C"553 elif hdd < 4000:554 return "5A" if avg_humidity > 60 else "5B" if avg_humidity < 40 else "5C"555 elif hdd < 5000:556 return "6A" if avg_humidity > 60 else "6B"557 elif hdd < 7000:558 return "7"559 else:560 return "8"561 562 @staticmethod563 def visualize_data(location: ClimateLocation, epw_data: Optional[pd.DataFrame] = None):564 """Visualize monthly temperature and humidity data."""565 st.subheader("Monthly Climate Data Visualization")566 567 months = list(range(1, 13))568 month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]569 temps_avg = [location.monthly_temps[m] for m in month_names]570 humidity_avg = [location.monthly_humidity[m] for m in month_names]571 572 fig_temp = go.Figure()573 fig_temp.add_trace(go.Scatter(574 x=months,575 y=temps_avg,576 mode='lines+markers',577 name='Avg Temperature (°C)',578 line=dict(color='red'),579 marker=dict(size=8)580 ))581 582 if epw_data is not None:583 dry_bulb = epw_data[6].values584 month_col = epw_data[1].values585 temps_min = []586 temps_max = []587 for i in range(1, 13):588 month_mask = (month_col == i)589 temps_min.append(round(np.nanmin(dry_bulb[month_mask]), 1))590 temps_max.append(round(np.nanmax(dry_bulb[month_mask]), 1))591 fig_temp.add_trace(go.Scatter(592 x=months,593 y=temps_max,594 mode='lines',595 name='Max Temperature (°C)',596 line=dict(color='red', dash='dash'),597 opacity=0.5598 ))599 fig_temp.add_trace(go.Scatter(600 x=months,601 y=temps_min,602 mode='lines',603 name='Min Temperature (°C)',604 line=dict(color='red', dash='dash'),605 opacity=0.5,606 fill='tonexty',607 fillcolor='rgba(255, 0, 0, 0.1)'608 ))609 610 fig_temp.update_layout(611 title='Monthly Temperatures',612 xaxis_title='Month',613 yaxis_title='Temperature (°C)',614 xaxis=dict(tickmode='array', tickvals=months, ticktext=month_names),615 legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)616 )617 st.plotly_chart(fig_temp, use_container_width=True)618 619 fig_hum = go.Figure()620 fig_hum.add_trace(go.Scatter(621 x=months,622 y=humidity_avg,623 mode='lines+markers',624 name='Avg Humidity (%)',625 line=dict(color='blue'),626 marker=dict(size=8)627 ))628 629 if epw_data is not None:630 humidity = epw_data[8].values631 month_col = epw_data[1].values632 humidity_min = []633 humidity_max = []634 for i in range(1, 13):635 month_mask = (month_col == i)636 humidity_min.append(round(np.nanmin(humidity[month_mask]), 1))637 humidity_max.append(round(np.nanmax(humidity[month_mask]), 1))638 fig_hum.add_trace(go.Scatter(639 x=months,640 y=humidity_max,641 mode='lines',642 name='Max Humidity (%)',643 line=dict(color='blue', dash='dash'),644 opacity=0.5645 ))646 fig_hum.add_trace(go.Scatter(647 x=months,648 y=humidity_min,649 mode='lines',650 name='Min Humidity (%)',651 line=dict(color='blue', dash='dash'),652 opacity=0.5,653 fill='tonexty',654 fillcolor='rgba(0, 0, 255, 0.1)'655 ))656 657 fig_hum.update_layout(658 title='Monthly Relative Humidity',659 xaxis_title='Month',660 yaxis_title='Relative Humidity (%)',661 xaxis=dict(tickmode='array', tickvals=months, ticktext=month_names),662 legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)663 )664 st.plotly_chart(fig_hum, use_container_width=True)665 666 def export_to_json(self, file_path: str) -> None:667 """Export all climate data to a JSON file."""668 data = {loc_id: loc.to_dict() for loc_id, loc in self.locations.items()}669 with open(file_path, 'w') as f:670 json.dump(data, f, indent=4)671 672 @classmethod673 def from_json(cls, file_path: str) -> 'ClimateData':674 """Load climate data from a JSON file."""675 with open(file_path, 'r') as f:676 data = json.load(f)677 climate_data = cls()678 for loc_id, loc_dict in data.items():679 location = ClimateLocation(**loc_dict)680 climate_data.add_location(location)681 return climate_data682 683if __name__ == "__main__":684 climate_data = ClimateData()685 session_state = {"building_info": {"country": "Iceland", "city": "Reykjavik"}, "page": "Climate Data"}686 climate_data.display_climate_input(session_state)