CoolFace
Apppublic

Building-science/HVAC-03

sourceHugging Faceupdated 1y agoView on Hugging Face
1likes
climate_data.py686 linesDownload Raw Back to data
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)