|
|
""" |
|
|
Multiple building management module for HVAC Load Calculator. |
|
|
This module provides functionality for managing multiple buildings. |
|
|
""" |
|
|
|
|
|
import streamlit as st |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
from typing import Dict, List, Any, Optional, Tuple |
|
|
from models.building import Building, CoolingLoadResult |
|
|
from controllers.enhanced_cooling_load_calculator import calculate_cooling_load |
|
|
from controllers.monthly_breakdown import calculate_monthly_breakdown |
|
|
from views.building_comparison import compare_buildings |
|
|
from utils.data_io import export_building_to_json, import_building_from_json |
|
|
|
|
|
class BuildingManager: |
|
|
""" |
|
|
Manager for multiple buildings. |
|
|
""" |
|
|
|
|
|
def __init__(self): |
|
|
""" |
|
|
Initialize building manager. |
|
|
""" |
|
|
|
|
|
if 'buildings' not in st.session_state: |
|
|
st.session_state.buildings = {} |
|
|
|
|
|
if 'results' not in st.session_state: |
|
|
st.session_state.results = {} |
|
|
|
|
|
if 'monthly_breakdowns' not in st.session_state: |
|
|
st.session_state.monthly_breakdowns = {} |
|
|
|
|
|
if 'current_building' not in st.session_state: |
|
|
st.session_state.current_building = None |
|
|
|
|
|
def get_buildings(self) -> Dict[str, Building]: |
|
|
""" |
|
|
Get all buildings. |
|
|
|
|
|
Returns: |
|
|
Dictionary of buildings {name: Building} |
|
|
""" |
|
|
return st.session_state.buildings |
|
|
|
|
|
def get_results(self) -> Dict[str, CoolingLoadResult]: |
|
|
""" |
|
|
Get all results. |
|
|
|
|
|
Returns: |
|
|
Dictionary of results {name: CoolingLoadResult} |
|
|
""" |
|
|
return st.session_state.results |
|
|
|
|
|
def get_monthly_breakdowns(self) -> Dict[str, Dict[str, Any]]: |
|
|
""" |
|
|
Get all monthly breakdowns. |
|
|
|
|
|
Returns: |
|
|
Dictionary of monthly breakdowns {name: monthly_breakdown} |
|
|
""" |
|
|
return st.session_state.monthly_breakdowns |
|
|
|
|
|
def get_current_building(self) -> Optional[str]: |
|
|
""" |
|
|
Get current building name. |
|
|
|
|
|
Returns: |
|
|
Current building name or None if no building is selected |
|
|
""" |
|
|
return st.session_state.current_building |
|
|
|
|
|
def set_current_building(self, name: str): |
|
|
""" |
|
|
Set current building. |
|
|
|
|
|
Args: |
|
|
name: Building name |
|
|
""" |
|
|
st.session_state.current_building = name |
|
|
|
|
|
def add_building(self, building: Building) -> bool: |
|
|
""" |
|
|
Add building to manager. |
|
|
|
|
|
Args: |
|
|
building: Building to add |
|
|
|
|
|
Returns: |
|
|
True if building was added, False if building with same name already exists |
|
|
""" |
|
|
if building.settings.name in st.session_state.buildings: |
|
|
return False |
|
|
|
|
|
st.session_state.buildings[building.settings.name] = building |
|
|
return True |
|
|
|
|
|
def update_building(self, building: Building) -> bool: |
|
|
""" |
|
|
Update existing building. |
|
|
|
|
|
Args: |
|
|
building: Building to update |
|
|
|
|
|
Returns: |
|
|
True if building was updated, False if building does not exist |
|
|
""" |
|
|
if building.settings.name not in st.session_state.buildings: |
|
|
return False |
|
|
|
|
|
st.session_state.buildings[building.settings.name] = building |
|
|
|
|
|
|
|
|
if building.settings.name in st.session_state.results: |
|
|
del st.session_state.results[building.settings.name] |
|
|
|
|
|
if building.settings.name in st.session_state.monthly_breakdowns: |
|
|
del st.session_state.monthly_breakdowns[building.settings.name] |
|
|
|
|
|
return True |
|
|
|
|
|
def remove_building(self, name: str) -> bool: |
|
|
""" |
|
|
Remove building from manager. |
|
|
|
|
|
Args: |
|
|
name: Building name |
|
|
|
|
|
Returns: |
|
|
True if building was removed, False if building does not exist |
|
|
""" |
|
|
if name not in st.session_state.buildings: |
|
|
return False |
|
|
|
|
|
del st.session_state.buildings[name] |
|
|
|
|
|
|
|
|
if name in st.session_state.results: |
|
|
del st.session_state.results[name] |
|
|
|
|
|
if name in st.session_state.monthly_breakdowns: |
|
|
del st.session_state.monthly_breakdowns[name] |
|
|
|
|
|
|
|
|
if st.session_state.current_building == name: |
|
|
if st.session_state.buildings: |
|
|
st.session_state.current_building = next(iter(st.session_state.buildings)) |
|
|
else: |
|
|
st.session_state.current_building = None |
|
|
|
|
|
return True |
|
|
|
|
|
def rename_building(self, old_name: str, new_name: str) -> bool: |
|
|
""" |
|
|
Rename building. |
|
|
|
|
|
Args: |
|
|
old_name: Old building name |
|
|
new_name: New building name |
|
|
|
|
|
Returns: |
|
|
True if building was renamed, False if building does not exist or new name already exists |
|
|
""" |
|
|
if old_name not in st.session_state.buildings: |
|
|
return False |
|
|
|
|
|
if new_name in st.session_state.buildings: |
|
|
return False |
|
|
|
|
|
|
|
|
building = st.session_state.buildings[old_name] |
|
|
|
|
|
|
|
|
building.settings.name = new_name |
|
|
|
|
|
|
|
|
st.session_state.buildings[new_name] = building |
|
|
|
|
|
|
|
|
if old_name in st.session_state.results: |
|
|
st.session_state.results[new_name] = st.session_state.results[old_name] |
|
|
del st.session_state.results[old_name] |
|
|
|
|
|
if old_name in st.session_state.monthly_breakdowns: |
|
|
st.session_state.monthly_breakdowns[new_name] = st.session_state.monthly_breakdowns[old_name] |
|
|
del st.session_state.monthly_breakdowns[old_name] |
|
|
|
|
|
|
|
|
del st.session_state.buildings[old_name] |
|
|
|
|
|
|
|
|
if st.session_state.current_building == old_name: |
|
|
st.session_state.current_building = new_name |
|
|
|
|
|
return True |
|
|
|
|
|
def duplicate_building(self, name: str, new_name: str) -> bool: |
|
|
""" |
|
|
Duplicate building. |
|
|
|
|
|
Args: |
|
|
name: Building name to duplicate |
|
|
new_name: New building name |
|
|
|
|
|
Returns: |
|
|
True if building was duplicated, False if building does not exist or new name already exists |
|
|
""" |
|
|
if name not in st.session_state.buildings: |
|
|
return False |
|
|
|
|
|
if new_name in st.session_state.buildings: |
|
|
return False |
|
|
|
|
|
|
|
|
building = st.session_state.buildings[name] |
|
|
|
|
|
|
|
|
building_json = export_building_to_json(building) |
|
|
|
|
|
|
|
|
new_building = import_building_from_json(building_json) |
|
|
|
|
|
|
|
|
new_building.settings.name = new_name |
|
|
|
|
|
|
|
|
st.session_state.buildings[new_name] = new_building |
|
|
|
|
|
return True |
|
|
|
|
|
def calculate_building(self, name: str) -> Tuple[CoolingLoadResult, Dict[str, Any]]: |
|
|
""" |
|
|
Calculate cooling load for building. |
|
|
|
|
|
Args: |
|
|
name: Building name |
|
|
|
|
|
Returns: |
|
|
Tuple of (result, monthly_breakdown) |
|
|
""" |
|
|
if name not in st.session_state.buildings: |
|
|
return None, None |
|
|
|
|
|
|
|
|
building = st.session_state.buildings[name] |
|
|
|
|
|
|
|
|
result = calculate_cooling_load(building) |
|
|
|
|
|
|
|
|
monthly_breakdown = calculate_monthly_breakdown(building) |
|
|
|
|
|
|
|
|
st.session_state.results[name] = result |
|
|
st.session_state.monthly_breakdowns[name] = monthly_breakdown |
|
|
|
|
|
return result, monthly_breakdown |
|
|
|
|
|
def display_building_manager(self): |
|
|
""" |
|
|
Display building manager UI. |
|
|
""" |
|
|
st.subheader("Building Manager") |
|
|
|
|
|
|
|
|
buildings = self.get_buildings() |
|
|
|
|
|
if not buildings: |
|
|
st.info("No buildings available. Create a new building to get started.") |
|
|
return |
|
|
|
|
|
|
|
|
col1, col2, col3 = st.columns([2, 1, 1]) |
|
|
|
|
|
with col1: |
|
|
|
|
|
building_names = list(buildings.keys()) |
|
|
current_building = self.get_current_building() |
|
|
|
|
|
if current_building not in building_names and building_names: |
|
|
current_building = building_names[0] |
|
|
|
|
|
selected_building = st.selectbox( |
|
|
"Select Building", |
|
|
building_names, |
|
|
index=building_names.index(current_building) if current_building in building_names else 0 |
|
|
) |
|
|
|
|
|
|
|
|
self.set_current_building(selected_building) |
|
|
|
|
|
with col2: |
|
|
|
|
|
if st.button("Rename Building"): |
|
|
new_name = st.text_input("Enter new name:", value=selected_building) |
|
|
if new_name and new_name != selected_building: |
|
|
if self.rename_building(selected_building, new_name): |
|
|
st.success(f"Building renamed to {new_name}") |
|
|
else: |
|
|
st.error(f"Failed to rename building. Name {new_name} already exists.") |
|
|
|
|
|
with col3: |
|
|
|
|
|
if st.button("Remove Building"): |
|
|
if st.checkbox(f"Confirm removal of {selected_building}"): |
|
|
if self.remove_building(selected_building): |
|
|
st.success(f"Building {selected_building} removed") |
|
|
else: |
|
|
st.error(f"Failed to remove building {selected_building}") |
|
|
|
|
|
|
|
|
st.subheader("Building Actions") |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
|
|
with col1: |
|
|
|
|
|
if st.button("Duplicate Building"): |
|
|
new_name = st.text_input("Enter name for duplicate:", value=f"{selected_building} (Copy)") |
|
|
if new_name: |
|
|
if self.duplicate_building(selected_building, new_name): |
|
|
st.success(f"Building duplicated as {new_name}") |
|
|
else: |
|
|
st.error(f"Failed to duplicate building. Name {new_name} already exists.") |
|
|
|
|
|
with col2: |
|
|
|
|
|
if st.button("Calculate Building"): |
|
|
with st.spinner(f"Calculating cooling load for {selected_building}..."): |
|
|
result, monthly_breakdown = self.calculate_building(selected_building) |
|
|
if result: |
|
|
st.success(f"Calculation completed for {selected_building}") |
|
|
else: |
|
|
st.error(f"Failed to calculate cooling load for {selected_building}") |
|
|
|
|
|
with col3: |
|
|
|
|
|
if st.button("Compare Buildings"): |
|
|
st.session_state.show_comparison = True |
|
|
|
|
|
def display_building_comparison(self): |
|
|
""" |
|
|
Display building comparison UI. |
|
|
""" |
|
|
|
|
|
buildings = self.get_buildings() |
|
|
results = self.get_results() |
|
|
monthly_breakdowns = self.get_monthly_breakdowns() |
|
|
|
|
|
|
|
|
if len(buildings) < 2: |
|
|
st.warning("At least two buildings are required for comparison. Please add more buildings.") |
|
|
return |
|
|
|
|
|
|
|
|
missing_results = [name for name in buildings if name not in results] |
|
|
if missing_results: |
|
|
st.warning(f"Missing results for buildings: {', '.join(missing_results)}. Please calculate these buildings first.") |
|
|
|
|
|
|
|
|
if st.button("Calculate Missing Results"): |
|
|
with st.spinner("Calculating missing results..."): |
|
|
for name in missing_results: |
|
|
self.calculate_building(name) |
|
|
st.success("All calculations completed") |
|
|
|
|
|
return |
|
|
|
|
|
|
|
|
compare_buildings(buildings, results, monthly_breakdowns) |
|
|
|
|
|
def display_building_list(self): |
|
|
""" |
|
|
Display building list UI. |
|
|
""" |
|
|
st.subheader("Building List") |
|
|
|
|
|
|
|
|
buildings = self.get_buildings() |
|
|
results = self.get_results() |
|
|
monthly_breakdowns = self.get_monthly_breakdowns() |
|
|
|
|
|
if not buildings: |
|
|
st.info("No buildings available. Create a new building to get started.") |
|
|
return |
|
|
|
|
|
|
|
|
table_data = [] |
|
|
for name, building in buildings.items(): |
|
|
result = results.get(name) |
|
|
monthly_breakdown = monthly_breakdowns.get(name) |
|
|
|
|
|
peak_load = result.peak_total_load if result else None |
|
|
peak_load_per_m2 = peak_load / building.settings.floor_area if peak_load else None |
|
|
|
|
|
annual_energy = monthly_breakdown.get("annual", {}).get("energy_kwh") if monthly_breakdown else None |
|
|
annual_energy_per_m2 = annual_energy / building.settings.floor_area if annual_energy else None |
|
|
|
|
|
table_data.append({ |
|
|
"Building": name, |
|
|
"Location": building.location.city, |
|
|
"Floor Area (m²)": building.settings.floor_area, |
|
|
"Peak Load (kW)": peak_load / 1000 if peak_load else None, |
|
|
"Peak Load (W/m²)": peak_load_per_m2 if peak_load_per_m2 else None, |
|
|
"Annual Energy (kWh)": annual_energy if annual_energy else None, |
|
|
"Annual Energy (kWh/m²)": annual_energy_per_m2 if annual_energy_per_m2 else None, |
|
|
"Calculated": "Yes" if result else "No" |
|
|
}) |
|
|
|
|
|
|
|
|
df = pd.DataFrame(table_data) |
|
|
|
|
|
|
|
|
st.dataframe(df, use_container_width=True) |
|
|
|
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
|
|
|
if st.button("Calculate All Buildings"): |
|
|
with st.spinner("Calculating all buildings..."): |
|
|
for name in buildings: |
|
|
self.calculate_building(name) |
|
|
st.success("All calculations completed") |
|
|
|
|
|
with col2: |
|
|
|
|
|
if st.button("Compare All Buildings"): |
|
|
|
|
|
missing_results = [name for name in buildings if name not in results] |
|
|
if missing_results: |
|
|
st.warning(f"Missing results for buildings: {', '.join(missing_results)}. Please calculate these buildings first.") |
|
|
return |
|
|
|
|
|
st.session_state.show_comparison = True |
|
|
|