Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| # Import data models | |
| from data.building_components import Wall, Window, Door, Orientation, ComponentType | |
| # Define paths | |
| 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 | |
| # Remove all windows and doors associated with this wall | |
| 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 | |
| # Remove window from associated wall | |
| 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 | |
| # Remove door from associated wall | |
| 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] | |
| # Remove window from previous wall if assigned | |
| 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) | |
| # Assign window to new wall | |
| window.wall_id = wall_id | |
| window.orientation = wall.orientation | |
| # Add window to wall's window list if not already there | |
| if window_id not in wall.windows: | |
| wall.windows.append(window_id) | |
| # Update wall net area | |
| 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] | |
| # Remove door from previous wall if assigned | |
| 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) | |
| # Assign door to new wall | |
| door.wall_id = wall_id | |
| door.orientation = wall.orientation | |
| # Add door to wall's door list if not already there | |
| if door_id not in wall.doors: | |
| wall.doors.append(door_id) | |
| # Update wall net area | |
| 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] | |
| # Calculate total window area | |
| total_window_area = sum(self.windows[window_id].area | |
| for window_id in wall.windows | |
| if window_id in self.windows) | |
| # Calculate total door area | |
| total_door_area = sum(self.doors[door_id].area | |
| for door_id in wall.doors | |
| if door_id in self.doors) | |
| # Update wall net area | |
| 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 # Update the main area property | |
| 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 = [] | |
| # Check for negative or zero net wall areas | |
| 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." | |
| }) | |
| # Check for windows without walls | |
| 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})." | |
| }) | |
| # Check for doors without walls | |
| 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})." | |
| }) | |
| # Check for windows and doors with zero area | |
| 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] | |
| # Calculate total window area | |
| window_area = sum(self.windows[window_id].area | |
| for window_id in wall.windows | |
| if window_id in self.windows) | |
| # Calculate total door area | |
| 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 | |
| """ | |
| # Calculate total wall 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) | |
| # Calculate total window area | |
| total_window_area = sum(window.area for window in self.windows.values()) | |
| # Calculate total door area | |
| 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 | |
| """ | |
| # Initialize result dictionary | |
| result = {} | |
| # Process walls | |
| 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 | |
| # Process windows | |
| 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 | |
| # Process doors | |
| 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 | |
| # Import walls | |
| 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}") | |
| # Import windows | |
| 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}") | |
| # Import doors | |
| 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}") | |
| # Update all net areas | |
| self.update_all_net_areas() | |
| return (wall_count, window_count, door_count) | |
| # Create a singleton instance | |
| area_calculation_system = AreaCalculationSystem() | |
| # Export area calculation system to JSON if needed | |
| if __name__ == "__main__": | |
| area_calculation_system.export_to_json(os.path.join(DATA_DIR, "data", "area_calculation_system.json")) | |