HVAC-03 / data /climate_data.py
mabuseif's picture
Update data/climate_data.py
e163fed verified
"""
ASHRAE 169 climate data module for HVAC Load Calculator.
This module provides access to climate data for various locations based on ASHRAE 169 standard.
Author: Dr Majed Abuseif
Date: March 2025
Version: 1.0.0
"""
from typing import Dict, List, Any, Optional
import pandas as pd
import numpy as np
import os
import json
from dataclasses import dataclass
import streamlit as st
import plotly.graph_objects as go
from io import StringIO
# Define paths
DATA_DIR = os.path.dirname(os.path.abspath(__file__))
@dataclass
class ClimateLocation:
"""Class representing a climate location with ASHRAE 169 data."""
id: str
country: str
state_province: str
city: str
latitude: float
longitude: float
elevation: float # meters
climate_zone: str
heating_degree_days: float # base 18°C
cooling_degree_days: float # base 18°C
winter_design_temp: float # 99.6% heating design temperature (°C)
summer_design_temp_db: float # 0.4% cooling design dry-bulb temperature (°C)
summer_design_temp_wb: float # 0.4% cooling design wet-bulb temperature (°C)
summer_daily_range: float # Mean daily temperature range in summer (°C)
monthly_temps: Dict[str, float] # Average monthly temperatures (°C)
monthly_humidity: Dict[str, float] # Average monthly relative humidity (%)
wind_speed: float # Mean wind speed (m/s)
pressure: float # Atmospheric pressure (Pa)
def __init__(self, epw_file=None, manual_data=None, **kwargs):
"""Initialize ClimateLocation with EPW file or manual data."""
if epw_file is not None and isinstance(epw_file, pd.DataFrame):
# Extract from EPW (epw_data[6] for dry-bulb temperature)
temps = np.array(epw_file[6], dtype=float)
self.winter_design_temp = np.percentile(temps[~np.isnan(temps)], 0.4) # 99.6% percentile
self.wind_speed = round(np.nanmean(epw_file[21]), 1) # Wind speed (m/s, index 21)
self.pressure = round(np.nanmean(epw_file[9]), 1) # Atmospheric pressure (Pa, index 9)
# Populate other fields from EPW processing
self.id = kwargs.get("id")
self.country = kwargs.get("country")
self.state_province = kwargs.get("state_province")
self.city = kwargs.get("city")
self.latitude = kwargs.get("latitude")
self.longitude = kwargs.get("longitude")
self.elevation = kwargs.get("elevation")
self.climate_zone = kwargs.get("climate_zone")
self.heating_degree_days = kwargs.get("heating_degree_days")
self.cooling_degree_days = kwargs.get("cooling_degree_days")
self.summer_design_temp_db = kwargs.get("summer_design_temp_db")
self.summer_design_temp_wb = kwargs.get("summer_design_temp_wb")
self.summer_daily_range = kwargs.get("summer_daily_range")
self.monthly_temps = kwargs.get("monthly_temps")
self.monthly_humidity = kwargs.get("monthly_humidity")
elif manual_data:
self.winter_design_temp = manual_data.get("winter_temp", -10.0)
self.wind_speed = manual_data.get("wind_speed", 5.0)
self.pressure = manual_data.get("pressure", 101325.0) # Use provided pressure
# Populate other fields from manual data
for key, value in kwargs.items():
setattr(self, key, value)
else:
# Default initialization with kwargs
for key, value in kwargs.items():
setattr(self, key, value)
self.winter_design_temp = kwargs.get("winter_design_temp", -10.0)
self.wind_speed = kwargs.get("wind_speed", 5.0)
self.pressure = self.adjust_pressure_for_altitude(kwargs.get("elevation", 0.0))
def adjust_pressure_for_altitude(self, elevation: float) -> float:
"""Calculate atmospheric pressure based on elevation."""
if elevation is None:
return 101325.0 # Default sea-level pressure if elevation is None
return 101325 * (1 - 2.25577e-5 * elevation)**5.25588
def to_dict(self) -> Dict[str, Any]:
"""Convert the climate location to a dictionary."""
return {
"id": self.id,
"country": self.country,
"state_province": self.state_province,
"city": self.city,
"latitude": self.latitude,
"longitude": self.longitude,
"elevation": self.elevation,
"climate_zone": self.climate_zone,
"heating_degree_days": self.heating_degree_days,
"cooling_degree_days": self.cooling_degree_days,
"winter_design_temp": self.winter_design_temp,
"summer_design_temp_db": self.summer_design_temp_db,
"summer_design_temp_wb": self.summer_design_temp_wb,
"summer_daily_range": self.summer_daily_range,
"monthly_temps": self.monthly_temps,
"monthly_humidity": self.monthly_humidity,
"wind_speed": self.wind_speed,
"pressure": self.pressure
}
class ClimateData:
"""Class for managing ASHRAE 169 climate data."""
def __init__(self):
"""Initialize climate data."""
self.locations = {}
self.countries = []
self.country_states = {}
def _group_locations_by_country_state(self) -> Dict[str, Dict[str, List[str]]]:
"""Group locations by country and state/province."""
result = {}
for loc in self.locations.values():
if loc.country not in result:
result[loc.country] = {}
if loc.state_province not in result[loc.country]:
result[loc.country][loc.state_province] = []
result[loc.country][loc.state_province].append(loc.city)
for country in result:
for state in result[country]:
result[country][state] = sorted(result[country][state])
return result
def add_location(self, location: ClimateLocation):
"""Add a new location to the dictionary."""
self.locations[location.id] = location
self.countries = sorted(list(set(loc.country for loc in self.locations.values())))
self.country_states = self._group_locations_by_country_state()
def get_location_by_id(self, location_id: str, session_state: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Retrieve climate data by ID from session state or locations."""
if "climate_data" in session_state and session_state["climate_data"].get("id") == location_id:
return session_state["climate_data"]
if location_id in self.locations:
return self.locations[location_id].to_dict()
return None
@staticmethod
def validate_climate_data(data: Dict[str, Any]) -> bool:
"""Validate climate data for required fields and ranges."""
required_fields = [
"id", "country", "city", "latitude", "longitude", "elevation",
"climate_zone", "heating_degree_days", "cooling_degree_days",
"winter_design_temp", "summer_design_temp_db", "summer_design_temp_wb",
"summer_daily_range", "monthly_temps", "monthly_humidity",
"wind_speed", "pressure"
]
month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
for field in required_fields:
if field not in data:
return False
if not (-90 <= data["latitude"] <= 90 and -180 <= data["longitude"] <= 180):
return False
if data["elevation"] < 0:
return False
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"]:
return False
if not (data["heating_degree_days"] >= 0 and data["cooling_degree_days"] >= 0):
return False
if not (-50 <= data["winter_design_temp"] <= 20):
return False
if not (0 <= data["summer_design_temp_db"] <= 50 and 0 <= data["summer_design_temp_wb"] <= 40):
return False
if data["summer_daily_range"] < 0:
return False
if not (0 <= data["wind_speed"] <= 20):
return False
if not (50000 <= data["pressure"] <= 120000):
return False
for month in month_names:
if month not in data["monthly_temps"] or month not in data["monthly_humidity"]:
return False
if not (-50 <= data["monthly_temps"][month] <= 50):
return False
if not (0 <= data["monthly_humidity"][month] <= 100):
return False
return True
@staticmethod
def calculate_wet_bulb(dry_bulb: np.ndarray, relative_humidity: np.ndarray) -> np.ndarray:
"""Calculate Wet Bulb Temperature using Stull (2011) approximation."""
db = np.array(dry_bulb, dtype=float)
rh = np.array(relative_humidity, dtype=float)
term1 = db * np.arctan(0.151977 * (rh + 8.313659)**0.5)
term2 = np.arctan(db + rh)
term3 = np.arctan(rh - 1.676331)
term4 = 0.00391838 * rh**1.5 * np.arctan(0.023101 * rh)
term5 = -4.686035
wet_bulb = term1 + term2 - term3 + term4 + term5
invalid_mask = (rh < 5) | (rh > 99) | (db < -20) | (db > 50) | np.isnan(db) | np.isnan(rh)
wet_bulb[invalid_mask] = np.nan
return wet_bulb
def display_climate_input(self, session_state: Dict[str, Any]):
"""Display form for EPW upload or manual input in Streamlit."""
st.title("Climate Data")
if not session_state.building_info.get("country") or not session_state.building_info.get("city"):
st.warning("Please enter country and city in Building Information first.")
st.button("Go to Building Information", on_click=lambda: setattr(session_state, "page", "Building Information"))
return
st.subheader(f"Location: {session_state.building_info['country']}, {session_state.building_info['city']}")
tab1, tab2 = st.tabs(["Upload EPW File", "Manual Input"])
# EPW Upload Tab
with tab1:
uploaded_file = st.file_uploader("Upload EPW File", type=["epw"])
if uploaded_file:
try:
epw_content = uploaded_file.read().decode("utf-8")
epw_lines = epw_content.splitlines()
header = next(line for line in epw_lines if line.startswith("LOCATION"))
header_parts = header.split(",")
latitude = float(header_parts[6])
longitude = float(header_parts[7])
elevation = float(header_parts[8])
data_start_idx = next(i for i, line in enumerate(epw_lines) if line.startswith("DATA PERIODS")) + 1
epw_data = pd.read_csv(StringIO("\n".join(epw_lines[data_start_idx:])), header=None, dtype=str)
# Validate row and column counts
if len(epw_data) != 8760:
raise ValueError(f"EPW file has {len(epw_data)} records, expected 8760.")
if len(epw_data.columns) != 35:
raise ValueError(f"EPW file has {len(epw_data.columns)} columns, expected 35.")
# Convert relevant columns to numeric
for col in [1, 6, 8, 9, 21]:
epw_data[col] = pd.to_numeric(epw_data[col], errors='coerce')
if epw_data[col].isna().all():
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.")
months = epw_data[1].values # Month
dry_bulb = epw_data[6].values # Dry-bulb temperature (°C)
humidity = epw_data[8].values # Relative humidity (%)
pressure = epw_data[9].values # Atmospheric pressure (Pa)
wind_speed = epw_data[21].values # Wind speed (m/s)
wet_bulb = self.calculate_wet_bulb(dry_bulb, humidity)
if np.all(np.isnan(dry_bulb)) or np.all(np.isnan(humidity)) or np.all(np.isnan(wet_bulb)):
raise ValueError("Dry bulb, humidity, or calculated wet bulb data is entirely NaN.")
daily_temps = np.nanmean(dry_bulb.reshape(-1, 24), axis=1)
hdd = round(np.nansum(np.maximum(18 - daily_temps, 0)))
cdd = round(np.nansum(np.maximum(daily_temps - 18, 0)))
winter_design_temp = round(np.nanpercentile(dry_bulb, 0.4), 1)
summer_design_temp_db = round(np.nanpercentile(dry_bulb, 99.6), 1)
summer_design_temp_wb = round(np.nanpercentile(wet_bulb, 99.6), 1)
summer_mask = (months >= 6) & (months <= 8)
summer_temps = dry_bulb[summer_mask].reshape(-1, 24)
summer_daily_range = round(np.nanmean(np.nanmax(summer_temps, axis=1) - np.nanmin(summer_temps, axis=1)), 1)
monthly_temps = {}
monthly_humidity = {}
month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
for i in range(1, 13):
month_mask = (months == i)
monthly_temps[month_names[i-1]] = round(np.nanmean(dry_bulb[month_mask]), 1)
monthly_humidity[month_names[i-1]] = round(np.nanmean(humidity[month_mask]), 1)
avg_humidity = np.nanmean(humidity)
climate_zone = self.assign_climate_zone(hdd, cdd, avg_humidity)
location = ClimateLocation(
epw_file=epw_data,
id=f"{session_state.building_info['country'][:2].upper()}-{session_state.building_info['city'][:3].upper()}",
country=session_state.building_info["country"],
state_province="N/A",
city=session_state.building_info["city"],
latitude=latitude,
longitude=longitude,
elevation=elevation,
climate_zone=climate_zone,
heating_degree_days=hdd,
cooling_degree_days=cdd,
summer_design_temp_db=summer_design_temp_db,
summer_design_temp_wb=summer_design_temp_wb,
summer_daily_range=summer_daily_range,
monthly_temps=monthly_temps,
monthly_humidity=monthly_humidity
)
self.add_location(location)
climate_data_dict = location.to_dict()
if not self.validate_climate_data(climate_data_dict):
raise ValueError("Invalid climate data extracted from EPW file.")
session_state["climate_data"] = climate_data_dict # Save to session state
st.success("Climate data extracted from EPW file with calculated Wet Bulb Temperature!")
st.write(f"Debug: Saved climate data for {location.city} (ID: {location.id}): {climate_data_dict}") # Debug
self.display_design_conditions(location)
self.visualize_data(location, epw_data=epw_data)
except Exception as e:
st.error(f"Error processing EPW file: {str(e)}. Ensure it has 8760 hourly records and correct format.")
# Manual Input Tab
with tab2:
with st.form("manual_climate_form"):
col1, col2 = st.columns(2)
with col1:
latitude = st.number_input(
"Latitude",
min_value=-90.0,
max_value=90.0,
value=0.0,
step=0.1,
help="Enter the latitude of the location in degrees (e.g., 64.1 for Reykjavik)"
)
longitude = st.number_input(
"Longitude",
min_value=-180.0,
max_value=180.0,
value=0.0,
step=0.1,
help="Enter the longitude of the location in degrees (e.g., -21.9 for Reykjavik)"
)
elevation = st.number_input(
"Elevation (m)",
min_value=0.0,
value=0.0,
step=10.0,
help="Enter the elevation of the location above sea level in meters"
)
climate_zone = st.selectbox(
"Climate Zone",
["0A", "0B", "1A", "1B", "2A", "2B", "3A", "3B", "3C", "4A", "4B", "4C", "5A", "5B", "5C", "6A", "6B", "7", "8"],
help="Select the ASHRAE climate zone for the location (e.g., 6A for cold, humid climates)"
)
with col2:
hdd = st.number_input(
"Heating Degree Days (base 18°C)",
min_value=0.0,
value=0.0,
step=100.0,
help="Enter the annual heating degree days using an 18°C base temperature"
)
cdd = st.number_input(
"Cooling Degree Days (base 18°C)",
min_value=0.0,
value=0.0,
step=100.0,
help="Enter the annual cooling degree days using an 18°C base temperature"
)
winter_design_temp = st.number_input(
"Winter Design Temp (99.6%) (°C)",
min_value=-50.0,
max_value=10.0,
value=0.0,
step=0.5,
help="Enter the winter design temperature in °C"
)
summer_design_temp_db = st.number_input(
"Summer Design Temp DB (0.4%) (°C)",
min_value=0.0,
max_value=50.0,
value=35.0,
step=0.5,
help="Enter the 0.4% summer design dry-bulb temperature in °C (extreme hot condition)"
)
summer_design_temp_wb = st.number_input(
"Summer Design Temp WB (0.4%) (°C)",
min_value=0.0,
max_value=40.0,
value=25.0,
step=0.5,
help="Enter the 0.4% summer design wet-bulb temperature in °C (for humidity consideration)"
)
summer_daily_range = st.number_input(
"Summer Daily Range (°C)",
min_value=0.0,
value=5.0,
step=0.5,
help="Enter the average daily temperature range in summer in °C"
)
wind_speed = st.number_input(
"Wind Speed (m/s)",
min_value=0.0,
max_value=20.0,
value=5.0,
step=0.1,
help="Enter the average wind speed in meters per second"
)
pressure = st.number_input(
"Atmospheric Pressure (Pa)",
min_value=50000.0,
max_value=120000.0,
value=101325.0,
step=100.0,
help="Enter the average atmospheric pressure in Pascals (e.g., 101325 Pa for sea level)"
)
# Monthly Data with clear titles (no help added here)
monthly_temps = {}
monthly_humidity = {}
month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
st.subheader("Monthly Temperatures")
col1, col2 = st.columns(2)
with col1:
for month in month_names[:6]:
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}")
with col2:
for month in month_names[6:]:
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}")
st.subheader("Monthly Humidity")
col1, col2 = st.columns(2)
with col1:
for month in month_names[:6]:
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}")
with col2:
for month in month_names[6:]:
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}")
if st.form_submit_button("Save Climate Data"):
try:
# Generate ID internally using country and city from session_state
generated_id = f"{session_state.building_info['country'][:2].upper()}-{session_state.building_info['city'][:3].upper()}"
manual_data = {
"winter_temp": winter_design_temp,
"wind_speed": wind_speed,
"pressure": pressure # Use user-provided pressure
}
location = ClimateLocation(
manual_data=manual_data,
id=generated_id,
country=session_state.building_info["country"],
state_province="N/A",
city=session_state.building_info["city"],
latitude=latitude,
longitude=longitude,
elevation=elevation,
climate_zone=climate_zone,
heating_degree_days=hdd,
cooling_degree_days=cdd,
summer_design_temp_db=summer_design_temp_db,
summer_design_temp_wb=summer_design_temp_wb,
summer_daily_range=summer_daily_range,
monthly_temps=monthly_temps,
monthly_humidity=monthly_humidity
)
self.add_location(location)
climate_data_dict = location.to_dict()
if not self.validate_climate_data(climate_data_dict):
raise ValueError("Invalid climate data. Please check all inputs.")
session_state["climate_data"] = climate_data_dict # Save to session state
st.success("Climate data saved manually!")
st.write(f"Debug: Saved climate data for {location.city} (ID: {location.id}): {climate_data_dict}") # Debug
self.display_design_conditions(location)
self.visualize_data(location, epw_data=None)
except Exception as e:
st.error(f"Error saving climate data: {str(e)}. Please check inputs and try again.")
col1, col2 = st.columns(2)
with col1:
st.button("Back to Building Information", on_click=lambda: setattr(session_state, "page", "Building Information"))
with col2:
if self.locations:
st.button("Continue to Building Components", on_click=lambda: setattr(session_state, "page", "Building Components"))
else:
st.button("Continue to Building Components", disabled=True)
# Display saved session state data (if any)
if "climate_data" in session_state and session_state["climate_data"]:
st.subheader("Saved Climate Data")
st.json(session_state["climate_data"]) # Display as JSON for clarity
def display_design_conditions(self, location: ClimateLocation):
"""Display a table of design conditions including additional parameters for HVAC calculations."""
st.subheader("Design Conditions for HVAC Calculations")
design_data = pd.DataFrame({
"Parameter": [
"Latitude",
"Longitude",
"Elevation (m)",
"Climate Zone",
"Heating Degree Days (base 18°C)",
"Cooling Degree Days (base 18°C)",
"Winter Design Temperature (99.6%)",
"Summer Design Dry-Bulb Temp (0.4%)",
"Summer Design Wet-Bulb Temp (0.4%)",
"Summer Daily Temperature Range",
"Wind Speed (m/s)",
"Atmospheric Pressure (Pa)"
],
"Value": [
f"{location.latitude}°",
f"{location.longitude}°",
f"{location.elevation} m",
location.climate_zone,
f"{location.heating_degree_days} HDD",
f"{location.cooling_degree_days} CDD",
f"{location.winter_design_temp} °C",
f"{location.summer_design_temp_db} °C",
f"{location.summer_design_temp_wb} °C",
f"{location.summer_daily_range} °C",
f"{location.wind_speed} m/s",
f"{location.pressure} Pa"
]
})
month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
monthly_temp_data = pd.DataFrame({
"Parameter": [f"{month} Avg Temp" for month in month_names],
"Value": [f"{location.monthly_temps[month]} °C" for month in month_names]
})
monthly_humidity_data = pd.DataFrame({
"Parameter": [f"{month} Avg Humidity" for month in month_names],
"Value": [f"{location.monthly_humidity[month]} %" for month in month_names]
})
full_design_data = pd.concat([design_data, monthly_temp_data, monthly_humidity_data], ignore_index=True)
st.table(full_design_data)
@staticmethod
def assign_climate_zone(hdd: float, cdd: float, avg_humidity: float) -> str:
"""Assign ASHRAE 169 climate zone based on HDD, CDD, and humidity."""
if cdd > 10000:
return "0A" if avg_humidity > 60 else "0B"
elif cdd > 5000:
return "1A" if avg_humidity > 60 else "1B"
elif cdd > 2500:
return "2A" if avg_humidity > 60 else "2B"
elif hdd < 2000 and cdd > 1000:
return "3A" if avg_humidity > 60 else "3B" if avg_humidity < 40 else "3C"
elif hdd < 3000:
return "4A" if avg_humidity > 60 else "4B" if avg_humidity < 40 else "4C"
elif hdd < 4000:
return "5A" if avg_humidity > 60 else "5B" if avg_humidity < 40 else "5C"
elif hdd < 5000:
return "6A" if avg_humidity > 60 else "6B"
elif hdd < 7000:
return "7"
else:
return "8"
@staticmethod
def visualize_data(location: ClimateLocation, epw_data: Optional[pd.DataFrame] = None):
"""Visualize monthly temperature and humidity data."""
st.subheader("Monthly Climate Data Visualization")
months = list(range(1, 13))
month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
temps_avg = [location.monthly_temps[m] for m in month_names]
humidity_avg = [location.monthly_humidity[m] for m in month_names]
fig_temp = go.Figure()
fig_temp.add_trace(go.Scatter(
x=months,
y=temps_avg,
mode='lines+markers',
name='Avg Temperature (°C)',
line=dict(color='red'),
marker=dict(size=8)
))
if epw_data is not None:
dry_bulb = epw_data[6].values
month_col = epw_data[1].values
temps_min = []
temps_max = []
for i in range(1, 13):
month_mask = (month_col == i)
temps_min.append(round(np.nanmin(dry_bulb[month_mask]), 1))
temps_max.append(round(np.nanmax(dry_bulb[month_mask]), 1))
fig_temp.add_trace(go.Scatter(
x=months,
y=temps_max,
mode='lines',
name='Max Temperature (°C)',
line=dict(color='red', dash='dash'),
opacity=0.5
))
fig_temp.add_trace(go.Scatter(
x=months,
y=temps_min,
mode='lines',
name='Min Temperature (°C)',
line=dict(color='red', dash='dash'),
opacity=0.5,
fill='tonexty',
fillcolor='rgba(255, 0, 0, 0.1)'
))
fig_temp.update_layout(
title='Monthly Temperatures',
xaxis_title='Month',
yaxis_title='Temperature (°C)',
xaxis=dict(tickmode='array', tickvals=months, ticktext=month_names),
legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)
)
st.plotly_chart(fig_temp, use_container_width=True)
fig_hum = go.Figure()
fig_hum.add_trace(go.Scatter(
x=months,
y=humidity_avg,
mode='lines+markers',
name='Avg Humidity (%)',
line=dict(color='blue'),
marker=dict(size=8)
))
if epw_data is not None:
humidity = epw_data[8].values
month_col = epw_data[1].values
humidity_min = []
humidity_max = []
for i in range(1, 13):
month_mask = (month_col == i)
humidity_min.append(round(np.nanmin(humidity[month_mask]), 1))
humidity_max.append(round(np.nanmax(humidity[month_mask]), 1))
fig_hum.add_trace(go.Scatter(
x=months,
y=humidity_max,
mode='lines',
name='Max Humidity (%)',
line=dict(color='blue', dash='dash'),
opacity=0.5
))
fig_hum.add_trace(go.Scatter(
x=months,
y=humidity_min,
mode='lines',
name='Min Humidity (%)',
line=dict(color='blue', dash='dash'),
opacity=0.5,
fill='tonexty',
fillcolor='rgba(0, 0, 255, 0.1)'
))
fig_hum.update_layout(
title='Monthly Relative Humidity',
xaxis_title='Month',
yaxis_title='Relative Humidity (%)',
xaxis=dict(tickmode='array', tickvals=months, ticktext=month_names),
legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01)
)
st.plotly_chart(fig_hum, use_container_width=True)
def export_to_json(self, file_path: str) -> None:
"""Export all climate data to a JSON file."""
data = {loc_id: loc.to_dict() for loc_id, loc in self.locations.items()}
with open(file_path, 'w') as f:
json.dump(data, f, indent=4)
@classmethod
def from_json(cls, file_path: str) -> 'ClimateData':
"""Load climate data from a JSON file."""
with open(file_path, 'r') as f:
data = json.load(f)
climate_data = cls()
for loc_id, loc_dict in data.items():
location = ClimateLocation(**loc_dict)
climate_data.add_location(location)
return climate_data
if __name__ == "__main__":
climate_data = ClimateData()
session_state = {"building_info": {"country": "Iceland", "city": "Reykjavik"}, "page": "Climate Data"}
climate_data.display_climate_input(session_state)