""" ASHRAE 169 climate data module for HVAC Load Calculator. This module provides access to climate data for various locations based on ASHRAE 169 standard. """ from typing import Dict, List, Any, Optional, Tuple import pandas as pd import numpy as np import os import json from dataclasses import dataclass # 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 # Design conditions 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 data monthly_temps: Dict[str, float] # Average monthly temperatures (°C) monthly_humidity: Dict[str, float] # Average monthly relative humidity (%) 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 } class ClimateData: """Class for managing ASHRAE 169 climate data.""" def __init__(self): """Initialize climate data.""" self.locations = self._load_climate_locations() self.countries = sorted(list(set(loc.country for loc in self.locations.values()))) self.country_states = self._group_locations_by_country_state() @staticmethod def get_design_conditions(climate_zone: str) -> Dict[str, Dict[str, float]]: """ Get design conditions for a specific climate zone. Args: climate_zone: ASHRAE climate zone (e.g., '1A', '3B', '5A') Returns: Dictionary with summer and winter design conditions """ # Default design conditions by climate zone design_conditions_by_zone = { # Hot-Humid "1A": { "summer": {"db": 35.0, "wb": 28.0, "dp": 25.5}, "winter": {"db": 12.0, "rh": 80.0} }, # Hot-Dry "1B": { "summer": {"db": 42.0, "wb": 24.0, "dp": 18.0}, "winter": {"db": 10.0, "rh": 40.0} }, # Hot-Humid "2A": { "summer": {"db": 34.0, "wb": 26.5, "dp": 24.0}, "winter": {"db": 8.0, "rh": 75.0} }, # Hot-Dry "2B": { "summer": {"db": 40.0, "wb": 23.0, "dp": 16.5}, "winter": {"db": 6.0, "rh": 45.0} }, # Warm-Humid "3A": { "summer": {"db": 33.0, "wb": 25.0, "dp": 22.5}, "winter": {"db": 2.0, "rh": 70.0} }, # Warm-Dry "3B": { "summer": {"db": 38.0, "wb": 22.0, "dp": 15.0}, "winter": {"db": 4.0, "rh": 40.0} }, # Warm-Marine "3C": { "summer": {"db": 28.0, "wb": 20.0, "dp": 17.0}, "winter": {"db": 5.0, "rh": 80.0} }, # Mixed-Humid "4A": { "summer": {"db": 32.0, "wb": 24.0, "dp": 21.0}, "winter": {"db": -5.0, "rh": 70.0} }, # Mixed-Dry "4B": { "summer": {"db": 35.0, "wb": 20.0, "dp": 13.0}, "winter": {"db": -3.0, "rh": 45.0} }, # Mixed-Marine "4C": { "summer": {"db": 27.0, "wb": 19.0, "dp": 16.0}, "winter": {"db": -2.0, "rh": 80.0} }, # Cool-Humid "5A": { "summer": {"db": 31.0, "wb": 23.0, "dp": 20.0}, "winter": {"db": -15.0, "rh": 70.0} }, # Cool-Dry "5B": { "summer": {"db": 33.0, "wb": 18.0, "dp": 11.0}, "winter": {"db": -10.0, "rh": 45.0} }, # Cool-Marine "5C": { "summer": {"db": 25.0, "wb": 18.0, "dp": 15.0}, "winter": {"db": -5.0, "rh": 80.0} }, # Cold-Humid "6A": { "summer": {"db": 30.0, "wb": 22.0, "dp": 19.0}, "winter": {"db": -20.0, "rh": 70.0} }, # Cold-Dry "6B": { "summer": {"db": 31.0, "wb": 17.0, "dp": 10.0}, "winter": {"db": -15.0, "rh": 45.0} }, # Very Cold "7": { "summer": {"db": 28.0, "wb": 20.0, "dp": 17.0}, "winter": {"db": -25.0, "rh": 70.0} }, # Subarctic/Arctic "8": { "summer": {"db": 25.0, "wb": 18.0, "dp": 15.0}, "winter": {"db": -30.0, "rh": 70.0} } } # Return design conditions for the specified climate zone # If climate zone not found, return default values if climate_zone in design_conditions_by_zone: return design_conditions_by_zone[climate_zone] else: # Default to 4A if climate zone not found return design_conditions_by_zone["4A"] @staticmethod def get_monthly_temperatures(climate_zone: str) -> Dict[int, Dict[str, float]]: """ Get monthly average temperatures for a specific climate zone. Args: climate_zone: ASHRAE climate zone (e.g., '1A', '3B', '5A') Returns: Dictionary with monthly average temperatures indexed by month number (1-12) Each month contains 'avg_db', 'max_db', and 'min_db' values """ # Helper function to convert month name format to numeric format with min/max values def convert_month_format(month_data): month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] result = {} for i, month in enumerate(month_names, 1): avg_temp = month_data[month] # Generate reasonable min/max values based on average min_temp = avg_temp - 5.0 max_temp = avg_temp + 5.0 result[i] = { 'avg_db': avg_temp, 'min_db': min_temp, 'max_db': max_temp } return result # Default monthly temperatures by climate zone (in month name format) monthly_temps_raw = { # Hot-Humid (like Miami) "1A": { "Jan": 20.0, "Feb": 20.5, "Mar": 22.0, "Apr": 24.0, "May": 26.0, "Jun": 28.0, "Jul": 29.0, "Aug": 29.0, "Sep": 28.0, "Oct": 26.0, "Nov": 23.0, "Dec": 21.0 }, # Hot-Dry (like Riyadh) "1B": { "Jan": 15.0, "Feb": 17.0, "Mar": 22.0, "Apr": 27.0, "May": 32.0, "Jun": 35.0, "Jul": 37.0, "Aug": 36.0, "Sep": 33.0, "Oct": 28.0, "Nov": 22.0, "Dec": 17.0 }, # Hot-Humid (like Houston) "2A": { "Jan": 12.0, "Feb": 13.5, "Mar": 17.0, "Apr": 21.0, "May": 25.0, "Jun": 28.0, "Jul": 29.0, "Aug": 29.0, "Sep": 27.0, "Oct": 22.0, "Nov": 17.0, "Dec": 13.0 }, # Hot-Dry (like Phoenix) "2B": { "Jan": 13.0, "Feb": 15.0, "Mar": 18.0, "Apr": 23.0, "May": 28.0, "Jun": 33.0, "Jul": 35.0, "Aug": 34.0, "Sep": 31.0, "Oct": 25.0, "Nov": 18.0, "Dec": 13.0 }, # Warm-Humid (like Atlanta) "3A": { "Jan": 6.0, "Feb": 8.0, "Mar": 12.0, "Apr": 17.0, "May": 21.0, "Jun": 25.0, "Jul": 27.0, "Aug": 26.0, "Sep": 23.0, "Oct": 18.0, "Nov": 12.0, "Dec": 7.0 }, # Warm-Dry (like Los Angeles) "3B": { "Jan": 14.6, "Feb": 15.1, "Mar": 15.8, "Apr": 17.1, "May": 18.3, "Jun": 20.1, "Jul": 22.3, "Aug": 22.9, "Sep": 22.1, "Oct": 20.3, "Nov": 17.2, "Dec": 14.9 }, # Warm-Marine (like San Francisco) "3C": { "Jan": 10.0, "Feb": 11.0, "Mar": 12.0, "Apr": 13.0, "May": 14.0, "Jun": 16.0, "Jul": 17.0, "Aug": 17.0, "Sep": 18.0, "Oct": 16.0, "Nov": 13.0, "Dec": 10.0 }, # Mixed-Humid (like New York) "4A": { "Jan": 0.5, "Feb": 2.1, "Mar": 6.3, "Apr": 12.5, "May": 18.2, "Jun": 23.1, "Jul": 25.8, "Aug": 24.9, "Sep": 20.7, "Oct": 14.3, "Nov": 8.2, "Dec": 2.4 }, # Mixed-Dry (like Albuquerque) "4B": { "Jan": 3.0, "Feb": 5.0, "Mar": 9.0, "Apr": 14.0, "May": 19.0, "Jun": 24.0, "Jul": 26.0, "Aug": 25.0, "Sep": 21.0, "Oct": 15.0, "Nov": 8.0, "Dec": 3.0 }, # Mixed-Marine (like Seattle) "4C": { "Jan": 5.0, "Feb": 6.0, "Mar": 8.0, "Apr": 10.0, "May": 13.0, "Jun": 16.0, "Jul": 18.0, "Aug": 18.0, "Sep": 16.0, "Oct": 12.0, "Nov": 8.0, "Dec": 5.0 }, # Cool-Humid (like Chicago) "5A": { "Jan": -3.5, "Feb": -1.2, "Mar": 4.1, "Apr": 10.3, "May": 16.5, "Jun": 22.1, "Jul": 24.8, "Aug": 23.9, "Sep": 19.7, "Oct": 12.8, "Nov": 5.2, "Dec": -1.4 }, # Cool-Dry (like Denver) "5B": { "Jan": 0.0, "Feb": 2.0, "Mar": 6.0, "Apr": 10.0, "May": 15.0, "Jun": 20.0, "Jul": 23.0, "Aug": 22.0, "Sep": 18.0, "Oct": 12.0, "Nov": 5.0, "Dec": 0.0 }, # Cool-Marine (like Vancouver) "5C": { "Jan": 3.0, "Feb": 4.0, "Mar": 6.0, "Apr": 9.0, "May": 12.0, "Jun": 15.0, "Jul": 17.0, "Aug": 17.0, "Sep": 14.0, "Oct": 10.0, "Nov": 6.0, "Dec": 3.0 }, # Cold-Humid (like Minneapolis) "6A": { "Jan": -9.0, "Feb": -6.0, "Mar": 0.0, "Apr": 8.0, "May": 15.0, "Jun": 20.0, "Jul": 23.0, "Aug": 22.0, "Sep": 17.0, "Oct": 10.0, "Nov": 1.0, "Dec": -6.0 }, # Cold-Dry (like Helena) "6B": { "Jan": -5.0, "Feb": -2.0, "Mar": 2.0, "Apr": 7.0, "May": 12.0, "Jun": 17.0, "Jul": 21.0, "Aug": 20.0, "Sep": 15.0, "Oct": 9.0, "Nov": 1.0, "Dec": -4.0 }, # Very Cold (like Duluth) "7": { "Jan": -12.0, "Feb": -9.0, "Mar": -3.0, "Apr": 5.0, "May": 12.0, "Jun": 17.0, "Jul": 20.0, "Aug": 19.0, "Sep": 14.0, "Oct": 7.0, "Nov": -1.0, "Dec": -9.0 }, # Subarctic/Arctic (like Fairbanks) "8": { "Jan": -20.0, "Feb": -16.0, "Mar": -10.0, "Apr": 0.0, "May": 10.0, "Jun": 16.0, "Jul": 18.0, "Aug": 15.0, "Sep": 8.0, "Oct": -2.0, "Nov": -12.0, "Dec": -18.0 } } # Return monthly temperatures for the specified climate zone # If climate zone not found, return default values if climate_zone in monthly_temps_raw: return convert_month_format(monthly_temps_raw[climate_zone]) else: # Default to 4A if climate zone not found return convert_month_format(monthly_temps_raw["4A"]) def _load_climate_locations(self) -> Dict[str, ClimateLocation]: """ Load climate location data. Returns: Dictionary of climate locations indexed by ID """ # This would typically load from a JSON or CSV file with ASHRAE 169 data # For now, we'll define some sample locations inline # Sample monthly data (for all locations in this example) months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] # New York monthly temperatures (°C) ny_temps = { "Jan": 0.5, "Feb": 2.1, "Mar": 6.3, "Apr": 12.5, "May": 18.2, "Jun": 23.1, "Jul": 25.8, "Aug": 24.9, "Sep": 20.7, "Oct": 14.3, "Nov": 8.2, "Dec": 2.4 } # New York monthly humidity (%) ny_humidity = { "Jan": 65, "Feb": 62, "Mar": 58, "Apr": 55, "May": 60, "Jun": 65, "Jul": 68, "Aug": 70, "Sep": 68, "Oct": 63, "Nov": 67, "Dec": 68 } # Los Angeles monthly temperatures (°C) la_temps = { "Jan": 14.6, "Feb": 15.1, "Mar": 15.8, "Apr": 17.1, "May": 18.3, "Jun": 20.1, "Jul": 22.3, "Aug": 22.9, "Sep": 22.1, "Oct": 20.3, "Nov": 17.2, "Dec": 14.9 } # Los Angeles monthly humidity (%) la_humidity = { "Jan": 63, "Feb": 67, "Mar": 70, "Apr": 71, "May": 74, "Jun": 75, "Jul": 76, "Aug": 76, "Sep": 74, "Oct": 70, "Nov": 65, "Dec": 63 } # Chicago monthly temperatures (°C) chi_temps = { "Jan": -3.5, "Feb": -1.2, "Mar": 4.1, "Apr": 10.3, "May": 16.5, "Jun": 22.1, "Jul": 24.8, "Aug": 23.9, "Sep": 19.7, "Oct": 12.8, "Nov": 5.2, "Dec": -1.4 } # Chicago monthly humidity (%) chi_humidity = { "Jan": 72, "Feb": 70, "Mar": 65, "Apr": 60, "May": 64, "Jun": 67, "Jul": 70, "Aug": 73, "Sep": 71, "Oct": 68, "Nov": 72, "Dec": 75 } # London monthly temperatures (°C) lon_temps = { "Jan": 5.2, "Feb": 5.5, "Mar": 7.4, "Apr": 9.9, "May": 13.3, "Jun": 16.7, "Jul": 18.7, "Aug": 18.3, "Sep": 15.9, "Oct": 12.2, "Nov": 8.3, "Dec": 5.9 } # London monthly humidity (%) lon_humidity = { "Jan": 84, "Feb": 80, "Mar": 76, "Apr": 72, "May": 70, "Jun": 70, "Jul": 71, "Aug": 72, "Sep": 75, "Oct": 80, "Nov": 84, "Dec": 86 } # Sydney monthly temperatures (°C) syd_temps = { "Jan": 23.5, "Feb": 23.4, "Mar": 22.1, "Apr": 19.5, "May": 16.5, "Jun": 14.1, "Jul": 13.4, "Aug": 14.5, "Sep": 16.6, "Oct": 18.8, "Nov": 20.6, "Dec": 22.6 } # Sydney monthly humidity (%) syd_humidity = { "Jan": 65, "Feb": 68, "Mar": 68, "Apr": 67, "May": 70, "Jun": 70, "Jul": 68, "Aug": 63, "Sep": 60, "Oct": 60, "Nov": 62, "Dec": 63 } # Create sample locations locations = { "US-NY-NYC": ClimateLocation( id="US-NY-NYC", country="United States", state_province="New York", city="New York", latitude=40.7128, longitude=-74.0060, elevation=10.0, climate_zone="4A", heating_degree_days=2600, cooling_degree_days=1200, winter_design_temp=-8.3, summer_design_temp_db=32.8, summer_design_temp_wb=25.6, summer_daily_range=8.3, monthly_temps=ny_temps, monthly_humidity=ny_humidity ), "US-CA-LAX": ClimateLocation( id="US-CA-LAX", country="United States", state_province="California", city="Los Angeles", latitude=34.0522, longitude=-118.2437, elevation=93.0, climate_zone="3B", heating_degree_days=800, cooling_degree_days=1200, winter_design_temp=8.3, summer_design_temp_db=32.2, summer_design_temp_wb=23.3, summer_daily_range=6.7, monthly_temps=la_temps, monthly_humidity=la_humidity ), "US-IL-CHI": ClimateLocation( id="US-IL-CHI", country="United States", state_province="Illinois", city="Chicago", latitude=41.8781, longitude=-87.6298, elevation=179.0, climate_zone="5A", heating_degree_days=3500, cooling_degree_days=1000, winter_design_temp=-16.7, summer_design_temp_db=33.3, summer_design_temp_wb=25.6, summer_daily_range=8.9, monthly_temps=chi_temps, monthly_humidity=chi_humidity ), "UK-LDN": ClimateLocation( id="UK-LDN", country="United Kingdom", state_province="England", city="London", latitude=51.5074, longitude=-0.1278, elevation=35.0, climate_zone="4A", heating_degree_days=2500, cooling_degree_days=200, winter_design_temp=-3.9, summer_design_temp_db=28.3, summer_design_temp_wb=20.0, summer_daily_range=10.0, monthly_temps=lon_temps, monthly_humidity=lon_humidity ), "AU-NSW-SYD": ClimateLocation( id="AU-NSW-SYD", country="Australia", state_province="New South Wales", city="Sydney", latitude=-33.8688, longitude=151.2093, elevation=3.0, climate_zone="3C", heating_degree_days=600, cooling_degree_days=900, winter_design_temp=7.2, summer_design_temp_db=31.1, summer_design_temp_wb=24.4, summer_daily_range=7.8, monthly_temps=syd_temps, monthly_humidity=syd_humidity ) } return locations def _group_locations_by_country_state(self) -> Dict[str, Dict[str, List[str]]]: """ Group locations by country and state/province. Returns: Nested dictionary of countries, states, and cities """ 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) # Sort states and cities for country in result: for state in result[country]: result[country][state] = sorted(result[country][state]) return result def get_location(self, location_id: str) -> Optional[ClimateLocation]: """ Get climate location by ID. Args: location_id: Location identifier Returns: ClimateLocation object or None if not found """ return self.locations.get(location_id) def find_location(self, country: str, state_province: str = None, city: str = None) -> Optional[ClimateLocation]: """ Find a climate location by country, state/province, and city. Args: country: Country name state_province: State or province name (optional) city: City name (optional) Returns: ClimateLocation object or None if not found """ for loc in self.locations.values(): if loc.country == country: if state_province is None or loc.state_province == state_province: if city is None or loc.city == city: return loc return None def find_locations_by_climate_zone(self, climate_zone: str) -> List[ClimateLocation]: """ Find climate locations by climate zone. Args: climate_zone: ASHRAE climate zone Returns: List of ClimateLocation objects """ return [loc for loc in self.locations.values() if loc.climate_zone == climate_zone] def get_states_for_country(self, country: str) -> List[str]: """ Get states/provinces for a country. Args: country: Country name Returns: List of state/province names """ if country in self.country_states: return sorted(self.country_states[country].keys()) return [] def get_cities_for_state(self, country: str, state_province: str) -> List[str]: """ Get cities for a state/province. Args: country: Country name state_province: State or province name Returns: List of city names """ if country in self.country_states and state_province in self.country_states[country]: return self.country_states[country][state_province] return [] def get_location_id(self, country: str, state_province: str, city: str) -> Optional[str]: """ Get location ID for a city. Args: country: Country name state_province: State or province name city: City name Returns: Location ID or None if not found """ for loc_id, loc in self.locations.items(): if (loc.country == country and loc.state_province == state_province and loc.city == city): return loc_id return None def export_to_json(self, file_path: str) -> None: """ Export all climate data to a JSON file. Args: file_path: Path to the output 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': """ Create a ClimateData instance from a JSON file. Args: file_path: Path to the input JSON file Returns: A new ClimateData instance """ with open(file_path, 'r') as f: data = json.load(f) climate_data = cls() climate_data.locations = {} for loc_id, loc_dict in data.items(): climate_data.locations[loc_id] = ClimateLocation( id=loc_dict["id"], country=loc_dict["country"], state_province=loc_dict["state_province"], city=loc_dict["city"], latitude=loc_dict["latitude"], longitude=loc_dict["longitude"], elevation=loc_dict["elevation"], climate_zone=loc_dict["climate_zone"], heating_degree_days=loc_dict["heating_degree_days"], cooling_degree_days=loc_dict["cooling_degree_days"], winter_design_temp=loc_dict["winter_design_temp"], summer_design_temp_db=loc_dict["summer_design_temp_db"], summer_design_temp_wb=loc_dict["summer_design_temp_wb"], summer_daily_range=loc_dict["summer_daily_range"], monthly_temps=loc_dict["monthly_temps"], monthly_humidity=loc_dict["monthly_humidity"] ) climate_data.countries = sorted(list(set(loc.country for loc in climate_data.locations.values()))) climate_data.country_states = climate_data._group_locations_by_country_state() return climate_data # Create a singleton instance climate_data = ClimateData() # Export climate data to JSON if needed if __name__ == "__main__": climate_data.export_to_json(os.path.join(DATA_DIR, "climate_data.json"))