Download data/calculation.py from mabuseif/CTF-TFM: direct link, hf CLI and curl.
- Browser
- Download file 35 kB
-
https://huggingface.co/spaces/mabuseif/CTF-TFM/resolve/main/data/calculation.py
- Command line
-
hf download hf://spaces/mabuseif/CTF-TFM/data/calculation.py
-
curl -L -o calculation.py https://huggingface.co/spaces/mabuseif/CTF-TFM/resolve/main/data/calculation.py
35 kB
| """ | |
| HVAC Calculator Code Documentation | |
| Developed by: Dr Majed Abuseif, Deakin University | |
| © 2025 | |
| """ | |
| import numpy as np | |
| import pandas as pd | |
| from typing import Dict, List, Optional, NamedTuple, Any | |
| from enum import Enum | |
| from data.material_library import Construction, GlazingMaterial, DoorMaterial | |
| from data.internal_loads import PEOPLE_ACTIVITY_LEVELS, DIVERSITY_FACTORS, LIGHTING_FIXTURE_TYPES, EQUIPMENT_HEAT_GAINS, VENTILATION_RATES, INFILTRATION_SETTINGS | |
| from datetime import datetime | |
| from collections import defaultdict | |
| import logging | |
| import streamlit as st | |
| import plotly.graph_objects as go | |
| from utils.ctf_calculations import CTFCalculator, ComponentType, CTFCoefficients | |
| import math | |
| # Configure logging | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger(__name__) | |
| # Initialize session_state for climate data | |
| if "climate_data" not in st.session_state: | |
| st.session_state["climate_data"] = { | |
| "latitude": 0.0, | |
| "longitude": 0.0, | |
| "timezone": 0.0 | |
| } | |
| class SolarCalculations: | |
| """Class for performing solar radiation and angle calculations based on ASHRAE methodologies.""" | |
| def day_of_year(month: int, day: int, year: int) -> int: | |
| """Calculate day of the year (n) from month, day, and year, accounting for leap years.""" | |
| days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] | |
| if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0): | |
| days_in_month[1] = 29 | |
| return sum(days_in_month[:month-1]) + day | |
| def equation_of_time(n: int) -> float: | |
| """Calculate Equation of Time (EOT) in minutes using Spencer's formula.""" | |
| B = (n - 1) * 360 / 365 | |
| B_rad = math.radians(B) | |
| EOT = 229.2 * (0.000075 + 0.001868 * math.cos(B_rad) - 0.032077 * math.sin(B_rad) - | |
| 0.014615 * math.cos(2 * B_rad) - 0.04089 * math.sin(2 * B_rad)) | |
| return EOT | |
| def calculate_solar_parameters( | |
| hourly_data: List[Dict[str, Any]], | |
| latitude: float, | |
| longitude: float, | |
| timezone: float, | |
| ground_reflectivity: float, | |
| components: Dict | |
| ) -> List[Dict[str, Any]]: | |
| """Calculate solar angles and ground-reflected radiation for hourly data with GHI > 0.""" | |
| # Display input parameters in Streamlit UI | |
| st.write("### Input Parameters") | |
| st.write(f"- **Latitude**: {latitude}°") | |
| st.write(f"- **Longitude**: {longitude}°") | |
| st.write(f"- **Timezone**: {timezone} hours") | |
| st.write(f"- **Ground Reflectivity**: {ground_reflectivity}") | |
| year = 2025 | |
| st.write(f"- **Year**: {year}") | |
| st.write("") # Add spacing | |
| results = [] | |
| lambda_std = 15 * timezone # Standard meridian longitude (°) | |
| first_hour = True | |
| for record in hourly_data: | |
| if record["global_horizontal_radiation"] <= 0: | |
| continue # Skip hours with no solar radiation | |
| # Step 1: Extract data | |
| month = record["month"] | |
| day = record["day"] | |
| hour = record["hour"] | |
| ghi = record["global_horizontal_radiation"] | |
| dni = record["direct_normal_radiation"] | |
| dhi = record["diffuse_horizontal_radiation"] | |
| if first_hour: | |
| st.write(f"### Calculations for First Hour (Month: {month}, Day: {day}, Hour: {hour})") | |
| st.write(f"- **Global Horizontal Radiation (GHI)**: {ghi} W/m²") | |
| st.write(f"- **Direct Normal Radiation (DNI)**: {dni} W/m²") | |
| st.write(f"- **Diffuse Horizontal Radiation (DHI)**: {dhi} W/m²") | |
| # Step 2: Local Solar Time (LST) with Equation of Time | |
| n = SolarCalculations.day_of_year(month, day, year) | |
| if first_hour: | |
| st.write(f"- **Day of Year (n)**: {n}") | |
| EOT = SolarCalculations.equation_of_time(n) | |
| if first_hour: | |
| st.write(f"- **Equation of Time (EOT)**: {EOT:.2f} minutes") | |
| standard_time = hour - 1 + 0.5 # Convert to decimal, assume mid-hour | |
| LST = standard_time + (4 * (lambda_std - longitude) + EOT)/60 | |
| if first_hour: | |
| st.write(f"- **Local Solar Time (LST)**: {LST:.2f} hours") | |
| # Step 3: Solar Declination (δ) | |
| delta = 23.45 * math.sin(math.radians(360 / 365 * (284 + n))) | |
| if first_hour: | |
| st.write(f"- **Solar Declination (δ)**: {delta:.2f}°") | |
| # Step 4: Hour Angle (HRA) | |
| hra = 15 * (LST - 12) | |
| if first_hour: | |
| st.write(f"- **Hour Angle (HRA)**: {hra:.2f}°") | |
| # Step 5: Solar Altitude (α) and Azimuth (Az) | |
| phi = math.radians(latitude) | |
| delta_rad = math.radians(delta) | |
| hra_rad = math.radians(hra) | |
| sin_alpha = math.sin(phi) * math.sin(delta_rad) + math.cos(phi) * math.cos(delta_rad) * math.cos(hra_rad) | |
| alpha = math.degrees(math.asin(sin_alpha)) | |
| if first_hour: | |
| st.write(f"- **Solar Altitude (α)**: {alpha:.2f}°") | |
| if abs(math.cos(math.radians(alpha))) < 0.01: | |
| azimuth = 0 # North at sunrise/sunset | |
| else: | |
| sin_az = math.cos(delta_rad) * math.sin(hra_rad) / math.cos(math.radians(alpha)) | |
| cos_az = (sin_alpha * math.sin(phi) - math.sin(delta_rad)) / (math.cos(math.radians(alpha)) * math.cos(phi)) | |
| azimuth = math.degrees(math.atan2(sin_az, cos_az)) | |
| if hra > 0: # Afternoon | |
| azimuth = 360 - azimuth if azimuth > 0 else -azimuth | |
| if first_hour: | |
| st.write(f"- **Solar Azimuth (Az)**: {azimuth:.2f}°") | |
| # Step 6: Calculate component-specific solar parameters | |
| component_results = [] | |
| first_component = True # Initialize first_component before the loop | |
| for comp_type, comp_list in components.items(): | |
| for comp in comp_list: | |
| surface_tilt = getattr(comp, 'tilt', 0.0) # Default to 0 if not specified | |
| surface_azimuth = getattr(comp, 'azimuth', 0.0) # Default to 0 if not specified | |
| u_value = getattr(comp, 'u_value', 0.0) # U-value for heat transfer, default 0 | |
| view_factor = (1 - math.cos(math.radians(surface_tilt))) / 2 | |
| ground_reflected = ground_reflectivity * ghi * view_factor | |
| # Calculate incidence angle for direct radiation | |
| cos_theta = (math.sin(delta_rad) * math.sin(phi) * math.cos(math.radians(surface_tilt)) + | |
| math.sin(delta_rad) * math.cos(phi) * math.sin(math.radians(surface_tilt)) * math.cos(math.radians(azimuth - surface_azimuth)) + | |
| math.cos(delta_rad) * math.cos(phi) * math.cos(math.radians(surface_tilt)) * math.cos(hra_rad) + | |
| math.cos(delta_rad) * math.sin(math.radians(surface_tilt)) * math.sin(hra_rad) * math.cos(math.radians(azimuth - surface_azimuth))) | |
| cos_theta = max(0, min(1, cos_theta)) # Ensure within [0,1] | |
| # Calculate solar heat gain for windows/skylights | |
| solar_heat_gain = 0 | |
| shgc = getattr(comp, 'shgc', 0.4) if comp.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT] else None # SHGC only for windows/skylights | |
| if comp.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]: | |
| shgc = getattr(comp, 'shgc', 0.4) # Solar Heat Gain Coefficient, default 0.4 | |
| direct_radiation = dni * cos_theta | |
| diffuse_radiation = dhi * view_factor | |
| solar_heat_gain = comp.area * shgc * (direct_radiation + diffuse_radiation + ground_reflected) / 1000 # Convert to kW | |
| # Calculate sol-air temperature for opaque surfaces | |
| sol_air_temp = None | |
| if comp.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.DOOR]: | |
| absorptivity = getattr(comp, 'absorptivity', 0.6) # Default absorptivity | |
| direct_radiation = dni * cos_theta | |
| diffuse_radiation = dhi * view_factor | |
| total_radiation = direct_radiation + diffuse_radiation + ground_reflected | |
| sol_air_temp = record["dry_bulb"] + (absorptivity * total_radiation) / comp.h_out - (comp.eps * comp.delta_R) / comp.h_out | |
| sol_air_temp = round(sol_air_temp, 2) | |
| component_results.append({ | |
| "component_id": getattr(comp, 'id', 'unknown_component'), | |
| "sol_air_temp": sol_air_temp, | |
| "solar_heat_gain": round(solar_heat_gain, 2) | |
| }) | |
| # Display first component's parameters and results | |
| if first_hour and first_component: | |
| st.write(f"#### First Component Parameters (ID: {getattr(comp, 'id', 'unknown_component')})") | |
| st.write(f"- **Component Type**: {comp.component_type}") | |
| st.write(f"- **Area**: {comp.area:.2f} m²") | |
| st.write(f"- **U-Value**: {u_value:.2f} W/m²·K") | |
| st.write(f"- **Surface Tilt**: {surface_tilt:.2f}°") | |
| st.write(f"- **Surface Azimuth**: {surface_azimuth:.2f}°") | |
| # Debug component type | |
| st.write(f"- **Debug: Component Type Value**: {comp.component_type}") | |
| logger.info(f"Processing component ID: {getattr(comp, 'id', 'unknown_component')}, Type: {comp.component_type}") | |
| if comp.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]: | |
| st.write(f"- **Solar Heat Gain Coefficient (SHGC)**: {shgc:.2f}") | |
| st.write(f"- **Direct Radiation**: {direct_radiation:.2f} W/m²") | |
| st.write(f"- **Diffuse Radiation**: {diffuse_radiation:.2f} W/m²") | |
| st.write(f"- **Ground-Reflected Radiation**: {ground_reflected:.2f} W/m²") | |
| st.write(f"- **Solar Heat Gain**: {solar_heat_gain:.2f} kW") | |
| elif comp.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.DOOR]: | |
| st.write(f"- **Absorptivity**: {absorptivity:.2f}") | |
| st.write(f"- **Outdoor Dry Bulb Temperature**: {record['dry_bulb']:.2f}°C") | |
| st.write(f"- **External Heat Transfer Coefficient (h_out)**: {comp.h_out:.2f} W/m²·K") | |
| st.write(f"- **Emissivity (eps)**: {comp.eps:.2f}") | |
| st.write(f"- **Delta R**: {comp.delta_R:.2f} m²·K/W") | |
| st.write(f"- **Direct Radiation**: {direct_radiation:.2f} W/m²") | |
| st.write(f"- **Diffuse Radiation**: {diffuse_radiation:.2f} W/m²") | |
| st.write(f"- **Ground-Reflected Radiation**: {ground_reflected:.2f} W/m²") | |
| st.write(f"- **Total Radiation**: {total_radiation:.2f} W/m²") | |
| st.write(f"- **Sol-Air Temperature**: {sol_air_temp:.2f}°C") | |
| else: | |
| st.warning(f"Unexpected component type: {comp.component_type}") | |
| logger.warning(f"Unexpected component type: {comp.component_type} for component ID: {getattr(comp, 'id', 'unknown_component')}") | |
| first_component = False | |
| if first_hour: | |
| st.write(f"- **Ground-Reflected Radiation (I_gr)**: {ground_reflected:.2f} W/m²") | |
| st.write("") # Add spacing | |
| first_hour = False | |
| # Store results | |
| result = { | |
| "month": month, | |
| "day": day, | |
| "hour": hour, | |
| "declination": round(delta, 2), | |
| "LST": round(LST, 2), | |
| "HRA": round(hra, 2), | |
| "altitude": round(alpha, 2), | |
| "azimuth": round(azimuth, 2), | |
| "ground_reflected": round(ground_reflected, 2), | |
| "component_results": component_results | |
| } | |
| results.append(result) | |
| return results | |
| class TFMCalculations: | |
| def calculate_conduction_load(component, outdoor_temp: float, indoor_temp: float, hour: int, sol_air_temp: Optional[float] = None, mode: str = "none") -> tuple[float, float]: | |
| """Calculate conduction load for heating and cooling in kW based on mode.""" | |
| if mode == "none": | |
| return 0, 0 | |
| # Use sol-air temperature for opaque surfaces (walls, roofs, doors), otherwise outdoor temperature | |
| delta_t = (sol_air_temp if sol_air_temp is not None and component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.DOOR] else outdoor_temp) - indoor_temp | |
| if mode == "cooling" and delta_t <= 0: | |
| return 0, 0 | |
| if mode == "heating" and delta_t >= 0: | |
| return 0, 0 | |
| # Get CTF coefficients using CTFCalculator | |
| ctf = CTFCalculator.calculate_ctf_coefficients(component) | |
| # Initialize history terms (simplified: assume steady-state history for demonstration) | |
| # In practice, maintain temperature and flux histories | |
| load = component.u_value * component.area * delta_t | |
| for i in range(len(ctf.Y)): | |
| load += component.area * ctf.Y[i] * (outdoor_temp - indoor_temp) * np.exp(-i * 3600 / 3600) | |
| load -= component.area * ctf.Z[i] * (outdoor_temp - indoor_temp) * np.exp(-i * 3600 / 3600) | |
| # Note: F terms require flux history, omitted here for simplicity | |
| cooling_load = load / 1000 if mode == "cooling" else 0 | |
| heating_load = -load / 1000 if mode == "heating" else 0 | |
| return cooling_load, heating_load | |
| def calculate_internal_load(internal_loads: Dict, hour: int, operation_hours: int, area: float) -> float: | |
| """Calculate total internal load in kW.""" | |
| total_load = 0 | |
| for group in internal_loads.get("people", []): | |
| activity_data = group["activity_data"] | |
| sensible = (activity_data["sensible_min_w"] + activity_data["sensible_max_w"]) / 2 | |
| latent = (activity_data["latent_min_w"] + activity_data["latent_max_w"]) / 2 | |
| load_per_person = sensible + latent | |
| total_load += group["num_people"] * load_per_person * group["diversity_factor"] | |
| for light in internal_loads.get("lighting", []): | |
| lpd = light["lpd"] | |
| lighting_operating_hours = light["operating_hours"] | |
| fraction = min(lighting_operating_hours, operation_hours) / operation_hours if operation_hours > 0 else 0 | |
| lighting_load = lpd * area * fraction | |
| total_load += lighting_load | |
| equipment = internal_loads.get("equipment") | |
| if equipment: | |
| total_power_density = equipment.get("total_power_density", 0) | |
| equipment_load = total_power_density * area | |
| total_load += equipment_load | |
| return total_load / 1000 | |
| def calculate_ventilation_load(internal_loads: Dict, outdoor_temp: float, indoor_temp: float, area: float, building_info: Dict, mode: str = "none") -> tuple[float, float]: | |
| """Calculate ventilation load for heating and cooling in kW based on mode.""" | |
| if mode == "none": | |
| return 0, 0 | |
| ventilation = internal_loads.get("ventilation") | |
| if not ventilation: | |
| return 0, 0 | |
| space_rate = ventilation.get("space_rate", 0.3) # L/s/m² | |
| people_rate = ventilation.get("people_rate", 2.5) # L/s/person | |
| num_people = sum(group["num_people"] for group in internal_loads.get("people", [])) | |
| ventilation_flow = (space_rate * area + people_rate * num_people) / 1000 # m³/s | |
| air_density = 1.2 # kg/m³ | |
| specific_heat = 1000 # J/kg·K | |
| delta_t = outdoor_temp - indoor_temp | |
| if mode == "cooling" and delta_t <= 0: | |
| return 0, 0 | |
| if mode == "heating" and delta_t >= 0: | |
| return 0, 0 | |
| load = ventilation_flow * air_density * specific_heat * delta_t / 1000 # kW | |
| cooling_load = load if mode == "cooling" else 0 | |
| heating_load = -load if mode == "heating" else 0 | |
| return cooling_load, heating_load | |
| def calculate_infiltration_load(internal_loads: Dict, outdoor_temp: float, indoor_temp: float, area: float, building_info: Dict, mode: str = "none") -> tuple[float, float]: | |
| """Calculate infiltration load for heating and cooling in kW based on mode.""" | |
| if mode == "none": | |
| return 0, 0 | |
| infiltration = internal_loads.get("infiltration") | |
| if not infiltration: | |
| return 0, 0 | |
| method = infiltration.get("method", "ACH") | |
| settings = infiltration.get("settings", {}) | |
| building_height = building_info.get("building_height", 3.0) | |
| volume = area * building_height # m³ | |
| air_density = 1.2 # kg/m³ | |
| specific_heat = 1000 # J/kg·K | |
| delta_t = outdoor_temp - indoor_temp | |
| if mode == "cooling" and delta_t <= 0: | |
| return 0, 0 | |
| if mode == "heating" and delta_t >= 0: | |
| return 0, 0 | |
| if method == "ACH": | |
| ach = settings.get("rate", 0.5) | |
| infiltration_flow = ach * volume / 3600 # m³/s | |
| elif method == "Crack Flow": | |
| ela = settings.get("ela", 0.0001) # m²/m² | |
| wind_speed = 4.0 # m/s (assumed) | |
| infiltration_flow = ela * area * wind_speed / 2 # m³/s | |
| else: # Empirical Equations | |
| c = settings.get("c", 0.1) | |
| n = settings.get("n", 0.65) | |
| delta_t_abs = abs(delta_t) | |
| infiltration_flow = c * (delta_t_abs ** n) * area / 3600 # m³/s | |
| load = infiltration_flow * air_density * specific_heat * delta_t / 1000 # kW | |
| cooling_load = load if mode == "cooling" else 0 | |
| heating_load = -load if mode == "heating" else 0 | |
| return cooling_load, heating_load | |
| def get_adaptive_comfort_temp(outdoor_temp: float) -> float: | |
| """Calculate adaptive comfort temperature per ASHRAE 55.""" | |
| if 10 <= outdoor_temp <= 33.5: | |
| return 0.31 * outdoor_temp + 17.8 | |
| return 24.0 # Default to standard setpoint if outside range | |
| def filter_hourly_data(hourly_data: List[Dict], sim_period: Dict, climate_data: Dict) -> List[Dict]: | |
| """Filter hourly data based on simulation period, ignoring year.""" | |
| if sim_period["type"] == "Full Year": | |
| return hourly_data | |
| filtered_data = [] | |
| if sim_period["type"] == "From-to": | |
| start_month = sim_period["start_date"].month | |
| start_day = sim_period["start_date"].day | |
| end_month = sim_period["end_date"].month | |
| end_day = sim_period["end_date"].day | |
| for data in hourly_data: | |
| month, day = data["month"], data["day"] | |
| if (month > start_month or (month == start_month and day >= start_day)) and \ | |
| (month < end_month or (month == end_month and day <= end_day)): | |
| filtered_data.append(data) | |
| elif sim_period["type"] in ["HDD", "CDD"]: | |
| base_temp = sim_period.get("base_temp", 18.3 if sim_period["type"] == "HDD" else 23.9) | |
| for data in hourly_data: | |
| temp = data["dry_bulb"] | |
| if (sim_period["type"] == "HDD" and temp < base_temp) or (sim_period["type"] == "CDD" and temp > base_temp): | |
| filtered_data.append(data) | |
| return filtered_data | |
| def get_indoor_conditions(indoor_conditions: Dict, hour: int, outdoor_temp: float) -> Dict: | |
| """Determine indoor conditions based on user settings.""" | |
| if indoor_conditions["type"] == "Fixed": | |
| mode = "none" if abs(outdoor_temp - 18) < 0.01 else "cooling" if outdoor_temp > 18 else "heating" | |
| if mode == "cooling": | |
| return { | |
| "temperature": indoor_conditions.get("cooling_setpoint", {}).get("temperature", 24.0), | |
| "rh": indoor_conditions.get("cooling_setpoint", {}).get("rh", 50.0) | |
| } | |
| elif mode == "heating": | |
| return { | |
| "temperature": indoor_conditions.get("heating_setpoint", {}).get("temperature", 22.0), | |
| "rh": indoor_conditions.get("heating_setpoint", {}).get("rh", 50.0) | |
| } | |
| else: | |
| return {"temperature": 24.0, "rh": 50.0} | |
| elif indoor_conditions["type"] == "Time-varying": | |
| schedule = indoor_conditions.get("schedule", []) | |
| if schedule: | |
| hour_idx = hour % 24 | |
| for entry in schedule: | |
| if entry["hour"] == hour_idx: | |
| return {"temperature": entry["temperature"], "rh": entry["rh"]} | |
| return {"temperature": 24.0, "rh": 50.0} | |
| else: # Adaptive | |
| return {"temperature": TFMCalculations.get_adaptive_comfort_temp(outdoor_temp), "rh": 50.0} | |
| def calculate_tfm_loads(components: Dict, hourly_data: List[Dict], indoor_conditions: Dict, internal_loads: Dict, building_info: Dict, sim_period: Dict, hvac_settings: Dict) -> List[Dict]: | |
| """Calculate TFM loads for heating and cooling with user-defined filters and temperature threshold.""" | |
| filtered_data = TFMCalculations.filter_hourly_data(hourly_data, sim_period, building_info) | |
| temp_loads = [] | |
| building_orientation = building_info.get("orientation_angle", 0.0) | |
| operating_periods = hvac_settings.get("operating_hours", [{"start": 8, "end": 18}]) | |
| area = building_info.get("floor_area", 100.0) | |
| # Pre-calculate CTF coefficients for all components using CTFCalculator | |
| for comp_list in components.values(): | |
| for comp in comp_list: | |
| comp.ctf = CTFCalculator.calculate_ctf_coefficients(comp) | |
| # Calculate solar parameters using integrated SolarCalculations class | |
| climate_data = building_info.get("climate_data", {}) | |
| ground_reflectivity = building_info.get("ground_reflectivity", 0.2) | |
| solar_results = SolarCalculations.calculate_solar_parameters( | |
| hourly_data=filtered_data, | |
| latitude=climate_data.get("latitude", 0.0), | |
| longitude=climate_data.get("longitude", 0.0), | |
| timezone=climate_data.get("timezone", 0.0), | |
| ground_reflectivity=ground_reflectivity, | |
| components=components | |
| ) | |
| # Create a dictionary for quick lookup of solar results by hour | |
| solar_results_by_hour = {(res["month"], res["day"], res["hour"]): res for res in solar_results} | |
| # Log presence of fenestration components | |
| fenestration_count = sum(len(comp_list) for comp_type, comp_list in components.items() | |
| if comp_type in ['windows', 'skylights']) | |
| logger.info(f"Number of fenestration components (windows/skylights): {fenestration_count}") | |
| for hour_data in filtered_data: | |
| hour = hour_data["hour"] | |
| outdoor_temp = hour_data["dry_bulb"] | |
| indoor_cond = TFMCalculations.get_indoor_conditions(indoor_conditions, hour, outdoor_temp) | |
| indoor_temp = indoor_cond["temperature"] | |
| # Initialize all loads to 0 | |
| conduction_cooling = conduction_heating = solar = internal = ventilation_cooling = ventilation_heating = infiltration_cooling = infiltration_heating = 0 | |
| # Check if hour is within operating periods | |
| is_operating = False | |
| for period in operating_periods: | |
| start_hour = period.get("start", 8) | |
| end_hour = period.get("end", 18) | |
| if start_hour <= hour % 24 <= end_hour: | |
| is_operating = True | |
| break | |
| # Determine mode based on temperature threshold (18°C) | |
| mode = "none" if abs(outdoor_temp - 18) < 0.01 else "cooling" if outdoor_temp > 18 else "heating" | |
| if is_operating and mode == "cooling": | |
| for comp_type, comp_list in components.items(): | |
| for comp in comp_list: | |
| # Get solar result for this component and hour | |
| solar_key = (hour_data["month"], hour_data["day"], hour) | |
| solar_result = next((res for res in solar_results_by_hour.get(solar_key, {}).get("component_results", []) | |
| if res["component_id"] == getattr(comp, 'id', 'unknown_component')), None) | |
| sol_air_temp = solar_result.get("sol_air_temp") if solar_result else None | |
| cool_load, _ = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, sol_air_temp, mode="cooling") | |
| conduction_cooling += cool_load | |
| if solar_result and comp.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]: | |
| solar_heat_gain = solar_result.get("solar_heat_gain", 0) | |
| solar += solar_heat_gain | |
| logger.info(f"Adding solar heat gain for component {getattr(comp, 'id', 'unknown_component')} " | |
| f"at {hour_data['month']}/{hour_data['day']}/{hour}: {solar_heat_gain:.2f} kW") | |
| logger.info(f"Total solar load for {hour_data['month']}/{hour_data['day']}/{hour}: {solar:.2f} kW") | |
| internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area) | |
| ventilation_cooling, _ = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="cooling") | |
| infiltration_cooling, _ = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="cooling") | |
| elif is_operating and mode == "heating": | |
| for comp_type, comp_list in components.items(): | |
| for comp in comp_list: | |
| # Get solar result for this component and hour | |
| solar_key = (hour_data["month"], hour_data["day"], hour) | |
| solar_result = next((res for res in solar_results_by_hour.get(solar_key, {}).get("component_results", []) | |
| if res["component_id"] == getattr(comp, 'id', 'unknown_component')), None) | |
| sol_air_temp = solar_result.get("sol_air_temp") if solar_result else None | |
| _, heat_load = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, sol_air_temp, mode="heating") | |
| conduction_heating += heat_load | |
| internal = TFMCalculations.calculate_internal_load(internal_loads, hour, max([p["end"] - p["start"] for p in operating_periods]), area) | |
| _, ventilation_heating = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="heating") | |
| _, infiltration_heating = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="heating") | |
| else: # mode == "none" or not is_operating | |
| internal = 0 # No internal loads when no heating or cooling is needed or outside operating hours | |
| # Calculate total loads, subtracting internal load for heating | |
| total_cooling = conduction_cooling + solar + internal + ventilation_cooling + infiltration_cooling | |
| total_heating = max(conduction_heating + ventilation_heating + infiltration_heating - internal, 0) | |
| # Enforce mutual exclusivity within hour | |
| if mode == "cooling": | |
| total_heating = 0 | |
| elif mode == "heating": | |
| total_cooling = 0 | |
| temp_loads.append({ | |
| "hour": hour, | |
| "month": hour_data["month"], | |
| "day": hour_data["day"], | |
| "conduction_cooling": conduction_cooling, | |
| "conduction_heating": conduction_heating, | |
| "solar": solar, | |
| "internal": internal, | |
| "ventilation_cooling": ventilation_cooling, | |
| "ventilation_heating": ventilation_heating, | |
| "infiltration_cooling": infiltration_cooling, | |
| "infiltration_heating": infiltration_heating, | |
| "total_cooling": total_cooling, | |
| "total_heating": total_heating | |
| }) | |
| # Group loads by day and apply daily control | |
| loads_by_day = defaultdict(list) | |
| for load in temp_loads: | |
| day_key = (load["month"], load["day"]) | |
| loads_by_day[day_key].append(load) | |
| final_loads = [] | |
| for day_key, day_loads in loads_by_day.items(): | |
| # Count hours with non-zero cooling and heating loads | |
| cooling_hours = sum(1 for load in day_loads if load["total_cooling"] > 0) | |
| heating_hours = sum(1 for load in day_loads if load["total_heating"] > 0) | |
| # Apply daily control | |
| for load in day_loads: | |
| if cooling_hours > heating_hours: | |
| load["total_heating"] = 0 # Keep cooling components, zero heating total | |
| elif heating_hours > cooling_hours: | |
| load["total_cooling"] = 0 # Keep heating components, zero cooling total | |
| else: # Equal hours | |
| load["total_cooling"] = 0 | |
| load["total_heating"] = 0 # Zero both totals, keep components | |
| final_loads.append(load) | |
| return final_loads | |
| def display_results(loads: List[Dict]): | |
| """Display simulation results including equipment sizing, load breakdown, monthly loads, and summary table.""" | |
| df = pd.DataFrame(loads) | |
| if df.empty: | |
| st.error("No load calculations available.") | |
| return | |
| # Equipment Sizing | |
| st.subheader("Equipment Sizing") | |
| peak_cooling_load = df["total_cooling"].max() if "total_cooling" in df else 0.0 | |
| peak_heating_load = df["total_heating"].max() if "total_heating" in df else 0.0 | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.metric("Cooling Equipment Size", f"{peak_cooling_load:.2f} kW", help="Peak hourly cooling load") | |
| with col2: | |
| st.metric("Heating Equipment Size", f"{peak_heating_load:.2f} kW", help="Peak hourly heating load") | |
| # Pie Charts for Load Breakdown | |
| st.subheader("Load Breakdown") | |
| cooling_totals = { | |
| "Conduction": df["conduction_cooling"].sum(), | |
| "Solar": df["solar"].sum(), | |
| "Internal": df["internal"].sum(), | |
| "Ventilation": df["ventilation_cooling"].sum(), | |
| "Infiltration": df["infiltration_cooling"].sum() | |
| } | |
| heating_totals = { | |
| "Conduction": df["conduction_heating"].sum(), | |
| "Ventilation": df["ventilation_heating"].sum(), | |
| "Infiltration": df["infiltration_heating"].sum() | |
| } | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| fig_cooling = go.Figure(data=[ | |
| go.Pie(labels=list(cooling_totals.keys()), values=list(cooling_totals.values())) | |
| ]) | |
| fig_cooling.update_layout(title="Cooling Load Breakdown (kWh)", width=400, height=400) | |
| st.plotly_chart(fig_cooling, use_container_width=True) | |
| with col2: | |
| fig_heating = go.Figure(data=[ | |
| go.Pie(labels=list(heating_totals.keys()), values=list(heating_totals.values())) | |
| ]) | |
| fig_heating.update_layout(title="Heating Load Breakdown (kWh)", width=400, height=400) | |
| st.plotly_chart(fig_heating, use_container_width=True) | |
| # Monthly Loads Bar Chart | |
| st.subheader("Monthly Heating and Cooling Loads") | |
| df["month_name"] = df["month"].map({ | |
| 1: "Jan", 2: "Feb", 3: "Mar", 4: "Apr", 5: "May", 6: "Jun", | |
| 7: "Jul", 8: "Aug", 9: "Sep", 10: "Oct", 11: "Nov", 12: "Dec" | |
| }) | |
| monthly_loads = df.groupby("month_name")[["total_cooling", "total_heating"]].sum().reindex( | |
| ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] | |
| ) | |
| fig_monthly = go.Figure(data=[ | |
| go.Bar(name="Cooling Load (kWh)", x=monthly_loads.index, y=monthly_loads["total_cooling"]), | |
| go.Bar(name="Heating Load (kWh)", x=monthly_loads.index, y=monthly_loads["total_heating"]) | |
| ]) | |
| fig_monthly.update_layout( | |
| title="Monthly Heating and Cooling Loads", | |
| xaxis_title="Month", | |
| yaxis_title="Load (kWh)", | |
| barmode="group", | |
| width=800, | |
| height=400 | |
| ) | |
| st.plotly_chart(fig_monthly, use_container_width=True) | |
| # Detailed Load Summary Table | |
| st.subheader("Load Summary") | |
| summary_row = { | |
| "hour": "Total", | |
| "month_name": "", | |
| "conduction_cooling": df["conduction_cooling"].sum(), | |
| "conduction_heating": df["conduction_heating"].sum(), | |
| "solar": df["solar"].sum(), | |
| "internal": df["internal"].sum(), | |
| "ventilation_cooling": df["ventilation_cooling"].sum(), | |
| "ventilation_heating": df["ventilation_heating"].sum(), | |
| "infiltration_cooling": df["infiltration_cooling"].sum(), | |
| "infiltration_heating": df["infiltration_heating"].sum(), | |
| "total_cooling": df["total_cooling"].sum(), | |
| "total_heating": df["total_heating"].sum() | |
| } | |
| display_df = df[["hour", "month_name", "conduction_cooling", "conduction_heating", "solar", | |
| "internal", "ventilation_cooling", "ventilation_heating", | |
| "infiltration_cooling", "infiltration_heating", "total_cooling", "total_heating"]] | |
| display_df = pd.concat([display_df, pd.DataFrame([summary_row])], ignore_index=True) | |
| st.dataframe(display_df.rename(columns={"month_name": "Month"}), use_container_width=True) |