|
|
""" |
|
|
Area calculation system module for HVAC Load Calculator. |
|
|
This module implements net wall area calculation and area validation 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 |
|
|
|
|
|
|
|
|
from data.building_components import Wall, Window, Door, Orientation, ComponentType |
|
|
|
|
|
|
|
|
DATA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
|
|
|
|
|
|
|
|
class AreaCalculationSystem: |
|
|
"""Class for managing area calculations and validations.""" |
|
|
|
|
|
def __init__(self): |
|
|
"""Initialize area calculation system.""" |
|
|
self.walls = {} |
|
|
self.windows = {} |
|
|
self.doors = {} |
|
|
|
|
|
def add_wall(self, wall: Wall) -> None: |
|
|
""" |
|
|
Add a wall to the area calculation system. |
|
|
|
|
|
Args: |
|
|
wall: Wall object |
|
|
""" |
|
|
self.walls[wall.id] = wall |
|
|
|
|
|
def add_window(self, window: Window) -> None: |
|
|
""" |
|
|
Add a window to the area calculation system. |
|
|
|
|
|
Args: |
|
|
window: Window object |
|
|
""" |
|
|
self.windows[window.id] = window |
|
|
|
|
|
def add_door(self, door: Door) -> None: |
|
|
""" |
|
|
Add a door to the area calculation system. |
|
|
|
|
|
Args: |
|
|
door: Door object |
|
|
""" |
|
|
self.doors[door.id] = door |
|
|
|
|
|
def remove_wall(self, wall_id: str) -> bool: |
|
|
""" |
|
|
Remove a wall from the area calculation system. |
|
|
|
|
|
Args: |
|
|
wall_id: Wall identifier |
|
|
|
|
|
Returns: |
|
|
True if the wall was removed, False otherwise |
|
|
""" |
|
|
if wall_id not in self.walls: |
|
|
return False |
|
|
|
|
|
|
|
|
for window_id, window in list(self.windows.items()): |
|
|
if window.wall_id == wall_id: |
|
|
del self.windows[window_id] |
|
|
|
|
|
for door_id, door in list(self.doors.items()): |
|
|
if door.wall_id == wall_id: |
|
|
del self.doors[door_id] |
|
|
|
|
|
del self.walls[wall_id] |
|
|
return True |
|
|
|
|
|
def remove_window(self, window_id: str) -> bool: |
|
|
""" |
|
|
Remove a window from the area calculation system. |
|
|
|
|
|
Args: |
|
|
window_id: Window identifier |
|
|
|
|
|
Returns: |
|
|
True if the window was removed, False otherwise |
|
|
""" |
|
|
if window_id not in self.windows: |
|
|
return False |
|
|
|
|
|
|
|
|
window = self.windows[window_id] |
|
|
if window.wall_id and window.wall_id in self.walls: |
|
|
wall = self.walls[window.wall_id] |
|
|
if window_id in wall.windows: |
|
|
wall.windows.remove(window_id) |
|
|
self._update_wall_net_area(wall.id) |
|
|
|
|
|
del self.windows[window_id] |
|
|
return True |
|
|
|
|
|
def remove_door(self, door_id: str) -> bool: |
|
|
""" |
|
|
Remove a door from the area calculation system. |
|
|
|
|
|
Args: |
|
|
door_id: Door identifier |
|
|
|
|
|
Returns: |
|
|
True if the door was removed, False otherwise |
|
|
""" |
|
|
if door_id not in self.doors: |
|
|
return False |
|
|
|
|
|
|
|
|
door = self.doors[door_id] |
|
|
if door.wall_id and door.wall_id in self.walls: |
|
|
wall = self.walls[door.wall_id] |
|
|
if door_id in wall.doors: |
|
|
wall.doors.remove(door_id) |
|
|
self._update_wall_net_area(wall.id) |
|
|
|
|
|
del self.doors[door_id] |
|
|
return True |
|
|
|
|
|
def assign_window_to_wall(self, window_id: str, wall_id: str) -> bool: |
|
|
""" |
|
|
Assign a window to a wall. |
|
|
|
|
|
Args: |
|
|
window_id: Window identifier |
|
|
wall_id: Wall identifier |
|
|
|
|
|
Returns: |
|
|
True if the window was assigned, False otherwise |
|
|
""" |
|
|
if window_id not in self.windows or wall_id not in self.walls: |
|
|
return False |
|
|
|
|
|
window = self.windows[window_id] |
|
|
wall = self.walls[wall_id] |
|
|
|
|
|
|
|
|
if window.wall_id and window.wall_id in self.walls and window.wall_id != wall_id: |
|
|
prev_wall = self.walls[window.wall_id] |
|
|
if window_id in prev_wall.windows: |
|
|
prev_wall.windows.remove(window_id) |
|
|
self._update_wall_net_area(prev_wall.id) |
|
|
|
|
|
|
|
|
window.wall_id = wall_id |
|
|
window.orientation = wall.orientation |
|
|
|
|
|
|
|
|
if window_id not in wall.windows: |
|
|
wall.windows.append(window_id) |
|
|
|
|
|
|
|
|
self._update_wall_net_area(wall_id) |
|
|
|
|
|
return True |
|
|
|
|
|
def assign_door_to_wall(self, door_id: str, wall_id: str) -> bool: |
|
|
""" |
|
|
Assign a door to a wall. |
|
|
|
|
|
Args: |
|
|
door_id: Door identifier |
|
|
wall_id: Wall identifier |
|
|
|
|
|
Returns: |
|
|
True if the door was assigned, False otherwise |
|
|
""" |
|
|
if door_id not in self.doors or wall_id not in self.walls: |
|
|
return False |
|
|
|
|
|
door = self.doors[door_id] |
|
|
wall = self.walls[wall_id] |
|
|
|
|
|
|
|
|
if door.wall_id and door.wall_id in self.walls and door.wall_id != wall_id: |
|
|
prev_wall = self.walls[door.wall_id] |
|
|
if door_id in prev_wall.doors: |
|
|
prev_wall.doors.remove(door_id) |
|
|
self._update_wall_net_area(prev_wall.id) |
|
|
|
|
|
|
|
|
door.wall_id = wall_id |
|
|
door.orientation = wall.orientation |
|
|
|
|
|
|
|
|
if door_id not in wall.doors: |
|
|
wall.doors.append(door_id) |
|
|
|
|
|
|
|
|
self._update_wall_net_area(wall_id) |
|
|
|
|
|
return True |
|
|
|
|
|
def _update_wall_net_area(self, wall_id: str) -> None: |
|
|
""" |
|
|
Update the net area of a wall by subtracting windows and doors. |
|
|
|
|
|
Args: |
|
|
wall_id: Wall identifier |
|
|
""" |
|
|
if wall_id not in self.walls: |
|
|
return |
|
|
|
|
|
wall = self.walls[wall_id] |
|
|
|
|
|
|
|
|
total_window_area = sum(self.windows[window_id].area |
|
|
for window_id in wall.windows |
|
|
if window_id in self.windows) |
|
|
|
|
|
|
|
|
total_door_area = sum(self.doors[door_id].area |
|
|
for door_id in wall.doors |
|
|
if door_id in self.doors) |
|
|
|
|
|
|
|
|
if wall.gross_area is None: |
|
|
wall.gross_area = wall.area |
|
|
|
|
|
wall.net_area = wall.gross_area - total_window_area - total_door_area |
|
|
wall.area = wall.net_area |
|
|
|
|
|
def update_all_net_areas(self) -> None: |
|
|
"""Update the net areas of all walls.""" |
|
|
for wall_id in self.walls: |
|
|
self._update_wall_net_area(wall_id) |
|
|
|
|
|
def validate_areas(self) -> List[Dict[str, Any]]: |
|
|
""" |
|
|
Validate all areas and return a list of validation issues. |
|
|
|
|
|
Returns: |
|
|
List of validation issues |
|
|
""" |
|
|
issues = [] |
|
|
|
|
|
|
|
|
for wall_id, wall in self.walls.items(): |
|
|
if wall.net_area <= 0: |
|
|
issues.append({ |
|
|
"type": "error", |
|
|
"component_id": wall_id, |
|
|
"component_type": "Wall", |
|
|
"message": f"Wall '{wall.name}' has a negative or zero net area. " |
|
|
f"Gross area: {wall.gross_area} m², " |
|
|
f"Window area: {sum(self.windows[window_id].area for window_id in wall.windows if window_id in self.windows)} m², " |
|
|
f"Door area: {sum(self.doors[door_id].area for door_id in wall.doors if door_id in self.doors)} m², " |
|
|
f"Net area: {wall.net_area} m²." |
|
|
}) |
|
|
elif wall.net_area < 0.5: |
|
|
issues.append({ |
|
|
"type": "warning", |
|
|
"component_id": wall_id, |
|
|
"component_type": "Wall", |
|
|
"message": f"Wall '{wall.name}' has a very small net area ({wall.net_area} m²). " |
|
|
f"Consider adjusting window and door sizes." |
|
|
}) |
|
|
|
|
|
|
|
|
for window_id, window in self.windows.items(): |
|
|
if not window.wall_id: |
|
|
issues.append({ |
|
|
"type": "warning", |
|
|
"component_id": window_id, |
|
|
"component_type": "Window", |
|
|
"message": f"Window '{window.name}' is not assigned to any wall." |
|
|
}) |
|
|
elif window.wall_id not in self.walls: |
|
|
issues.append({ |
|
|
"type": "error", |
|
|
"component_id": window_id, |
|
|
"component_type": "Window", |
|
|
"message": f"Window '{window.name}' is assigned to a non-existent wall (ID: {window.wall_id})." |
|
|
}) |
|
|
|
|
|
|
|
|
for door_id, door in self.doors.items(): |
|
|
if not door.wall_id: |
|
|
issues.append({ |
|
|
"type": "warning", |
|
|
"component_id": door_id, |
|
|
"component_type": "Door", |
|
|
"message": f"Door '{door.name}' is not assigned to any wall." |
|
|
}) |
|
|
elif door.wall_id not in self.walls: |
|
|
issues.append({ |
|
|
"type": "error", |
|
|
"component_id": door_id, |
|
|
"component_type": "Door", |
|
|
"message": f"Door '{door.name}' is assigned to a non-existent wall (ID: {door.wall_id})." |
|
|
}) |
|
|
|
|
|
|
|
|
for window_id, window in self.windows.items(): |
|
|
if window.area <= 0: |
|
|
issues.append({ |
|
|
"type": "error", |
|
|
"component_id": window_id, |
|
|
"component_type": "Window", |
|
|
"message": f"Window '{window.name}' has a zero or negative area ({window.area} m²)." |
|
|
}) |
|
|
|
|
|
for door_id, door in self.doors.items(): |
|
|
if door.area <= 0: |
|
|
issues.append({ |
|
|
"type": "error", |
|
|
"component_id": door_id, |
|
|
"component_type": "Door", |
|
|
"message": f"Door '{door.name}' has a zero or negative area ({door.area} m²)." |
|
|
}) |
|
|
|
|
|
return issues |
|
|
|
|
|
def get_wall_components(self, wall_id: str) -> Dict[str, List[str]]: |
|
|
""" |
|
|
Get all components (windows and doors) associated with a wall. |
|
|
|
|
|
Args: |
|
|
wall_id: Wall identifier |
|
|
|
|
|
Returns: |
|
|
Dictionary with lists of window and door IDs |
|
|
""" |
|
|
if wall_id not in self.walls: |
|
|
return {"windows": [], "doors": []} |
|
|
|
|
|
wall = self.walls[wall_id] |
|
|
return { |
|
|
"windows": [window_id for window_id in wall.windows if window_id in self.windows], |
|
|
"doors": [door_id for door_id in wall.doors if door_id in self.doors] |
|
|
} |
|
|
|
|
|
def get_wall_area_breakdown(self, wall_id: str) -> Dict[str, float]: |
|
|
""" |
|
|
Get a breakdown of wall areas (gross, net, windows, doors). |
|
|
|
|
|
Args: |
|
|
wall_id: Wall identifier |
|
|
|
|
|
Returns: |
|
|
Dictionary with area breakdown |
|
|
""" |
|
|
if wall_id not in self.walls: |
|
|
return {} |
|
|
|
|
|
wall = self.walls[wall_id] |
|
|
|
|
|
|
|
|
window_area = sum(self.windows[window_id].area |
|
|
for window_id in wall.windows |
|
|
if window_id in self.windows) |
|
|
|
|
|
|
|
|
door_area = sum(self.doors[door_id].area |
|
|
for door_id in wall.doors |
|
|
if door_id in self.doors) |
|
|
|
|
|
return { |
|
|
"gross_area": wall.gross_area, |
|
|
"net_area": wall.net_area, |
|
|
"window_area": window_area, |
|
|
"door_area": door_area |
|
|
} |
|
|
|
|
|
def get_total_areas(self) -> Dict[str, float]: |
|
|
""" |
|
|
Get total areas for all component types. |
|
|
|
|
|
Returns: |
|
|
Dictionary with total areas |
|
|
""" |
|
|
|
|
|
total_wall_gross_area = sum(wall.gross_area for wall in self.walls.values() if wall.gross_area is not None) |
|
|
total_wall_net_area = sum(wall.net_area for wall in self.walls.values() if wall.net_area is not None) |
|
|
|
|
|
|
|
|
total_window_area = sum(window.area for window in self.windows.values()) |
|
|
|
|
|
|
|
|
total_door_area = sum(door.area for door in self.doors.values()) |
|
|
|
|
|
return { |
|
|
"total_wall_gross_area": total_wall_gross_area, |
|
|
"total_wall_net_area": total_wall_net_area, |
|
|
"total_window_area": total_window_area, |
|
|
"total_door_area": total_door_area |
|
|
} |
|
|
|
|
|
def get_areas_by_orientation(self) -> Dict[str, Dict[str, float]]: |
|
|
""" |
|
|
Get areas for all component types grouped by orientation. |
|
|
|
|
|
Returns: |
|
|
Dictionary with areas by orientation |
|
|
""" |
|
|
|
|
|
result = {} |
|
|
|
|
|
|
|
|
for wall in self.walls.values(): |
|
|
orientation = wall.orientation.value |
|
|
if orientation not in result: |
|
|
result[orientation] = { |
|
|
"wall_gross_area": 0, |
|
|
"wall_net_area": 0, |
|
|
"window_area": 0, |
|
|
"door_area": 0 |
|
|
} |
|
|
|
|
|
result[orientation]["wall_gross_area"] += wall.gross_area if wall.gross_area is not None else 0 |
|
|
result[orientation]["wall_net_area"] += wall.net_area if wall.net_area is not None else 0 |
|
|
|
|
|
|
|
|
for window in self.windows.values(): |
|
|
orientation = window.orientation.value |
|
|
if orientation not in result: |
|
|
result[orientation] = { |
|
|
"wall_gross_area": 0, |
|
|
"wall_net_area": 0, |
|
|
"window_area": 0, |
|
|
"door_area": 0 |
|
|
} |
|
|
|
|
|
result[orientation]["window_area"] += window.area |
|
|
|
|
|
|
|
|
for door in self.doors.values(): |
|
|
orientation = door.orientation.value |
|
|
if orientation not in result: |
|
|
result[orientation] = { |
|
|
"wall_gross_area": 0, |
|
|
"wall_net_area": 0, |
|
|
"window_area": 0, |
|
|
"door_area": 0 |
|
|
} |
|
|
|
|
|
result[orientation]["door_area"] += door.area |
|
|
|
|
|
return result |
|
|
|
|
|
def export_to_json(self, file_path: str) -> None: |
|
|
""" |
|
|
Export all components to a JSON file. |
|
|
|
|
|
Args: |
|
|
file_path: Path to the output JSON file |
|
|
""" |
|
|
data = { |
|
|
"walls": {wall_id: wall.to_dict() for wall_id, wall in self.walls.items()}, |
|
|
"windows": {window_id: window.to_dict() for window_id, window in self.windows.items()}, |
|
|
"doors": {door_id: door.to_dict() for door_id, door in self.doors.items()} |
|
|
} |
|
|
|
|
|
with open(file_path, 'w') as f: |
|
|
json.dump(data, f, indent=4) |
|
|
|
|
|
def import_from_json(self, file_path: str) -> Tuple[int, int, int]: |
|
|
""" |
|
|
Import components from a JSON file. |
|
|
|
|
|
Args: |
|
|
file_path: Path to the input JSON file |
|
|
|
|
|
Returns: |
|
|
Tuple with counts of walls, windows, and doors imported |
|
|
""" |
|
|
from data.building_components import BuildingComponentFactory |
|
|
|
|
|
with open(file_path, 'r') as f: |
|
|
data = json.load(f) |
|
|
|
|
|
wall_count = 0 |
|
|
window_count = 0 |
|
|
door_count = 0 |
|
|
|
|
|
|
|
|
for wall_id, wall_data in data.get("walls", {}).items(): |
|
|
try: |
|
|
wall = BuildingComponentFactory.create_component(wall_data) |
|
|
self.walls[wall_id] = wall |
|
|
wall_count += 1 |
|
|
except Exception as e: |
|
|
print(f"Error importing wall {wall_id}: {e}") |
|
|
|
|
|
|
|
|
for window_id, window_data in data.get("windows", {}).items(): |
|
|
try: |
|
|
window = BuildingComponentFactory.create_component(window_data) |
|
|
self.windows[window_id] = window |
|
|
window_count += 1 |
|
|
except Exception as e: |
|
|
print(f"Error importing window {window_id}: {e}") |
|
|
|
|
|
|
|
|
for door_id, door_data in data.get("doors", {}).items(): |
|
|
try: |
|
|
door = BuildingComponentFactory.create_component(door_data) |
|
|
self.doors[door_id] = door |
|
|
door_count += 1 |
|
|
except Exception as e: |
|
|
print(f"Error importing door {door_id}: {e}") |
|
|
|
|
|
|
|
|
self.update_all_net_areas() |
|
|
|
|
|
return (wall_count, window_count, door_count) |
|
|
|
|
|
|
|
|
|
|
|
area_calculation_system = AreaCalculationSystem() |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
area_calculation_system.export_to_json(os.path.join(DATA_DIR, "data", "area_calculation_system.json")) |
|
|
|