Spaces:
Running
Running
""" | |
U-Value calculator module for HVAC Load Calculator. | |
This module implements the layer-by-layer assembly builder and U-value calculation functions. | |
""" | |
from typing import Dict, List, Any, Optional, Tuple | |
import pandas as pd | |
import numpy as np | |
import os | |
import json | |
from dataclasses import dataclass, field | |
# Import data models | |
from data.building_components import MaterialLayer | |
from data.reference_data import reference_data | |
# Define paths | |
DATA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
class MaterialAssembly: | |
"""Class representing a material assembly for U-value calculation.""" | |
name: str | |
description: str = "" | |
layers: List[MaterialLayer] = field(default_factory=list) | |
# Surface resistances (m²·K/W) | |
r_si: float = 0.13 # Interior surface resistance | |
r_se: float = 0.04 # Exterior surface resistance | |
def add_layer(self, layer: MaterialLayer) -> None: | |
""" | |
Add a material layer to the assembly. | |
Args: | |
layer: MaterialLayer object | |
""" | |
self.layers.append(layer) | |
def remove_layer(self, index: int) -> bool: | |
""" | |
Remove a material layer from the assembly. | |
Args: | |
index: Index of the layer to remove | |
Returns: | |
True if the layer was removed, False otherwise | |
""" | |
if index < 0 or index >= len(self.layers): | |
return False | |
self.layers.pop(index) | |
return True | |
def move_layer(self, from_index: int, to_index: int) -> bool: | |
""" | |
Move a material layer within the assembly. | |
Args: | |
from_index: Current index of the layer | |
to_index: New index for the layer | |
Returns: | |
True if the layer was moved, False otherwise | |
""" | |
if (from_index < 0 or from_index >= len(self.layers) or | |
to_index < 0 or to_index >= len(self.layers)): | |
return False | |
layer = self.layers.pop(from_index) | |
self.layers.insert(to_index, layer) | |
return True | |
def total_thickness(self) -> float: | |
"""Calculate the total thickness of the assembly in meters.""" | |
return sum(layer.thickness for layer in self.layers) | |
def r_value_layers(self) -> float: | |
"""Calculate the total thermal resistance of all layers in m²·K/W.""" | |
return sum(layer.r_value for layer in self.layers) | |
def r_value_total(self) -> float: | |
"""Calculate the total thermal resistance including surface resistances in m²·K/W.""" | |
return self.r_si + self.r_value_layers + self.r_se | |
def u_value(self) -> float: | |
"""Calculate the U-value of the assembly in W/(m²·K).""" | |
if self.r_value_total == 0: | |
return float('inf') | |
return 1 / self.r_value_total | |
def thermal_mass(self) -> Optional[float]: | |
"""Calculate the total thermal mass of the assembly in J/(m²·K).""" | |
masses = [layer.thermal_mass for layer in self.layers] | |
if None in masses: | |
return None | |
return sum(masses) | |
def to_dict(self) -> Dict[str, Any]: | |
"""Convert the material assembly to a dictionary.""" | |
return { | |
"name": self.name, | |
"description": self.description, | |
"layers": [layer.to_dict() for layer in self.layers], | |
"r_si": self.r_si, | |
"r_se": self.r_se, | |
"total_thickness": self.total_thickness, | |
"r_value_layers": self.r_value_layers, | |
"r_value_total": self.r_value_total, | |
"u_value": self.u_value, | |
"thermal_mass": self.thermal_mass | |
} | |
class UValueCalculator: | |
"""Class for calculating U-values of material assemblies.""" | |
def __init__(self): | |
"""Initialize U-value calculator.""" | |
self.assemblies = {} | |
self.load_preset_assemblies() | |
def load_preset_assemblies(self) -> None: | |
"""Load preset material assemblies.""" | |
# Create preset assemblies from reference data | |
# Wall assemblies | |
for wall_id, wall_data in reference_data.wall_types.items(): | |
# Create material layers | |
layers = [] | |
for layer_data in wall_data.get("layers", []): | |
material_id = layer_data.get("material") | |
thickness = layer_data.get("thickness") | |
material = reference_data.get_material(material_id) | |
if material: | |
layer = MaterialLayer( | |
name=material["name"], | |
thickness=thickness, | |
conductivity=material["conductivity"], | |
density=material.get("density"), | |
specific_heat=material.get("specific_heat") | |
) | |
layers.append(layer) | |
# Create assembly | |
assembly_id = f"preset_wall_{wall_id}" | |
assembly = MaterialAssembly( | |
name=wall_data["name"], | |
description=wall_data["description"], | |
layers=layers | |
) | |
self.assemblies[assembly_id] = assembly | |
# Roof assemblies | |
for roof_id, roof_data in reference_data.roof_types.items(): | |
# Create material layers | |
layers = [] | |
for layer_data in roof_data.get("layers", []): | |
material_id = layer_data.get("material") | |
thickness = layer_data.get("thickness") | |
material = reference_data.get_material(material_id) | |
if material: | |
layer = MaterialLayer( | |
name=material["name"], | |
thickness=thickness, | |
conductivity=material["conductivity"], | |
density=material.get("density"), | |
specific_heat=material.get("specific_heat") | |
) | |
layers.append(layer) | |
# Create assembly | |
assembly_id = f"preset_roof_{roof_id}" | |
assembly = MaterialAssembly( | |
name=roof_data["name"], | |
description=roof_data["description"], | |
layers=layers | |
) | |
self.assemblies[assembly_id] = assembly | |
# Floor assemblies | |
for floor_id, floor_data in reference_data.floor_types.items(): | |
# Create material layers | |
layers = [] | |
for layer_data in floor_data.get("layers", []): | |
material_id = layer_data.get("material") | |
thickness = layer_data.get("thickness") | |
material = reference_data.get_material(material_id) | |
if material: | |
layer = MaterialLayer( | |
name=material["name"], | |
thickness=thickness, | |
conductivity=material["conductivity"], | |
density=material.get("density"), | |
specific_heat=material.get("specific_heat") | |
) | |
layers.append(layer) | |
# Create assembly | |
assembly_id = f"preset_floor_{floor_id}" | |
assembly = MaterialAssembly( | |
name=floor_data["name"], | |
description=floor_data["description"], | |
layers=layers | |
) | |
self.assemblies[assembly_id] = assembly | |
def get_assembly(self, assembly_id: str) -> Optional[MaterialAssembly]: | |
""" | |
Get a material assembly by ID. | |
Args: | |
assembly_id: Assembly identifier | |
Returns: | |
MaterialAssembly object or None if not found | |
""" | |
return self.assemblies.get(assembly_id) | |
def get_preset_assemblies(self) -> Dict[str, MaterialAssembly]: | |
""" | |
Get all preset material assemblies. | |
Returns: | |
Dictionary of preset MaterialAssembly objects | |
""" | |
return {assembly_id: assembly for assembly_id, assembly in self.assemblies.items() | |
if assembly_id.startswith("preset_")} | |
def get_custom_assemblies(self) -> Dict[str, MaterialAssembly]: | |
""" | |
Get all custom material assemblies. | |
Returns: | |
Dictionary of custom MaterialAssembly objects | |
""" | |
return {assembly_id: assembly for assembly_id, assembly in self.assemblies.items() | |
if assembly_id.startswith("custom_")} | |
def create_assembly(self, name: str, description: str = "") -> str: | |
""" | |
Create a new material assembly. | |
Args: | |
name: Assembly name | |
description: Assembly description | |
Returns: | |
Assembly ID | |
""" | |
import uuid | |
assembly_id = f"custom_assembly_{str(uuid.uuid4())[:8]}" | |
assembly = MaterialAssembly(name=name, description=description) | |
self.assemblies[assembly_id] = assembly | |
return assembly_id | |
def add_layer_to_assembly(self, assembly_id: str, material_id: str, thickness: float) -> bool: | |
""" | |
Add a material layer to an assembly. | |
Args: | |
assembly_id: Assembly identifier | |
material_id: Material identifier | |
thickness: Layer thickness in meters | |
Returns: | |
True if the layer was added, False otherwise | |
""" | |
if assembly_id not in self.assemblies: | |
return False | |
material = reference_data.get_material(material_id) | |
if not material: | |
return False | |
layer = MaterialLayer( | |
name=material["name"], | |
thickness=thickness, | |
conductivity=material["conductivity"], | |
density=material.get("density"), | |
specific_heat=material.get("specific_heat") | |
) | |
self.assemblies[assembly_id].add_layer(layer) | |
return True | |
def add_custom_layer_to_assembly(self, assembly_id: str, name: str, thickness: float, | |
conductivity: float, density: float = None, | |
specific_heat: float = None) -> bool: | |
""" | |
Add a custom material layer to an assembly. | |
Args: | |
assembly_id: Assembly identifier | |
name: Layer name | |
thickness: Layer thickness in meters | |
conductivity: Thermal conductivity in W/(m·K) | |
density: Density in kg/m³ (optional) | |
specific_heat: Specific heat capacity in J/(kg·K) (optional) | |
Returns: | |
True if the layer was added, False otherwise | |
""" | |
if assembly_id not in self.assemblies: | |
return False | |
layer = MaterialLayer( | |
name=name, | |
thickness=thickness, | |
conductivity=conductivity, | |
density=density, | |
specific_heat=specific_heat | |
) | |
self.assemblies[assembly_id].add_layer(layer) | |
return True | |
def remove_layer_from_assembly(self, assembly_id: str, layer_index: int) -> bool: | |
""" | |
Remove a material layer from an assembly. | |
Args: | |
assembly_id: Assembly identifier | |
layer_index: Index of the layer to remove | |
Returns: | |
True if the layer was removed, False otherwise | |
""" | |
if assembly_id not in self.assemblies: | |
return False | |
return self.assemblies[assembly_id].remove_layer(layer_index) | |
def move_layer_in_assembly(self, assembly_id: str, from_index: int, to_index: int) -> bool: | |
""" | |
Move a material layer within an assembly. | |
Args: | |
assembly_id: Assembly identifier | |
from_index: Current index of the layer | |
to_index: New index for the layer | |
Returns: | |
True if the layer was moved, False otherwise | |
""" | |
if assembly_id not in self.assemblies: | |
return False | |
return self.assemblies[assembly_id].move_layer(from_index, to_index) | |
def calculate_u_value(self, assembly_id: str) -> Optional[float]: | |
""" | |
Calculate the U-value of an assembly. | |
Args: | |
assembly_id: Assembly identifier | |
Returns: | |
U-value in W/(m²·K) or None if the assembly was not found | |
""" | |
if assembly_id not in self.assemblies: | |
return None | |
return self.assemblies[assembly_id].u_value | |
def calculate_r_value(self, assembly_id: str) -> Optional[float]: | |
""" | |
Calculate the R-value of an assembly. | |
Args: | |
assembly_id: Assembly identifier | |
Returns: | |
R-value in m²·K/W or None if the assembly was not found | |
""" | |
if assembly_id not in self.assemblies: | |
return None | |
return self.assemblies[assembly_id].r_value_total | |
def export_to_json(self, file_path: str) -> None: | |
""" | |
Export all assemblies to a JSON file. | |
Args: | |
file_path: Path to the output JSON file | |
""" | |
data = {assembly_id: assembly.to_dict() for assembly_id, assembly in self.assemblies.items()} | |
with open(file_path, 'w') as f: | |
json.dump(data, f, indent=4) | |
def import_from_json(self, file_path: str) -> int: | |
""" | |
Import assemblies from a JSON file. | |
Args: | |
file_path: Path to the input JSON file | |
Returns: | |
Number of assemblies imported | |
""" | |
with open(file_path, 'r') as f: | |
data = json.load(f) | |
count = 0 | |
for assembly_id, assembly_data in data.items(): | |
try: | |
# Create assembly | |
assembly = MaterialAssembly( | |
name=assembly_data["name"], | |
description=assembly_data.get("description", ""), | |
r_si=assembly_data.get("r_si", 0.13), | |
r_se=assembly_data.get("r_se", 0.04) | |
) | |
# Add layers | |
for layer_data in assembly_data.get("layers", []): | |
layer = MaterialLayer( | |
name=layer_data["name"], | |
thickness=layer_data["thickness"], | |
conductivity=layer_data["conductivity"], | |
density=layer_data.get("density"), | |
specific_heat=layer_data.get("specific_heat") | |
) | |
assembly.add_layer(layer) | |
self.assemblies[assembly_id] = assembly | |
count += 1 | |
except Exception as e: | |
print(f"Error importing assembly {assembly_id}: {e}") | |
return count | |
# Create a singleton instance | |
u_value_calculator = UValueCalculator() | |
# Export U-value calculator to JSON if needed | |
if __name__ == "__main__": | |
u_value_calculator.export_to_json(os.path.join(DATA_DIR, "data", "u_value_calculator.json")) | |