| """ |
| Cooling load calculation module for HVAC Load Calculator. |
| This module implements the CLTD/CLF method for calculating cooling loads. |
| """ |
|
|
| 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 |
|
|
| |
| from data.building_components import Wall, Roof, Floor, Window, Door, Orientation, ComponentType |
| from data.ashrae_tables import ashrae_tables |
| from utils.psychrometrics import Psychrometrics |
| from utils.heat_transfer import HeatTransferCalculations |
|
|
| |
| DATA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
|
|
|
|
| class CoolingLoadCalculator: |
| """Class for calculating cooling loads using the CLTD/CLF method.""" |
| |
| def __init__(self): |
| """Initialize cooling load calculator.""" |
| self.heat_transfer = HeatTransferCalculations() |
| self.psychrometrics = Psychrometrics() |
| self.ashrae_tables = ashrae_tables |
| |
| def calculate_wall_cooling_load(self, wall: Wall, outdoor_temp: float, indoor_temp: float, |
| month: str, hour: int, latitude: str = "40N", |
| color: str = "Dark") -> float: |
| """ |
| Calculate cooling load through a wall using the CLTD method. |
| |
| Args: |
| wall: Wall object |
| outdoor_temp: Outdoor temperature in °C |
| indoor_temp: Indoor temperature in °C |
| month: Month (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec) |
| hour: Hour of the day (0-23) |
| latitude: Latitude (24N, 32N, 40N, 48N, 56N) |
| color: Surface color (Dark, Medium, Light) |
| |
| Returns: |
| Cooling load in W |
| """ |
| |
| u_value = wall.u_value |
| area = wall.area |
| orientation = wall.orientation.value |
| wall_group = wall.wall_group |
| |
| |
| cltd = self.ashrae_tables.calculate_corrected_cltd_wall( |
| wall_group=wall_group, |
| orientation=orientation, |
| hour=hour, |
| color=color, |
| month=month, |
| latitude=latitude, |
| indoor_temp=indoor_temp, |
| outdoor_temp=outdoor_temp |
| ) |
| |
| |
| cooling_load = u_value * area * cltd |
| |
| return cooling_load |
| |
| def calculate_roof_cooling_load(self, roof: Roof, outdoor_temp: float, indoor_temp: float, |
| month: str, hour: int, latitude: str = "40N", |
| color: str = "Dark") -> float: |
| """ |
| Calculate cooling load through a roof using the CLTD method. |
| |
| Args: |
| roof: Roof object |
| outdoor_temp: Outdoor temperature in °C |
| indoor_temp: Indoor temperature in °C |
| month: Month (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec) |
| hour: Hour of the day (0-23) |
| latitude: Latitude (24N, 32N, 40N, 48N, 56N) |
| color: Surface color (Dark, Medium, Light) |
| |
| Returns: |
| Cooling load in W |
| """ |
| |
| u_value = roof.u_value |
| area = roof.area |
| roof_group = roof.roof_group |
| |
| |
| cltd = self.ashrae_tables.calculate_corrected_cltd_roof( |
| roof_group=roof_group, |
| hour=hour, |
| color=color, |
| month=month, |
| latitude=latitude, |
| indoor_temp=indoor_temp, |
| outdoor_temp=outdoor_temp |
| ) |
| |
| |
| cooling_load = u_value * area * cltd |
| |
| return cooling_load |
| |
| def calculate_window_cooling_load(self, window: Window, outdoor_temp: float, indoor_temp: float, |
| month: str, hour: int, latitude: str = "40N_JUL", |
| shading_coefficient: float = 1.0) -> Dict[str, float]: |
| """ |
| Calculate cooling load through a window using the CLTD/SCL method. |
| |
| Args: |
| window: Window object |
| outdoor_temp: Outdoor temperature in °C |
| indoor_temp: Indoor temperature in °C |
| month: Month (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec) |
| hour: Hour of the day (0-23) |
| latitude: Latitude and month key (default: "40N_JUL") |
| shading_coefficient: Shading coefficient (0-1) |
| |
| Returns: |
| Dictionary with conduction, solar, and total cooling loads in W |
| """ |
| |
| u_value = window.u_value |
| area = window.area |
| orientation = window.orientation.value |
| shgc = window.shgc |
| |
| |
| delta_t = outdoor_temp - indoor_temp |
| conduction_load = u_value * area * delta_t |
| |
| |
| scl = self.ashrae_tables.get_scl(orientation, hour, latitude) |
| solar_load = area * shgc * shading_coefficient * scl |
| |
| |
| total_load = conduction_load + solar_load |
| |
| return { |
| "conduction": conduction_load, |
| "solar": solar_load, |
| "total": total_load |
| } |
| |
| def calculate_door_cooling_load(self, door: Door, outdoor_temp: float, indoor_temp: float) -> float: |
| """ |
| Calculate cooling load through a door using simple conduction. |
| |
| Args: |
| door: Door object |
| outdoor_temp: Outdoor temperature in °C |
| indoor_temp: Indoor temperature in °C |
| |
| Returns: |
| Cooling load in W |
| """ |
| |
| u_value = door.u_value |
| area = door.area |
| |
| |
| delta_t = outdoor_temp - indoor_temp |
| cooling_load = u_value * area * delta_t |
| |
| return cooling_load |
| |
| def calculate_floor_cooling_load(self, floor: Floor, ground_temp: float, indoor_temp: float) -> float: |
| """ |
| Calculate cooling load through a floor. |
| |
| Args: |
| floor: Floor object |
| ground_temp: Ground or adjacent space temperature in °C |
| indoor_temp: Indoor temperature in °C |
| |
| Returns: |
| Cooling load in W |
| """ |
| |
| u_value = floor.u_value |
| area = floor.area |
| |
| |
| delta_t = ground_temp - indoor_temp |
| cooling_load = u_value * area * delta_t |
| |
| |
| return max(0, cooling_load) |
| |
| def calculate_infiltration_cooling_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float, |
| outdoor_rh: float, indoor_rh: float) -> Dict[str, float]: |
| """ |
| Calculate sensible and latent cooling loads due to infiltration. |
| |
| Args: |
| flow_rate: Infiltration 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 cooling loads in W |
| """ |
| |
| sensible_load = self.heat_transfer.infiltration_heat_transfer( |
| flow_rate=flow_rate, |
| delta_t=outdoor_temp - indoor_temp |
| ) |
| |
| |
| w_outdoor = self.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh) |
| w_indoor = self.psychrometrics.humidity_ratio(indoor_temp, indoor_rh) |
| |
| |
| latent_load = self.heat_transfer.infiltration_latent_heat_transfer( |
| flow_rate=flow_rate, |
| delta_w=w_outdoor - w_indoor |
| ) |
| |
| |
| total_load = sensible_load + latent_load |
| |
| return { |
| "sensible": sensible_load, |
| "latent": latent_load, |
| "total": total_load |
| } |
| |
| def calculate_ventilation_cooling_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float, |
| outdoor_rh: float, indoor_rh: float) -> Dict[str, float]: |
| """ |
| Calculate sensible and latent cooling 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 cooling loads in W |
| """ |
| |
| return self.calculate_infiltration_cooling_load( |
| flow_rate=flow_rate, |
| outdoor_temp=outdoor_temp, |
| indoor_temp=indoor_temp, |
| outdoor_rh=outdoor_rh, |
| indoor_rh=indoor_rh |
| ) |
| |
| def calculate_people_cooling_load(self, num_people: int, activity_level: str, |
| hours_occupancy: str, hour: int) -> Dict[str, float]: |
| """ |
| Calculate sensible and latent cooling loads due to people. |
| |
| Args: |
| num_people: Number of people |
| activity_level: Activity level (Seated/Resting, Light work, Medium work, Heavy work) |
| hours_occupancy: Hours of occupancy (8h, 10h, 12h, 14h, 16h, 18h, 24h) |
| hour: Hour of the day (0-23) |
| |
| Returns: |
| Dictionary with sensible, latent, and total cooling loads in W |
| """ |
| |
| activity_gains = { |
| "Seated/Resting": {"sensible": 70, "latent": 45}, |
| "Light work": {"sensible": 75, "latent": 55}, |
| "Medium work": {"sensible": 85, "latent": 80}, |
| "Heavy work": {"sensible": 95, "latent": 145} |
| } |
| |
| |
| if activity_level not in activity_gains: |
| raise ValueError(f"Invalid activity level: {activity_level}") |
| |
| sensible_gain = activity_gains[activity_level]["sensible"] |
| latent_gain = activity_gains[activity_level]["latent"] |
| |
| |
| clf = self.ashrae_tables.get_clf_people(hour, hours_occupancy) |
| |
| |
| sensible_load = num_people * sensible_gain * clf |
| latent_load = num_people * latent_gain |
| total_load = sensible_load + latent_load |
| |
| return { |
| "sensible": sensible_load, |
| "latent": latent_load, |
| "total": total_load, |
| "activity_level": activity_level, |
| "sensible_gain_per_person": sensible_gain, |
| "latent_gain_per_person": latent_gain |
| } |
| |
| def calculate_lights_cooling_load(self, power: float, use_factor: float, |
| special_allowance: float, hours_operation: str, |
| hour: int, lighting_type: str = "Fluorescent") -> Dict[str, float]: |
| """ |
| Calculate cooling load due to lights. |
| |
| Args: |
| power: Installed lighting power in W |
| use_factor: Usage factor (0-1) |
| special_allowance: Special allowance factor for fixtures (0-1) |
| hours_operation: Hours of operation (8h, 10h, 12h, 14h, 16h, 18h, 24h) |
| hour: Hour of the day (0-23) |
| lighting_type: Type of lighting (LED, Fluorescent, Halogen, Incandescent) |
| |
| Returns: |
| Dictionary with cooling load information |
| """ |
| |
| |
| lighting_factors = { |
| "LED": 0.80, |
| "Fluorescent": 0.85, |
| "Halogen": 0.95, |
| "Incandescent": 0.98 |
| } |
| |
| |
| if lighting_type not in lighting_factors: |
| raise ValueError(f"Invalid lighting type: {lighting_type}") |
| |
| heat_emission_factor = lighting_factors[lighting_type] |
| |
| |
| clf = self.ashrae_tables.get_clf_lights(hour, hours_operation) |
| |
| |
| cooling_load = power * use_factor * (1 + special_allowance) * heat_emission_factor * clf |
| |
| return { |
| "total": cooling_load, |
| "lighting_type": lighting_type, |
| "heat_emission_factor": heat_emission_factor, |
| "clf": clf |
| } |
| |
| def calculate_equipment_cooling_load(self, power: float, use_factor: float, |
| radiation_factor: float, hours_operation: str, |
| hour: int, equipment_type: str = "General") -> Dict[str, float]: |
| """ |
| Calculate sensible and latent cooling loads due to equipment. |
| |
| Args: |
| power: Equipment power in W |
| use_factor: Usage factor (0-1) |
| radiation_factor: Radiation factor (0-1) |
| hours_operation: Hours of operation (8h, 10h, 12h, 14h, 16h, 18h, 24h) |
| hour: Hour of the day (0-23) |
| equipment_type: Type of equipment (General, Computer, Kitchen, Medical, Laboratory) |
| |
| Returns: |
| Dictionary with sensible, latent, and total cooling loads in W |
| """ |
| |
| |
| equipment_factors = { |
| "General": {"sensible_factor": 1.0, "latent_factor": 0.0}, |
| "Computer": {"sensible_factor": 0.95, "latent_factor": 0.0}, |
| "Kitchen": {"sensible_factor": 0.80, "latent_factor": 0.20}, |
| "Medical": {"sensible_factor": 0.90, "latent_factor": 0.10}, |
| "Laboratory": {"sensible_factor": 0.85, "latent_factor": 0.15} |
| } |
| |
| |
| if equipment_type not in equipment_factors: |
| raise ValueError(f"Invalid equipment type: {equipment_type}") |
| |
| sensible_factor = equipment_factors[equipment_type]["sensible_factor"] |
| latent_factor = equipment_factors[equipment_type]["latent_factor"] |
| |
| |
| clf = self.ashrae_tables.get_clf_equipment(hour, hours_operation) |
| |
| |
| adjusted_power = power * use_factor |
| |
| |
| sensible_load = adjusted_power * sensible_factor * radiation_factor * clf |
| |
| |
| latent_load = adjusted_power * latent_factor |
| |
| |
| total_load = sensible_load + latent_load |
| |
| return { |
| "sensible": sensible_load, |
| "latent": latent_load, |
| "total": total_load, |
| "equipment_type": equipment_type, |
| "sensible_factor": sensible_factor, |
| "latent_factor": latent_factor, |
| "clf": clf |
| } |
| |
| def calculate_hourly_cooling_loads(self, building_components: Dict[str, List[Any]], |
| outdoor_conditions: Dict[str, Any], |
| indoor_conditions: Dict[str, Any], |
| internal_loads: Dict[str, Any]) -> Dict[int, Dict[str, float]]: |
| """ |
| Calculate hourly cooling loads for a building. |
| |
| 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 |
| |
| Returns: |
| Dictionary with hourly cooling 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("temperature", 35.0) |
| outdoor_rh = outdoor_conditions.get("relative_humidity", 50.0) |
| ground_temp = outdoor_conditions.get("ground_temperature", 20.0) |
| month = outdoor_conditions.get("month", "Jul") |
| latitude = outdoor_conditions.get("latitude", "40N") |
| |
| |
| indoor_temp = indoor_conditions.get("temperature", 24.0) |
| indoor_rh = indoor_conditions.get("relative_humidity", 50.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", {}) |
| |
| |
| hourly_loads = {} |
| |
| |
| for hour in range(24): |
| |
| loads = { |
| "walls": 0, |
| "roofs": 0, |
| "floors": 0, |
| "windows_conduction": 0, |
| "windows_solar": 0, |
| "doors": 0, |
| "people_sensible": 0, |
| "people_latent": 0, |
| "lights": 0, |
| "equipment_sensible": 0, |
| "equipment_latent": 0, |
| "infiltration_sensible": 0, |
| "infiltration_latent": 0, |
| "ventilation_sensible": 0, |
| "ventilation_latent": 0 |
| } |
| |
| |
| for wall in walls: |
| loads["walls"] += self.calculate_wall_cooling_load( |
| wall=wall, |
| outdoor_temp=outdoor_temp, |
| indoor_temp=indoor_temp, |
| month=month, |
| hour=hour, |
| latitude=latitude |
| ) |
| |
| |
| for roof in roofs: |
| loads["roofs"] += self.calculate_roof_cooling_load( |
| roof=roof, |
| outdoor_temp=outdoor_temp, |
| indoor_temp=indoor_temp, |
| month=month, |
| hour=hour, |
| latitude=latitude |
| ) |
| |
| |
| for floor in floors: |
| loads["floors"] += self.calculate_floor_cooling_load( |
| floor=floor, |
| ground_temp=ground_temp, |
| indoor_temp=indoor_temp |
| ) |
| |
| |
| for window in windows: |
| window_loads = self.calculate_window_cooling_load( |
| window=window, |
| outdoor_temp=outdoor_temp, |
| indoor_temp=indoor_temp, |
| month=month, |
| hour=hour, |
| latitude=f"{latitude}_{month.upper()}" |
| ) |
| loads["windows_conduction"] += window_loads["conduction"] |
| loads["windows_solar"] += window_loads["solar"] |
| |
| |
| for door in doors: |
| loads["doors"] += self.calculate_door_cooling_load( |
| door=door, |
| outdoor_temp=outdoor_temp, |
| indoor_temp=indoor_temp |
| ) |
| |
| |
| if "number" in people and "activity_level" in people and "hours_occupancy" in people: |
| people_loads = self.calculate_people_cooling_load( |
| num_people=people["number"], |
| activity_level=people["activity_level"], |
| hours_occupancy=people["hours_occupancy"], |
| hour=hour |
| ) |
| loads["people_sensible"] += people_loads["sensible"] |
| loads["people_latent"] += people_loads["latent"] |
| |
| |
| if "power" in lights and "use_factor" in lights and "hours_operation" in lights: |
| lighting_loads = self.calculate_lights_cooling_load( |
| power=lights["power"], |
| use_factor=lights["use_factor"], |
| special_allowance=lights.get("special_allowance", 0.1), |
| hours_operation=lights["hours_operation"], |
| hour=hour, |
| lighting_type=lights.get("lighting_type", "Fluorescent") |
| ) |
| loads["lights"] += lighting_loads["total"] |
| |
| |
| if "power" in equipment and "use_factor" in equipment and "hours_operation" in equipment: |
| equipment_loads = self.calculate_equipment_cooling_load( |
| power=equipment["power"], |
| use_factor=equipment["use_factor"], |
| radiation_factor=equipment.get("radiation_factor", 0.3), |
| hours_operation=equipment["hours_operation"], |
| hour=hour, |
| equipment_type=equipment.get("equipment_type", "General") |
| ) |
| loads["equipment_sensible"] += equipment_loads["sensible"] |
| loads["equipment_latent"] += equipment_loads["latent"] |
| |
| |
| if "flow_rate" in infiltration: |
| infiltration_loads = self.calculate_infiltration_cooling_load( |
| flow_rate=infiltration["flow_rate"], |
| outdoor_temp=outdoor_temp, |
| indoor_temp=indoor_temp, |
| outdoor_rh=outdoor_rh, |
| indoor_rh=indoor_rh |
| ) |
| loads["infiltration_sensible"] += infiltration_loads["sensible"] |
| loads["infiltration_latent"] += infiltration_loads["latent"] |
| |
| |
| if "flow_rate" in ventilation: |
| ventilation_loads = self.calculate_ventilation_cooling_load( |
| flow_rate=ventilation["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"] |
| |
| |
| hourly_loads[hour] = loads |
| |
| return hourly_loads |
| |
| def calculate_design_cooling_load(self, hourly_loads: Dict[int, Dict[str, float]]) -> Dict[str, float]: |
| """ |
| Calculate design cooling load from hourly loads. |
| |
| Args: |
| hourly_loads: Dictionary with hourly cooling loads |
| |
| Returns: |
| Dictionary with design cooling loads |
| """ |
| |
| design_loads = { |
| "walls": 0, |
| "roofs": 0, |
| "floors": 0, |
| "windows_conduction": 0, |
| "windows_solar": 0, |
| "doors": 0, |
| "people_sensible": 0, |
| "people_latent": 0, |
| "lights": 0, |
| "equipment_sensible": 0, |
| "equipment_latent": 0, |
| "infiltration_sensible": 0, |
| "infiltration_latent": 0, |
| "ventilation_sensible": 0, |
| "ventilation_latent": 0, |
| "design_hour": 0 |
| } |
| |
| |
| hourly_totals = {} |
| for hour, loads in hourly_loads.items(): |
| total = sum(loads.values()) |
| hourly_totals[hour] = total |
| |
| |
| design_hour = max(hourly_totals, key=hourly_totals.get) |
| design_loads["design_hour"] = design_hour |
| |
| |
| design_hour_loads = hourly_loads[design_hour] |
| |
| |
| for key, value in design_hour_loads.items(): |
| design_loads[key] = value |
| |
| return design_loads |
| |
| def calculate_cooling_load_summary(self, design_loads: Dict[str, float]) -> Dict[str, float]: |
| """ |
| Calculate cooling load summary from design loads. |
| |
| Args: |
| design_loads: Dictionary with design cooling loads |
| |
| Returns: |
| Dictionary with cooling load summary |
| """ |
| |
| total_sensible = ( |
| design_loads["walls"] + |
| design_loads["roofs"] + |
| design_loads["floors"] + |
| design_loads["windows_conduction"] + |
| design_loads["windows_solar"] + |
| design_loads["doors"] + |
| design_loads["people_sensible"] + |
| design_loads["lights"] + |
| design_loads["equipment_sensible"] + |
| design_loads["infiltration_sensible"] + |
| design_loads["ventilation_sensible"] |
| ) |
| |
| |
| total_latent = ( |
| design_loads["people_latent"] + |
| design_loads["equipment_latent"] + |
| design_loads["infiltration_latent"] + |
| design_loads["ventilation_latent"] |
| ) |
| |
| |
| total = total_sensible + total_latent |
| |
| |
| safety_factor = 0.1 * total |
| |
| |
| grand_total = total + safety_factor |
| |
| return { |
| "total_sensible": total_sensible, |
| "total_latent": total_latent, |
| "total": total, |
| "safety_factor": safety_factor, |
| "grand_total": grand_total |
| } |
|
|