Spaces:
Sleeping
Sleeping
| """ | |
| Heating load calculation module for HVAC Load Calculator. | |
| This module implements enhanced steady-state methods with thermal mass effects, | |
| pressure-driven infiltration, and schedule-based internal gains. | |
| """ | |
| from typing import Dict, List, Any, Optional, Tuple | |
| import math | |
| import numpy as np | |
| import pandas as pd | |
| import os | |
| from datetime import datetime, timedelta | |
| from enum import Enum | |
| # Import data models and utilities | |
| from data.building_components import Wall, Roof, Floor, Window, Door, Orientation, ComponentType | |
| from utils.psychrometrics import Psychrometrics | |
| from utils.heat_transfer import HeatTransferCalculations | |
| # Define paths | |
| DATA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| class HeatingLoadCalculator: | |
| """Class for calculating heating loads using enhanced steady-state methods.""" | |
| def __init__(self): | |
| """Initialize heating load calculator with heat transfer and psychrometrics utilities.""" | |
| self.heat_transfer = HeatTransferCalculations() | |
| self.psychrometrics = Psychrometrics() | |
| def validate_inputs(self, temp: float, rh: float, area: float, u_value: float) -> None: | |
| """ | |
| Validate input parameters for calculations. | |
| Args: | |
| temp: Temperature in °C | |
| rh: Relative humidity in % | |
| area: Area in m² | |
| u_value: U-value in W/(m²·K) | |
| Raises: | |
| ValueError: If inputs are out of acceptable ranges | |
| """ | |
| if not -50 <= temp <= 60: | |
| raise ValueError(f"Temperature {temp}°C is outside valid range (-50 to 60°C)") | |
| if not 0 <= rh <= 100: | |
| raise ValueError(f"Relative humidity {rh}% is outside valid range (0 to 100%)") | |
| if area < 0: | |
| raise ValueError(f"Area {area}m² cannot be negative") | |
| if u_value < 0: | |
| raise ValueError(f"U-value {u_value} W/(m²·K) cannot be negative") | |
| def calculate_wall_heating_load(self, wall: Wall, outdoor_temp: float, indoor_temp: float) -> float: | |
| """ | |
| Calculate heating load through a wall with thermal mass effects. | |
| Args: | |
| wall: Wall object | |
| outdoor_temp: Outdoor temperature in °C | |
| indoor_temp: Indoor temperature in °C | |
| Returns: | |
| Heating load in W | |
| """ | |
| self.validate_inputs(outdoor_temp, 80.0, wall.area, wall.u_value) | |
| u_value = wall.u_value | |
| area = wall.area | |
| delta_t = indoor_temp - outdoor_temp | |
| # Apply thermal mass effect | |
| thermal_mass = getattr(wall, "thermal_mass", 100000) # J/K, default | |
| time_constant = getattr(wall, "time_constant", 2.0) # hours, default | |
| lag_factor = self.heat_transfer.thermal_lag_factor(thermal_mass, time_constant, 1.0) | |
| heating_load = u_value * area * delta_t * lag_factor | |
| return max(0, heating_load) | |
| def calculate_roof_heating_load(self, roof: Roof, outdoor_temp: float, indoor_temp: float) -> float: | |
| """ | |
| Calculate heating load through a roof with thermal mass effects. | |
| Args: | |
| roof: Roof object | |
| outdoor_temp: Outdoor temperature in °C | |
| indoor_temp: Indoor temperature in °C | |
| Returns: | |
| Heating load in W | |
| """ | |
| self.validate_inputs(outdoor_temp, 80.0, roof.area, roof.u_value) | |
| u_value = roof.u_value | |
| area = roof.area | |
| delta_t = indoor_temp - outdoor_temp | |
| # Apply thermal mass effect | |
| thermal_mass = getattr(roof, "thermal_mass", 200000) # J/K, default | |
| time_constant = getattr(roof, "time_constant", 3.0) # hours, default | |
| lag_factor = self.heat_transfer.thermal_lag_factor(thermal_mass, time_constant, 1.0) | |
| heating_load = u_value * area * delta_t * lag_factor | |
| return max(0, heating_load) | |
| def calculate_floor_heating_load(self, floor: Floor, ground_temp: float, indoor_temp: float) -> float: | |
| """ | |
| Calculate heating load through a floor with thermal mass effects. | |
| Args: | |
| floor: Floor object | |
| ground_temp: Ground or adjacent space temperature in °C | |
| indoor_temp: Indoor temperature in °C | |
| Returns: | |
| Heating load in W | |
| """ | |
| self.validate_inputs(ground_temp, 80.0, floor.area, floor.u_value) | |
| u_value = floor.u_value | |
| area = floor.area | |
| delta_t = indoor_temp - ground_temp | |
| # Apply thermal mass effect | |
| thermal_mass = getattr(floor, "thermal_mass", 150000) # J/K, default | |
| time_constant = getattr(floor, "time_constant", 2.5) # hours, default | |
| lag_factor = self.heat_transfer.thermal_lag_factor(thermal_mass, time_constant, 1.0) | |
| heating_load = u_value * area * delta_t * lag_factor | |
| return max(0, heating_load) | |
| def calculate_window_heating_load(self, window: Window, outdoor_temp: float, indoor_temp: float) -> float: | |
| """ | |
| Calculate heating load through a window using steady-state conduction. | |
| Args: | |
| window: Window object | |
| outdoor_temp: Outdoor temperature in °C | |
| indoor_temp: Indoor temperature in °C | |
| Returns: | |
| Heating load in W | |
| """ | |
| self.validate_inputs(outdoor_temp, 80.0, window.area, window.u_value) | |
| u_value = window.u_value | |
| area = window.area | |
| delta_t = indoor_temp - outdoor_temp | |
| heating_load = u_value * area * delta_t | |
| return max(0, heating_load) | |
| def calculate_door_heating_load(self, door: Door, outdoor_temp: float, indoor_temp: float) -> float: | |
| """ | |
| Calculate heating load through a door using steady-state conduction. | |
| Args: | |
| door: Door object | |
| outdoor_temp: Outdoor temperature in °C | |
| indoor_temp: Indoor temperature in °C | |
| Returns: | |
| Heating load in W | |
| """ | |
| self.validate_inputs(outdoor_temp, 80.0, door.area, door.u_value) | |
| u_value = door.u_value | |
| area = door.area | |
| delta_t = indoor_temp - outdoor_temp | |
| heating_load = u_value * area * delta_t | |
| return max(0, heating_load) | |
| def calculate_infiltration_heating_load(self, building_volume: float, outdoor_temp: float, | |
| indoor_temp: float, outdoor_rh: float, indoor_rh: float, | |
| wind_speed: float = 4.0, height: float = 3.0, | |
| crack_length: float = 10.0) -> Dict[str, float]: | |
| """ | |
| Calculate sensible and latent heating loads due to pressure-driven infiltration. | |
| Args: | |
| building_volume: Building volume in m³ | |
| outdoor_temp: Outdoor temperature in °C | |
| indoor_temp: Indoor temperature in °C | |
| outdoor_rh: Outdoor relative humidity in % | |
| indoor_rh: Indoor relative humidity in % | |
| wind_speed: Wind speed in m/s (default: 4.0 m/s) | |
| height: Building height in m (default: 3.0 m) | |
| crack_length: Total crack length in m (default: 10.0 m) | |
| Returns: | |
| Dictionary with sensible, latent, and total heating loads in W | |
| """ | |
| self.validate_inputs(outdoor_temp, outdoor_rh, building_volume, 0.0) | |
| # Calculate infiltration flow rate | |
| wind_pd = self.heat_transfer.wind_pressure_difference(wind_speed, wind_coefficient=0.4) | |
| stack_pd = self.heat_transfer.stack_pressure_difference( | |
| height=height, | |
| indoor_temp=indoor_temp + 273.15, | |
| outdoor_temp=outdoor_temp + 273.15 | |
| ) | |
| total_pd = self.heat_transfer.combined_pressure_difference(wind_pd, stack_pd) | |
| flow_rate = self.heat_transfer.crack_method_infiltration( | |
| crack_length=crack_length, | |
| coefficient=0.0001, | |
| pressure_difference=total_pd | |
| ) | |
| # Calculate sensible heating load | |
| sensible_load = self.heat_transfer.infiltration_heat_transfer( | |
| flow_rate=flow_rate, | |
| delta_t=indoor_temp - outdoor_temp | |
| ) | |
| # Calculate humidity ratios | |
| w_outdoor = self.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh) | |
| w_indoor = self.psychrometrics.humidity_ratio(indoor_temp, indoor_rh) | |
| # Calculate latent heating load | |
| delta_w = w_indoor - w_outdoor | |
| latent_load = self.heat_transfer.infiltration_latent_heat_transfer( | |
| flow_rate=flow_rate, | |
| delta_w=delta_w | |
| ) if delta_w > 0 else 0 | |
| total_load = sensible_load + latent_load | |
| return { | |
| "sensible": max(0, sensible_load), | |
| "latent": max(0, latent_load), | |
| "total": max(0, total_load) | |
| } | |
| def calculate_ventilation_heating_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float, | |
| outdoor_rh: float, indoor_rh: float) -> Dict[str, float]: | |
| """ | |
| Calculate sensible and latent heating loads due to ventilation. | |
| Args: | |
| flow_rate: Ventilation flow rate in m³/s | |
| outdoor_temp: Outdoor temperature in °C | |
| indoor_temp: Indoor temperature in °C | |
| outdoor_rh: Outdoor relative humidity in % | |
| indoor_rh: Indoor relative humidity in % | |
| Returns: | |
| Dictionary with sensible, latent, and total heating loads in W | |
| """ | |
| self.validate_inputs(outdoor_temp, outdoor_rh, 0.0, 0.0) | |
| sensible_load = self.heat_transfer.infiltration_heat_transfer( | |
| flow_rate=flow_rate, | |
| delta_t=indoor_temp - outdoor_temp | |
| ) | |
| w_outdoor = self.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh) | |
| w_indoor = self.psychrometrics.humidity_ratio(indoor_temp, indoor_rh) | |
| delta_w = w_indoor - w_outdoor | |
| latent_load = self.heat_transfer.infiltration_latent_heat_transfer( | |
| flow_rate=flow_rate, | |
| delta_w=delta_w | |
| ) if delta_w > 0 else 0 | |
| total_load = sensible_load + latent_load | |
| return { | |
| "sensible": max(0, sensible_load), | |
| "latent": max(0, latent_load), | |
| "total": max(0, total_load) | |
| } | |
| def calculate_internal_gains_offset(self, people_load: float, lights_load: float, | |
| equipment_load: float, usage_factor: float = 0.7, | |
| hour: int = 0, schedule: Optional[List[float]] = None) -> float: | |
| """ | |
| Calculate internal gains offset for heating load with schedule. | |
| Args: | |
| people_load: Heat gain from people in W | |
| lights_load: Heat gain from lights in W | |
| equipment_load: Heat gain from equipment in W | |
| usage_factor: Usage factor for internal gains (0-1) | |
| hour: Hour of the day (0-23) | |
| schedule: List of 24 hourly gain factors (0-1, default: None) | |
| Returns: | |
| Internal gains offset in W | |
| """ | |
| total_gains = people_load + lights_load + equipment_load | |
| schedule_factor = 1.0 | |
| if schedule and len(schedule) == 24: | |
| schedule_factor = schedule[hour] | |
| offset = total_gains * usage_factor * schedule_factor | |
| return max(0, offset) | |
| def calculate_design_heating_load(self, building_components: Dict[str, List[Any]], | |
| outdoor_conditions: Dict[str, Any], | |
| indoor_conditions: Dict[str, Any], | |
| internal_loads: Dict[str, Any], | |
| building_volume: float = 300.0, | |
| safety_factor: float = 1.15) -> Dict[str, float]: | |
| """ | |
| Calculate design heating load for a building with enhanced calculations. | |
| Args: | |
| building_components: Dictionary with lists of building components | |
| outdoor_conditions: Dictionary with outdoor conditions | |
| indoor_conditions: Dictionary with indoor conditions | |
| internal_loads: Dictionary with internal loads | |
| building_volume: Building volume in m³ (default: 300 m³) | |
| safety_factor: Safety factor for heating load (default: 1.15) | |
| Returns: | |
| Dictionary with design heating loads | |
| """ | |
| walls = building_components.get("walls", []) | |
| roofs = building_components.get("roofs", []) | |
| floors = building_components.get("floors", []) | |
| windows = building_components.get("windows", []) | |
| doors = building_components.get("doors", []) | |
| outdoor_temp = outdoor_conditions.get("design_temperature", -10.0) | |
| outdoor_rh = outdoor_conditions.get("design_relative_humidity", 80.0) | |
| ground_temp = outdoor_conditions.get("ground_temperature", 10.0) | |
| wind_speed = outdoor_conditions.get("wind_speed", 4.0) | |
| indoor_temp = indoor_conditions.get("temperature", 21.0) | |
| indoor_rh = indoor_conditions.get("relative_humidity", 40.0) | |
| people = internal_loads.get("people", {}) | |
| lights = internal_loads.get("lights", {}) | |
| equipment = internal_loads.get("equipment", {}) | |
| infiltration = internal_loads.get("infiltration", {}) | |
| ventilation = internal_loads.get("ventilation", {}) | |
| loads = { | |
| "walls": 0, | |
| "roofs": 0, | |
| "floors": 0, | |
| "windows": 0, | |
| "doors": 0, | |
| "infiltration_sensible": 0, | |
| "infiltration_latent": 0, | |
| "ventilation_sensible": 0, | |
| "ventilation_latent": 0, | |
| "internal_gains_offset": 0, | |
| "subtotal": 0, | |
| "safety_factor": safety_factor, | |
| "total": 0 | |
| } | |
| # Calculate loads | |
| for wall in walls: | |
| loads["walls"] += self.calculate_wall_heating_load(wall, outdoor_temp, indoor_temp) | |
| for roof in roofs: | |
| loads["roofs"] += self.calculate_roof_heating_load(roof, outdoor_temp, indoor_temp) | |
| for floor in floors: | |
| loads["floors"] += self.calculate_floor_heating_load(floor, ground_temp, indoor_temp) | |
| for window in windows: | |
| loads["windows"] += self.calculate_window_heating_load(window, outdoor_temp, indoor_temp) | |
| for door in doors: | |
| loads["doors"] += self.calculate_door_heating_load(door, outdoor_temp, indoor_temp) | |
| if infiltration: | |
| infiltration_loads = self.calculate_infiltration_heating_load( | |
| building_volume=building_volume, | |
| outdoor_temp=outdoor_temp, | |
| indoor_temp=indoor_temp, | |
| outdoor_rh=outdoor_rh, | |
| indoor_rh=indoor_rh, | |
| wind_speed=wind_speed, | |
| height=infiltration.get("height", 3.0), | |
| crack_length=infiltration.get("crack_length", 10.0) | |
| ) | |
| loads["infiltration_sensible"] = infiltration_loads["sensible"] | |
| loads["infiltration_latent"] = infiltration_loads["latent"] | |
| if ventilation: | |
| flow_rate = ventilation.get("flow_rate", 0.0) | |
| ventilation_loads = self.calculate_ventilation_heating_load( | |
| flow_rate=flow_rate, | |
| outdoor_temp=outdoor_temp, | |
| indoor_temp=indoor_temp, | |
| outdoor_rh=outdoor_rh, | |
| indoor_rh=indoor_rh | |
| ) | |
| loads["ventilation_sensible"] = ventilation_loads["sensible"] | |
| loads["ventilation_latent"] = ventilation_loads["latent"] | |
| people_load = people.get("number", 0) * people.get("sensible_gain", 70) | |
| lights_load = lights.get("power", 0) * lights.get("use_factor", 1.0) | |
| equipment_load = equipment.get("power", 0) * equipment.get("use_factor", 1.0) | |
| loads["internal_gains_offset"] = self.calculate_internal_gains_offset( | |
| people_load=people_load, | |
| lights_load=lights_load, | |
| equipment_load=equipment_load, | |
| usage_factor=internal_loads.get("usage_factor", 0.7), | |
| hour=0, | |
| schedule=internal_loads.get("gains_schedule", None) | |
| ) | |
| loads["subtotal"] = ( | |
| loads["walls"] + loads["roofs"] + loads["floors"] + | |
| loads["windows"] + loads["doors"] + | |
| loads["infiltration_sensible"] + loads["infiltration_latent"] + | |
| loads["ventilation_sensible"] + loads["ventilation_latent"] - | |
| loads["internal_gains_offset"] | |
| ) | |
| loads["total"] = loads["subtotal"] * safety_factor | |
| return loads | |
| def calculate_heating_load_summary(self, design_loads: Dict[str, float]) -> Dict[str, float]: | |
| """ | |
| Calculate heating load summary. | |
| Args: | |
| design_loads: Dictionary with design heating loads | |
| Returns: | |
| Dictionary with heating load summary | |
| """ | |
| envelope_loads = sum([ | |
| design_loads["walls"], design_loads["roofs"], design_loads["floors"], | |
| design_loads["windows"], design_loads["doors"] | |
| ]) | |
| ventilation_loads = design_loads["ventilation_sensible"] + design_loads["ventilation_latent"] | |
| infiltration_loads = design_loads["infiltration_sensible"] + design_loads["infiltration_latent"] | |
| return { | |
| "envelope_loads": envelope_loads, | |
| "ventilation_loads": ventilation_loads, | |
| "infiltration_loads": infiltration_loads, | |
| "internal_gains_offset": design_loads["internal_gains_offset"], | |
| "subtotal": design_loads["subtotal"], | |
| "safety_factor": design_loads["safety_factor"], | |
| "total": design_loads["total"] | |
| } | |
| def calculate_monthly_heating_loads(self, design_loads: Dict[str, float], | |
| monthly_temps: Dict[str, float], | |
| design_temp: float, indoor_temp: float) -> Dict[str, float]: | |
| """ | |
| Calculate monthly heating loads based on design load and monthly temperatures. | |
| Args: | |
| design_loads: Dictionary with design heating loads | |
| monthly_temps: Dictionary with monthly average temperatures | |
| design_temp: Design outdoor temperature in °C | |
| indoor_temp: Indoor temperature in °C | |
| Returns: | |
| Dictionary with monthly heating loads | |
| """ | |
| design_delta_t = indoor_temp - design_temp | |
| monthly_loads = {} | |
| for month, temp in monthly_temps.items(): | |
| delta_t = indoor_temp - temp | |
| if delta_t <= 0: | |
| monthly_loads[month] = 0 | |
| continue | |
| load_ratio = delta_t / design_delta_t | |
| monthly_loads[month] = design_loads["total"] * load_ratio | |
| return monthly_loads | |
| def calculate_heating_degree_days(self, monthly_temps: Dict[str, float], | |
| base_temp: float = 18.0) -> Dict[str, float]: | |
| """ | |
| Calculate heating degree days for each month with precise days. | |
| Args: | |
| monthly_temps: Dictionary with monthly average temperatures | |
| base_temp: Base temperature for degree days in °C (default: 18°C) | |
| Returns: | |
| Dictionary with monthly heating degree days | |
| """ | |
| days_per_month = { | |
| "Jan": 31, "Feb": 28, "Mar": 31, "Apr": 30, "May": 31, "Jun": 30, | |
| "Jul": 31, "Aug": 31, "Sep": 30, "Oct": 31, "Nov": 30, "Dec": 31 | |
| } | |
| monthly_hdds = {} | |
| for month, temp in monthly_temps.items(): | |
| daily_hdd = max(0, base_temp - temp) | |
| monthly_hdds[month] = daily_hdd * days_per_month[month] | |
| return monthly_hdds | |
| def calculate_annual_heating_energy(self, monthly_loads: Dict[str, float], | |
| heating_system_efficiency: float = 0.8) -> Dict[str, float]: | |
| """ | |
| Calculate annual heating energy consumption with precise days. | |
| Args: | |
| monthly_loads: Dictionary with monthly heating loads in W | |
| heating_system_efficiency: Heating system efficiency (0-1) | |
| Returns: | |
| Dictionary with monthly and annual heating energy in kWh | |
| """ | |
| days_per_month = { | |
| "Jan": 31, "Feb": 28, "Mar": 31, "Apr": 30, "May": 31, "Jun": 30, | |
| "Jul": 31, "Aug": 31, "Sep": 30, "Oct": 31, "Nov": 30, "Dec": 31 | |
| } | |
| monthly_energy = {} | |
| annual_energy = 0 | |
| for month, load in monthly_loads.items(): | |
| hours_in_month = 24 * days_per_month[month] | |
| energy = load * hours_in_month / 1000 / heating_system_efficiency | |
| monthly_energy[month] = energy | |
| annual_energy += energy | |
| monthly_energy["annual"] = annual_energy | |
| return monthly_energy | |
| # Create a singleton instance | |
| heating_load_calculator = HeatingLoadCalculator() | |
| # Example usage | |
| if __name__ == "__main__": | |
| from data.building_components import Wall, Roof, Window, Door, Orientation, ComponentType | |
| wall = Wall( | |
| id="wall1", | |
| name="Exterior Wall", | |
| component_type=ComponentType.WALL, | |
| u_value=0.5, | |
| area=20.0, | |
| orientation=Orientation.NORTH, | |
| wall_type="Brick", | |
| wall_group="B", | |
| thermal_mass=100000, | |
| time_constant=2.0 | |
| ) | |
| roof = Roof( | |
| id="roof1", | |
| name="Flat Roof", | |
| component_type=ComponentType.ROOF, | |
| u_value=0.3, | |
| area=50.0, | |
| orientation=Orientation.HORIZONTAL, | |
| roof_type="Concrete", | |
| roof_group="C", | |
| thermal_mass=200000, | |
| time_constant=3.0 | |
| ) | |
| window = Window( | |
| id="window1", | |
| name="North Window", | |
| component_type=ComponentType.WINDOW, | |
| u_value=2.8, | |
| area=5.0, | |
| orientation=Orientation.NORTH, | |
| shgc=0.7, | |
| vt=0.8, | |
| window_type="Double Glazed", | |
| glazing_layers=2, | |
| gas_fill="Air", | |
| low_e_coating=False | |
| ) | |
| building_components = { | |
| "walls": [wall], | |
| "roofs": [roof], | |
| "windows": [window], | |
| "doors": [], | |
| "floors": [] | |
| } | |
| outdoor_conditions = { | |
| "design_temperature": -10.0, | |
| "design_relative_humidity": 80.0, | |
| "ground_temperature": 10.0, | |
| "wind_speed": 4.0 | |
| } | |
| indoor_conditions = { | |
| "temperature": 21.0, | |
| "relative_humidity": 40.0 | |
| } | |
| gains_schedule = [0.2] * 8 + [0.8] * 8 + [0.4] * 8 # 8h low, 8h high, 8h medium | |
| internal_loads = { | |
| "people": { | |
| "number": 3, | |
| "sensible_gain": 70 | |
| }, | |
| "lights": { | |
| "power": 500.0, | |
| "use_factor": 0.9 | |
| }, | |
| "equipment": { | |
| "power": 1000.0, | |
| "use_factor": 0.7 | |
| }, | |
| "infiltration": { | |
| "height": 3.0, | |
| "crack_length": 10.0 | |
| }, | |
| "ventilation": { | |
| "flow_rate": 0.1 | |
| }, | |
| "usage_factor": 0.7, | |
| "gains_schedule": gains_schedule | |
| } | |
| design_loads = heating_load_calculator.calculate_design_heating_load( | |
| building_components=building_components, | |
| outdoor_conditions=outdoor_conditions, | |
| indoor_conditions=indoor_conditions, | |
| internal_loads=internal_loads, | |
| building_volume=300.0 | |
| ) | |
| summary = heating_load_calculator.calculate_heating_load_summary(design_loads) | |
| monthly_temps = { | |
| "Jan": -5.0, "Feb": -3.0, "Mar": 2.0, "Apr": 8.0, "May": 14.0, | |
| "Jun": 18.0, "Jul": 21.0, "Aug": 20.0, "Sep": 16.0, "Oct": 10.0, | |
| "Nov": 4.0, "Dec": -2.0 | |
| } | |
| monthly_loads = heating_load_calculator.calculate_monthly_heating_loads( | |
| design_loads=design_loads, | |
| monthly_temps=monthly_temps, | |
| design_temp=outdoor_conditions["design_temperature"], | |
| indoor_temp=indoor_conditions["temperature"] | |
| ) | |
| hdds = heating_load_calculator.calculate_heating_degree_days(monthly_temps) | |
| energy = heating_load_calculator.calculate_annual_heating_energy(monthly_loads) | |
| print("Heating Load Summary:") | |
| for key, value in summary.items(): | |
| print(f"{key.replace('_', ' ').title()}: {value:.2f} W") | |
| print("\nMonthly Heating Loads:") | |
| for month, load in monthly_loads.items(): | |
| print(f"{month}: {load:.2f} W") | |
| print("\nHeating Degree Days:") | |
| for month, hdd in hdds.items(): | |
| print(f"{month}: {hdd:.2f} HDD") | |
| print("\nAnnual Heating Energy:") | |
| print(f"Total: {energy['annual']:.2f} kWh") |