Spaces:
Sleeping
Sleeping
Upload 11 files
Browse files- app/building_energy.py +1136 -0
- app/building_information.py +328 -0
- app/climate_data.py +548 -0
- app/components.py +504 -0
- app/construction.py +823 -0
- app/embodied_energy.py +997 -0
- app/hvac_loads.py +683 -0
- app/internal_loads.py +883 -0
- app/materials_cost.py +889 -0
- app/materials_library.py +1215 -0
- app/renewable_energy.py +626 -0
app/building_energy.py
ADDED
@@ -0,0 +1,1136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
HVAC Load Calculator - Building Energy Module
|
3 |
+
|
4 |
+
This module handles the building energy consumption calculations based on the HVAC loads,
|
5 |
+
system efficiencies, and operational parameters. It provides energy use estimates,
|
6 |
+
cost analysis, and carbon emissions calculations.
|
7 |
+
|
8 |
+
Developed by: Dr Majed Abuseif, Deakin University
|
9 |
+
© 2025
|
10 |
+
"""
|
11 |
+
|
12 |
+
import streamlit as st
|
13 |
+
import pandas as pd
|
14 |
+
import numpy as np
|
15 |
+
import json
|
16 |
+
import logging
|
17 |
+
import plotly.graph_objects as go
|
18 |
+
import plotly.express as px
|
19 |
+
from typing import Dict, List, Any, Optional, Tuple, Union
|
20 |
+
from datetime import datetime
|
21 |
+
|
22 |
+
# Configure logging
|
23 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
24 |
+
logger = logging.getLogger(__name__)
|
25 |
+
|
26 |
+
# Constants
|
27 |
+
MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
28 |
+
DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] # Non-leap year
|
29 |
+
HOURS_IN_YEAR = 8760
|
30 |
+
|
31 |
+
# Default HVAC system parameters
|
32 |
+
DEFAULT_HVAC_SYSTEMS = {
|
33 |
+
"Split System Heat Pump": {
|
34 |
+
"cooling_cop": 3.5,
|
35 |
+
"heating_cop": 4.0,
|
36 |
+
"fan_power_ratio": 0.1, # Fan power as fraction of total capacity
|
37 |
+
"pump_power_ratio": 0.05, # Pump power as fraction of total capacity
|
38 |
+
"part_load_performance": "Good", # Qualitative assessment
|
39 |
+
"cost_per_kw": 500, # Installation cost per kW of capacity
|
40 |
+
"lifespan_years": 15,
|
41 |
+
"maintenance_cost_ratio": 0.02 # Annual maintenance as fraction of installation cost
|
42 |
+
},
|
43 |
+
"VRF System": {
|
44 |
+
"cooling_cop": 4.2,
|
45 |
+
"heating_cop": 4.5,
|
46 |
+
"fan_power_ratio": 0.08,
|
47 |
+
"pump_power_ratio": 0.04,
|
48 |
+
"part_load_performance": "Excellent",
|
49 |
+
"cost_per_kw": 800,
|
50 |
+
"lifespan_years": 20,
|
51 |
+
"maintenance_cost_ratio": 0.015
|
52 |
+
},
|
53 |
+
"Chiller with Gas Boiler": {
|
54 |
+
"cooling_cop": 5.0,
|
55 |
+
"heating_cop": 0.85, # Gas boiler efficiency
|
56 |
+
"fan_power_ratio": 0.12,
|
57 |
+
"pump_power_ratio": 0.08,
|
58 |
+
"part_load_performance": "Good",
|
59 |
+
"cost_per_kw": 1000,
|
60 |
+
"lifespan_years": 25,
|
61 |
+
"maintenance_cost_ratio": 0.025
|
62 |
+
},
|
63 |
+
"Air-Cooled Packaged Unit": {
|
64 |
+
"cooling_cop": 3.2,
|
65 |
+
"heating_cop": 3.8,
|
66 |
+
"fan_power_ratio": 0.15,
|
67 |
+
"pump_power_ratio": 0.03,
|
68 |
+
"part_load_performance": "Fair",
|
69 |
+
"cost_per_kw": 400,
|
70 |
+
"lifespan_years": 12,
|
71 |
+
"maintenance_cost_ratio": 0.03
|
72 |
+
},
|
73 |
+
"Water-Source Heat Pump": {
|
74 |
+
"cooling_cop": 4.8,
|
75 |
+
"heating_cop": 5.2,
|
76 |
+
"fan_power_ratio": 0.07,
|
77 |
+
"pump_power_ratio": 0.1,
|
78 |
+
"part_load_performance": "Excellent",
|
79 |
+
"cost_per_kw": 900,
|
80 |
+
"lifespan_years": 20,
|
81 |
+
"maintenance_cost_ratio": 0.02
|
82 |
+
},
|
83 |
+
"Custom System": {
|
84 |
+
"cooling_cop": 4.0,
|
85 |
+
"heating_cop": 4.0,
|
86 |
+
"fan_power_ratio": 0.1,
|
87 |
+
"pump_power_ratio": 0.05,
|
88 |
+
"part_load_performance": "Good",
|
89 |
+
"cost_per_kw": 600,
|
90 |
+
"lifespan_years": 15,
|
91 |
+
"maintenance_cost_ratio": 0.02
|
92 |
+
}
|
93 |
+
}
|
94 |
+
|
95 |
+
# Default energy rates
|
96 |
+
DEFAULT_ENERGY_RATES = {
|
97 |
+
"electricity": {
|
98 |
+
"rate": 0.25, # $/kWh
|
99 |
+
"demand_charge": 15.0, # $/kW-month
|
100 |
+
"carbon_intensity": 0.5 # kg CO2e/kWh
|
101 |
+
},
|
102 |
+
"natural_gas": {
|
103 |
+
"rate": 0.08, # $/kWh
|
104 |
+
"demand_charge": 0.0, # $/kW-month
|
105 |
+
"carbon_intensity": 0.2 # kg CO2e/kWh
|
106 |
+
}
|
107 |
+
}
|
108 |
+
|
109 |
+
def display_building_energy_page():
|
110 |
+
"""
|
111 |
+
Display the building energy page.
|
112 |
+
This is the main function called by main.py when the Building Energy page is selected.
|
113 |
+
"""
|
114 |
+
st.title("Building Energy Consumption")
|
115 |
+
|
116 |
+
# Display help information in an expandable section
|
117 |
+
with st.expander("Help & Information"):
|
118 |
+
display_building_energy_help()
|
119 |
+
|
120 |
+
# Check if HVAC loads have been calculated
|
121 |
+
if "hvac_loads" not in st.session_state.project_data or not st.session_state.project_data["hvac_loads"]:
|
122 |
+
st.warning("Please complete the HVAC Loads calculations before proceeding to Building Energy analysis.")
|
123 |
+
|
124 |
+
# Navigation buttons
|
125 |
+
col1, col2 = st.columns(2)
|
126 |
+
with col1:
|
127 |
+
if st.button("Back to HVAC Loads", key="back_to_hvac_loads"):
|
128 |
+
st.session_state.current_page = "HVAC Loads"
|
129 |
+
st.rerun()
|
130 |
+
return
|
131 |
+
|
132 |
+
# Initialize building energy data if not present
|
133 |
+
initialize_building_energy_data()
|
134 |
+
|
135 |
+
# Create tabs for different aspects of energy analysis
|
136 |
+
tabs = st.tabs(["HVAC System", "Energy Consumption", "Energy Costs", "Carbon Emissions"])
|
137 |
+
|
138 |
+
with tabs[0]:
|
139 |
+
display_hvac_system_tab()
|
140 |
+
|
141 |
+
with tabs[1]:
|
142 |
+
display_energy_consumption_tab()
|
143 |
+
|
144 |
+
with tabs[2]:
|
145 |
+
display_energy_costs_tab()
|
146 |
+
|
147 |
+
with tabs[3]:
|
148 |
+
display_carbon_emissions_tab()
|
149 |
+
|
150 |
+
# Navigation buttons
|
151 |
+
col1, col2 = st.columns(2)
|
152 |
+
|
153 |
+
with col1:
|
154 |
+
if st.button("Back to HVAC Loads", key="back_to_hvac_loads"):
|
155 |
+
st.session_state.current_page = "HVAC Loads"
|
156 |
+
st.rerun()
|
157 |
+
|
158 |
+
with col2:
|
159 |
+
if st.button("Continue to Renewable Energy", key="continue_to_renewable_energy"):
|
160 |
+
st.session_state.current_page = "Renewable Energy"
|
161 |
+
st.rerun()
|
162 |
+
|
163 |
+
def initialize_building_energy_data():
|
164 |
+
"""Initialize building energy data in session state if not present."""
|
165 |
+
if "building_energy" not in st.session_state.project_data:
|
166 |
+
st.session_state.project_data["building_energy"] = {
|
167 |
+
"hvac_system": {
|
168 |
+
"system_type": "Split System Heat Pump",
|
169 |
+
"cooling_cop": DEFAULT_HVAC_SYSTEMS["Split System Heat Pump"]["cooling_cop"],
|
170 |
+
"heating_cop": DEFAULT_HVAC_SYSTEMS["Split System Heat Pump"]["heating_cop"],
|
171 |
+
"fan_power_ratio": DEFAULT_HVAC_SYSTEMS["Split System Heat Pump"]["fan_power_ratio"],
|
172 |
+
"pump_power_ratio": DEFAULT_HVAC_SYSTEMS["Split System Heat Pump"]["pump_power_ratio"],
|
173 |
+
"part_load_performance": DEFAULT_HVAC_SYSTEMS["Split System Heat Pump"]["part_load_performance"],
|
174 |
+
"cost_per_kw": DEFAULT_HVAC_SYSTEMS["Split System Heat Pump"]["cost_per_kw"],
|
175 |
+
"lifespan_years": DEFAULT_HVAC_SYSTEMS["Split System Heat Pump"]["lifespan_years"],
|
176 |
+
"maintenance_cost_ratio": DEFAULT_HVAC_SYSTEMS["Split System Heat Pump"]["maintenance_cost_ratio"]
|
177 |
+
},
|
178 |
+
"energy_rates": {
|
179 |
+
"electricity": DEFAULT_ENERGY_RATES["electricity"].copy(),
|
180 |
+
"natural_gas": DEFAULT_ENERGY_RATES["natural_gas"].copy()
|
181 |
+
},
|
182 |
+
"results": None
|
183 |
+
}
|
184 |
+
|
185 |
+
def display_hvac_system_tab():
|
186 |
+
"""Display the HVAC system selection and configuration tab."""
|
187 |
+
st.header("HVAC System Configuration")
|
188 |
+
|
189 |
+
# Get current HVAC system data
|
190 |
+
hvac_system = st.session_state.project_data["building_energy"]["hvac_system"]
|
191 |
+
|
192 |
+
# System type selection
|
193 |
+
system_type = st.selectbox(
|
194 |
+
"HVAC System Type",
|
195 |
+
list(DEFAULT_HVAC_SYSTEMS.keys()),
|
196 |
+
index=list(DEFAULT_HVAC_SYSTEMS.keys()).index(hvac_system["system_type"]),
|
197 |
+
help="Select the type of HVAC system for the building."
|
198 |
+
)
|
199 |
+
|
200 |
+
# If system type changed, update default values
|
201 |
+
if system_type != hvac_system["system_type"] and system_type != "Custom System":
|
202 |
+
hvac_system.update({
|
203 |
+
"system_type": system_type,
|
204 |
+
"cooling_cop": DEFAULT_HVAC_SYSTEMS[system_type]["cooling_cop"],
|
205 |
+
"heating_cop": DEFAULT_HVAC_SYSTEMS[system_type]["heating_cop"],
|
206 |
+
"fan_power_ratio": DEFAULT_HVAC_SYSTEMS[system_type]["fan_power_ratio"],
|
207 |
+
"pump_power_ratio": DEFAULT_HVAC_SYSTEMS[system_type]["pump_power_ratio"],
|
208 |
+
"part_load_performance": DEFAULT_HVAC_SYSTEMS[system_type]["part_load_performance"],
|
209 |
+
"cost_per_kw": DEFAULT_HVAC_SYSTEMS[system_type]["cost_per_kw"],
|
210 |
+
"lifespan_years": DEFAULT_HVAC_SYSTEMS[system_type]["lifespan_years"],
|
211 |
+
"maintenance_cost_ratio": DEFAULT_HVAC_SYSTEMS[system_type]["maintenance_cost_ratio"]
|
212 |
+
})
|
213 |
+
elif system_type != hvac_system["system_type"] and system_type == "Custom System":
|
214 |
+
hvac_system["system_type"] = system_type
|
215 |
+
|
216 |
+
# System parameters
|
217 |
+
st.subheader("System Performance Parameters")
|
218 |
+
|
219 |
+
col1, col2 = st.columns(2)
|
220 |
+
|
221 |
+
with col1:
|
222 |
+
cooling_cop = st.number_input(
|
223 |
+
"Cooling COP",
|
224 |
+
min_value=1.0,
|
225 |
+
max_value=10.0,
|
226 |
+
value=float(hvac_system["cooling_cop"]),
|
227 |
+
step=0.1,
|
228 |
+
format="%.1f",
|
229 |
+
help="Coefficient of Performance for cooling (higher is more efficient)."
|
230 |
+
)
|
231 |
+
|
232 |
+
fan_power_ratio = st.number_input(
|
233 |
+
"Fan Power Ratio",
|
234 |
+
min_value=0.01,
|
235 |
+
max_value=0.5,
|
236 |
+
value=float(hvac_system["fan_power_ratio"]),
|
237 |
+
step=0.01,
|
238 |
+
format="%.2f",
|
239 |
+
help="Fan power as a fraction of total system capacity."
|
240 |
+
)
|
241 |
+
|
242 |
+
with col2:
|
243 |
+
heating_cop = st.number_input(
|
244 |
+
"Heating COP/Efficiency",
|
245 |
+
min_value=0.5,
|
246 |
+
max_value=10.0,
|
247 |
+
value=float(hvac_system["heating_cop"]),
|
248 |
+
step=0.1,
|
249 |
+
format="%.1f",
|
250 |
+
help="Coefficient of Performance for heating (for heat pumps) or efficiency (for boilers)."
|
251 |
+
)
|
252 |
+
|
253 |
+
pump_power_ratio = st.number_input(
|
254 |
+
"Pump Power Ratio",
|
255 |
+
min_value=0.0,
|
256 |
+
max_value=0.5,
|
257 |
+
value=float(hvac_system["pump_power_ratio"]),
|
258 |
+
step=0.01,
|
259 |
+
format="%.2f",
|
260 |
+
help="Pump power as a fraction of total system capacity."
|
261 |
+
)
|
262 |
+
|
263 |
+
# System cost parameters
|
264 |
+
st.subheader("System Cost Parameters")
|
265 |
+
|
266 |
+
col1, col2, col3 = st.columns(3)
|
267 |
+
|
268 |
+
with col1:
|
269 |
+
cost_per_kw = st.number_input(
|
270 |
+
"Installation Cost ($/kW)",
|
271 |
+
min_value=100.0,
|
272 |
+
max_value=2000.0,
|
273 |
+
value=float(hvac_system["cost_per_kw"]),
|
274 |
+
step=50.0,
|
275 |
+
format="%.0f",
|
276 |
+
help="Installation cost per kW of system capacity."
|
277 |
+
)
|
278 |
+
|
279 |
+
with col2:
|
280 |
+
lifespan_years = st.number_input(
|
281 |
+
"System Lifespan (years)",
|
282 |
+
min_value=5,
|
283 |
+
max_value=30,
|
284 |
+
value=int(hvac_system["lifespan_years"]),
|
285 |
+
step=1,
|
286 |
+
help="Expected lifespan of the HVAC system in years."
|
287 |
+
)
|
288 |
+
|
289 |
+
with col3:
|
290 |
+
maintenance_cost_ratio = st.number_input(
|
291 |
+
"Annual Maintenance Cost Ratio",
|
292 |
+
min_value=0.005,
|
293 |
+
max_value=0.1,
|
294 |
+
value=float(hvac_system["maintenance_cost_ratio"]),
|
295 |
+
step=0.005,
|
296 |
+
format="%.3f",
|
297 |
+
help="Annual maintenance cost as a fraction of installation cost."
|
298 |
+
)
|
299 |
+
|
300 |
+
# Energy rates
|
301 |
+
st.subheader("Energy Rates")
|
302 |
+
|
303 |
+
energy_rates = st.session_state.project_data["building_energy"]["energy_rates"]
|
304 |
+
|
305 |
+
col1, col2 = st.columns(2)
|
306 |
+
|
307 |
+
with col1:
|
308 |
+
st.write("Electricity")
|
309 |
+
electricity_rate = st.number_input(
|
310 |
+
"Electricity Rate ($/kWh)",
|
311 |
+
min_value=0.05,
|
312 |
+
max_value=1.0,
|
313 |
+
value=float(energy_rates["electricity"]["rate"]),
|
314 |
+
step=0.01,
|
315 |
+
format="%.2f",
|
316 |
+
help="Cost of electricity per kWh."
|
317 |
+
)
|
318 |
+
|
319 |
+
electricity_demand_charge = st.number_input(
|
320 |
+
"Electricity Demand Charge ($/kW-month)",
|
321 |
+
min_value=0.0,
|
322 |
+
max_value=50.0,
|
323 |
+
value=float(energy_rates["electricity"]["demand_charge"]),
|
324 |
+
step=1.0,
|
325 |
+
format="%.1f",
|
326 |
+
help="Monthly demand charge for peak electricity usage."
|
327 |
+
)
|
328 |
+
|
329 |
+
electricity_carbon = st.number_input(
|
330 |
+
"Electricity Carbon Intensity (kg CO2e/kWh)",
|
331 |
+
min_value=0.0,
|
332 |
+
max_value=2.0,
|
333 |
+
value=float(energy_rates["electricity"]["carbon_intensity"]),
|
334 |
+
step=0.05,
|
335 |
+
format="%.2f",
|
336 |
+
help="Carbon emissions per kWh of electricity."
|
337 |
+
)
|
338 |
+
|
339 |
+
with col2:
|
340 |
+
st.write("Natural Gas")
|
341 |
+
gas_rate = st.number_input(
|
342 |
+
"Natural Gas Rate ($/kWh)",
|
343 |
+
min_value=0.01,
|
344 |
+
max_value=0.5,
|
345 |
+
value=float(energy_rates["natural_gas"]["rate"]),
|
346 |
+
step=0.01,
|
347 |
+
format="%.2f",
|
348 |
+
help="Cost of natural gas per kWh equivalent."
|
349 |
+
)
|
350 |
+
|
351 |
+
gas_demand_charge = st.number_input(
|
352 |
+
"Natural Gas Demand Charge ($/kW-month)",
|
353 |
+
min_value=0.0,
|
354 |
+
max_value=20.0,
|
355 |
+
value=float(energy_rates["natural_gas"]["demand_charge"]),
|
356 |
+
step=1.0,
|
357 |
+
format="%.1f",
|
358 |
+
help="Monthly demand charge for peak natural gas usage."
|
359 |
+
)
|
360 |
+
|
361 |
+
gas_carbon = st.number_input(
|
362 |
+
"Natural Gas Carbon Intensity (kg CO2e/kWh)",
|
363 |
+
min_value=0.1,
|
364 |
+
max_value=1.0,
|
365 |
+
value=float(energy_rates["natural_gas"]["carbon_intensity"]),
|
366 |
+
step=0.05,
|
367 |
+
format="%.2f",
|
368 |
+
help="Carbon emissions per kWh equivalent of natural gas."
|
369 |
+
)
|
370 |
+
|
371 |
+
# Update HVAC system data
|
372 |
+
hvac_system.update({
|
373 |
+
"cooling_cop": cooling_cop,
|
374 |
+
"heating_cop": heating_cop,
|
375 |
+
"fan_power_ratio": fan_power_ratio,
|
376 |
+
"pump_power_ratio": pump_power_ratio,
|
377 |
+
"cost_per_kw": cost_per_kw,
|
378 |
+
"lifespan_years": lifespan_years,
|
379 |
+
"maintenance_cost_ratio": maintenance_cost_ratio
|
380 |
+
})
|
381 |
+
|
382 |
+
# Update energy rates
|
383 |
+
energy_rates["electricity"].update({
|
384 |
+
"rate": electricity_rate,
|
385 |
+
"demand_charge": electricity_demand_charge,
|
386 |
+
"carbon_intensity": electricity_carbon
|
387 |
+
})
|
388 |
+
|
389 |
+
energy_rates["natural_gas"].update({
|
390 |
+
"rate": gas_rate,
|
391 |
+
"demand_charge": gas_demand_charge,
|
392 |
+
"carbon_intensity": gas_carbon
|
393 |
+
})
|
394 |
+
|
395 |
+
# Calculate energy consumption button
|
396 |
+
if st.button("Calculate Energy Consumption", key="calculate_energy"):
|
397 |
+
try:
|
398 |
+
results = calculate_energy_consumption()
|
399 |
+
st.session_state.project_data["building_energy"]["results"] = results
|
400 |
+
st.success("Energy consumption calculated successfully.")
|
401 |
+
logger.info("Energy consumption calculated.")
|
402 |
+
st.rerun() # Refresh to show results
|
403 |
+
except Exception as e:
|
404 |
+
st.error(f"Error calculating energy consumption: {e}")
|
405 |
+
logger.error(f"Error calculating energy consumption: {e}", exc_info=True)
|
406 |
+
st.session_state.project_data["building_energy"]["results"] = None
|
407 |
+
|
408 |
+
def display_energy_consumption_tab():
|
409 |
+
"""Display the energy consumption analysis tab."""
|
410 |
+
st.header("Energy Consumption Analysis")
|
411 |
+
|
412 |
+
# Check if results are available
|
413 |
+
results = st.session_state.project_data["building_energy"].get("results")
|
414 |
+
if not results:
|
415 |
+
st.info("Please calculate energy consumption using the HVAC System tab.")
|
416 |
+
return
|
417 |
+
|
418 |
+
# Display annual energy summary
|
419 |
+
st.subheader("Annual Energy Summary")
|
420 |
+
|
421 |
+
col1, col2, col3 = st.columns(3)
|
422 |
+
|
423 |
+
with col1:
|
424 |
+
st.metric(
|
425 |
+
"Total Annual Energy",
|
426 |
+
f"{results['annual_total_energy'] / 1000:.1f} MWh",
|
427 |
+
help="Total annual energy consumption for heating, cooling, and auxiliary systems."
|
428 |
+
)
|
429 |
+
|
430 |
+
with col2:
|
431 |
+
st.metric(
|
432 |
+
"Energy Use Intensity",
|
433 |
+
f"{results['energy_use_intensity']:.1f} kWh/m²",
|
434 |
+
help="Annual energy consumption per square meter of floor area."
|
435 |
+
)
|
436 |
+
|
437 |
+
with col3:
|
438 |
+
st.metric(
|
439 |
+
"Peak Demand",
|
440 |
+
f"{results['peak_demand'] / 1000:.2f} kW",
|
441 |
+
help="Maximum power demand throughout the year."
|
442 |
+
)
|
443 |
+
|
444 |
+
# Display energy breakdown
|
445 |
+
st.subheader("Energy Consumption Breakdown")
|
446 |
+
|
447 |
+
# Create pie chart of energy components
|
448 |
+
energy_components = {
|
449 |
+
"Cooling": results["annual_cooling_energy"],
|
450 |
+
"Heating": results["annual_heating_energy"],
|
451 |
+
"Fans": results["annual_fan_energy"],
|
452 |
+
"Pumps": results["annual_pump_energy"]
|
453 |
+
}
|
454 |
+
|
455 |
+
fig_pie = px.pie(
|
456 |
+
values=list(energy_components.values()),
|
457 |
+
names=list(energy_components.keys()),
|
458 |
+
title="Annual Energy Consumption by Component"
|
459 |
+
)
|
460 |
+
st.plotly_chart(fig_pie, use_container_width=True)
|
461 |
+
|
462 |
+
# Display monthly energy consumption
|
463 |
+
st.subheader("Monthly Energy Consumption")
|
464 |
+
|
465 |
+
# Create bar chart of monthly energy
|
466 |
+
monthly_data = {
|
467 |
+
"Month": MONTHS,
|
468 |
+
"Cooling (kWh)": results["monthly_cooling_energy"],
|
469 |
+
"Heating (kWh)": results["monthly_heating_energy"],
|
470 |
+
"Fans (kWh)": results["monthly_fan_energy"],
|
471 |
+
"Pumps (kWh)": results["monthly_pump_energy"]
|
472 |
+
}
|
473 |
+
|
474 |
+
monthly_df = pd.DataFrame(monthly_data)
|
475 |
+
|
476 |
+
fig_monthly = px.bar(
|
477 |
+
monthly_df,
|
478 |
+
x="Month",
|
479 |
+
y=["Cooling (kWh)", "Heating (kWh)", "Fans (kWh)", "Pumps (kWh)"],
|
480 |
+
title="Monthly Energy Consumption by Component",
|
481 |
+
barmode="stack"
|
482 |
+
)
|
483 |
+
st.plotly_chart(fig_monthly, use_container_width=True)
|
484 |
+
|
485 |
+
# Display hourly energy profile for a typical day
|
486 |
+
st.subheader("Hourly Energy Profile")
|
487 |
+
|
488 |
+
# Allow user to select month for hourly profile
|
489 |
+
selected_month = st.selectbox(
|
490 |
+
"Select Month for Hourly Profile",
|
491 |
+
MONTHS,
|
492 |
+
index=6, # Default to July
|
493 |
+
help="Select a month to view the typical daily energy profile."
|
494 |
+
)
|
495 |
+
|
496 |
+
month_index = MONTHS.index(selected_month)
|
497 |
+
|
498 |
+
# Get hourly data for the selected month
|
499 |
+
month_start_hour = sum(DAYS_IN_MONTH[:month_index]) * 24
|
500 |
+
month_hours = DAYS_IN_MONTH[month_index] * 24
|
501 |
+
|
502 |
+
# Calculate average hourly profile for the month
|
503 |
+
hourly_profile = calculate_average_daily_profile(
|
504 |
+
results["hourly_total_energy"][month_start_hour:month_start_hour + month_hours],
|
505 |
+
DAYS_IN_MONTH[month_index]
|
506 |
+
)
|
507 |
+
|
508 |
+
# Create hourly profile chart
|
509 |
+
fig_hourly = px.line(
|
510 |
+
x=list(range(24)),
|
511 |
+
y=hourly_profile,
|
512 |
+
title=f"Average Daily Energy Profile for {selected_month}",
|
513 |
+
labels={"x": "Hour of Day", "y": "Energy (kWh)"}
|
514 |
+
)
|
515 |
+
st.plotly_chart(fig_hourly, use_container_width=True)
|
516 |
+
|
517 |
+
# Display load duration curve
|
518 |
+
st.subheader("Load Duration Curve")
|
519 |
+
|
520 |
+
# Sort hourly energy from highest to lowest
|
521 |
+
load_duration = sorted(results["hourly_total_energy"], reverse=True)
|
522 |
+
|
523 |
+
fig_duration = px.line(
|
524 |
+
x=list(range(1, HOURS_IN_YEAR + 1)),
|
525 |
+
y=load_duration,
|
526 |
+
title="Load Duration Curve",
|
527 |
+
labels={"x": "Hours", "y": "Energy (kWh)"}
|
528 |
+
)
|
529 |
+
fig_duration.update_xaxes(type="log")
|
530 |
+
st.plotly_chart(fig_duration, use_container_width=True)
|
531 |
+
|
532 |
+
def display_energy_costs_tab():
|
533 |
+
"""Display the energy costs analysis tab."""
|
534 |
+
st.header("Energy Cost Analysis")
|
535 |
+
|
536 |
+
# Check if results are available
|
537 |
+
results = st.session_state.project_data["building_energy"].get("results")
|
538 |
+
if not results:
|
539 |
+
st.info("Please calculate energy consumption using the HVAC System tab.")
|
540 |
+
return
|
541 |
+
|
542 |
+
# Display annual cost summary
|
543 |
+
st.subheader("Annual Cost Summary")
|
544 |
+
|
545 |
+
col1, col2, col3 = st.columns(3)
|
546 |
+
|
547 |
+
with col1:
|
548 |
+
st.metric(
|
549 |
+
"Total Annual Energy Cost",
|
550 |
+
f"${results['annual_energy_cost']:.0f}",
|
551 |
+
help="Total annual cost for all energy consumption."
|
552 |
+
)
|
553 |
+
|
554 |
+
with col2:
|
555 |
+
st.metric(
|
556 |
+
"Energy Cost per Area",
|
557 |
+
f"${results['energy_cost_per_area']:.2f}/m²",
|
558 |
+
help="Annual energy cost per square meter of floor area."
|
559 |
+
)
|
560 |
+
|
561 |
+
with col3:
|
562 |
+
st.metric(
|
563 |
+
"Average Energy Rate",
|
564 |
+
f"${results['average_energy_rate']:.3f}/kWh",
|
565 |
+
help="Average cost per kWh of energy consumed."
|
566 |
+
)
|
567 |
+
|
568 |
+
# Display cost breakdown
|
569 |
+
st.subheader("Cost Breakdown")
|
570 |
+
|
571 |
+
# Create pie chart of cost components
|
572 |
+
cost_components = {
|
573 |
+
"Electricity Consumption": results["annual_electricity_consumption_cost"],
|
574 |
+
"Electricity Demand": results["annual_electricity_demand_cost"],
|
575 |
+
"Natural Gas Consumption": results["annual_gas_consumption_cost"],
|
576 |
+
"Natural Gas Demand": results["annual_gas_demand_cost"]
|
577 |
+
}
|
578 |
+
|
579 |
+
fig_cost_pie = px.pie(
|
580 |
+
values=list(cost_components.values()),
|
581 |
+
names=list(cost_components.keys()),
|
582 |
+
title="Annual Energy Cost Breakdown"
|
583 |
+
)
|
584 |
+
st.plotly_chart(fig_cost_pie, use_container_width=True)
|
585 |
+
|
586 |
+
# Display monthly energy costs
|
587 |
+
st.subheader("Monthly Energy Costs")
|
588 |
+
|
589 |
+
# Create bar chart of monthly costs
|
590 |
+
monthly_cost_data = {
|
591 |
+
"Month": MONTHS,
|
592 |
+
"Electricity Consumption": results["monthly_electricity_consumption_cost"],
|
593 |
+
"Electricity Demand": results["monthly_electricity_demand_cost"],
|
594 |
+
"Natural Gas Consumption": results["monthly_gas_consumption_cost"],
|
595 |
+
"Natural Gas Demand": results["monthly_gas_demand_cost"]
|
596 |
+
}
|
597 |
+
|
598 |
+
monthly_cost_df = pd.DataFrame(monthly_cost_data)
|
599 |
+
|
600 |
+
fig_monthly_cost = px.bar(
|
601 |
+
monthly_cost_df,
|
602 |
+
x="Month",
|
603 |
+
y=["Electricity Consumption", "Electricity Demand", "Natural Gas Consumption", "Natural Gas Demand"],
|
604 |
+
title="Monthly Energy Costs by Component",
|
605 |
+
barmode="stack"
|
606 |
+
)
|
607 |
+
st.plotly_chart(fig_monthly_cost, use_container_width=True)
|
608 |
+
|
609 |
+
# Display lifecycle cost analysis
|
610 |
+
st.subheader("Lifecycle Cost Analysis")
|
611 |
+
|
612 |
+
# Get HVAC system data
|
613 |
+
hvac_system = st.session_state.project_data["building_energy"]["hvac_system"]
|
614 |
+
|
615 |
+
# Calculate lifecycle costs
|
616 |
+
lifecycle_years = 30 # Standard lifecycle analysis period
|
617 |
+
|
618 |
+
# Initial cost
|
619 |
+
peak_load = max(
|
620 |
+
results["peak_cooling_load"],
|
621 |
+
results["peak_heating_load"]
|
622 |
+
)
|
623 |
+
initial_cost = peak_load * hvac_system["cost_per_kw"]
|
624 |
+
|
625 |
+
# Replacement costs
|
626 |
+
num_replacements = lifecycle_years // hvac_system["lifespan_years"]
|
627 |
+
replacement_cost = initial_cost * num_replacements
|
628 |
+
|
629 |
+
# Maintenance costs
|
630 |
+
annual_maintenance = initial_cost * hvac_system["maintenance_cost_ratio"]
|
631 |
+
total_maintenance = annual_maintenance * lifecycle_years
|
632 |
+
|
633 |
+
# Energy costs
|
634 |
+
total_energy_cost = results["annual_energy_cost"] * lifecycle_years
|
635 |
+
|
636 |
+
# Total lifecycle cost
|
637 |
+
total_lifecycle_cost = initial_cost + replacement_cost + total_maintenance + total_energy_cost
|
638 |
+
|
639 |
+
# Create lifecycle cost breakdown
|
640 |
+
lifecycle_components = {
|
641 |
+
"Initial Installation": initial_cost,
|
642 |
+
"Replacement": replacement_cost,
|
643 |
+
"Maintenance": total_maintenance,
|
644 |
+
"Energy": total_energy_cost
|
645 |
+
}
|
646 |
+
|
647 |
+
fig_lifecycle = px.pie(
|
648 |
+
values=list(lifecycle_components.values()),
|
649 |
+
names=list(lifecycle_components.keys()),
|
650 |
+
title=f"{lifecycle_years}-Year Lifecycle Cost Breakdown"
|
651 |
+
)
|
652 |
+
st.plotly_chart(fig_lifecycle, use_container_width=True)
|
653 |
+
|
654 |
+
# Display lifecycle cost summary
|
655 |
+
col1, col2 = st.columns(2)
|
656 |
+
|
657 |
+
with col1:
|
658 |
+
st.metric(
|
659 |
+
"Total Lifecycle Cost",
|
660 |
+
f"${total_lifecycle_cost:.0f}",
|
661 |
+
help=f"Total cost over {lifecycle_years} years including installation, replacement, maintenance, and energy."
|
662 |
+
)
|
663 |
+
|
664 |
+
with col2:
|
665 |
+
st.metric(
|
666 |
+
"Annualized Cost",
|
667 |
+
f"${total_lifecycle_cost / lifecycle_years:.0f}/year",
|
668 |
+
help=f"Average annual cost over {lifecycle_years} years."
|
669 |
+
)
|
670 |
+
|
671 |
+
def display_carbon_emissions_tab():
|
672 |
+
"""Display the carbon emissions analysis tab."""
|
673 |
+
st.header("Carbon Emissions Analysis")
|
674 |
+
|
675 |
+
# Check if results are available
|
676 |
+
results = st.session_state.project_data["building_energy"].get("results")
|
677 |
+
if not results:
|
678 |
+
st.info("Please calculate energy consumption using the HVAC System tab.")
|
679 |
+
return
|
680 |
+
|
681 |
+
# Display annual emissions summary
|
682 |
+
st.subheader("Annual Emissions Summary")
|
683 |
+
|
684 |
+
col1, col2 = st.columns(2)
|
685 |
+
|
686 |
+
with col1:
|
687 |
+
st.metric(
|
688 |
+
"Total Annual Emissions",
|
689 |
+
f"{results['annual_carbon_emissions']:.1f} tonnes CO2e",
|
690 |
+
help="Total annual carbon emissions from all energy consumption."
|
691 |
+
)
|
692 |
+
|
693 |
+
with col2:
|
694 |
+
st.metric(
|
695 |
+
"Emissions Intensity",
|
696 |
+
f"{results['carbon_emissions_intensity']:.1f} kg CO2e/m²",
|
697 |
+
help="Annual carbon emissions per square meter of floor area."
|
698 |
+
)
|
699 |
+
|
700 |
+
# Display emissions breakdown
|
701 |
+
st.subheader("Emissions Breakdown")
|
702 |
+
|
703 |
+
# Create pie chart of emissions sources
|
704 |
+
emissions_components = {
|
705 |
+
"Electricity": results["annual_electricity_emissions"],
|
706 |
+
"Natural Gas": results["annual_gas_emissions"]
|
707 |
+
}
|
708 |
+
|
709 |
+
fig_emissions_pie = px.pie(
|
710 |
+
values=list(emissions_components.values()),
|
711 |
+
names=list(emissions_components.keys()),
|
712 |
+
title="Annual Carbon Emissions by Source"
|
713 |
+
)
|
714 |
+
st.plotly_chart(fig_emissions_pie, use_container_width=True)
|
715 |
+
|
716 |
+
# Display monthly emissions
|
717 |
+
st.subheader("Monthly Carbon Emissions")
|
718 |
+
|
719 |
+
# Create bar chart of monthly emissions
|
720 |
+
monthly_emissions_data = {
|
721 |
+
"Month": MONTHS,
|
722 |
+
"Electricity Emissions": results["monthly_electricity_emissions"],
|
723 |
+
"Natural Gas Emissions": results["monthly_gas_emissions"]
|
724 |
+
}
|
725 |
+
|
726 |
+
monthly_emissions_df = pd.DataFrame(monthly_emissions_data)
|
727 |
+
|
728 |
+
fig_monthly_emissions = px.bar(
|
729 |
+
monthly_emissions_df,
|
730 |
+
x="Month",
|
731 |
+
y=["Electricity Emissions", "Natural Gas Emissions"],
|
732 |
+
title="Monthly Carbon Emissions by Source",
|
733 |
+
barmode="stack"
|
734 |
+
)
|
735 |
+
st.plotly_chart(fig_monthly_emissions, use_container_width=True)
|
736 |
+
|
737 |
+
# Display emissions benchmark comparison
|
738 |
+
st.subheader("Emissions Benchmark Comparison")
|
739 |
+
|
740 |
+
# Define benchmark emissions intensities (kg CO2e/m²/year)
|
741 |
+
benchmarks = {
|
742 |
+
"This Building": results["carbon_emissions_intensity"],
|
743 |
+
"Low Energy Building": 15.0,
|
744 |
+
"Average Building": 50.0,
|
745 |
+
"High Energy Building": 100.0
|
746 |
+
}
|
747 |
+
|
748 |
+
# Create benchmark comparison chart
|
749 |
+
fig_benchmark = px.bar(
|
750 |
+
x=list(benchmarks.keys()),
|
751 |
+
y=list(benchmarks.values()),
|
752 |
+
title="Carbon Emissions Intensity Comparison",
|
753 |
+
labels={"x": "Building Type", "y": "Emissions Intensity (kg CO2e/m²/year)"}
|
754 |
+
)
|
755 |
+
st.plotly_chart(fig_benchmark, use_container_width=True)
|
756 |
+
|
757 |
+
# Display emissions reduction potential
|
758 |
+
st.subheader("Emissions Reduction Potential")
|
759 |
+
|
760 |
+
# Calculate potential reductions
|
761 |
+
potential_reductions = {
|
762 |
+
"Current Emissions": results["annual_carbon_emissions"],
|
763 |
+
"With 25% More Efficient HVAC": results["annual_carbon_emissions"] * 0.75,
|
764 |
+
"With 50% Renewable Electricity": results["annual_carbon_emissions"] - (results["annual_electricity_emissions"] * 0.5),
|
765 |
+
"With Both Measures": results["annual_carbon_emissions"] * 0.75 - (results["annual_electricity_emissions"] * 0.5 * 0.75)
|
766 |
+
}
|
767 |
+
|
768 |
+
# Create reduction potential chart
|
769 |
+
fig_reduction = px.bar(
|
770 |
+
x=list(potential_reductions.keys()),
|
771 |
+
y=list(potential_reductions.values()),
|
772 |
+
title="Carbon Emissions Reduction Potential",
|
773 |
+
labels={"x": "Scenario", "y": "Annual Emissions (tonnes CO2e)"}
|
774 |
+
)
|
775 |
+
st.plotly_chart(fig_reduction, use_container_width=True)
|
776 |
+
|
777 |
+
def calculate_energy_consumption() -> Dict[str, Any]:
|
778 |
+
"""
|
779 |
+
Calculate building energy consumption based on HVAC loads and system parameters.
|
780 |
+
|
781 |
+
Returns:
|
782 |
+
Dictionary containing energy consumption results.
|
783 |
+
"""
|
784 |
+
logger.info("Starting energy consumption calculations...")
|
785 |
+
|
786 |
+
# Get required data
|
787 |
+
hvac_loads = st.session_state.project_data["hvac_loads"]
|
788 |
+
building_info = st.session_state.project_data["building_info"]
|
789 |
+
hvac_system = st.session_state.project_data["building_energy"]["hvac_system"]
|
790 |
+
energy_rates = st.session_state.project_data["building_energy"]["energy_rates"]
|
791 |
+
|
792 |
+
# Get hourly load data
|
793 |
+
hourly_cooling_sensible = np.array(hvac_loads["hourly_cooling_sensible"])
|
794 |
+
hourly_cooling_latent = np.array(hvac_loads["hourly_cooling_latent"])
|
795 |
+
hourly_cooling_total = np.array(hvac_loads["hourly_cooling_total"])
|
796 |
+
hourly_heating = np.array(hvac_loads["hourly_heating"])
|
797 |
+
|
798 |
+
# Get system parameters
|
799 |
+
cooling_cop = hvac_system["cooling_cop"]
|
800 |
+
heating_cop = hvac_system["heating_cop"]
|
801 |
+
fan_power_ratio = hvac_system["fan_power_ratio"]
|
802 |
+
pump_power_ratio = hvac_system["pump_power_ratio"]
|
803 |
+
|
804 |
+
# Calculate peak loads (W)
|
805 |
+
peak_cooling_load = hvac_loads["peak_cooling_total"]
|
806 |
+
peak_heating_load = hvac_loads["peak_heating"]
|
807 |
+
|
808 |
+
# Calculate hourly energy consumption (kWh)
|
809 |
+
# Convert from W to kWh by dividing by 1000
|
810 |
+
hourly_cooling_energy = hourly_cooling_total / cooling_cop / 1000
|
811 |
+
hourly_heating_energy = hourly_heating / heating_cop / 1000
|
812 |
+
|
813 |
+
# Calculate fan and pump energy
|
814 |
+
# Fans run whenever there's heating or cooling
|
815 |
+
hourly_fan_energy = (
|
816 |
+
(hourly_cooling_total + hourly_heating) * fan_power_ratio / 1000
|
817 |
+
)
|
818 |
+
|
819 |
+
# Pumps run primarily for cooling, but also for some heating systems
|
820 |
+
hourly_pump_energy = (
|
821 |
+
hourly_cooling_total * pump_power_ratio / 1000 +
|
822 |
+
hourly_heating * pump_power_ratio * 0.5 / 1000 # Assume 50% pump usage for heating
|
823 |
+
)
|
824 |
+
|
825 |
+
# Calculate total hourly energy
|
826 |
+
hourly_total_energy = (
|
827 |
+
hourly_cooling_energy +
|
828 |
+
hourly_heating_energy +
|
829 |
+
hourly_fan_energy +
|
830 |
+
hourly_pump_energy
|
831 |
+
)
|
832 |
+
|
833 |
+
# Calculate monthly energy consumption
|
834 |
+
monthly_cooling_energy = calculate_monthly_totals(hourly_cooling_energy)
|
835 |
+
monthly_heating_energy = calculate_monthly_totals(hourly_heating_energy)
|
836 |
+
monthly_fan_energy = calculate_monthly_totals(hourly_fan_energy)
|
837 |
+
monthly_pump_energy = calculate_monthly_totals(hourly_pump_energy)
|
838 |
+
monthly_total_energy = calculate_monthly_totals(hourly_total_energy)
|
839 |
+
|
840 |
+
# Calculate annual energy consumption
|
841 |
+
annual_cooling_energy = sum(monthly_cooling_energy)
|
842 |
+
annual_heating_energy = sum(monthly_heating_energy)
|
843 |
+
annual_fan_energy = sum(monthly_fan_energy)
|
844 |
+
annual_pump_energy = sum(monthly_pump_energy)
|
845 |
+
annual_total_energy = sum(monthly_total_energy)
|
846 |
+
|
847 |
+
# Calculate energy use intensity (kWh/m²)
|
848 |
+
floor_area = building_info["floor_area"]
|
849 |
+
energy_use_intensity = annual_total_energy / floor_area
|
850 |
+
|
851 |
+
# Calculate peak demand (kW)
|
852 |
+
peak_demand = max(hourly_total_energy)
|
853 |
+
|
854 |
+
# Calculate energy costs
|
855 |
+
# Determine energy source for heating (electricity or gas)
|
856 |
+
is_electric_heating = hvac_system["system_type"] not in ["Chiller with Gas Boiler"]
|
857 |
+
|
858 |
+
# Calculate electricity and gas consumption
|
859 |
+
if is_electric_heating:
|
860 |
+
# All electric system
|
861 |
+
hourly_electricity = hourly_total_energy
|
862 |
+
hourly_gas = np.zeros(HOURS_IN_YEAR)
|
863 |
+
else:
|
864 |
+
# Gas heating, electric cooling
|
865 |
+
hourly_electricity = (
|
866 |
+
hourly_cooling_energy +
|
867 |
+
hourly_fan_energy +
|
868 |
+
hourly_pump_energy
|
869 |
+
)
|
870 |
+
hourly_gas = hourly_heating_energy
|
871 |
+
|
872 |
+
# Calculate monthly electricity and gas consumption
|
873 |
+
monthly_electricity = calculate_monthly_totals(hourly_electricity)
|
874 |
+
monthly_gas = calculate_monthly_totals(hourly_gas)
|
875 |
+
|
876 |
+
# Calculate annual electricity and gas consumption
|
877 |
+
annual_electricity = sum(monthly_electricity)
|
878 |
+
annual_gas = sum(monthly_gas)
|
879 |
+
|
880 |
+
# Calculate monthly peak demand
|
881 |
+
monthly_electricity_peak = calculate_monthly_peaks(hourly_electricity)
|
882 |
+
monthly_gas_peak = calculate_monthly_peaks(hourly_gas)
|
883 |
+
|
884 |
+
# Calculate energy costs
|
885 |
+
# Consumption costs
|
886 |
+
monthly_electricity_consumption_cost = [
|
887 |
+
monthly_electricity[i] * energy_rates["electricity"]["rate"]
|
888 |
+
for i in range(12)
|
889 |
+
]
|
890 |
+
|
891 |
+
monthly_gas_consumption_cost = [
|
892 |
+
monthly_gas[i] * energy_rates["natural_gas"]["rate"]
|
893 |
+
for i in range(12)
|
894 |
+
]
|
895 |
+
|
896 |
+
# Demand costs
|
897 |
+
monthly_electricity_demand_cost = [
|
898 |
+
monthly_electricity_peak[i] * energy_rates["electricity"]["demand_charge"]
|
899 |
+
for i in range(12)
|
900 |
+
]
|
901 |
+
|
902 |
+
monthly_gas_demand_cost = [
|
903 |
+
monthly_gas_peak[i] * energy_rates["natural_gas"]["demand_charge"]
|
904 |
+
for i in range(12)
|
905 |
+
]
|
906 |
+
|
907 |
+
# Total monthly costs
|
908 |
+
monthly_total_cost = [
|
909 |
+
monthly_electricity_consumption_cost[i] +
|
910 |
+
monthly_electricity_demand_cost[i] +
|
911 |
+
monthly_gas_consumption_cost[i] +
|
912 |
+
monthly_gas_demand_cost[i]
|
913 |
+
for i in range(12)
|
914 |
+
]
|
915 |
+
|
916 |
+
# Annual costs
|
917 |
+
annual_electricity_consumption_cost = sum(monthly_electricity_consumption_cost)
|
918 |
+
annual_electricity_demand_cost = sum(monthly_electricity_demand_cost)
|
919 |
+
annual_gas_consumption_cost = sum(monthly_gas_consumption_cost)
|
920 |
+
annual_gas_demand_cost = sum(monthly_gas_demand_cost)
|
921 |
+
annual_energy_cost = sum(monthly_total_cost)
|
922 |
+
|
923 |
+
# Calculate cost metrics
|
924 |
+
energy_cost_per_area = annual_energy_cost / floor_area
|
925 |
+
average_energy_rate = annual_energy_cost / annual_total_energy if annual_total_energy > 0 else 0
|
926 |
+
|
927 |
+
# Calculate carbon emissions
|
928 |
+
# Monthly emissions
|
929 |
+
monthly_electricity_emissions = [
|
930 |
+
monthly_electricity[i] * energy_rates["electricity"]["carbon_intensity"]
|
931 |
+
for i in range(12)
|
932 |
+
]
|
933 |
+
|
934 |
+
monthly_gas_emissions = [
|
935 |
+
monthly_gas[i] * energy_rates["natural_gas"]["carbon_intensity"]
|
936 |
+
for i in range(12)
|
937 |
+
]
|
938 |
+
|
939 |
+
monthly_total_emissions = [
|
940 |
+
monthly_electricity_emissions[i] + monthly_gas_emissions[i]
|
941 |
+
for i in range(12)
|
942 |
+
]
|
943 |
+
|
944 |
+
# Annual emissions
|
945 |
+
annual_electricity_emissions = sum(monthly_electricity_emissions)
|
946 |
+
annual_gas_emissions = sum(monthly_gas_emissions)
|
947 |
+
annual_carbon_emissions = annual_electricity_emissions + annual_gas_emissions
|
948 |
+
|
949 |
+
# Convert from kg to tonnes
|
950 |
+
annual_carbon_emissions /= 1000
|
951 |
+
annual_electricity_emissions /= 1000
|
952 |
+
annual_gas_emissions /= 1000
|
953 |
+
|
954 |
+
# Calculate emissions intensity
|
955 |
+
carbon_emissions_intensity = annual_carbon_emissions * 1000 / floor_area # kg CO2e/m²
|
956 |
+
|
957 |
+
# Compile results
|
958 |
+
results = {
|
959 |
+
# Energy consumption
|
960 |
+
"hourly_cooling_energy": hourly_cooling_energy.tolist(),
|
961 |
+
"hourly_heating_energy": hourly_heating_energy.tolist(),
|
962 |
+
"hourly_fan_energy": hourly_fan_energy.tolist(),
|
963 |
+
"hourly_pump_energy": hourly_pump_energy.tolist(),
|
964 |
+
"hourly_total_energy": hourly_total_energy.tolist(),
|
965 |
+
"hourly_electricity": hourly_electricity.tolist(),
|
966 |
+
"hourly_gas": hourly_gas.tolist(),
|
967 |
+
|
968 |
+
"monthly_cooling_energy": monthly_cooling_energy,
|
969 |
+
"monthly_heating_energy": monthly_heating_energy,
|
970 |
+
"monthly_fan_energy": monthly_fan_energy,
|
971 |
+
"monthly_pump_energy": monthly_pump_energy,
|
972 |
+
"monthly_total_energy": monthly_total_energy,
|
973 |
+
"monthly_electricity": monthly_electricity,
|
974 |
+
"monthly_gas": monthly_gas,
|
975 |
+
"monthly_electricity_peak": monthly_electricity_peak,
|
976 |
+
"monthly_gas_peak": monthly_gas_peak,
|
977 |
+
|
978 |
+
"annual_cooling_energy": annual_cooling_energy,
|
979 |
+
"annual_heating_energy": annual_heating_energy,
|
980 |
+
"annual_fan_energy": annual_fan_energy,
|
981 |
+
"annual_pump_energy": annual_pump_energy,
|
982 |
+
"annual_total_energy": annual_total_energy,
|
983 |
+
"annual_electricity": annual_electricity,
|
984 |
+
"annual_gas": annual_gas,
|
985 |
+
|
986 |
+
"energy_use_intensity": energy_use_intensity,
|
987 |
+
"peak_demand": peak_demand,
|
988 |
+
"peak_cooling_load": peak_cooling_load,
|
989 |
+
"peak_heating_load": peak_heating_load,
|
990 |
+
|
991 |
+
# Energy costs
|
992 |
+
"monthly_electricity_consumption_cost": monthly_electricity_consumption_cost,
|
993 |
+
"monthly_electricity_demand_cost": monthly_electricity_demand_cost,
|
994 |
+
"monthly_gas_consumption_cost": monthly_gas_consumption_cost,
|
995 |
+
"monthly_gas_demand_cost": monthly_gas_demand_cost,
|
996 |
+
"monthly_total_cost": monthly_total_cost,
|
997 |
+
|
998 |
+
"annual_electricity_consumption_cost": annual_electricity_consumption_cost,
|
999 |
+
"annual_electricity_demand_cost": annual_electricity_demand_cost,
|
1000 |
+
"annual_gas_consumption_cost": annual_gas_consumption_cost,
|
1001 |
+
"annual_gas_demand_cost": annual_gas_demand_cost,
|
1002 |
+
"annual_energy_cost": annual_energy_cost,
|
1003 |
+
|
1004 |
+
"energy_cost_per_area": energy_cost_per_area,
|
1005 |
+
"average_energy_rate": average_energy_rate,
|
1006 |
+
|
1007 |
+
# Carbon emissions
|
1008 |
+
"monthly_electricity_emissions": monthly_electricity_emissions,
|
1009 |
+
"monthly_gas_emissions": monthly_gas_emissions,
|
1010 |
+
"monthly_total_emissions": monthly_total_emissions,
|
1011 |
+
|
1012 |
+
"annual_electricity_emissions": annual_electricity_emissions,
|
1013 |
+
"annual_gas_emissions": annual_gas_emissions,
|
1014 |
+
"annual_carbon_emissions": annual_carbon_emissions,
|
1015 |
+
|
1016 |
+
"carbon_emissions_intensity": carbon_emissions_intensity,
|
1017 |
+
|
1018 |
+
# Calculation timestamp
|
1019 |
+
"calculation_timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
1020 |
+
}
|
1021 |
+
|
1022 |
+
logger.info("Energy consumption calculations completed.")
|
1023 |
+
return results
|
1024 |
+
|
1025 |
+
def calculate_monthly_totals(hourly_data: np.ndarray) -> List[float]:
|
1026 |
+
"""
|
1027 |
+
Calculate monthly totals from hourly data.
|
1028 |
+
|
1029 |
+
Args:
|
1030 |
+
hourly_data: Numpy array of hourly values
|
1031 |
+
|
1032 |
+
Returns:
|
1033 |
+
List of monthly totals
|
1034 |
+
"""
|
1035 |
+
monthly_totals = []
|
1036 |
+
hour_index = 0
|
1037 |
+
|
1038 |
+
for days in DAYS_IN_MONTH:
|
1039 |
+
hours_in_month = days * 24
|
1040 |
+
month_total = sum(hourly_data[hour_index:hour_index + hours_in_month])
|
1041 |
+
monthly_totals.append(month_total)
|
1042 |
+
hour_index += hours_in_month
|
1043 |
+
|
1044 |
+
return monthly_totals
|
1045 |
+
|
1046 |
+
def calculate_monthly_peaks(hourly_data: np.ndarray) -> List[float]:
|
1047 |
+
"""
|
1048 |
+
Calculate monthly peak values from hourly data.
|
1049 |
+
|
1050 |
+
Args:
|
1051 |
+
hourly_data: Numpy array of hourly values
|
1052 |
+
|
1053 |
+
Returns:
|
1054 |
+
List of monthly peak values
|
1055 |
+
"""
|
1056 |
+
monthly_peaks = []
|
1057 |
+
hour_index = 0
|
1058 |
+
|
1059 |
+
for days in DAYS_IN_MONTH:
|
1060 |
+
hours_in_month = days * 24
|
1061 |
+
month_data = hourly_data[hour_index:hour_index + hours_in_month]
|
1062 |
+
monthly_peaks.append(max(month_data) if len(month_data) > 0 else 0)
|
1063 |
+
hour_index += hours_in_month
|
1064 |
+
|
1065 |
+
return monthly_peaks
|
1066 |
+
|
1067 |
+
def calculate_average_daily_profile(hourly_data: np.ndarray, days: int) -> List[float]:
|
1068 |
+
"""
|
1069 |
+
Calculate average daily profile from hourly data for a month.
|
1070 |
+
|
1071 |
+
Args:
|
1072 |
+
hourly_data: Numpy array of hourly values for the month
|
1073 |
+
days: Number of days in the month
|
1074 |
+
|
1075 |
+
Returns:
|
1076 |
+
List of 24 hourly average values
|
1077 |
+
"""
|
1078 |
+
daily_profile = [0.0] * 24
|
1079 |
+
|
1080 |
+
for hour in range(len(hourly_data)):
|
1081 |
+
hour_of_day = hour % 24
|
1082 |
+
daily_profile[hour_of_day] += hourly_data[hour]
|
1083 |
+
|
1084 |
+
# Calculate averages
|
1085 |
+
for i in range(24):
|
1086 |
+
daily_profile[i] /= days
|
1087 |
+
|
1088 |
+
return daily_profile
|
1089 |
+
|
1090 |
+
def display_building_energy_help():
|
1091 |
+
"""
|
1092 |
+
Display help information for the building energy page.
|
1093 |
+
"""
|
1094 |
+
st.markdown("""
|
1095 |
+
### Building Energy Consumption Help
|
1096 |
+
|
1097 |
+
This section calculates the building's energy consumption, costs, and carbon emissions based on the HVAC loads and system parameters.
|
1098 |
+
|
1099 |
+
**Key Concepts:**
|
1100 |
+
|
1101 |
+
* **COP (Coefficient of Performance)**: Ratio of useful heating or cooling provided to energy input. Higher values indicate better efficiency.
|
1102 |
+
* **Energy Use Intensity (EUI)**: Annual energy consumption per unit floor area (kWh/m²).
|
1103 |
+
* **Peak Demand**: Maximum power required at any point during the year.
|
1104 |
+
* **Demand Charges**: Utility charges based on peak power demand, typically monthly.
|
1105 |
+
* **Carbon Emissions**: Greenhouse gas emissions associated with energy consumption.
|
1106 |
+
|
1107 |
+
**Workflow:**
|
1108 |
+
|
1109 |
+
1. **HVAC System Tab**:
|
1110 |
+
* Select the type of HVAC system for your building.
|
1111 |
+
* Adjust system performance parameters (COP, fan power, etc.).
|
1112 |
+
* Set energy rates and carbon intensities.
|
1113 |
+
* Click "Calculate Energy Consumption" to perform the analysis.
|
1114 |
+
|
1115 |
+
2. **Energy Consumption Tab**:
|
1116 |
+
* Review annual and monthly energy consumption.
|
1117 |
+
* Examine energy breakdown by component.
|
1118 |
+
* Analyze hourly energy profiles and load duration curve.
|
1119 |
+
|
1120 |
+
3. **Energy Costs Tab**:
|
1121 |
+
* Review annual and monthly energy costs.
|
1122 |
+
* Examine cost breakdown by component.
|
1123 |
+
* Analyze lifecycle cost including installation, maintenance, and energy.
|
1124 |
+
|
1125 |
+
4. **Carbon Emissions Tab**:
|
1126 |
+
* Review annual and monthly carbon emissions.
|
1127 |
+
* Compare emissions to benchmarks.
|
1128 |
+
* Explore emissions reduction potential.
|
1129 |
+
|
1130 |
+
**Important:**
|
1131 |
+
|
1132 |
+
* Energy calculations are based on the HVAC loads calculated in the previous section.
|
1133 |
+
* System efficiency has a major impact on energy consumption and costs.
|
1134 |
+
* Consider both initial costs and lifecycle costs when evaluating systems.
|
1135 |
+
* Carbon emissions vary significantly based on the energy source and local grid mix.
|
1136 |
+
""")
|
app/building_information.py
ADDED
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
HVAC Load Calculator - Building Information Module
|
3 |
+
|
4 |
+
This module handles the building information input page of the HVAC Load Calculator application,
|
5 |
+
allowing users to define basic building parameters such as project name, floor area, building height,
|
6 |
+
indoor design conditions, orientation, and building type.
|
7 |
+
|
8 |
+
Developed by: Dr Majed Abuseif, Deakin University
|
9 |
+
© 2025
|
10 |
+
"""
|
11 |
+
|
12 |
+
import streamlit as st
|
13 |
+
import logging
|
14 |
+
import math
|
15 |
+
from typing import Dict, Any, Optional, List, Tuple
|
16 |
+
from data.internal_loads import BUILDING_TYPES
|
17 |
+
|
18 |
+
# Configure logging
|
19 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
20 |
+
logger = logging.getLogger(__name__)
|
21 |
+
|
22 |
+
def display_building_info_page():
|
23 |
+
"""
|
24 |
+
Display the building information input page.
|
25 |
+
This is the main function called by main.py when the Building Information page is selected.
|
26 |
+
"""
|
27 |
+
st.title("Building Information")
|
28 |
+
|
29 |
+
# Display help information in an expandable section
|
30 |
+
with st.expander("Help & Information"):
|
31 |
+
display_building_info_help()
|
32 |
+
|
33 |
+
# Create the main form for building information input
|
34 |
+
with st.form("building_info_form"):
|
35 |
+
# Get current values from session state or use defaults
|
36 |
+
current_values = st.session_state.project_data["building_info"]
|
37 |
+
|
38 |
+
# Project Name
|
39 |
+
project_name = st.text_input(
|
40 |
+
"Project Name",
|
41 |
+
value=current_values["project_name"],
|
42 |
+
help="Enter a unique identifier for your project."
|
43 |
+
)
|
44 |
+
|
45 |
+
# Create two columns for layout
|
46 |
+
col1, col2 = st.columns(2)
|
47 |
+
|
48 |
+
with col1:
|
49 |
+
# Floor Area
|
50 |
+
floor_area = st.number_input(
|
51 |
+
"Floor Area (square meters)",
|
52 |
+
min_value=1.0,
|
53 |
+
max_value=100000.0,
|
54 |
+
value=float(current_values["floor_area"]),
|
55 |
+
step=10.0,
|
56 |
+
format="%.1f",
|
57 |
+
help="Total conditioned floor area of the building in square meters."
|
58 |
+
)
|
59 |
+
|
60 |
+
# Building Height
|
61 |
+
building_height = st.number_input(
|
62 |
+
"Building Height (meters)",
|
63 |
+
min_value=2.0,
|
64 |
+
max_value=100.0,
|
65 |
+
value=float(current_values["building_height"]),
|
66 |
+
step=0.1,
|
67 |
+
format="%.1f",
|
68 |
+
help="Average ceiling height of the building in meters."
|
69 |
+
)
|
70 |
+
|
71 |
+
# Building Type
|
72 |
+
building_type = st.selectbox(
|
73 |
+
"Building Type",
|
74 |
+
options=BUILDING_TYPES,
|
75 |
+
index=BUILDING_TYPES.index(current_values["building_type"]) if current_values["building_type"] in BUILDING_TYPES else 0,
|
76 |
+
help="Primary use of the building, which affects default internal loads and ventilation rates."
|
77 |
+
)
|
78 |
+
|
79 |
+
with col2:
|
80 |
+
# Indoor Design Temperature
|
81 |
+
indoor_design_temp = st.number_input(
|
82 |
+
"Indoor Design Temperature (°C)",
|
83 |
+
min_value=15.0,
|
84 |
+
max_value=30.0,
|
85 |
+
value=float(current_values["indoor_design_temp"]),
|
86 |
+
step=0.5,
|
87 |
+
format="%.1f",
|
88 |
+
help="Target indoor temperature for cooling season comfort (15–30°C)."
|
89 |
+
)
|
90 |
+
|
91 |
+
# Indoor Design Relative Humidity
|
92 |
+
indoor_design_rh = st.number_input(
|
93 |
+
"Indoor Design Relative Humidity (%)",
|
94 |
+
min_value=20.0,
|
95 |
+
max_value=80.0,
|
96 |
+
value=float(current_values["indoor_design_rh"]),
|
97 |
+
step=5.0,
|
98 |
+
format="%.1f",
|
99 |
+
help="Target indoor relative humidity for comfort (20–80%)."
|
100 |
+
)
|
101 |
+
|
102 |
+
# Ventilation Rate
|
103 |
+
ventilation_rate = st.number_input(
|
104 |
+
"Ventilation Rate (L/s/m²)",
|
105 |
+
min_value=0.0,
|
106 |
+
max_value=10.0,
|
107 |
+
value=float(current_values["ventilation_rate"]),
|
108 |
+
step=0.1,
|
109 |
+
format="%.2f",
|
110 |
+
help="Fresh air ventilation rate in liters per second per square meter."
|
111 |
+
)
|
112 |
+
|
113 |
+
# Building Orientation
|
114 |
+
st.subheader("Building Orientation")
|
115 |
+
|
116 |
+
# Display orientation diagram
|
117 |
+
display_orientation_diagram()
|
118 |
+
|
119 |
+
orientation_angle = st.slider(
|
120 |
+
"Orientation Angle (°)",
|
121 |
+
min_value=-180,
|
122 |
+
max_value=180,
|
123 |
+
value=int(current_values["orientation_angle"]),
|
124 |
+
step=5,
|
125 |
+
help="Sets the building's rotation angle relative to north (0°). Component orientations are defined as A (North), B (South), C (East), D (West) at 0°. Adjusting this angle rotates all component orientations accordingly."
|
126 |
+
)
|
127 |
+
|
128 |
+
# Operation Hours
|
129 |
+
operation_hours = st.slider(
|
130 |
+
"Operation Hours (hours/day)",
|
131 |
+
min_value=0,
|
132 |
+
max_value=24,
|
133 |
+
value=int(current_values["operation_hours"]),
|
134 |
+
step=1,
|
135 |
+
help="Daily hours the building is occupied or operational, affecting internal loads (0–24 hours)."
|
136 |
+
)
|
137 |
+
|
138 |
+
# Form submission button
|
139 |
+
submit_button = st.form_submit_button("Save Building Information")
|
140 |
+
|
141 |
+
if submit_button:
|
142 |
+
# Validate inputs
|
143 |
+
validation_errors = validate_building_info(
|
144 |
+
project_name, floor_area, building_height, building_type,
|
145 |
+
indoor_design_temp, indoor_design_rh, ventilation_rate,
|
146 |
+
orientation_angle, operation_hours
|
147 |
+
)
|
148 |
+
|
149 |
+
if validation_errors:
|
150 |
+
# Display validation errors
|
151 |
+
for error in validation_errors:
|
152 |
+
st.error(error)
|
153 |
+
else:
|
154 |
+
# Update session state with validated inputs
|
155 |
+
st.session_state.project_data["project_name"] = project_name
|
156 |
+
st.session_state.project_data["building_info"].update({
|
157 |
+
"project_name": project_name,
|
158 |
+
"floor_area": floor_area,
|
159 |
+
"building_height": building_height,
|
160 |
+
"building_type": building_type,
|
161 |
+
"indoor_design_temp": indoor_design_temp,
|
162 |
+
"indoor_design_rh": indoor_design_rh,
|
163 |
+
"ventilation_rate": ventilation_rate,
|
164 |
+
"orientation_angle": float(orientation_angle),
|
165 |
+
"operation_hours": operation_hours
|
166 |
+
})
|
167 |
+
|
168 |
+
# Log the update
|
169 |
+
logger.info(f"Building information updated for project: {project_name}")
|
170 |
+
|
171 |
+
# Show success message
|
172 |
+
st.success("Building information saved successfully!")
|
173 |
+
|
174 |
+
# Navigation buttons
|
175 |
+
col1, col2 = st.columns(2)
|
176 |
+
|
177 |
+
with col1:
|
178 |
+
if st.button("Back to Intro", key="back_to_intro"):
|
179 |
+
st.session_state.current_page = "Intro"
|
180 |
+
st.rerun()
|
181 |
+
|
182 |
+
with col2:
|
183 |
+
if st.button("Continue to Climate Data", key="continue_to_climate"):
|
184 |
+
# Check if required fields are filled before proceeding
|
185 |
+
if not st.session_state.project_data["building_info"]["project_name"]:
|
186 |
+
st.error("Please enter a project name before continuing.")
|
187 |
+
else:
|
188 |
+
st.session_state.current_page = "Climate Data"
|
189 |
+
st.rerun()
|
190 |
+
|
191 |
+
def validate_building_info(
|
192 |
+
project_name: str,
|
193 |
+
floor_area: float,
|
194 |
+
building_height: float,
|
195 |
+
building_type: str,
|
196 |
+
indoor_design_temp: float,
|
197 |
+
indoor_design_rh: float,
|
198 |
+
ventilation_rate: float,
|
199 |
+
orientation_angle: int,
|
200 |
+
operation_hours: int
|
201 |
+
) -> List[str]:
|
202 |
+
"""
|
203 |
+
Validate building information inputs.
|
204 |
+
|
205 |
+
Args:
|
206 |
+
project_name: Project name
|
207 |
+
floor_area: Floor area in square meters
|
208 |
+
building_height: Building height in meters
|
209 |
+
building_type: Building type
|
210 |
+
indoor_design_temp: Indoor design temperature in °C
|
211 |
+
indoor_design_rh: Indoor design relative humidity in %
|
212 |
+
ventilation_rate: Ventilation rate in L/s/m²
|
213 |
+
orientation_angle: Building orientation angle in degrees
|
214 |
+
operation_hours: Building operation hours per day
|
215 |
+
|
216 |
+
Returns:
|
217 |
+
List of validation error messages, empty if all inputs are valid
|
218 |
+
"""
|
219 |
+
errors = []
|
220 |
+
|
221 |
+
# Validate project name
|
222 |
+
if not project_name or project_name.strip() == "":
|
223 |
+
errors.append("Project name is required.")
|
224 |
+
|
225 |
+
# Validate floor area
|
226 |
+
if floor_area <= 0:
|
227 |
+
errors.append("Floor area must be greater than zero.")
|
228 |
+
elif floor_area > 100000:
|
229 |
+
errors.append("Floor area exceeds maximum value (100,000 m²).")
|
230 |
+
|
231 |
+
# Validate building height
|
232 |
+
if building_height < 2.0:
|
233 |
+
errors.append("Building height must be at least 2.0 meters.")
|
234 |
+
elif building_height > 100.0:
|
235 |
+
errors.append("Building height exceeds maximum value (100 meters).")
|
236 |
+
|
237 |
+
# Validate building type
|
238 |
+
if building_type not in BUILDING_TYPES:
|
239 |
+
errors.append("Please select a valid building type.")
|
240 |
+
|
241 |
+
# Validate indoor design temperature
|
242 |
+
if indoor_design_temp < 15.0 or indoor_design_temp > 30.0:
|
243 |
+
errors.append("Indoor design temperature must be between 15°C and 30°C.")
|
244 |
+
|
245 |
+
# Validate indoor design relative humidity
|
246 |
+
if indoor_design_rh < 20.0 or indoor_design_rh > 80.0:
|
247 |
+
errors.append("Indoor design relative humidity must be between 20% and 80%.")
|
248 |
+
|
249 |
+
# Validate ventilation rate
|
250 |
+
if ventilation_rate < 0.0:
|
251 |
+
errors.append("Ventilation rate cannot be negative.")
|
252 |
+
elif ventilation_rate > 10.0:
|
253 |
+
errors.append("Ventilation rate exceeds maximum value (10 L/s/m²).")
|
254 |
+
|
255 |
+
# Validate orientation angle
|
256 |
+
if orientation_angle < -180 or orientation_angle > 180:
|
257 |
+
errors.append("Orientation angle must be between -180° and 180°.")
|
258 |
+
|
259 |
+
# Validate operation hours
|
260 |
+
if operation_hours < 0 or operation_hours > 24:
|
261 |
+
errors.append("Operation hours must be between 0 and 24.")
|
262 |
+
|
263 |
+
return errors
|
264 |
+
|
265 |
+
def display_building_info_help():
|
266 |
+
"""Display help information for the building information page."""
|
267 |
+
st.markdown("""
|
268 |
+
### Building Information Help
|
269 |
+
|
270 |
+
This section collects basic information about your building project. The inputs provided here will be used throughout the calculation process.
|
271 |
+
|
272 |
+
**Key Parameters:**
|
273 |
+
|
274 |
+
* **Project Name**: A unique identifier for your project.
|
275 |
+
* **Floor Area**: The total conditioned floor area of the building in square meters.
|
276 |
+
* **Building Height**: The average ceiling height of the building in meters.
|
277 |
+
* **Building Type**: The primary use of the building, which affects default internal loads and ventilation rates.
|
278 |
+
* **Indoor Design Temperature**: The target indoor temperature for cooling season comfort (typically 22-26°C).
|
279 |
+
* **Indoor Design Relative Humidity**: The target indoor relative humidity for comfort (typically 40-60%).
|
280 |
+
* **Ventilation Rate**: The fresh air ventilation rate in liters per second per square meter.
|
281 |
+
* **Orientation Angle**: The building's rotation angle relative to north (0°).
|
282 |
+
* **Operation Hours**: Daily hours the building is occupied or operational, affecting internal loads.
|
283 |
+
|
284 |
+
**Building Orientation:**
|
285 |
+
|
286 |
+
The orientation angle rotates the entire building relative to true north. At 0°, the building facades are aligned with the cardinal directions:
|
287 |
+
* Facade A: North (0°)
|
288 |
+
* Facade B: South (180°)
|
289 |
+
* Facade C: East (90°)
|
290 |
+
* Facade D: West (270°)
|
291 |
+
|
292 |
+
Adjusting the orientation angle will rotate all facades accordingly.
|
293 |
+
""")
|
294 |
+
|
295 |
+
def display_orientation_diagram():
|
296 |
+
"""Display a simple ASCII diagram showing building orientation."""
|
297 |
+
orientation_diagram = """
|
298 |
+
North (0°)
|
299 |
+
^
|
300 |
+
|
|
301 |
+
|
|
302 |
+
West (270°) <---+---> East (90°)
|
303 |
+
|
|
304 |
+
|
|
305 |
+
v
|
306 |
+
South (180°)
|
307 |
+
|
308 |
+
Facades at 0° orientation:
|
309 |
+
- Facade A: North (0°)
|
310 |
+
- Facade B: South (180°)
|
311 |
+
- Facade C: East (90°)
|
312 |
+
- Facade D: West (270°)
|
313 |
+
"""
|
314 |
+
|
315 |
+
st.text(orientation_diagram)
|
316 |
+
|
317 |
+
def calculate_volume(floor_area: float, height: float) -> float:
|
318 |
+
"""
|
319 |
+
Calculate the building volume.
|
320 |
+
|
321 |
+
Args:
|
322 |
+
floor_area: Floor area in square meters
|
323 |
+
height: Building height in meters
|
324 |
+
|
325 |
+
Returns:
|
326 |
+
Building volume in cubic meters
|
327 |
+
"""
|
328 |
+
return floor_area * height
|
app/climate_data.py
ADDED
@@ -0,0 +1,548 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
HVAC Load Calculator - Climate Data Module
|
3 |
+
|
4 |
+
This module handles the climate data selection, EPW file processing, and display of climate information
|
5 |
+
for the HVAC Load Calculator application. It allows users to upload EPW weather files and extracts
|
6 |
+
relevant climate data for use in load calculations.
|
7 |
+
|
8 |
+
Developed by: Dr Majed Abuseif, Deakin University
|
9 |
+
© 2025
|
10 |
+
"""
|
11 |
+
|
12 |
+
import streamlit as st
|
13 |
+
import pandas as pd
|
14 |
+
import numpy as np
|
15 |
+
import os
|
16 |
+
import json
|
17 |
+
import io
|
18 |
+
import logging
|
19 |
+
import plotly.graph_objects as go
|
20 |
+
import plotly.express as px
|
21 |
+
from datetime import datetime
|
22 |
+
from typing import Dict, List, Any, Optional, Tuple, Union
|
23 |
+
import math
|
24 |
+
|
25 |
+
# Configure logging
|
26 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
27 |
+
logger = logging.getLogger(__name__)
|
28 |
+
|
29 |
+
# Define constants
|
30 |
+
MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
31 |
+
CLIMATE_ZONES = ["0A", "0B", "1A", "1B", "2A", "2B", "3A", "3B", "3C", "4A", "4B", "4C", "5A", "5B", "5C", "6A", "6B", "7", "8"]
|
32 |
+
|
33 |
+
class ClimateDataManager:
|
34 |
+
"""Class for managing climate data from EPW files."""
|
35 |
+
|
36 |
+
def __init__(self):
|
37 |
+
"""Initialize climate data manager."""
|
38 |
+
pass
|
39 |
+
|
40 |
+
def load_epw(self, uploaded_file) -> Dict[str, Any]:
|
41 |
+
"""
|
42 |
+
Parse an EPW file and extract climate data.
|
43 |
+
|
44 |
+
Args:
|
45 |
+
uploaded_file: The uploaded EPW file object
|
46 |
+
|
47 |
+
Returns:
|
48 |
+
Dict containing parsed climate data
|
49 |
+
"""
|
50 |
+
try:
|
51 |
+
# Read the EPW file
|
52 |
+
content = uploaded_file.getvalue().decode('utf-8')
|
53 |
+
lines = content.split('\n')
|
54 |
+
|
55 |
+
# Extract header information (first 8 lines)
|
56 |
+
header_lines = lines[:8]
|
57 |
+
|
58 |
+
# Parse location data from line 1
|
59 |
+
location_data = header_lines[0].split(',')
|
60 |
+
|
61 |
+
# Extract location information
|
62 |
+
location = {
|
63 |
+
"city": location_data[1].strip(),
|
64 |
+
"state_province": location_data[2].strip(),
|
65 |
+
"country": location_data[3].strip(),
|
66 |
+
"source": location_data[4].strip(),
|
67 |
+
"wmo": location_data[5].strip(),
|
68 |
+
"latitude": float(location_data[6]),
|
69 |
+
"longitude": float(location_data[7]),
|
70 |
+
"timezone": float(location_data[8]),
|
71 |
+
"elevation": float(location_data[9])
|
72 |
+
}
|
73 |
+
|
74 |
+
# Parse data rows (starting from line 9)
|
75 |
+
data_lines = lines[8:]
|
76 |
+
|
77 |
+
# Create a DataFrame from the data rows
|
78 |
+
data = []
|
79 |
+
for line in data_lines:
|
80 |
+
if line.strip(): # Skip empty lines
|
81 |
+
data.append(line.split(','))
|
82 |
+
|
83 |
+
# Create DataFrame
|
84 |
+
columns = [
|
85 |
+
"year", "month", "day", "hour", "minute", "data_source", "dry_bulb_temp",
|
86 |
+
"dew_point_temp", "relative_humidity", "atmospheric_pressure", "extraterrestrial_radiation",
|
87 |
+
"extraterrestrial_radiation_normal", "horizontal_infrared_radiation", "global_horizontal_radiation",
|
88 |
+
"direct_normal_radiation", "diffuse_horizontal_radiation", "global_horizontal_illuminance",
|
89 |
+
"direct_normal_illuminance", "diffuse_horizontal_illuminance", "zenith_luminance",
|
90 |
+
"wind_direction", "wind_speed", "total_sky_cover", "opaque_sky_cover", "visibility",
|
91 |
+
"ceiling_height", "present_weather_observation", "present_weather_codes",
|
92 |
+
"precipitable_water", "aerosol_optical_depth", "snow_depth", "days_since_last_snowfall",
|
93 |
+
"albedo", "liquid_precipitation_depth", "liquid_precipitation_quantity"
|
94 |
+
]
|
95 |
+
|
96 |
+
df = pd.DataFrame(data, columns=columns)
|
97 |
+
|
98 |
+
# Convert numeric columns
|
99 |
+
numeric_columns = [
|
100 |
+
"dry_bulb_temp", "dew_point_temp", "relative_humidity", "atmospheric_pressure",
|
101 |
+
"global_horizontal_radiation", "direct_normal_radiation", "diffuse_horizontal_radiation",
|
102 |
+
"wind_direction", "wind_speed"
|
103 |
+
]
|
104 |
+
|
105 |
+
for col in numeric_columns:
|
106 |
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
107 |
+
|
108 |
+
# Calculate design conditions
|
109 |
+
design_conditions = self._calculate_design_conditions(df)
|
110 |
+
|
111 |
+
# Process hourly data
|
112 |
+
hourly_data = self._process_hourly_data(df)
|
113 |
+
|
114 |
+
# Determine climate zone based on HDD and CDD
|
115 |
+
climate_zone = self._determine_climate_zone(
|
116 |
+
design_conditions["heating_degree_days"],
|
117 |
+
design_conditions["cooling_degree_days"]
|
118 |
+
)
|
119 |
+
|
120 |
+
# Create climate data dictionary
|
121 |
+
climate_data = {
|
122 |
+
"id": f"{location['city']}_{location['country']}".replace(" ", "_"),
|
123 |
+
"location": location,
|
124 |
+
"design_conditions": design_conditions,
|
125 |
+
"climate_zone": climate_zone,
|
126 |
+
"hourly_data": hourly_data,
|
127 |
+
"epw_filename": uploaded_file.name
|
128 |
+
}
|
129 |
+
|
130 |
+
logger.info(f"EPW file processed successfully: {uploaded_file.name}")
|
131 |
+
return climate_data
|
132 |
+
|
133 |
+
except Exception as e:
|
134 |
+
logger.error(f"Error processing EPW file: {str(e)}")
|
135 |
+
raise ValueError(f"Error processing EPW file: {str(e)}")
|
136 |
+
|
137 |
+
def _calculate_design_conditions(self, df: pd.DataFrame) -> Dict[str, Any]:
|
138 |
+
"""
|
139 |
+
Calculate design conditions from EPW data.
|
140 |
+
|
141 |
+
Args:
|
142 |
+
df: DataFrame containing EPW data
|
143 |
+
|
144 |
+
Returns:
|
145 |
+
Dict containing design conditions
|
146 |
+
"""
|
147 |
+
try:
|
148 |
+
# Convert temperatures from C to K if needed
|
149 |
+
temp_col = df["dry_bulb_temp"].astype(float)
|
150 |
+
|
151 |
+
# Calculate design temperatures
|
152 |
+
winter_design_temp = np.percentile(temp_col, 0.4) # 99.6% heating design temperature
|
153 |
+
summer_design_temp_db = np.percentile(temp_col, 99.6) # 0.4% cooling design temperature
|
154 |
+
|
155 |
+
# Calculate wet-bulb temperature (simplified)
|
156 |
+
rh_col = df["relative_humidity"].astype(float)
|
157 |
+
wet_bulb_temp = self._calculate_wet_bulb(temp_col, rh_col)
|
158 |
+
summer_design_temp_wb = np.percentile(wet_bulb_temp, 99.6) # 0.4% cooling wet-bulb temperature
|
159 |
+
|
160 |
+
# Calculate degree days
|
161 |
+
df["month"] = df["month"].astype(int)
|
162 |
+
df["day"] = df["day"].astype(int)
|
163 |
+
df["hour"] = df["hour"].astype(int)
|
164 |
+
|
165 |
+
# Group by day and calculate average temperature
|
166 |
+
df["date"] = pd.to_datetime(df[["year", "month", "day"]].astype(int))
|
167 |
+
daily_temps = df.groupby("date")["dry_bulb_temp"].mean()
|
168 |
+
|
169 |
+
# Calculate heating and cooling degree days (base 18°C)
|
170 |
+
heating_degree_days = sum(max(0, 18 - temp) for temp in daily_temps)
|
171 |
+
cooling_degree_days = sum(max(0, temp - 18) for temp in daily_temps)
|
172 |
+
|
173 |
+
# Calculate monthly average temperatures
|
174 |
+
monthly_temps = df.groupby(df["month"])["dry_bulb_temp"].mean().tolist()
|
175 |
+
|
176 |
+
# Calculate monthly average radiation
|
177 |
+
monthly_radiation = df.groupby(df["month"])["global_horizontal_radiation"].mean().tolist()
|
178 |
+
|
179 |
+
# Calculate summer daily temperature range
|
180 |
+
# Summer months: 6-8 for Northern Hemisphere, 12-2 for Southern Hemisphere
|
181 |
+
# Determine hemisphere based on latitude
|
182 |
+
latitude = df["latitude"].iloc[0] if "latitude" in df.columns else 0
|
183 |
+
|
184 |
+
if latitude >= 0: # Northern Hemisphere
|
185 |
+
summer_months = [6, 7, 8]
|
186 |
+
else: # Southern Hemisphere
|
187 |
+
summer_months = [12, 1, 2]
|
188 |
+
|
189 |
+
summer_data = df[df["month"].isin(summer_months)]
|
190 |
+
summer_daily_range = 0
|
191 |
+
|
192 |
+
if not summer_data.empty:
|
193 |
+
summer_daily_max = summer_data.groupby(["month", "day"])["dry_bulb_temp"].max()
|
194 |
+
summer_daily_min = summer_data.groupby(["month", "day"])["dry_bulb_temp"].min()
|
195 |
+
summer_daily_range = (summer_daily_max - summer_daily_min).mean()
|
196 |
+
|
197 |
+
# Calculate mean wind speed and pressure
|
198 |
+
wind_speed = df["wind_speed"].mean()
|
199 |
+
pressure = df["atmospheric_pressure"].mean()
|
200 |
+
|
201 |
+
return {
|
202 |
+
"winter_design_temp": round(winter_design_temp, 1),
|
203 |
+
"summer_design_temp_db": round(summer_design_temp_db, 1),
|
204 |
+
"summer_design_temp_wb": round(summer_design_temp_wb, 1),
|
205 |
+
"heating_degree_days": round(heating_degree_days),
|
206 |
+
"cooling_degree_days": round(cooling_degree_days),
|
207 |
+
"monthly_average_temps": [round(t, 1) for t in monthly_temps],
|
208 |
+
"monthly_average_radiation": [round(r, 1) for r in monthly_radiation],
|
209 |
+
"summer_daily_range": round(summer_daily_range, 1),
|
210 |
+
"wind_speed": round(wind_speed, 1),
|
211 |
+
"pressure": round(pressure)
|
212 |
+
}
|
213 |
+
|
214 |
+
except Exception as e:
|
215 |
+
logger.error(f"Error calculating design conditions: {str(e)}")
|
216 |
+
# Return default values if calculation fails
|
217 |
+
return {
|
218 |
+
"winter_design_temp": 0.0,
|
219 |
+
"summer_design_temp_db": 30.0,
|
220 |
+
"summer_design_temp_wb": 25.0,
|
221 |
+
"heating_degree_days": 0,
|
222 |
+
"cooling_degree_days": 0,
|
223 |
+
"monthly_average_temps": [20.0] * 12,
|
224 |
+
"monthly_average_radiation": [150.0] * 12,
|
225 |
+
"summer_daily_range": 8.0,
|
226 |
+
"wind_speed": 3.0,
|
227 |
+
"pressure": 101325.0
|
228 |
+
}
|
229 |
+
|
230 |
+
def _process_hourly_data(self, df: pd.DataFrame) -> List[Dict[str, Any]]:
|
231 |
+
"""
|
232 |
+
Process hourly data from EPW DataFrame.
|
233 |
+
|
234 |
+
Args:
|
235 |
+
df: DataFrame containing EPW data
|
236 |
+
|
237 |
+
Returns:
|
238 |
+
List of hourly data records
|
239 |
+
"""
|
240 |
+
hourly_data = []
|
241 |
+
|
242 |
+
try:
|
243 |
+
# Ensure numeric columns
|
244 |
+
df["dry_bulb_temp"] = pd.to_numeric(df["dry_bulb_temp"], errors='coerce')
|
245 |
+
df["relative_humidity"] = pd.to_numeric(df["relative_humidity"], errors='coerce')
|
246 |
+
df["atmospheric_pressure"] = pd.to_numeric(df["atmospheric_pressure"], errors='coerce')
|
247 |
+
df["global_horizontal_radiation"] = pd.to_numeric(df["global_horizontal_radiation"], errors='coerce')
|
248 |
+
df["direct_normal_radiation"] = pd.to_numeric(df["direct_normal_radiation"], errors='coerce')
|
249 |
+
df["diffuse_horizontal_radiation"] = pd.to_numeric(df["diffuse_horizontal_radiation"], errors='coerce')
|
250 |
+
df["wind_speed"] = pd.to_numeric(df["wind_speed"], errors='coerce')
|
251 |
+
df["wind_direction"] = pd.to_numeric(df["wind_direction"], errors='coerce')
|
252 |
+
|
253 |
+
# Convert to integers for month, day, hour
|
254 |
+
df["month"] = pd.to_numeric(df["month"], errors='coerce').astype('Int64')
|
255 |
+
df["day"] = pd.to_numeric(df["day"], errors='coerce').astype('Int64')
|
256 |
+
df["hour"] = pd.to_numeric(df["hour"], errors='coerce').astype('Int64')
|
257 |
+
|
258 |
+
# Process each row
|
259 |
+
for _, row in df.iterrows():
|
260 |
+
if pd.isna(row["month"]) or pd.isna(row["day"]) or pd.isna(row["hour"]) or pd.isna(row["dry_bulb_temp"]):
|
261 |
+
continue # Skip rows with missing critical data
|
262 |
+
|
263 |
+
record = {
|
264 |
+
"month": int(row["month"]),
|
265 |
+
"day": int(row["day"]),
|
266 |
+
"hour": int(row["hour"]),
|
267 |
+
"dry_bulb": float(row["dry_bulb_temp"]) if not pd.isna(row["dry_bulb_temp"]) else 20.0,
|
268 |
+
"relative_humidity": float(row["relative_humidity"]) if not pd.isna(row["relative_humidity"]) else 50.0,
|
269 |
+
"atmospheric_pressure": float(row["atmospheric_pressure"]) if not pd.isna(row["atmospheric_pressure"]) else 101325.0,
|
270 |
+
"global_horizontal_radiation": float(row["global_horizontal_radiation"]) if not pd.isna(row["global_horizontal_radiation"]) else 0.0,
|
271 |
+
"direct_normal_radiation": float(row["direct_normal_radiation"]) if not pd.isna(row["direct_normal_radiation"]) else 0.0,
|
272 |
+
"diffuse_horizontal_radiation": float(row["diffuse_horizontal_radiation"]) if not pd.isna(row["diffuse_horizontal_radiation"]) else 0.0,
|
273 |
+
"wind_speed": float(row["wind_speed"]) if not pd.isna(row["wind_speed"]) else 0.0,
|
274 |
+
"wind_direction": float(row["wind_direction"]) if not pd.isna(row["wind_direction"]) else 0.0
|
275 |
+
}
|
276 |
+
hourly_data.append(record)
|
277 |
+
|
278 |
+
# Check if we have the expected number of records (8760 hours in a year)
|
279 |
+
if len(hourly_data) < 8700: # Allow for some missing data
|
280 |
+
logger.warning(f"Hourly data has {len(hourly_data)} records instead of 8760. Some records may be missing.")
|
281 |
+
|
282 |
+
return hourly_data
|
283 |
+
|
284 |
+
except Exception as e:
|
285 |
+
logger.error(f"Error processing hourly data: {str(e)}")
|
286 |
+
return []
|
287 |
+
|
288 |
+
def _determine_climate_zone(self, hdd: float, cdd: float) -> str:
|
289 |
+
"""
|
290 |
+
Determine ASHRAE climate zone based on heating and cooling degree days.
|
291 |
+
|
292 |
+
Args:
|
293 |
+
hdd: Heating degree days (base 18°C)
|
294 |
+
cdd: Cooling degree days (base 18°C)
|
295 |
+
|
296 |
+
Returns:
|
297 |
+
ASHRAE climate zone designation
|
298 |
+
"""
|
299 |
+
# Simplified climate zone determination based on ASHRAE 169
|
300 |
+
if hdd >= 7000:
|
301 |
+
return "8"
|
302 |
+
elif hdd >= 5400:
|
303 |
+
return "7"
|
304 |
+
elif hdd >= 3900:
|
305 |
+
return "6A" if cdd <= 450 else "6B"
|
306 |
+
elif hdd >= 2700:
|
307 |
+
return "5A" if cdd <= 900 else ("5B" if cdd <= 1800 else "5C")
|
308 |
+
elif hdd >= 1800:
|
309 |
+
return "4A" if cdd <= 1800 else ("4B" if cdd <= 2700 else "4C")
|
310 |
+
elif hdd >= 900:
|
311 |
+
return "3A" if cdd <= 2700 else ("3B" if cdd <= 3600 else "3C")
|
312 |
+
elif hdd >= 0:
|
313 |
+
return "2A" if cdd <= 3600 else "2B"
|
314 |
+
else:
|
315 |
+
return "1A" if cdd <= 4500 else "1B"
|
316 |
+
|
317 |
+
@staticmethod
|
318 |
+
def _calculate_wet_bulb(dry_bulb: np.ndarray, relative_humidity: np.ndarray) -> np.ndarray:
|
319 |
+
"""
|
320 |
+
Calculate wet-bulb temperature using a simplified formula.
|
321 |
+
|
322 |
+
Args:
|
323 |
+
dry_bulb: Dry-bulb temperature in °C
|
324 |
+
relative_humidity: Relative humidity in %
|
325 |
+
|
326 |
+
Returns:
|
327 |
+
Wet-bulb temperature in °C
|
328 |
+
"""
|
329 |
+
# Simplified formula for wet-bulb temperature
|
330 |
+
wet_bulb = dry_bulb * np.arctan(0.151977 * np.sqrt(relative_humidity + 8.313659)) + \
|
331 |
+
np.arctan(dry_bulb + relative_humidity) - np.arctan(relative_humidity - 1.676331) + \
|
332 |
+
0.00391838 * (relative_humidity)**(3/2) * np.arctan(0.023101 * relative_humidity) - 4.686035
|
333 |
+
|
334 |
+
# Ensure wet-bulb is not higher than dry-bulb
|
335 |
+
wet_bulb = np.minimum(wet_bulb, dry_bulb)
|
336 |
+
|
337 |
+
return wet_bulb
|
338 |
+
|
339 |
+
def display_climate_page():
|
340 |
+
"""
|
341 |
+
Display the climate data page.
|
342 |
+
This is the main function called by main.py when the Climate Data page is selected.
|
343 |
+
"""
|
344 |
+
st.title("Climate Data and Design Requirements")
|
345 |
+
|
346 |
+
# Display help information in an expandable section
|
347 |
+
with st.expander("Help & Information"):
|
348 |
+
display_climate_help()
|
349 |
+
|
350 |
+
# Initialize climate data manager
|
351 |
+
climate_manager = ClimateDataManager()
|
352 |
+
|
353 |
+
# Create tabs for different sections
|
354 |
+
tab1, tab2 = st.tabs(["EPW Data Input", "Climate Summary"])
|
355 |
+
|
356 |
+
# EPW Data Input tab
|
357 |
+
with tab1:
|
358 |
+
st.subheader("Upload EPW Weather File")
|
359 |
+
|
360 |
+
# File uploader for EPW files
|
361 |
+
uploaded_file = st.file_uploader(
|
362 |
+
"Upload EPW File",
|
363 |
+
type=["epw"],
|
364 |
+
help="Upload an EnergyPlus Weather (EPW) file for your location."
|
365 |
+
)
|
366 |
+
|
367 |
+
if uploaded_file is not None:
|
368 |
+
try:
|
369 |
+
# Process the uploaded EPW file
|
370 |
+
with st.spinner("Processing EPW file..."):
|
371 |
+
climate_data = climate_manager.load_epw(uploaded_file)
|
372 |
+
|
373 |
+
# Store climate data in session state
|
374 |
+
st.session_state.project_data["climate_data"] = climate_data
|
375 |
+
|
376 |
+
# Show success message
|
377 |
+
st.success(f"EPW file processed successfully: {uploaded_file.name}")
|
378 |
+
|
379 |
+
# Display basic location information
|
380 |
+
location = climate_data["location"]
|
381 |
+
st.subheader("Location Information")
|
382 |
+
|
383 |
+
col1, col2 = st.columns(2)
|
384 |
+
with col1:
|
385 |
+
st.write(f"**City:** {location['city']}")
|
386 |
+
st.write(f"**State/Province:** {location['state_province']}")
|
387 |
+
st.write(f"**Country:** {location['country']}")
|
388 |
+
|
389 |
+
with col2:
|
390 |
+
st.write(f"**Latitude:** {location['latitude']}°")
|
391 |
+
st.write(f"**Longitude:** {location['longitude']}°")
|
392 |
+
st.write(f"**Elevation:** {location['elevation']} m")
|
393 |
+
|
394 |
+
# Automatically switch to Climate Summary tab
|
395 |
+
st.button("View Climate Summary", on_click=lambda: st.session_state.update({"climate_tab": "Climate Summary"}))
|
396 |
+
|
397 |
+
except Exception as e:
|
398 |
+
st.error(f"Error processing EPW file: {str(e)}")
|
399 |
+
logger.error(f"Error processing EPW file: {str(e)}")
|
400 |
+
|
401 |
+
# Climate Summary tab
|
402 |
+
with tab2:
|
403 |
+
if "climate_data" in st.session_state.project_data and st.session_state.project_data["climate_data"]:
|
404 |
+
display_climate_summary(st.session_state.project_data["climate_data"])
|
405 |
+
else:
|
406 |
+
st.info("Please upload an EPW file in the 'EPW Data Input' tab to view climate summary.")
|
407 |
+
|
408 |
+
# Navigation buttons
|
409 |
+
col1, col2 = st.columns(2)
|
410 |
+
|
411 |
+
with col1:
|
412 |
+
if st.button("Back to Building Information", key="back_to_building"):
|
413 |
+
st.session_state.current_page = "Building Information"
|
414 |
+
st.rerun()
|
415 |
+
|
416 |
+
with col2:
|
417 |
+
if st.button("Continue to Material Library", key="continue_to_material"):
|
418 |
+
# Check if climate data is available before proceeding
|
419 |
+
if "climate_data" not in st.session_state.project_data or not st.session_state.project_data["climate_data"]:
|
420 |
+
st.warning("Please upload an EPW file before continuing.")
|
421 |
+
else:
|
422 |
+
st.session_state.current_page = "Material Library"
|
423 |
+
st.rerun()
|
424 |
+
|
425 |
+
def display_climate_summary(climate_data: Dict[str, Any]):
|
426 |
+
"""
|
427 |
+
Display climate summary information.
|
428 |
+
|
429 |
+
Args:
|
430 |
+
climate_data: Dictionary containing climate data
|
431 |
+
"""
|
432 |
+
st.subheader("Climate Summary")
|
433 |
+
|
434 |
+
# Extract design conditions
|
435 |
+
design = climate_data["design_conditions"]
|
436 |
+
location = climate_data["location"]
|
437 |
+
|
438 |
+
# Display climate zone
|
439 |
+
st.markdown(f"### ASHRAE Climate Zone: {climate_data['climate_zone']}")
|
440 |
+
|
441 |
+
# Create columns for layout
|
442 |
+
col1, col2 = st.columns(2)
|
443 |
+
|
444 |
+
# Design temperatures
|
445 |
+
with col1:
|
446 |
+
st.subheader("Design Temperatures")
|
447 |
+
st.write(f"**Winter Design Temperature:** {design['winter_design_temp']}°C")
|
448 |
+
st.write(f"**Summer Design Temperature (DB):** {design['summer_design_temp_db']}°C")
|
449 |
+
st.write(f"**Summer Design Temperature (WB):** {design['summer_design_temp_wb']}°C")
|
450 |
+
st.write(f"**Summer Daily Temperature Range:** {design['summer_daily_range']}°C")
|
451 |
+
|
452 |
+
# Degree days
|
453 |
+
with col2:
|
454 |
+
st.subheader("Degree Days")
|
455 |
+
st.write(f"**Heating Degree Days (Base 18°C):** {design['heating_degree_days']}")
|
456 |
+
st.write(f"**Cooling Degree Days (Base 18°C):** {design['cooling_degree_days']}")
|
457 |
+
st.write(f"**Average Wind Speed:** {design['wind_speed']} m/s")
|
458 |
+
st.write(f"**Average Atmospheric Pressure:** {design['pressure']} Pa")
|
459 |
+
|
460 |
+
# Monthly temperature chart
|
461 |
+
st.subheader("Monthly Average Temperatures")
|
462 |
+
|
463 |
+
fig_temp = go.Figure()
|
464 |
+
fig_temp.add_trace(go.Scatter(
|
465 |
+
x=MONTHS,
|
466 |
+
y=design["monthly_average_temps"],
|
467 |
+
mode='lines+markers',
|
468 |
+
name='Temperature',
|
469 |
+
line=dict(color='firebrick', width=2),
|
470 |
+
marker=dict(size=8)
|
471 |
+
))
|
472 |
+
|
473 |
+
fig_temp.update_layout(
|
474 |
+
xaxis_title="Month",
|
475 |
+
yaxis_title="Temperature (°C)",
|
476 |
+
height=400,
|
477 |
+
margin=dict(l=20, r=20, t=30, b=20),
|
478 |
+
)
|
479 |
+
|
480 |
+
st.plotly_chart(fig_temp, use_container_width=True)
|
481 |
+
|
482 |
+
# Monthly radiation chart
|
483 |
+
st.subheader("Monthly Average Solar Radiation")
|
484 |
+
|
485 |
+
fig_rad = go.Figure()
|
486 |
+
fig_rad.add_trace(go.Bar(
|
487 |
+
x=MONTHS,
|
488 |
+
y=design["monthly_average_radiation"],
|
489 |
+
name='Global Horizontal Radiation',
|
490 |
+
marker_color='gold'
|
491 |
+
))
|
492 |
+
|
493 |
+
fig_rad.update_layout(
|
494 |
+
xaxis_title="Month",
|
495 |
+
yaxis_title="Radiation (W/m²)",
|
496 |
+
height=400,
|
497 |
+
margin=dict(l=20, r=20, t=30, b=20),
|
498 |
+
)
|
499 |
+
|
500 |
+
st.plotly_chart(fig_rad, use_container_width=True)
|
501 |
+
|
502 |
+
# Display hourly data statistics
|
503 |
+
st.subheader("Hourly Data Statistics")
|
504 |
+
|
505 |
+
if "hourly_data" in climate_data and climate_data["hourly_data"]:
|
506 |
+
hourly_count = len(climate_data["hourly_data"])
|
507 |
+
st.write(f"**Number of Hourly Records:** {hourly_count}")
|
508 |
+
|
509 |
+
if hourly_count < 8760:
|
510 |
+
st.warning(f"Expected 8760 hourly records for a full year, but found {hourly_count}. Some data may be missing.")
|
511 |
+
else:
|
512 |
+
st.warning("No hourly data available.")
|
513 |
+
|
514 |
+
def display_climate_help():
|
515 |
+
"""Display help information for the climate data page."""
|
516 |
+
st.markdown("""
|
517 |
+
### Climate Data Help
|
518 |
+
|
519 |
+
This section allows you to upload and process weather data for your location, which is essential for accurate HVAC load calculations.
|
520 |
+
|
521 |
+
**EPW Files:**
|
522 |
+
|
523 |
+
EPW (EnergyPlus Weather) files contain hourly weather data for a specific location, including:
|
524 |
+
|
525 |
+
* Dry-bulb temperature
|
526 |
+
* Relative humidity
|
527 |
+
* Solar radiation (direct and diffuse)
|
528 |
+
* Wind speed and direction
|
529 |
+
* Atmospheric pressure
|
530 |
+
|
531 |
+
**Where to Find EPW Files:**
|
532 |
+
|
533 |
+
* [EnergyPlus Weather Data](https://energyplus.net/weather)
|
534 |
+
* [Climate.OneBuilding.Org](https://climate.onebuilding.org/)
|
535 |
+
* [ASHRAE International Weather for Energy Calculations (IWEC)](https://www.ashrae.org/technical-resources/bookstore/ashrae-international-weather-files-for-energy-calculations-2-0-iwec2)
|
536 |
+
|
537 |
+
**Climate Summary:**
|
538 |
+
|
539 |
+
After uploading an EPW file, the Climate Summary tab will display:
|
540 |
+
|
541 |
+
* ASHRAE Climate Zone
|
542 |
+
* Design temperatures for heating and cooling
|
543 |
+
* Heating and cooling degree days
|
544 |
+
* Monthly average temperatures and solar radiation
|
545 |
+
* Hourly data statistics
|
546 |
+
|
547 |
+
This information will be used throughout the HVAC load calculation process.
|
548 |
+
""")
|
app/components.py
ADDED
@@ -0,0 +1,504 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
HVAC Load Calculator - Building Components Module
|
3 |
+
|
4 |
+
This module handles the definition of building envelope components (walls, roofs, floors,
|
5 |
+
windows, doors, skylights) for the HVAC Load Calculator application. It allows users to
|
6 |
+
assign constructions and fenestrations to specific components, define their areas,
|
7 |
+
orientations, and other relevant properties.
|
8 |
+
|
9 |
+
Developed by: Dr Majed Abuseif, Deakin University
|
10 |
+
© 2025
|
11 |
+
"""
|
12 |
+
|
13 |
+
import streamlit as st
|
14 |
+
import pandas as pd
|
15 |
+
import numpy as np
|
16 |
+
import json
|
17 |
+
import logging
|
18 |
+
import uuid
|
19 |
+
from typing import Dict, List, Any, Optional, Tuple, Union
|
20 |
+
|
21 |
+
# Configure logging
|
22 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
23 |
+
logger = logging.getLogger(__name__)
|
24 |
+
|
25 |
+
# Define constants
|
26 |
+
COMPONENT_TYPES = ["Wall", "Roof", "Floor", "Window", "Door", "Skylight"]
|
27 |
+
ORIENTATION_OPTIONS = ["A (North)", "B (South)", "C (East)", "D (West)", "Horizontal"]
|
28 |
+
|
29 |
+
def display_components_page():
|
30 |
+
"""
|
31 |
+
Display the building components page.
|
32 |
+
This is the main function called by main.py when the Building Components page is selected.
|
33 |
+
"""
|
34 |
+
st.title("Building Components")
|
35 |
+
|
36 |
+
# Display help information in an expandable section
|
37 |
+
with st.expander("Help & Information"):
|
38 |
+
display_components_help()
|
39 |
+
|
40 |
+
# Initialize components in session state if not present
|
41 |
+
initialize_components()
|
42 |
+
|
43 |
+
# Create tabs for different component types
|
44 |
+
tabs = st.tabs(COMPONENT_TYPES)
|
45 |
+
|
46 |
+
for i, component_type in enumerate(COMPONENT_TYPES):
|
47 |
+
with tabs[i]:
|
48 |
+
display_component_tab(component_type)
|
49 |
+
|
50 |
+
# Navigation buttons
|
51 |
+
col1, col2 = st.columns(2)
|
52 |
+
|
53 |
+
with col1:
|
54 |
+
if st.button("Back to Construction", key="back_to_construction"):
|
55 |
+
st.session_state.current_page = "Construction"
|
56 |
+
st.rerun()
|
57 |
+
|
58 |
+
with col2:
|
59 |
+
if st.button("Continue to Internal Loads", key="continue_to_internal_loads"):
|
60 |
+
st.session_state.current_page = "Internal Loads"
|
61 |
+
st.rerun()
|
62 |
+
|
63 |
+
def initialize_components():
|
64 |
+
"""Initialize components in session state if not present."""
|
65 |
+
if "components" not in st.session_state.project_data:
|
66 |
+
st.session_state.project_data["components"] = {
|
67 |
+
"walls": [],
|
68 |
+
"roofs": [],
|
69 |
+
"floors": [],
|
70 |
+
"windows": [],
|
71 |
+
"doors": [],
|
72 |
+
"skylights": []
|
73 |
+
}
|
74 |
+
|
75 |
+
# Initialize component editor state
|
76 |
+
if "component_editor" not in st.session_state:
|
77 |
+
st.session_state.component_editor = {
|
78 |
+
"type": COMPONENT_TYPES[0],
|
79 |
+
"name": "",
|
80 |
+
"construction": "",
|
81 |
+
"fenestration": "",
|
82 |
+
"area": 10.0,
|
83 |
+
"orientation": ORIENTATION_OPTIONS[0],
|
84 |
+
"tilt": 90.0,
|
85 |
+
"parent_component": "",
|
86 |
+
"edit_mode": False,
|
87 |
+
"original_id": ""
|
88 |
+
}
|
89 |
+
|
90 |
+
def display_component_tab(component_type: str):
|
91 |
+
"""
|
92 |
+
Display the content for a specific component type tab.
|
93 |
+
|
94 |
+
Args:
|
95 |
+
component_type: The type of component (e.g., "Wall", "Window")
|
96 |
+
"""
|
97 |
+
st.subheader(f"{component_type}s")
|
98 |
+
|
99 |
+
# Get components of this type
|
100 |
+
component_key = component_type.lower() + "s"
|
101 |
+
components = st.session_state.project_data["components"].get(component_key, [])
|
102 |
+
|
103 |
+
# Display existing components
|
104 |
+
if components:
|
105 |
+
display_component_list(component_type, components)
|
106 |
+
else:
|
107 |
+
st.info(f"No {component_type.lower()}s added yet. Use the editor below to add components.")
|
108 |
+
|
109 |
+
# Component Editor
|
110 |
+
st.markdown("---")
|
111 |
+
st.subheader(f"{component_type} Editor")
|
112 |
+
display_component_editor(component_type)
|
113 |
+
|
114 |
+
def display_component_list(component_type: str, components: List[Dict[str, Any]]):
|
115 |
+
"""
|
116 |
+
Display the list of existing components for a given type.
|
117 |
+
|
118 |
+
Args:
|
119 |
+
component_type: The type of component
|
120 |
+
components: List of component dictionaries
|
121 |
+
"""
|
122 |
+
# Create a DataFrame for display
|
123 |
+
data = []
|
124 |
+
for i, comp in enumerate(components):
|
125 |
+
record = {
|
126 |
+
"#": i + 1,
|
127 |
+
"Name": comp["name"],
|
128 |
+
"Area (m²)": comp["area"],
|
129 |
+
"Orientation": comp["orientation"],
|
130 |
+
"Tilt (°) ": comp["tilt"]
|
131 |
+
}
|
132 |
+
|
133 |
+
if component_type in ["Wall", "Roof", "Floor"]:
|
134 |
+
record["Construction"] = comp["construction"]
|
135 |
+
else:
|
136 |
+
record["Fenestration"] = comp["fenestration"]
|
137 |
+
record["Parent Component"] = comp.get("parent_component", "N/A")
|
138 |
+
|
139 |
+
data.append(record)
|
140 |
+
|
141 |
+
df = pd.DataFrame(data)
|
142 |
+
st.dataframe(df, use_container_width=True, hide_index=True)
|
143 |
+
|
144 |
+
# Edit and delete options
|
145 |
+
col1, col2 = st.columns(2)
|
146 |
+
|
147 |
+
with col1:
|
148 |
+
selected_index = st.selectbox(
|
149 |
+
f"Select {component_type} # to Edit",
|
150 |
+
range(1, len(components) + 1),
|
151 |
+
key=f"edit_{component_type}_selector"
|
152 |
+
)
|
153 |
+
|
154 |
+
if st.button(f"Edit {component_type}", key=f"edit_{component_type}_button"):
|
155 |
+
# Load component data into editor
|
156 |
+
component_data = components[selected_index - 1]
|
157 |
+
st.session_state.component_editor = {
|
158 |
+
"type": component_type,
|
159 |
+
"name": component_data["name"],
|
160 |
+
"construction": component_data.get("construction", ""),
|
161 |
+
"fenestration": component_data.get("fenestration", ""),
|
162 |
+
"area": component_data["area"],
|
163 |
+
"orientation": component_data["orientation"],
|
164 |
+
"tilt": component_data["tilt"],
|
165 |
+
"parent_component": component_data.get("parent_component", ""),
|
166 |
+
"edit_mode": True,
|
167 |
+
"original_id": component_data["id"]
|
168 |
+
}
|
169 |
+
st.success(f"{component_type} \'{component_data['name']}\' loaded for editing.")
|
170 |
+
st.rerun()
|
171 |
+
|
172 |
+
with col2:
|
173 |
+
selected_index_delete = st.selectbox(
|
174 |
+
f"Select {component_type} # to Delete",
|
175 |
+
range(1, len(components) + 1),
|
176 |
+
key=f"delete_{component_type}_selector"
|
177 |
+
)
|
178 |
+
|
179 |
+
if st.button(f"Delete {component_type}", key=f"delete_{component_type}_button"):
|
180 |
+
# Delete component
|
181 |
+
component_key = component_type.lower() + "s"
|
182 |
+
deleted_component = st.session_state.project_data["components"][component_key].pop(selected_index_delete - 1)
|
183 |
+
st.success(f"{component_type} \'{deleted_component['name']}\' deleted.")
|
184 |
+
logger.info(f"Deleted {component_type} \'{deleted_component['name']}\'")
|
185 |
+
st.rerun()
|
186 |
+
|
187 |
+
def display_component_editor(component_type: str):
|
188 |
+
"""
|
189 |
+
Display the editor form for a specific component type.
|
190 |
+
|
191 |
+
Args:
|
192 |
+
component_type: The type of component
|
193 |
+
"""
|
194 |
+
# Get available constructions and fenestrations
|
195 |
+
available_constructions = get_available_constructions()
|
196 |
+
available_fenestrations = get_available_fenestrations()
|
197 |
+
available_walls = get_available_walls()
|
198 |
+
|
199 |
+
# Check if the editor is currently set to this component type
|
200 |
+
if st.session_state.component_editor["type"] != component_type and not st.session_state.component_editor["edit_mode"]:
|
201 |
+
reset_component_editor(component_type)
|
202 |
+
|
203 |
+
with st.form(f"{component_type}_editor_form"):
|
204 |
+
# Component name
|
205 |
+
name = st.text_input(
|
206 |
+
"Component Name",
|
207 |
+
value=st.session_state.component_editor["name"],
|
208 |
+
help="Enter a unique name for this component."
|
209 |
+
)
|
210 |
+
|
211 |
+
# Create two columns for layout
|
212 |
+
col1, col2 = st.columns(2)
|
213 |
+
|
214 |
+
with col1:
|
215 |
+
# Construction or Fenestration selection
|
216 |
+
if component_type in ["Wall", "Roof", "Floor"]:
|
217 |
+
construction = st.selectbox(
|
218 |
+
"Construction",
|
219 |
+
list(available_constructions.keys()),
|
220 |
+
index=list(available_constructions.keys()).index(st.session_state.component_editor["construction"]) if st.session_state.component_editor["construction"] in available_constructions else 0,
|
221 |
+
help="Select the construction assembly for this component."
|
222 |
+
)
|
223 |
+
fenestration = ""
|
224 |
+
else:
|
225 |
+
fenestration = st.selectbox(
|
226 |
+
"Fenestration",
|
227 |
+
list(available_fenestrations.keys()),
|
228 |
+
index=list(available_fenestrations.keys()).index(st.session_state.component_editor["fenestration"]) if st.session_state.component_editor["fenestration"] in available_fenestrations else 0,
|
229 |
+
help="Select the fenestration type for this component."
|
230 |
+
)
|
231 |
+
construction = ""
|
232 |
+
|
233 |
+
# Area
|
234 |
+
area = st.number_input(
|
235 |
+
"Area (m²)",
|
236 |
+
min_value=0.1,
|
237 |
+
max_value=10000.0,
|
238 |
+
value=float(st.session_state.component_editor["area"]),
|
239 |
+
format="%.2f",
|
240 |
+
help="Surface area of the component in square meters."
|
241 |
+
)
|
242 |
+
|
243 |
+
with col2:
|
244 |
+
# Orientation
|
245 |
+
orientation = st.selectbox(
|
246 |
+
"Orientation",
|
247 |
+
ORIENTATION_OPTIONS,
|
248 |
+
index=ORIENTATION_OPTIONS.index(st.session_state.component_editor["orientation"]) if st.session_state.component_editor["orientation"] in ORIENTATION_OPTIONS else 0,
|
249 |
+
help="Orientation of the component relative to the building orientation angle."
|
250 |
+
)
|
251 |
+
|
252 |
+
# Tilt
|
253 |
+
tilt = st.number_input(
|
254 |
+
"Tilt (°) ",
|
255 |
+
min_value=0.0,
|
256 |
+
max_value=180.0,
|
257 |
+
value=float(st.session_state.component_editor["tilt"]),
|
258 |
+
format="%.1f",
|
259 |
+
help="Tilt angle of the component relative to horizontal (0° = horizontal, 90° = vertical)."
|
260 |
+
)
|
261 |
+
|
262 |
+
# Parent component (for windows, doors, skylights)
|
263 |
+
parent_component = ""
|
264 |
+
if component_type in ["Window", "Door", "Skylight"]:
|
265 |
+
parent_component = st.selectbox(
|
266 |
+
"Parent Component (Wall/Roof)",
|
267 |
+
list(available_walls.keys()),
|
268 |
+
index=list(available_walls.keys()).index(st.session_state.component_editor["parent_component"]) if st.session_state.component_editor["parent_component"] in available_walls else 0,
|
269 |
+
help="Select the wall or roof component this fenestration belongs to."
|
270 |
+
)
|
271 |
+
|
272 |
+
# Form submission buttons
|
273 |
+
col1, col2 = st.columns(2)
|
274 |
+
|
275 |
+
with col1:
|
276 |
+
submit_button = st.form_submit_button("Save Component")
|
277 |
+
|
278 |
+
with col2:
|
279 |
+
clear_button = st.form_submit_button("Clear Form")
|
280 |
+
|
281 |
+
# Handle form submission
|
282 |
+
if submit_button:
|
283 |
+
# Validate inputs
|
284 |
+
validation_errors = validate_component(
|
285 |
+
component_type, name, construction, fenestration, area, orientation, tilt,
|
286 |
+
parent_component, st.session_state.component_editor["edit_mode"],
|
287 |
+
st.session_state.component_editor["original_id"]
|
288 |
+
)
|
289 |
+
|
290 |
+
if validation_errors:
|
291 |
+
# Display validation errors
|
292 |
+
for error in validation_errors:
|
293 |
+
st.error(error)
|
294 |
+
else:
|
295 |
+
# Create component data
|
296 |
+
component_data = {
|
297 |
+
"id": st.session_state.component_editor["original_id"] if st.session_state.component_editor["edit_mode"] else str(uuid.uuid4()),
|
298 |
+
"name": name,
|
299 |
+
"type": component_type,
|
300 |
+
"area": area,
|
301 |
+
"orientation": orientation,
|
302 |
+
"tilt": tilt
|
303 |
+
}
|
304 |
+
|
305 |
+
if component_type in ["Wall", "Roof", "Floor"]:
|
306 |
+
component_data["construction"] = construction
|
307 |
+
else:
|
308 |
+
component_data["fenestration"] = fenestration
|
309 |
+
component_data["parent_component"] = parent_component
|
310 |
+
|
311 |
+
# Handle edit mode
|
312 |
+
component_key = component_type.lower() + "s"
|
313 |
+
if st.session_state.component_editor["edit_mode"]:
|
314 |
+
# Find and update the component
|
315 |
+
components = st.session_state.project_data["components"][component_key]
|
316 |
+
for i, comp in enumerate(components):
|
317 |
+
if comp["id"] == st.session_state.component_editor["original_id"]:
|
318 |
+
components[i] = component_data
|
319 |
+
break
|
320 |
+
st.success(f"{component_type} \'{name}\' updated successfully.")
|
321 |
+
logger.info(f"Updated {component_type} \'{name}\'")
|
322 |
+
else:
|
323 |
+
# Add new component
|
324 |
+
st.session_state.project_data["components"][component_key].append(component_data)
|
325 |
+
st.success(f"{component_type} \'{name}\' added successfully.")
|
326 |
+
logger.info(f"Added new {component_type} \'{name}\'")
|
327 |
+
|
328 |
+
# Reset editor
|
329 |
+
reset_component_editor(component_type)
|
330 |
+
st.rerun()
|
331 |
+
|
332 |
+
# Handle clear button
|
333 |
+
if clear_button:
|
334 |
+
reset_component_editor(component_type)
|
335 |
+
st.rerun()
|
336 |
+
|
337 |
+
def get_available_constructions() -> Dict[str, Any]:
|
338 |
+
"""
|
339 |
+
Get all available constructions from both library and project.
|
340 |
+
|
341 |
+
Returns:
|
342 |
+
Dict of construction name to construction properties
|
343 |
+
"""
|
344 |
+
available_constructions = {}
|
345 |
+
|
346 |
+
# Add library constructions
|
347 |
+
if "constructions" in st.session_state.project_data and "library" in st.session_state.project_data["constructions"]:
|
348 |
+
available_constructions.update(st.session_state.project_data["constructions"]["library"])
|
349 |
+
|
350 |
+
# Add project constructions
|
351 |
+
if "constructions" in st.session_state.project_data and "project" in st.session_state.project_data["constructions"]:
|
352 |
+
available_constructions.update(st.session_state.project_data["constructions"]["project"])
|
353 |
+
|
354 |
+
return available_constructions
|
355 |
+
|
356 |
+
def get_available_fenestrations() -> Dict[str, Any]:
|
357 |
+
"""
|
358 |
+
Get all available fenestrations from both library and project.
|
359 |
+
|
360 |
+
Returns:
|
361 |
+
Dict of fenestration name to fenestration properties
|
362 |
+
"""
|
363 |
+
available_fenestrations = {}
|
364 |
+
|
365 |
+
# Add library fenestrations
|
366 |
+
if "fenestrations" in st.session_state.project_data and "library" in st.session_state.project_data["fenestrations"]:
|
367 |
+
available_fenestrations.update(st.session_state.project_data["fenestrations"]["library"])
|
368 |
+
|
369 |
+
# Add project fenestrations
|
370 |
+
if "fenestrations" in st.session_state.project_data and "project" in st.session_state.project_data["fenestrations"]:
|
371 |
+
available_fenestrations.update(st.session_state.project_data["fenestrations"]["project"])
|
372 |
+
|
373 |
+
return available_fenestrations
|
374 |
+
|
375 |
+
def get_available_walls() -> Dict[str, Any]:
|
376 |
+
"""
|
377 |
+
Get all available wall components.
|
378 |
+
|
379 |
+
Returns:
|
380 |
+
Dict of wall name to wall properties
|
381 |
+
"""
|
382 |
+
available_walls = {}
|
383 |
+
if "components" in st.session_state.project_data and "walls" in st.session_state.project_data["components"]:
|
384 |
+
for wall in st.session_state.project_data["components"]["walls"]:
|
385 |
+
available_walls[wall["name"]] = wall
|
386 |
+
return available_walls
|
387 |
+
|
388 |
+
def validate_component(
|
389 |
+
component_type: str, name: str, construction: str, fenestration: str, area: float,
|
390 |
+
orientation: str, tilt: float, parent_component: str, edit_mode: bool, original_id: str
|
391 |
+
) -> List[str]:
|
392 |
+
"""
|
393 |
+
Validate component inputs.
|
394 |
+
|
395 |
+
Args:
|
396 |
+
component_type: Type of component
|
397 |
+
name: Component name
|
398 |
+
construction: Selected construction name
|
399 |
+
fenestration: Selected fenestration name
|
400 |
+
area: Component area
|
401 |
+
orientation: Component orientation
|
402 |
+
tilt: Component tilt angle
|
403 |
+
parent_component: Parent component name (for fenestrations)
|
404 |
+
edit_mode: Whether in edit mode
|
405 |
+
original_id: Original ID if in edit mode
|
406 |
+
|
407 |
+
Returns:
|
408 |
+
List of validation error messages, empty if all inputs are valid
|
409 |
+
"""
|
410 |
+
errors = []
|
411 |
+
|
412 |
+
# Validate name
|
413 |
+
if not name or name.strip() == "":
|
414 |
+
errors.append("Component name is required.")
|
415 |
+
|
416 |
+
# Check for name uniqueness within the same component type
|
417 |
+
component_key = component_type.lower() + "s"
|
418 |
+
components = st.session_state.project_data["components"].get(component_key, [])
|
419 |
+
|
420 |
+
for comp in components:
|
421 |
+
if comp["name"] == name and (not edit_mode or comp["id"] != original_id):
|
422 |
+
errors.append(f"{component_type} name \'{name}\' already exists.")
|
423 |
+
break
|
424 |
+
|
425 |
+
# Validate construction or fenestration selection
|
426 |
+
if component_type in ["Wall", "Roof", "Floor"] and not construction:
|
427 |
+
errors.append("Construction selection is required.")
|
428 |
+
elif component_type in ["Window", "Door", "Skylight"] and not fenestration:
|
429 |
+
errors.append("Fenestration selection is required.")
|
430 |
+
|
431 |
+
# Validate area
|
432 |
+
if area <= 0:
|
433 |
+
errors.append("Area must be greater than zero.")
|
434 |
+
|
435 |
+
# Validate orientation
|
436 |
+
if orientation not in ORIENTATION_OPTIONS:
|
437 |
+
errors.append("Please select a valid orientation.")
|
438 |
+
|
439 |
+
# Validate tilt
|
440 |
+
if tilt < 0 or tilt > 180:
|
441 |
+
errors.append("Tilt angle must be between 0° and 180°.")
|
442 |
+
|
443 |
+
# Validate parent component for fenestrations
|
444 |
+
if component_type in ["Window", "Door", "Skylight"] and not parent_component:
|
445 |
+
errors.append("Parent component selection is required for fenestrations.")
|
446 |
+
|
447 |
+
return errors
|
448 |
+
|
449 |
+
def reset_component_editor(component_type: str):
|
450 |
+
"""
|
451 |
+
Reset the component editor to default values for the given type.
|
452 |
+
|
453 |
+
Args:
|
454 |
+
component_type: The type of component
|
455 |
+
"""
|
456 |
+
st.session_state.component_editor = {
|
457 |
+
"type": component_type,
|
458 |
+
"name": "",
|
459 |
+
"construction": "",
|
460 |
+
"fenestration": "",
|
461 |
+
"area": 10.0,
|
462 |
+
"orientation": ORIENTATION_OPTIONS[0],
|
463 |
+
"tilt": 90.0 if component_type == "Wall" else 0.0,
|
464 |
+
"parent_component": "",
|
465 |
+
"edit_mode": False,
|
466 |
+
"original_id": ""
|
467 |
+
}
|
468 |
+
|
469 |
+
def display_components_help():
|
470 |
+
"""
|
471 |
+
Display help information for the building components page.
|
472 |
+
"""
|
473 |
+
st.markdown("""
|
474 |
+
### Building Components Help
|
475 |
+
|
476 |
+
This section allows you to define the individual components of your building envelope, such as walls, roofs, floors, windows, doors, and skylights.
|
477 |
+
|
478 |
+
**Key Concepts:**
|
479 |
+
|
480 |
+
* **Component**: A specific part of the building envelope (e.g., "North Wall", "Living Room Window").
|
481 |
+
* **Construction**: The multi-layer assembly assigned to opaque components (walls, roofs, floors).
|
482 |
+
* **Fenestration**: The glazing or door system assigned to transparent or operable components (windows, doors, skylights).
|
483 |
+
* **Area**: The surface area of the component in square meters.
|
484 |
+
* **Orientation**: The direction the component faces relative to the building orientation angle (A=North, B=South, C=East, D=West, Horizontal).
|
485 |
+
* **Tilt**: The angle of the component relative to horizontal (0° = horizontal, 90° = vertical).
|
486 |
+
* **Parent Component**: For fenestrations, the wall or roof component they are part of.
|
487 |
+
|
488 |
+
**Workflow:**
|
489 |
+
|
490 |
+
1. Select the tab for the component type you want to define (e.g., "Walls").
|
491 |
+
2. Use the editor to add new components:
|
492 |
+
* Give the component a unique name.
|
493 |
+
* Select the appropriate construction (for walls, roofs, floors) or fenestration (for windows, doors, skylights) from your project library.
|
494 |
+
* Enter the area, orientation, and tilt.
|
495 |
+
* For fenestrations, select the parent wall or roof component.
|
496 |
+
3. Save the component.
|
497 |
+
4. Repeat for all building envelope components.
|
498 |
+
5. Edit or delete components as needed using the controls above the editor.
|
499 |
+
|
500 |
+
**Important:**
|
501 |
+
|
502 |
+
* Ensure all constructions and fenestrations you need are defined in the Material Library and Construction pages before defining components.
|
503 |
+
* The accuracy of your load calculations depends heavily on the correct definition of these components.
|
504 |
+
""")
|
app/construction.py
ADDED
@@ -0,0 +1,823 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
HVAC Load Calculator - Construction Module
|
3 |
+
|
4 |
+
This module handles the construction assembly functionality of the HVAC Load Calculator application,
|
5 |
+
allowing users to create and manage multi-layer constructions for walls, roofs, and floors.
|
6 |
+
It integrates with the material library to select materials for each layer and calculates
|
7 |
+
overall thermal properties.
|
8 |
+
|
9 |
+
Developed by: Dr Majed Abuseif, Deakin University
|
10 |
+
© 2025
|
11 |
+
"""
|
12 |
+
|
13 |
+
import streamlit as st
|
14 |
+
import pandas as pd
|
15 |
+
import numpy as np
|
16 |
+
import json
|
17 |
+
import logging
|
18 |
+
import uuid
|
19 |
+
from typing import Dict, List, Any, Optional, Tuple, Union
|
20 |
+
|
21 |
+
# Configure logging
|
22 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
23 |
+
logger = logging.getLogger(__name__)
|
24 |
+
|
25 |
+
# Define constants
|
26 |
+
CONSTRUCTION_TYPES = ["Wall", "Roof", "Floor"]
|
27 |
+
|
28 |
+
# Default library constructions
|
29 |
+
DEFAULT_CONSTRUCTIONS = {
|
30 |
+
"Brick Cavity Wall": {
|
31 |
+
"type": "Wall",
|
32 |
+
"layers": [
|
33 |
+
{"material": "Brick", "thickness": 0.1},
|
34 |
+
{"material": "Air Gap", "thickness": 0.05},
|
35 |
+
{"material": "Mineral Wool", "thickness": 0.1},
|
36 |
+
{"material": "Concrete Block", "thickness": 0.1},
|
37 |
+
{"material": "Gypsum Board", "thickness": 0.0125}
|
38 |
+
],
|
39 |
+
"u_value": 0.35, # W/m²·K
|
40 |
+
"r_value": 2.86, # m²·K/W
|
41 |
+
"thermal_mass": 220.0, # kJ/m²·K
|
42 |
+
"embodied_carbon": 120.0, # kg CO2e/m²
|
43 |
+
"cost": 180.0 # USD/m²
|
44 |
+
},
|
45 |
+
"Insulated Concrete Wall": {
|
46 |
+
"type": "Wall",
|
47 |
+
"layers": [
|
48 |
+
{"material": "Concrete", "thickness": 0.15},
|
49 |
+
{"material": "EPS Insulation", "thickness": 0.1},
|
50 |
+
{"material": "Gypsum Board", "thickness": 0.0125}
|
51 |
+
],
|
52 |
+
"u_value": 0.32,
|
53 |
+
"r_value": 3.13,
|
54 |
+
"thermal_mass": 360.0,
|
55 |
+
"embodied_carbon": 95.0,
|
56 |
+
"cost": 150.0
|
57 |
+
},
|
58 |
+
"Timber Frame Wall": {
|
59 |
+
"type": "Wall",
|
60 |
+
"layers": [
|
61 |
+
{"material": "Wood (Pine)", "thickness": 0.02},
|
62 |
+
{"material": "Glass Fiber Insulation", "thickness": 0.15},
|
63 |
+
{"material": "Plywood", "thickness": 0.012},
|
64 |
+
{"material": "Gypsum Board", "thickness": 0.0125}
|
65 |
+
],
|
66 |
+
"u_value": 0.25,
|
67 |
+
"r_value": 4.0,
|
68 |
+
"thermal_mass": 45.0,
|
69 |
+
"embodied_carbon": 35.0,
|
70 |
+
"cost": 120.0
|
71 |
+
},
|
72 |
+
"Concrete Flat Roof": {
|
73 |
+
"type": "Roof",
|
74 |
+
"layers": [
|
75 |
+
{"material": "Concrete", "thickness": 0.15},
|
76 |
+
{"material": "Polyurethane Insulation", "thickness": 0.15},
|
77 |
+
{"material": "Waterproofing Membrane", "thickness": 0.005}
|
78 |
+
],
|
79 |
+
"u_value": 0.18,
|
80 |
+
"r_value": 5.56,
|
81 |
+
"thermal_mass": 360.0,
|
82 |
+
"embodied_carbon": 110.0,
|
83 |
+
"cost": 200.0
|
84 |
+
},
|
85 |
+
"Timber Pitched Roof": {
|
86 |
+
"type": "Roof",
|
87 |
+
"layers": [
|
88 |
+
{"material": "Roof Tiles", "thickness": 0.02},
|
89 |
+
{"material": "Waterproofing Membrane", "thickness": 0.002},
|
90 |
+
{"material": "Wood (Pine)", "thickness": 0.025},
|
91 |
+
{"material": "Glass Fiber Insulation", "thickness": 0.2},
|
92 |
+
{"material": "Gypsum Board", "thickness": 0.0125}
|
93 |
+
],
|
94 |
+
"u_value": 0.16,
|
95 |
+
"r_value": 6.25,
|
96 |
+
"thermal_mass": 40.0,
|
97 |
+
"embodied_carbon": 45.0,
|
98 |
+
"cost": 160.0
|
99 |
+
},
|
100 |
+
"Concrete Floor Slab": {
|
101 |
+
"type": "Floor",
|
102 |
+
"layers": [
|
103 |
+
{"material": "Ceramic Tile", "thickness": 0.01},
|
104 |
+
{"material": "Concrete", "thickness": 0.1},
|
105 |
+
{"material": "EPS Insulation", "thickness": 0.1},
|
106 |
+
{"material": "Damp Proof Membrane", "thickness": 0.002},
|
107 |
+
{"material": "Gravel", "thickness": 0.15}
|
108 |
+
],
|
109 |
+
"u_value": 0.25,
|
110 |
+
"r_value": 4.0,
|
111 |
+
"thermal_mass": 240.0,
|
112 |
+
"embodied_carbon": 85.0,
|
113 |
+
"cost": 140.0
|
114 |
+
},
|
115 |
+
"Timber Floor": {
|
116 |
+
"type": "Floor",
|
117 |
+
"layers": [
|
118 |
+
{"material": "Wood (Pine)", "thickness": 0.02},
|
119 |
+
{"material": "Air Gap", "thickness": 0.05},
|
120 |
+
{"material": "Glass Fiber Insulation", "thickness": 0.15},
|
121 |
+
{"material": "Plywood", "thickness": 0.018}
|
122 |
+
],
|
123 |
+
"u_value": 0.22,
|
124 |
+
"r_value": 4.55,
|
125 |
+
"thermal_mass": 30.0,
|
126 |
+
"embodied_carbon": 25.0,
|
127 |
+
"cost": 110.0
|
128 |
+
}
|
129 |
+
}
|
130 |
+
|
131 |
+
# Additional materials needed for constructions but not in material library
|
132 |
+
ADDITIONAL_MATERIALS = {
|
133 |
+
"Air Gap": {
|
134 |
+
"category": "Sub-Structural Materials",
|
135 |
+
"thermal_conductivity": 0.026,
|
136 |
+
"density": 1.2,
|
137 |
+
"specific_heat": 1000.0,
|
138 |
+
"thickness_range": [0.01, 0.1],
|
139 |
+
"default_thickness": 0.05,
|
140 |
+
"embodied_carbon": 0.0,
|
141 |
+
"cost": 0.0
|
142 |
+
},
|
143 |
+
"Waterproofing Membrane": {
|
144 |
+
"category": "Finishing Materials",
|
145 |
+
"thermal_conductivity": 0.17,
|
146 |
+
"density": 1100.0,
|
147 |
+
"specific_heat": 1000.0,
|
148 |
+
"thickness_range": [0.001, 0.01],
|
149 |
+
"default_thickness": 0.002,
|
150 |
+
"embodied_carbon": 4.2,
|
151 |
+
"cost": 15.0
|
152 |
+
},
|
153 |
+
"Damp Proof Membrane": {
|
154 |
+
"category": "Sub-Structural Materials",
|
155 |
+
"thermal_conductivity": 0.17,
|
156 |
+
"density": 1100.0,
|
157 |
+
"specific_heat": 1000.0,
|
158 |
+
"thickness_range": [0.001, 0.005],
|
159 |
+
"default_thickness": 0.002,
|
160 |
+
"embodied_carbon": 4.0,
|
161 |
+
"cost": 12.0
|
162 |
+
},
|
163 |
+
"Roof Tiles": {
|
164 |
+
"category": "Finishing Materials",
|
165 |
+
"thermal_conductivity": 1.0,
|
166 |
+
"density": 1900.0,
|
167 |
+
"specific_heat": 800.0,
|
168 |
+
"thickness_range": [0.01, 0.03],
|
169 |
+
"default_thickness": 0.02,
|
170 |
+
"embodied_carbon": 110.0,
|
171 |
+
"cost": 75.0
|
172 |
+
},
|
173 |
+
"Gravel": {
|
174 |
+
"category": "Sub-Structural Materials",
|
175 |
+
"thermal_conductivity": 0.7,
|
176 |
+
"density": 1800.0,
|
177 |
+
"specific_heat": 840.0,
|
178 |
+
"thickness_range": [0.05, 0.3],
|
179 |
+
"default_thickness": 0.15,
|
180 |
+
"embodied_carbon": 1.5,
|
181 |
+
"cost": 30.0
|
182 |
+
}
|
183 |
+
}
|
184 |
+
|
185 |
+
def display_construction_page():
|
186 |
+
"""
|
187 |
+
Display the construction page.
|
188 |
+
This is the main function called by main.py when the Construction page is selected.
|
189 |
+
"""
|
190 |
+
st.title("Construction Library")
|
191 |
+
|
192 |
+
# Display help information in an expandable section
|
193 |
+
with st.expander("Help & Information"):
|
194 |
+
display_construction_help()
|
195 |
+
|
196 |
+
# Initialize constructions in session state if not present
|
197 |
+
initialize_constructions()
|
198 |
+
|
199 |
+
# Create columns for library and project constructions
|
200 |
+
col1, col2 = st.columns(2)
|
201 |
+
|
202 |
+
# Library Constructions
|
203 |
+
with col1:
|
204 |
+
st.subheader("Library Constructions")
|
205 |
+
display_library_constructions()
|
206 |
+
|
207 |
+
# Project Constructions
|
208 |
+
with col2:
|
209 |
+
st.subheader("Project Constructions")
|
210 |
+
display_project_constructions()
|
211 |
+
|
212 |
+
# Construction Editor
|
213 |
+
st.markdown("---")
|
214 |
+
st.subheader("Construction Editor")
|
215 |
+
display_construction_editor()
|
216 |
+
|
217 |
+
# Navigation buttons
|
218 |
+
col1, col2 = st.columns(2)
|
219 |
+
|
220 |
+
with col1:
|
221 |
+
if st.button("Back to Material Library", key="back_to_materials"):
|
222 |
+
st.session_state.current_page = "Material Library"
|
223 |
+
st.rerun()
|
224 |
+
|
225 |
+
with col2:
|
226 |
+
if st.button("Continue to Building Components", key="continue_to_components"):
|
227 |
+
st.session_state.current_page = "Building Components"
|
228 |
+
st.rerun()
|
229 |
+
|
230 |
+
def initialize_constructions():
|
231 |
+
"""Initialize constructions in session state if not present."""
|
232 |
+
if "constructions" not in st.session_state.project_data:
|
233 |
+
st.session_state.project_data["constructions"] = {
|
234 |
+
"library": {},
|
235 |
+
"project": {}
|
236 |
+
}
|
237 |
+
|
238 |
+
# Initialize library constructions if empty
|
239 |
+
if not st.session_state.project_data["constructions"]["library"]:
|
240 |
+
st.session_state.project_data["constructions"]["library"] = DEFAULT_CONSTRUCTIONS.copy()
|
241 |
+
|
242 |
+
# Add additional materials to library if not present
|
243 |
+
for material_name, material_data in ADDITIONAL_MATERIALS.items():
|
244 |
+
if material_name not in st.session_state.project_data["materials"]["library"]:
|
245 |
+
st.session_state.project_data["materials"]["library"][material_name] = material_data
|
246 |
+
|
247 |
+
# Initialize construction editor state
|
248 |
+
if "construction_editor" not in st.session_state:
|
249 |
+
st.session_state.construction_editor = {
|
250 |
+
"name": "",
|
251 |
+
"type": CONSTRUCTION_TYPES[0],
|
252 |
+
"layers": [],
|
253 |
+
"edit_mode": False,
|
254 |
+
"original_name": ""
|
255 |
+
}
|
256 |
+
|
257 |
+
def display_library_constructions():
|
258 |
+
"""Display the library constructions section."""
|
259 |
+
# Filter options
|
260 |
+
type_filter = st.selectbox(
|
261 |
+
"Filter by Type",
|
262 |
+
["All"] + CONSTRUCTION_TYPES,
|
263 |
+
key="library_construction_type_filter"
|
264 |
+
)
|
265 |
+
|
266 |
+
# Get library constructions
|
267 |
+
library_constructions = st.session_state.project_data["constructions"]["library"]
|
268 |
+
|
269 |
+
# Apply filter
|
270 |
+
if type_filter != "All":
|
271 |
+
filtered_constructions = {
|
272 |
+
name: props for name, props in library_constructions.items()
|
273 |
+
if props["type"] == type_filter
|
274 |
+
}
|
275 |
+
else:
|
276 |
+
filtered_constructions = library_constructions
|
277 |
+
|
278 |
+
# Display constructions in a table
|
279 |
+
if filtered_constructions:
|
280 |
+
# Create a DataFrame for display
|
281 |
+
data = []
|
282 |
+
for name, props in filtered_constructions.items():
|
283 |
+
data.append({
|
284 |
+
"Name": name,
|
285 |
+
"Type": props["type"],
|
286 |
+
"U-Value (W/m²·K)": props["u_value"],
|
287 |
+
"R-Value (m²·K/W)": props["r_value"],
|
288 |
+
"Thermal Mass (kJ/m²·K)": props["thermal_mass"],
|
289 |
+
"Layers": len(props["layers"])
|
290 |
+
})
|
291 |
+
|
292 |
+
df = pd.DataFrame(data)
|
293 |
+
st.dataframe(df, use_container_width=True, hide_index=True)
|
294 |
+
|
295 |
+
# Add to project button
|
296 |
+
selected_construction = st.selectbox(
|
297 |
+
"Select Construction to Add to Project",
|
298 |
+
list(filtered_constructions.keys()),
|
299 |
+
key="library_construction_selector"
|
300 |
+
)
|
301 |
+
|
302 |
+
# Display selected construction details
|
303 |
+
if selected_construction:
|
304 |
+
st.subheader(f"Details: {selected_construction}")
|
305 |
+
display_construction_details(filtered_constructions[selected_construction])
|
306 |
+
|
307 |
+
if st.button("Add to Project", key="add_library_construction_to_project"):
|
308 |
+
# Check if construction already exists in project
|
309 |
+
if selected_construction in st.session_state.project_data["constructions"]["project"]:
|
310 |
+
st.warning(f"Construction '{selected_construction}' already exists in your project.")
|
311 |
+
else:
|
312 |
+
# Add to project constructions
|
313 |
+
st.session_state.project_data["constructions"]["project"][selected_construction] = \
|
314 |
+
st.session_state.project_data["constructions"]["library"][selected_construction].copy()
|
315 |
+
st.success(f"Construction '{selected_construction}' added to your project.")
|
316 |
+
logger.info(f"Added library construction '{selected_construction}' to project")
|
317 |
+
else:
|
318 |
+
st.info("No constructions found in the selected type.")
|
319 |
+
|
320 |
+
def display_project_constructions():
|
321 |
+
"""Display the project constructions section."""
|
322 |
+
# Get project constructions
|
323 |
+
project_constructions = st.session_state.project_data["constructions"]["project"]
|
324 |
+
|
325 |
+
if project_constructions:
|
326 |
+
# Create a DataFrame for display
|
327 |
+
data = []
|
328 |
+
for name, props in project_constructions.items():
|
329 |
+
data.append({
|
330 |
+
"Name": name,
|
331 |
+
"Type": props["type"],
|
332 |
+
"U-Value (W/m²·K)": props["u_value"],
|
333 |
+
"R-Value (m²·K/W)": props["r_value"],
|
334 |
+
"Thermal Mass (kJ/m²·K)": props["thermal_mass"],
|
335 |
+
"Layers": len(props["layers"])
|
336 |
+
})
|
337 |
+
|
338 |
+
df = pd.DataFrame(data)
|
339 |
+
st.dataframe(df, use_container_width=True, hide_index=True)
|
340 |
+
|
341 |
+
# Select construction to view details
|
342 |
+
selected_construction = st.selectbox(
|
343 |
+
"Select Construction to View Details",
|
344 |
+
list(project_constructions.keys()),
|
345 |
+
key="project_construction_selector"
|
346 |
+
)
|
347 |
+
|
348 |
+
# Display selected construction details
|
349 |
+
if selected_construction:
|
350 |
+
st.subheader(f"Details: {selected_construction}")
|
351 |
+
display_construction_details(project_constructions[selected_construction])
|
352 |
+
|
353 |
+
# Edit and delete options
|
354 |
+
col1, col2 = st.columns(2)
|
355 |
+
|
356 |
+
with col1:
|
357 |
+
selected_construction_edit = st.selectbox(
|
358 |
+
"Select Construction to Edit",
|
359 |
+
list(project_constructions.keys()),
|
360 |
+
key="project_construction_edit_selector"
|
361 |
+
)
|
362 |
+
|
363 |
+
if st.button("Edit Construction", key="edit_project_construction"):
|
364 |
+
# Load construction data into editor
|
365 |
+
construction_data = project_constructions[selected_construction_edit]
|
366 |
+
st.session_state.construction_editor = {
|
367 |
+
"name": selected_construction_edit,
|
368 |
+
"type": construction_data["type"],
|
369 |
+
"layers": construction_data["layers"].copy(),
|
370 |
+
"edit_mode": True,
|
371 |
+
"original_name": selected_construction_edit
|
372 |
+
}
|
373 |
+
st.success(f"Construction '{selected_construction_edit}' loaded for editing.")
|
374 |
+
st.rerun()
|
375 |
+
|
376 |
+
with col2:
|
377 |
+
selected_construction_delete = st.selectbox(
|
378 |
+
"Select Construction to Delete",
|
379 |
+
list(project_constructions.keys()),
|
380 |
+
key="project_construction_delete_selector"
|
381 |
+
)
|
382 |
+
|
383 |
+
if st.button("Delete Construction", key="delete_project_construction"):
|
384 |
+
# Check if construction is in use
|
385 |
+
is_in_use = check_construction_in_use(selected_construction_delete)
|
386 |
+
|
387 |
+
if is_in_use:
|
388 |
+
st.error(f"Cannot delete construction '{selected_construction_delete}' because it is in use in building components.")
|
389 |
+
else:
|
390 |
+
# Delete construction
|
391 |
+
del st.session_state.project_data["constructions"]["project"][selected_construction_delete]
|
392 |
+
st.success(f"Construction '{selected_construction_delete}' deleted from your project.")
|
393 |
+
logger.info(f"Deleted construction '{selected_construction_delete}' from project")
|
394 |
+
else:
|
395 |
+
st.info("No constructions in your project. Add constructions from the library or create custom constructions.")
|
396 |
+
|
397 |
+
def display_construction_details(construction: Dict[str, Any]):
|
398 |
+
"""
|
399 |
+
Display detailed information about a construction.
|
400 |
+
|
401 |
+
Args:
|
402 |
+
construction: Construction data dictionary
|
403 |
+
"""
|
404 |
+
# Display basic properties
|
405 |
+
col1, col2, col3 = st.columns(3)
|
406 |
+
|
407 |
+
with col1:
|
408 |
+
st.write(f"**Type:** {construction['type']}")
|
409 |
+
st.write(f"**U-Value:** {construction['u_value']} W/m²·K")
|
410 |
+
|
411 |
+
with col2:
|
412 |
+
st.write(f"**R-Value:** {construction['r_value']} m²·K/W")
|
413 |
+
st.write(f"**Thermal Mass:** {construction['thermal_mass']} kJ/m²·K")
|
414 |
+
|
415 |
+
with col3:
|
416 |
+
st.write(f"**Embodied Carbon:** {construction['embodied_carbon']} kg CO₂e/m²")
|
417 |
+
st.write(f"**Cost:** ${construction['cost']}/m²")
|
418 |
+
|
419 |
+
# Display layers
|
420 |
+
st.write("**Layers (from outside to inside):**")
|
421 |
+
|
422 |
+
# Create a DataFrame for layers
|
423 |
+
layers_data = []
|
424 |
+
for i, layer in enumerate(construction["layers"]):
|
425 |
+
layers_data.append({
|
426 |
+
"Layer": i + 1,
|
427 |
+
"Material": layer["material"],
|
428 |
+
"Thickness (m)": layer["thickness"]
|
429 |
+
})
|
430 |
+
|
431 |
+
layers_df = pd.DataFrame(layers_data)
|
432 |
+
st.dataframe(layers_df, use_container_width=True, hide_index=True)
|
433 |
+
|
434 |
+
def display_construction_editor():
|
435 |
+
"""Display the construction editor form."""
|
436 |
+
# Get available materials
|
437 |
+
available_materials = get_available_materials()
|
438 |
+
|
439 |
+
with st.form("construction_editor_form"):
|
440 |
+
# Construction name
|
441 |
+
name = st.text_input(
|
442 |
+
"Construction Name",
|
443 |
+
value=st.session_state.construction_editor["name"],
|
444 |
+
help="Enter a unique name for the construction."
|
445 |
+
)
|
446 |
+
|
447 |
+
# Construction type
|
448 |
+
construction_type = st.selectbox(
|
449 |
+
"Construction Type",
|
450 |
+
CONSTRUCTION_TYPES,
|
451 |
+
index=CONSTRUCTION_TYPES.index(st.session_state.construction_editor["type"]) if st.session_state.construction_editor["type"] in CONSTRUCTION_TYPES else 0,
|
452 |
+
help="Select the construction type."
|
453 |
+
)
|
454 |
+
|
455 |
+
# Layers section
|
456 |
+
st.subheader("Layers (from outside to inside)")
|
457 |
+
|
458 |
+
# Display current layers
|
459 |
+
if st.session_state.construction_editor["layers"]:
|
460 |
+
layers_data = []
|
461 |
+
for i, layer in enumerate(st.session_state.construction_editor["layers"]):
|
462 |
+
layers_data.append({
|
463 |
+
"Layer": i + 1,
|
464 |
+
"Material": layer["material"],
|
465 |
+
"Thickness (m)": layer["thickness"]
|
466 |
+
})
|
467 |
+
|
468 |
+
layers_df = pd.DataFrame(layers_data)
|
469 |
+
st.dataframe(layers_df, use_container_width=True, hide_index=True)
|
470 |
+
else:
|
471 |
+
st.info("No layers added yet. Use the controls below to add layers.")
|
472 |
+
|
473 |
+
# Layer controls
|
474 |
+
st.subheader("Add/Edit Layer")
|
475 |
+
|
476 |
+
col1, col2 = st.columns(2)
|
477 |
+
|
478 |
+
with col1:
|
479 |
+
layer_material = st.selectbox(
|
480 |
+
"Material",
|
481 |
+
list(available_materials.keys()),
|
482 |
+
key="layer_material_selector",
|
483 |
+
help="Select a material for this layer."
|
484 |
+
)
|
485 |
+
|
486 |
+
# Get material properties
|
487 |
+
if layer_material in available_materials:
|
488 |
+
material_props = available_materials[layer_material]
|
489 |
+
min_thickness = material_props["thickness_range"][0]
|
490 |
+
max_thickness = material_props["thickness_range"][1]
|
491 |
+
default_thickness = material_props["default_thickness"]
|
492 |
+
else:
|
493 |
+
min_thickness = 0.001
|
494 |
+
max_thickness = 0.5
|
495 |
+
default_thickness = 0.05
|
496 |
+
|
497 |
+
with col2:
|
498 |
+
layer_thickness = st.number_input(
|
499 |
+
"Thickness (m)",
|
500 |
+
min_value=float(min_thickness),
|
501 |
+
max_value=float(max_thickness),
|
502 |
+
value=float(default_thickness),
|
503 |
+
format="%.3f",
|
504 |
+
help=f"Enter the thickness for this layer (range: {min_thickness}-{max_thickness} m)."
|
505 |
+
)
|
506 |
+
|
507 |
+
# Layer action buttons
|
508 |
+
col1, col2, col3 = st.columns(3)
|
509 |
+
|
510 |
+
with col1:
|
511 |
+
add_layer = st.form_submit_button("Add Layer")
|
512 |
+
|
513 |
+
with col2:
|
514 |
+
layer_index_to_remove = st.number_input(
|
515 |
+
"Layer # to Remove",
|
516 |
+
min_value=1,
|
517 |
+
max_value=max(1, len(st.session_state.construction_editor["layers"])),
|
518 |
+
value=len(st.session_state.construction_editor["layers"]) if st.session_state.construction_editor["layers"] else 1,
|
519 |
+
step=1,
|
520 |
+
help="Enter the layer number to remove."
|
521 |
+
)
|
522 |
+
|
523 |
+
with col3:
|
524 |
+
remove_layer = st.form_submit_button("Remove Layer")
|
525 |
+
|
526 |
+
# Calculate button and results
|
527 |
+
st.subheader("Thermal Properties")
|
528 |
+
calculate = st.form_submit_button("Calculate Properties")
|
529 |
+
|
530 |
+
# Form submission buttons
|
531 |
+
col1, col2, col3 = st.columns(3)
|
532 |
+
|
533 |
+
with col1:
|
534 |
+
save_button = st.form_submit_button("Save Construction")
|
535 |
+
|
536 |
+
with col2:
|
537 |
+
clear_button = st.form_submit_button("Clear Form")
|
538 |
+
|
539 |
+
with col3:
|
540 |
+
cancel_button = st.form_submit_button("Cancel Edit")
|
541 |
+
|
542 |
+
# Handle form actions outside the form
|
543 |
+
if add_layer:
|
544 |
+
# Add new layer
|
545 |
+
new_layer = {
|
546 |
+
"material": layer_material,
|
547 |
+
"thickness": layer_thickness
|
548 |
+
}
|
549 |
+
st.session_state.construction_editor["layers"].append(new_layer)
|
550 |
+
st.success(f"Layer added: {layer_material} ({layer_thickness} m)")
|
551 |
+
st.rerun()
|
552 |
+
|
553 |
+
if remove_layer and st.session_state.construction_editor["layers"]:
|
554 |
+
# Remove layer
|
555 |
+
if 1 <= layer_index_to_remove <= len(st.session_state.construction_editor["layers"]):
|
556 |
+
removed_layer = st.session_state.construction_editor["layers"].pop(layer_index_to_remove - 1)
|
557 |
+
st.success(f"Layer {layer_index_to_remove} removed: {removed_layer['material']} ({removed_layer['thickness']} m)")
|
558 |
+
st.rerun()
|
559 |
+
else:
|
560 |
+
st.error("Invalid layer index.")
|
561 |
+
|
562 |
+
if calculate:
|
563 |
+
# Calculate thermal properties
|
564 |
+
if st.session_state.construction_editor["layers"]:
|
565 |
+
u_value, r_value, thermal_mass, embodied_carbon, cost = calculate_construction_properties(
|
566 |
+
st.session_state.construction_editor["layers"],
|
567 |
+
available_materials
|
568 |
+
)
|
569 |
+
|
570 |
+
col1, col2, col3 = st.columns(3)
|
571 |
+
|
572 |
+
with col1:
|
573 |
+
st.metric("U-Value", f"{u_value:.3f} W/m²·K")
|
574 |
+
st.metric("R-Value", f"{r_value:.3f} m²·K/W")
|
575 |
+
|
576 |
+
with col2:
|
577 |
+
st.metric("Thermal Mass", f"{thermal_mass:.1f} kJ/m²·K")
|
578 |
+
st.metric("Total Thickness", f"{sum(layer['thickness'] for layer in st.session_state.construction_editor['layers']):.3f} m")
|
579 |
+
|
580 |
+
with col3:
|
581 |
+
st.metric("Embodied Carbon", f"{embodied_carbon:.1f} kg CO₂e/m²")
|
582 |
+
st.metric("Cost", f"${cost:.2f}/m²")
|
583 |
+
else:
|
584 |
+
st.warning("Add layers to calculate thermal properties.")
|
585 |
+
|
586 |
+
if save_button:
|
587 |
+
# Validate inputs
|
588 |
+
validation_errors = validate_construction(
|
589 |
+
name, construction_type, st.session_state.construction_editor["layers"],
|
590 |
+
st.session_state.construction_editor["edit_mode"], st.session_state.construction_editor["original_name"]
|
591 |
+
)
|
592 |
+
|
593 |
+
if validation_errors:
|
594 |
+
# Display validation errors
|
595 |
+
for error in validation_errors:
|
596 |
+
st.error(error)
|
597 |
+
else:
|
598 |
+
# Calculate properties
|
599 |
+
u_value, r_value, thermal_mass, embodied_carbon, cost = calculate_construction_properties(
|
600 |
+
st.session_state.construction_editor["layers"],
|
601 |
+
available_materials
|
602 |
+
)
|
603 |
+
|
604 |
+
# Create construction data
|
605 |
+
construction_data = {
|
606 |
+
"type": construction_type,
|
607 |
+
"layers": st.session_state.construction_editor["layers"].copy(),
|
608 |
+
"u_value": u_value,
|
609 |
+
"r_value": r_value,
|
610 |
+
"thermal_mass": thermal_mass,
|
611 |
+
"embodied_carbon": embodied_carbon,
|
612 |
+
"cost": cost
|
613 |
+
}
|
614 |
+
|
615 |
+
# Handle edit mode
|
616 |
+
if st.session_state.construction_editor["edit_mode"]:
|
617 |
+
original_name = st.session_state.construction_editor["original_name"]
|
618 |
+
|
619 |
+
# If name changed, delete old entry and create new one
|
620 |
+
if original_name != name:
|
621 |
+
del st.session_state.project_data["constructions"]["project"][original_name]
|
622 |
+
|
623 |
+
# Update construction
|
624 |
+
st.session_state.project_data["constructions"]["project"][name] = construction_data
|
625 |
+
st.success(f"Construction '{name}' updated successfully.")
|
626 |
+
logger.info(f"Updated construction '{name}' in project")
|
627 |
+
else:
|
628 |
+
# Add new construction
|
629 |
+
st.session_state.project_data["constructions"]["project"][name] = construction_data
|
630 |
+
st.success(f"Construction '{name}' added to your project.")
|
631 |
+
logger.info(f"Added new construction '{name}' to project")
|
632 |
+
|
633 |
+
# Reset editor
|
634 |
+
reset_construction_editor()
|
635 |
+
st.rerun()
|
636 |
+
|
637 |
+
if clear_button:
|
638 |
+
reset_construction_editor()
|
639 |
+
st.rerun()
|
640 |
+
|
641 |
+
if cancel_button and st.session_state.construction_editor["edit_mode"]:
|
642 |
+
reset_construction_editor()
|
643 |
+
st.success("Edit cancelled.")
|
644 |
+
st.rerun()
|
645 |
+
|
646 |
+
def get_available_materials() -> Dict[str, Any]:
|
647 |
+
"""
|
648 |
+
Get all available materials from both library and project.
|
649 |
+
|
650 |
+
Returns:
|
651 |
+
Dict of material name to material properties
|
652 |
+
"""
|
653 |
+
available_materials = {}
|
654 |
+
|
655 |
+
# Add library materials
|
656 |
+
if "materials" in st.session_state.project_data and "library" in st.session_state.project_data["materials"]:
|
657 |
+
available_materials.update(st.session_state.project_data["materials"]["library"])
|
658 |
+
|
659 |
+
# Add project materials
|
660 |
+
if "materials" in st.session_state.project_data and "project" in st.session_state.project_data["materials"]:
|
661 |
+
available_materials.update(st.session_state.project_data["materials"]["project"])
|
662 |
+
|
663 |
+
return available_materials
|
664 |
+
|
665 |
+
def calculate_construction_properties(
|
666 |
+
layers: List[Dict[str, Any]],
|
667 |
+
available_materials: Dict[str, Any]
|
668 |
+
) -> Tuple[float, float, float, float, float]:
|
669 |
+
"""
|
670 |
+
Calculate thermal properties of a construction.
|
671 |
+
|
672 |
+
Args:
|
673 |
+
layers: List of layer dictionaries with material and thickness
|
674 |
+
available_materials: Dictionary of available materials
|
675 |
+
|
676 |
+
Returns:
|
677 |
+
Tuple of (u_value, r_value, thermal_mass, embodied_carbon, cost)
|
678 |
+
"""
|
679 |
+
# Initialize values
|
680 |
+
r_value_total = 0.0 # m²·K/W
|
681 |
+
thermal_mass_total = 0.0 # kJ/m²·K
|
682 |
+
embodied_carbon_total = 0.0 # kg CO₂e/m²
|
683 |
+
cost_total = 0.0 # USD/m²
|
684 |
+
|
685 |
+
# Add standard surface resistances
|
686 |
+
r_value_total += 0.13 # Interior surface resistance
|
687 |
+
r_value_total += 0.04 # Exterior surface resistance
|
688 |
+
|
689 |
+
# Calculate properties for each layer
|
690 |
+
for layer in layers:
|
691 |
+
material_name = layer["material"]
|
692 |
+
thickness = layer["thickness"]
|
693 |
+
|
694 |
+
if material_name in available_materials:
|
695 |
+
material = available_materials[material_name]
|
696 |
+
|
697 |
+
# Calculate R-value for this layer
|
698 |
+
r_value_layer = thickness / material["thermal_conductivity"]
|
699 |
+
r_value_total += r_value_layer
|
700 |
+
|
701 |
+
# Calculate thermal mass for this layer
|
702 |
+
thermal_mass_layer = material["density"] * material["specific_heat"] * thickness / 1000 # Convert J to kJ
|
703 |
+
thermal_mass_total += thermal_mass_layer
|
704 |
+
|
705 |
+
# Calculate embodied carbon for this layer
|
706 |
+
embodied_carbon_layer = material["embodied_carbon"] * thickness
|
707 |
+
embodied_carbon_total += embodied_carbon_layer
|
708 |
+
|
709 |
+
# Calculate cost for this layer
|
710 |
+
cost_layer = material["cost"] * thickness
|
711 |
+
cost_total += cost_layer
|
712 |
+
|
713 |
+
# Calculate U-value from R-value
|
714 |
+
u_value = 1.0 / r_value_total if r_value_total > 0 else float('inf')
|
715 |
+
|
716 |
+
return u_value, r_value_total, thermal_mass_total, embodied_carbon_total, cost_total
|
717 |
+
|
718 |
+
def validate_construction(
|
719 |
+
name: str, construction_type: str, layers: List[Dict[str, Any]],
|
720 |
+
edit_mode: bool, original_name: str
|
721 |
+
) -> List[str]:
|
722 |
+
"""
|
723 |
+
Validate construction inputs.
|
724 |
+
|
725 |
+
Args:
|
726 |
+
name: Construction name
|
727 |
+
construction_type: Construction type
|
728 |
+
layers: List of layer dictionaries
|
729 |
+
edit_mode: Whether in edit mode
|
730 |
+
original_name: Original name if in edit mode
|
731 |
+
|
732 |
+
Returns:
|
733 |
+
List of validation error messages, empty if all inputs are valid
|
734 |
+
"""
|
735 |
+
errors = []
|
736 |
+
|
737 |
+
# Validate name
|
738 |
+
if not name or name.strip() == "":
|
739 |
+
errors.append("Construction name is required.")
|
740 |
+
|
741 |
+
# Check for name uniqueness if not in edit mode or if name changed
|
742 |
+
if not edit_mode or (edit_mode and name != original_name):
|
743 |
+
if name in st.session_state.project_data["constructions"]["project"]:
|
744 |
+
errors.append(f"Construction name '{name}' already exists in your project.")
|
745 |
+
|
746 |
+
# Validate construction type
|
747 |
+
if construction_type not in CONSTRUCTION_TYPES:
|
748 |
+
errors.append("Please select a valid construction type.")
|
749 |
+
|
750 |
+
# Validate layers
|
751 |
+
if not layers:
|
752 |
+
errors.append("At least one layer is required.")
|
753 |
+
|
754 |
+
return errors
|
755 |
+
|
756 |
+
def reset_construction_editor():
|
757 |
+
"""Reset the construction editor to default values."""
|
758 |
+
st.session_state.construction_editor = {
|
759 |
+
"name": "",
|
760 |
+
"type": CONSTRUCTION_TYPES[0],
|
761 |
+
"layers": [],
|
762 |
+
"edit_mode": False,
|
763 |
+
"original_name": ""
|
764 |
+
}
|
765 |
+
|
766 |
+
def check_construction_in_use(construction_name: str) -> bool:
|
767 |
+
"""
|
768 |
+
Check if a construction is in use in any building components.
|
769 |
+
|
770 |
+
Args:
|
771 |
+
construction_name: Name of the construction to check
|
772 |
+
|
773 |
+
Returns:
|
774 |
+
True if the construction is in use, False otherwise
|
775 |
+
"""
|
776 |
+
# This is a placeholder function that will be implemented when components are added
|
777 |
+
# For now, we'll assume constructions are not in use
|
778 |
+
return False
|
779 |
+
|
780 |
+
def display_construction_help():
|
781 |
+
"""Display help information for the construction page."""
|
782 |
+
st.markdown("""
|
783 |
+
### Construction Library Help
|
784 |
+
|
785 |
+
This section allows you to create and manage multi-layer constructions for your building envelope.
|
786 |
+
|
787 |
+
**Key Concepts:**
|
788 |
+
|
789 |
+
* **Construction**: A multi-layer assembly of materials used for walls, roofs, or floors.
|
790 |
+
* **Layers**: Individual material layers that make up a construction, defined from outside to inside.
|
791 |
+
* **U-Value**: Overall heat transfer coefficient (W/m²·K). Lower values indicate better insulation.
|
792 |
+
* **R-Value**: Thermal resistance (m²·K/W). Higher values indicate better insulation.
|
793 |
+
* **Thermal Mass**: Ability to store heat (kJ/m²·K). Higher values indicate better heat storage capacity.
|
794 |
+
|
795 |
+
**Library Constructions:**
|
796 |
+
|
797 |
+
The library contains pre-defined constructions with standard thermal properties. You can:
|
798 |
+
* Browse constructions by type (Wall, Roof, Floor)
|
799 |
+
* View detailed information about each construction
|
800 |
+
* Add library constructions to your project
|
801 |
+
|
802 |
+
**Project Constructions:**
|
803 |
+
|
804 |
+
These are constructions you've added to your project from the library or created custom. You can:
|
805 |
+
* View detailed information about each construction
|
806 |
+
* Edit existing constructions
|
807 |
+
* Delete constructions (if not in use)
|
808 |
+
|
809 |
+
**Construction Editor:**
|
810 |
+
|
811 |
+
The editor allows you to create new constructions or modify existing ones:
|
812 |
+
* Add layers from outside to inside
|
813 |
+
* Select materials from your material library
|
814 |
+
* Specify thickness for each layer
|
815 |
+
* Calculate overall thermal properties
|
816 |
+
|
817 |
+
**Workflow:**
|
818 |
+
|
819 |
+
1. Browse the library constructions
|
820 |
+
2. Add constructions to your project or create custom ones
|
821 |
+
3. Edit properties as needed for your specific project
|
822 |
+
4. Continue to the Building Components page to use these constructions
|
823 |
+
""")
|
app/embodied_energy.py
ADDED
@@ -0,0 +1,997 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
HVAC Load Calculator - Embodied Energy Module
|
3 |
+
|
4 |
+
This module handles the embodied carbon calculations for building materials,
|
5 |
+
life cycle assessment integration, carbon payback period analysis,
|
6 |
+
and embodied vs. operational carbon visualization.
|
7 |
+
|
8 |
+
Developed by: Dr Majed Abuseif, Deakin University
|
9 |
+
© 2025
|
10 |
+
"""
|
11 |
+
|
12 |
+
import streamlit as st
|
13 |
+
import pandas as pd
|
14 |
+
import numpy as np
|
15 |
+
import json
|
16 |
+
import logging
|
17 |
+
import plotly.graph_objects as go
|
18 |
+
import plotly.express as px
|
19 |
+
from typing import Dict, List, Any, Optional, Tuple, Union
|
20 |
+
from datetime import datetime
|
21 |
+
|
22 |
+
# Configure logging
|
23 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
24 |
+
logger = logging.getLogger(__name__)
|
25 |
+
|
26 |
+
# Constants
|
27 |
+
YEARS_FOR_ANALYSIS = 60 # Standard building lifecycle for embodied carbon analysis
|
28 |
+
REPLACEMENT_CYCLES = {
|
29 |
+
"Structure": 60, # Years before replacement (typically building lifetime)
|
30 |
+
"Envelope": 30,
|
31 |
+
"Finishes": 15,
|
32 |
+
"MEP Systems": 20,
|
33 |
+
"Furniture": 10
|
34 |
+
}
|
35 |
+
|
36 |
+
# Default embodied carbon factors (kg CO2e/kg)
|
37 |
+
DEFAULT_EMBODIED_CARBON = {
|
38 |
+
"Concrete": 0.12,
|
39 |
+
"Steel": 1.46,
|
40 |
+
"Timber": 0.42,
|
41 |
+
"Brick": 0.24,
|
42 |
+
"Glass": 0.86,
|
43 |
+
"Aluminum": 8.24,
|
44 |
+
"Insulation (mineral wool)": 1.28,
|
45 |
+
"Insulation (EPS)": 3.29,
|
46 |
+
"Gypsum board": 0.39,
|
47 |
+
"Carpet": 3.89,
|
48 |
+
"Ceramic tile": 0.78,
|
49 |
+
"PVC": 3.10,
|
50 |
+
"Paint": 2.54,
|
51 |
+
"HVAC equipment": 3.62,
|
52 |
+
"Electrical equipment": 4.26,
|
53 |
+
"Plumbing fixtures": 2.11
|
54 |
+
}
|
55 |
+
|
56 |
+
def display_embodied_energy_page():
|
57 |
+
"""
|
58 |
+
Display the embodied energy page.
|
59 |
+
This is the main function called by main.py when the Embodied Energy page is selected.
|
60 |
+
"""
|
61 |
+
st.title("Embodied Energy Analysis")
|
62 |
+
|
63 |
+
# Display help information in an expandable section
|
64 |
+
with st.expander("Help & Information"):
|
65 |
+
display_embodied_energy_help()
|
66 |
+
|
67 |
+
# Check if building components have been defined
|
68 |
+
if "components" not in st.session_state.project_data or not st.session_state.project_data["components"]:
|
69 |
+
st.warning("Please define building components before proceeding to Embodied Energy analysis.")
|
70 |
+
|
71 |
+
# Navigation buttons
|
72 |
+
col1, col2 = st.columns(2)
|
73 |
+
with col1:
|
74 |
+
if st.button("Back to Building Components", key="back_to_components_ee"):
|
75 |
+
st.session_state.current_page = "Building Components"
|
76 |
+
st.rerun()
|
77 |
+
return
|
78 |
+
|
79 |
+
# Initialize embodied energy data if not present
|
80 |
+
initialize_embodied_energy_data()
|
81 |
+
|
82 |
+
# Create tabs for different aspects of embodied energy analysis
|
83 |
+
tabs = st.tabs(["Material Inventory", "Embodied Carbon", "Carbon Payback"])
|
84 |
+
|
85 |
+
with tabs[0]:
|
86 |
+
display_material_inventory_tab()
|
87 |
+
|
88 |
+
with tabs[1]:
|
89 |
+
display_embodied_carbon_tab()
|
90 |
+
|
91 |
+
with tabs[2]:
|
92 |
+
display_carbon_payback_tab()
|
93 |
+
|
94 |
+
# Navigation buttons
|
95 |
+
col1, col2 = st.columns(2)
|
96 |
+
|
97 |
+
with col1:
|
98 |
+
if st.button("Back to Renewable Energy", key="back_to_renewable_energy"):
|
99 |
+
st.session_state.current_page = "Renewable Energy"
|
100 |
+
st.rerun()
|
101 |
+
|
102 |
+
with col2:
|
103 |
+
if st.button("Continue to Materials Cost", key="continue_to_materials_cost"):
|
104 |
+
st.session_state.current_page = "Materials Cost"
|
105 |
+
st.rerun()
|
106 |
+
|
107 |
+
def initialize_embodied_energy_data():
|
108 |
+
"""Initialize embodied energy data in session state if not present."""
|
109 |
+
if "embodied_energy" not in st.session_state.project_data:
|
110 |
+
st.session_state.project_data["embodied_energy"] = {
|
111 |
+
"material_inventory": [],
|
112 |
+
"embodied_carbon_factors": DEFAULT_EMBODIED_CARBON.copy(),
|
113 |
+
"results": None
|
114 |
+
}
|
115 |
+
|
116 |
+
# Initialize material inventory if empty
|
117 |
+
if not st.session_state.project_data["embodied_energy"]["material_inventory"]:
|
118 |
+
# Generate initial material inventory from building components
|
119 |
+
material_inventory = generate_material_inventory_from_components()
|
120 |
+
st.session_state.project_data["embodied_energy"]["material_inventory"] = material_inventory
|
121 |
+
|
122 |
+
def generate_material_inventory_from_components() -> List[Dict[str, Any]]:
|
123 |
+
"""
|
124 |
+
Generate material inventory from building components.
|
125 |
+
|
126 |
+
Returns:
|
127 |
+
List of material items with quantities and properties.
|
128 |
+
"""
|
129 |
+
logger.info("Generating material inventory from building components...")
|
130 |
+
|
131 |
+
material_inventory = []
|
132 |
+
|
133 |
+
# Get building components
|
134 |
+
components = st.session_state.project_data["components"]
|
135 |
+
|
136 |
+
# Get constructions
|
137 |
+
constructions = st.session_state.project_data["constructions"]
|
138 |
+
|
139 |
+
# Get materials
|
140 |
+
materials = st.session_state.project_data["materials"]
|
141 |
+
|
142 |
+
# Process opaque components (walls, roofs, floors)
|
143 |
+
for comp_type in ["walls", "roofs", "floors"]:
|
144 |
+
for comp in components.get(comp_type, []):
|
145 |
+
# Get construction details
|
146 |
+
construction_id = comp.get("construction_id")
|
147 |
+
if not construction_id:
|
148 |
+
continue
|
149 |
+
|
150 |
+
construction = next((c for c in constructions if c["id"] == construction_id), None)
|
151 |
+
if not construction:
|
152 |
+
continue
|
153 |
+
|
154 |
+
# Calculate component area
|
155 |
+
area = comp.get("area", 0)
|
156 |
+
|
157 |
+
# Process each layer in the construction
|
158 |
+
for layer in construction.get("layers", []):
|
159 |
+
material_id = layer.get("material_id")
|
160 |
+
if not material_id:
|
161 |
+
continue
|
162 |
+
|
163 |
+
material = next((m for m in materials if m["id"] == material_id), None)
|
164 |
+
if not material:
|
165 |
+
continue
|
166 |
+
|
167 |
+
# Calculate material quantity
|
168 |
+
thickness = layer.get("thickness", 0) # m
|
169 |
+
density = material.get("density", 0) # kg/m³
|
170 |
+
volume = area * thickness # m³
|
171 |
+
mass = volume * density # kg
|
172 |
+
|
173 |
+
# Get material category
|
174 |
+
category = get_material_category(material.get("name", ""))
|
175 |
+
|
176 |
+
# Create material inventory item
|
177 |
+
item = {
|
178 |
+
"component_type": comp_type,
|
179 |
+
"component_name": comp.get("name", ""),
|
180 |
+
"material_name": material.get("name", ""),
|
181 |
+
"material_category": category,
|
182 |
+
"quantity": mass, # kg
|
183 |
+
"unit": "kg",
|
184 |
+
"volume": volume, # m³
|
185 |
+
"area": area, # m²
|
186 |
+
"thickness": thickness, # m
|
187 |
+
"density": density, # kg/m³
|
188 |
+
"replacement_cycle": REPLACEMENT_CYCLES.get(category, YEARS_FOR_ANALYSIS)
|
189 |
+
}
|
190 |
+
|
191 |
+
material_inventory.append(item)
|
192 |
+
|
193 |
+
# Process fenestration components (windows, doors, skylights)
|
194 |
+
for comp_type in ["windows", "doors", "skylights"]:
|
195 |
+
for comp in components.get(comp_type, []):
|
196 |
+
# Get fenestration details
|
197 |
+
fenestration_id = comp.get("fenestration_id")
|
198 |
+
if not fenestration_id:
|
199 |
+
continue
|
200 |
+
|
201 |
+
fenestration = next((f for f in materials if f["id"] == fenestration_id), None)
|
202 |
+
if not fenestration:
|
203 |
+
continue
|
204 |
+
|
205 |
+
# Calculate component area
|
206 |
+
area = comp.get("area", 0)
|
207 |
+
|
208 |
+
# Estimate material quantities based on fenestration type
|
209 |
+
if comp_type == "windows":
|
210 |
+
# Typical window composition
|
211 |
+
glass_mass = area * 2.5 * 2500 # 2.5 cm thick glass at 2500 kg/m³
|
212 |
+
frame_mass = area * 0.1 * 2700 # 10% frame area, aluminum at 2700 kg/m³
|
213 |
+
|
214 |
+
# Add glass
|
215 |
+
material_inventory.append({
|
216 |
+
"component_type": comp_type,
|
217 |
+
"component_name": comp.get("name", ""),
|
218 |
+
"material_name": "Glass",
|
219 |
+
"material_category": "Envelope",
|
220 |
+
"quantity": glass_mass,
|
221 |
+
"unit": "kg",
|
222 |
+
"volume": area * 0.025,
|
223 |
+
"area": area,
|
224 |
+
"thickness": 0.025,
|
225 |
+
"density": 2500,
|
226 |
+
"replacement_cycle": REPLACEMENT_CYCLES.get("Envelope", YEARS_FOR_ANALYSIS)
|
227 |
+
})
|
228 |
+
|
229 |
+
# Add frame
|
230 |
+
material_inventory.append({
|
231 |
+
"component_type": comp_type,
|
232 |
+
"component_name": comp.get("name", ""),
|
233 |
+
"material_name": "Aluminum",
|
234 |
+
"material_category": "Envelope",
|
235 |
+
"quantity": frame_mass,
|
236 |
+
"unit": "kg",
|
237 |
+
"volume": area * 0.1 * 0.05,
|
238 |
+
"area": area * 0.1,
|
239 |
+
"thickness": 0.05,
|
240 |
+
"density": 2700,
|
241 |
+
"replacement_cycle": REPLACEMENT_CYCLES.get("Envelope", YEARS_FOR_ANALYSIS)
|
242 |
+
})
|
243 |
+
|
244 |
+
elif comp_type == "doors":
|
245 |
+
# Typical door composition
|
246 |
+
if "glass" in fenestration.get("name", "").lower():
|
247 |
+
# Glass door
|
248 |
+
glass_mass = area * 1.2 * 2500 # 1.2 cm thick glass at 2500 kg/m³
|
249 |
+
frame_mass = area * 0.15 * 2700 # 15% frame area, aluminum at 2700 kg/m³
|
250 |
+
|
251 |
+
material_inventory.append({
|
252 |
+
"component_type": comp_type,
|
253 |
+
"component_name": comp.get("name", ""),
|
254 |
+
"material_name": "Glass",
|
255 |
+
"material_category": "Envelope",
|
256 |
+
"quantity": glass_mass,
|
257 |
+
"unit": "kg",
|
258 |
+
"volume": area * 0.012,
|
259 |
+
"area": area,
|
260 |
+
"thickness": 0.012,
|
261 |
+
"density": 2500,
|
262 |
+
"replacement_cycle": REPLACEMENT_CYCLES.get("Envelope", YEARS_FOR_ANALYSIS)
|
263 |
+
})
|
264 |
+
|
265 |
+
material_inventory.append({
|
266 |
+
"component_type": comp_type,
|
267 |
+
"component_name": comp.get("name", ""),
|
268 |
+
"material_name": "Aluminum",
|
269 |
+
"material_category": "Envelope",
|
270 |
+
"quantity": frame_mass,
|
271 |
+
"unit": "kg",
|
272 |
+
"volume": area * 0.15 * 0.05,
|
273 |
+
"area": area * 0.15,
|
274 |
+
"thickness": 0.05,
|
275 |
+
"density": 2700,
|
276 |
+
"replacement_cycle": REPLACEMENT_CYCLES.get("Envelope", YEARS_FOR_ANALYSIS)
|
277 |
+
})
|
278 |
+
else:
|
279 |
+
# Solid door
|
280 |
+
door_mass = area * 0.04 * 700 # 4 cm thick wood at 700 kg/m³
|
281 |
+
|
282 |
+
material_inventory.append({
|
283 |
+
"component_type": comp_type,
|
284 |
+
"component_name": comp.get("name", ""),
|
285 |
+
"material_name": "Timber",
|
286 |
+
"material_category": "Envelope",
|
287 |
+
"quantity": door_mass,
|
288 |
+
"unit": "kg",
|
289 |
+
"volume": area * 0.04,
|
290 |
+
"area": area,
|
291 |
+
"thickness": 0.04,
|
292 |
+
"density": 700,
|
293 |
+
"replacement_cycle": REPLACEMENT_CYCLES.get("Envelope", YEARS_FOR_ANALYSIS)
|
294 |
+
})
|
295 |
+
|
296 |
+
elif comp_type == "skylights":
|
297 |
+
# Typical skylight composition
|
298 |
+
glass_mass = area * 2.0 * 2500 # 2.0 cm thick glass at 2500 kg/m³
|
299 |
+
frame_mass = area * 0.12 * 2700 # 12% frame area, aluminum at 2700 kg/m³
|
300 |
+
|
301 |
+
material_inventory.append({
|
302 |
+
"component_type": comp_type,
|
303 |
+
"component_name": comp.get("name", ""),
|
304 |
+
"material_name": "Glass",
|
305 |
+
"material_category": "Envelope",
|
306 |
+
"quantity": glass_mass,
|
307 |
+
"unit": "kg",
|
308 |
+
"volume": area * 0.02,
|
309 |
+
"area": area,
|
310 |
+
"thickness": 0.02,
|
311 |
+
"density": 2500,
|
312 |
+
"replacement_cycle": REPLACEMENT_CYCLES.get("Envelope", YEARS_FOR_ANALYSIS)
|
313 |
+
})
|
314 |
+
|
315 |
+
material_inventory.append({
|
316 |
+
"component_type": comp_type,
|
317 |
+
"component_name": comp.get("name", ""),
|
318 |
+
"material_name": "Aluminum",
|
319 |
+
"material_category": "Envelope",
|
320 |
+
"quantity": frame_mass,
|
321 |
+
"unit": "kg",
|
322 |
+
"volume": area * 0.12 * 0.05,
|
323 |
+
"area": area * 0.12,
|
324 |
+
"thickness": 0.05,
|
325 |
+
"density": 2700,
|
326 |
+
"replacement_cycle": REPLACEMENT_CYCLES.get("Envelope", YEARS_FOR_ANALYSIS)
|
327 |
+
})
|
328 |
+
|
329 |
+
# Add HVAC equipment (estimated based on building size)
|
330 |
+
building_info = st.session_state.project_data["building_info"]
|
331 |
+
floor_area = building_info.get("floor_area", 0)
|
332 |
+
|
333 |
+
# HVAC equipment (estimated at 15 kg/m²)
|
334 |
+
hvac_mass = floor_area * 15
|
335 |
+
|
336 |
+
material_inventory.append({
|
337 |
+
"component_type": "systems",
|
338 |
+
"component_name": "HVAC System",
|
339 |
+
"material_name": "HVAC equipment",
|
340 |
+
"material_category": "MEP Systems",
|
341 |
+
"quantity": hvac_mass,
|
342 |
+
"unit": "kg",
|
343 |
+
"volume": 0,
|
344 |
+
"area": 0,
|
345 |
+
"thickness": 0,
|
346 |
+
"density": 0,
|
347 |
+
"replacement_cycle": REPLACEMENT_CYCLES.get("MEP Systems", YEARS_FOR_ANALYSIS)
|
348 |
+
})
|
349 |
+
|
350 |
+
# Electrical equipment (estimated at 5 kg/m²)
|
351 |
+
electrical_mass = floor_area * 5
|
352 |
+
|
353 |
+
material_inventory.append({
|
354 |
+
"component_type": "systems",
|
355 |
+
"component_name": "Electrical System",
|
356 |
+
"material_name": "Electrical equipment",
|
357 |
+
"material_category": "MEP Systems",
|
358 |
+
"quantity": electrical_mass,
|
359 |
+
"unit": "kg",
|
360 |
+
"volume": 0,
|
361 |
+
"area": 0,
|
362 |
+
"thickness": 0,
|
363 |
+
"density": 0,
|
364 |
+
"replacement_cycle": REPLACEMENT_CYCLES.get("MEP Systems", YEARS_FOR_ANALYSIS)
|
365 |
+
})
|
366 |
+
|
367 |
+
# Plumbing fixtures (estimated at 3 kg/m²)
|
368 |
+
plumbing_mass = floor_area * 3
|
369 |
+
|
370 |
+
material_inventory.append({
|
371 |
+
"component_type": "systems",
|
372 |
+
"component_name": "Plumbing System",
|
373 |
+
"material_name": "Plumbing fixtures",
|
374 |
+
"material_category": "MEP Systems",
|
375 |
+
"quantity": plumbing_mass,
|
376 |
+
"unit": "kg",
|
377 |
+
"volume": 0,
|
378 |
+
"area": 0,
|
379 |
+
"thickness": 0,
|
380 |
+
"density": 0,
|
381 |
+
"replacement_cycle": REPLACEMENT_CYCLES.get("MEP Systems", YEARS_FOR_ANALYSIS)
|
382 |
+
})
|
383 |
+
|
384 |
+
logger.info(f"Generated material inventory with {len(material_inventory)} items.")
|
385 |
+
return material_inventory
|
386 |
+
|
387 |
+
def get_material_category(material_name: str) -> str:
|
388 |
+
"""
|
389 |
+
Determine the material category based on the material name.
|
390 |
+
|
391 |
+
Args:
|
392 |
+
material_name: Name of the material
|
393 |
+
|
394 |
+
Returns:
|
395 |
+
Category of the material
|
396 |
+
"""
|
397 |
+
material_name_lower = material_name.lower()
|
398 |
+
|
399 |
+
if any(term in material_name_lower for term in ["concrete", "steel", "timber", "wood", "brick", "stone", "structural"]):
|
400 |
+
return "Structure"
|
401 |
+
|
402 |
+
elif any(term in material_name_lower for term in ["insulation", "cladding", "siding", "roofing", "glass", "window", "door"]):
|
403 |
+
return "Envelope"
|
404 |
+
|
405 |
+
elif any(term in material_name_lower for term in ["paint", "carpet", "tile", "flooring", "ceiling", "gypsum", "drywall"]):
|
406 |
+
return "Finishes"
|
407 |
+
|
408 |
+
elif any(term in material_name_lower for term in ["hvac", "electrical", "plumbing", "mechanical", "pipe", "duct", "wire"]):
|
409 |
+
return "MEP Systems"
|
410 |
+
|
411 |
+
elif any(term in material_name_lower for term in ["furniture", "fixture", "equipment", "appliance"]):
|
412 |
+
return "Furniture"
|
413 |
+
|
414 |
+
else:
|
415 |
+
return "Other"
|
416 |
+
|
417 |
+
def display_material_inventory_tab():
|
418 |
+
"""Display the material inventory tab."""
|
419 |
+
st.header("Material Inventory")
|
420 |
+
|
421 |
+
# Get material inventory
|
422 |
+
material_inventory = st.session_state.project_data["embodied_energy"]["material_inventory"]
|
423 |
+
|
424 |
+
# Allow user to edit material inventory
|
425 |
+
st.subheader("Edit Material Inventory")
|
426 |
+
|
427 |
+
# Create a DataFrame for display and editing
|
428 |
+
if material_inventory:
|
429 |
+
df = pd.DataFrame(material_inventory)
|
430 |
+
|
431 |
+
# Select columns for display
|
432 |
+
display_columns = ["component_type", "component_name", "material_name", "material_category", "quantity", "unit", "replacement_cycle"]
|
433 |
+
display_df = df[display_columns].copy()
|
434 |
+
|
435 |
+
# Format column names for display
|
436 |
+
display_df.columns = [col.replace("_", " ").title() for col in display_columns]
|
437 |
+
|
438 |
+
# Display the inventory table
|
439 |
+
st.dataframe(display_df)
|
440 |
+
|
441 |
+
# Allow adding new materials
|
442 |
+
st.subheader("Add New Material")
|
443 |
+
|
444 |
+
col1, col2 = st.columns(2)
|
445 |
+
|
446 |
+
with col1:
|
447 |
+
new_component_type = st.selectbox(
|
448 |
+
"Component Type",
|
449 |
+
["walls", "roofs", "floors", "windows", "doors", "skylights", "systems", "other"],
|
450 |
+
key="new_material_component_type"
|
451 |
+
)
|
452 |
+
|
453 |
+
new_component_name = st.text_input(
|
454 |
+
"Component Name",
|
455 |
+
key="new_material_component_name"
|
456 |
+
)
|
457 |
+
|
458 |
+
new_material_name = st.selectbox(
|
459 |
+
"Material Name",
|
460 |
+
list(DEFAULT_EMBODIED_CARBON.keys()),
|
461 |
+
key="new_material_name"
|
462 |
+
)
|
463 |
+
|
464 |
+
with col2:
|
465 |
+
new_material_category = st.selectbox(
|
466 |
+
"Material Category",
|
467 |
+
list(REPLACEMENT_CYCLES.keys()),
|
468 |
+
key="new_material_category"
|
469 |
+
)
|
470 |
+
|
471 |
+
new_quantity = st.number_input(
|
472 |
+
"Quantity (kg)",
|
473 |
+
min_value=0.0,
|
474 |
+
value=100.0,
|
475 |
+
step=10.0,
|
476 |
+
key="new_material_quantity"
|
477 |
+
)
|
478 |
+
|
479 |
+
new_replacement_cycle = st.number_input(
|
480 |
+
"Replacement Cycle (years)",
|
481 |
+
min_value=1,
|
482 |
+
max_value=100,
|
483 |
+
value=REPLACEMENT_CYCLES.get(new_material_category, YEARS_FOR_ANALYSIS),
|
484 |
+
step=1,
|
485 |
+
key="new_material_replacement_cycle"
|
486 |
+
)
|
487 |
+
|
488 |
+
if st.button("Add Material", key="add_material_button"):
|
489 |
+
# Create new material inventory item
|
490 |
+
new_item = {
|
491 |
+
"component_type": new_component_type,
|
492 |
+
"component_name": new_component_name,
|
493 |
+
"material_name": new_material_name,
|
494 |
+
"material_category": new_material_category,
|
495 |
+
"quantity": new_quantity,
|
496 |
+
"unit": "kg",
|
497 |
+
"volume": 0,
|
498 |
+
"area": 0,
|
499 |
+
"thickness": 0,
|
500 |
+
"density": 0,
|
501 |
+
"replacement_cycle": new_replacement_cycle
|
502 |
+
}
|
503 |
+
|
504 |
+
# Add to inventory
|
505 |
+
material_inventory.append(new_item)
|
506 |
+
st.success(f"Added {new_material_name} to inventory.")
|
507 |
+
st.rerun()
|
508 |
+
else:
|
509 |
+
st.info("No materials in inventory. Generate inventory from building components.")
|
510 |
+
|
511 |
+
# Button to regenerate inventory
|
512 |
+
if st.button("Regenerate Inventory from Components", key="regenerate_inventory"):
|
513 |
+
material_inventory = generate_material_inventory_from_components()
|
514 |
+
st.session_state.project_data["embodied_energy"]["material_inventory"] = material_inventory
|
515 |
+
st.success("Material inventory regenerated from building components.")
|
516 |
+
st.rerun()
|
517 |
+
|
518 |
+
# Display material inventory summary
|
519 |
+
if material_inventory:
|
520 |
+
st.subheader("Material Inventory Summary")
|
521 |
+
|
522 |
+
# Create summary by material category
|
523 |
+
category_summary = {}
|
524 |
+
for item in material_inventory:
|
525 |
+
category = item["material_category"]
|
526 |
+
quantity = item["quantity"]
|
527 |
+
|
528 |
+
if category in category_summary:
|
529 |
+
category_summary[category] += quantity
|
530 |
+
else:
|
531 |
+
category_summary[category] = quantity
|
532 |
+
|
533 |
+
# Create summary chart
|
534 |
+
fig = px.pie(
|
535 |
+
values=list(category_summary.values()),
|
536 |
+
names=list(category_summary.keys()),
|
537 |
+
title="Material Quantity by Category (kg)"
|
538 |
+
)
|
539 |
+
st.plotly_chart(fig, use_container_width=True)
|
540 |
+
|
541 |
+
# Display total material quantity
|
542 |
+
total_quantity = sum(category_summary.values())
|
543 |
+
st.metric("Total Material Quantity", f"{total_quantity:.0f} kg")
|
544 |
+
|
545 |
+
def display_embodied_carbon_tab():
|
546 |
+
"""Display the embodied carbon analysis tab."""
|
547 |
+
st.header("Embodied Carbon Analysis")
|
548 |
+
|
549 |
+
# Get material inventory and embodied carbon factors
|
550 |
+
material_inventory = st.session_state.project_data["embodied_energy"]["material_inventory"]
|
551 |
+
embodied_carbon_factors = st.session_state.project_data["embodied_energy"]["embodied_carbon_factors"]
|
552 |
+
|
553 |
+
if not material_inventory:
|
554 |
+
st.info("Please define material inventory in the Material Inventory tab.")
|
555 |
+
return
|
556 |
+
|
557 |
+
# Allow user to edit embodied carbon factors
|
558 |
+
st.subheader("Embodied Carbon Factors")
|
559 |
+
|
560 |
+
# Create a DataFrame for display and editing
|
561 |
+
factor_df = pd.DataFrame({
|
562 |
+
"Material": list(embodied_carbon_factors.keys()),
|
563 |
+
"Embodied Carbon (kg CO2e/kg)": list(embodied_carbon_factors.values())
|
564 |
+
})
|
565 |
+
|
566 |
+
# Display the factors table
|
567 |
+
edited_factor_df = st.data_editor(
|
568 |
+
factor_df,
|
569 |
+
column_config={
|
570 |
+
"Material": st.column_config.TextColumn("Material"),
|
571 |
+
"Embodied Carbon (kg CO2e/kg)": st.column_config.NumberColumn(
|
572 |
+
"Embodied Carbon (kg CO2e/kg)",
|
573 |
+
min_value=0.0,
|
574 |
+
max_value=20.0,
|
575 |
+
step=0.01,
|
576 |
+
format="%.2f"
|
577 |
+
)
|
578 |
+
},
|
579 |
+
use_container_width=True,
|
580 |
+
key="embodied_carbon_factors_editor"
|
581 |
+
)
|
582 |
+
|
583 |
+
# Update embodied carbon factors if edited
|
584 |
+
if not factor_df.equals(edited_factor_df):
|
585 |
+
updated_factors = dict(zip(edited_factor_df["Material"], edited_factor_df["Embodied Carbon (kg CO2e/kg)"]))
|
586 |
+
st.session_state.project_data["embodied_energy"]["embodied_carbon_factors"] = updated_factors
|
587 |
+
embodied_carbon_factors = updated_factors
|
588 |
+
|
589 |
+
# Calculate embodied carbon button
|
590 |
+
if st.button("Calculate Embodied Carbon", key="calculate_embodied_carbon"):
|
591 |
+
try:
|
592 |
+
results = calculate_embodied_carbon(material_inventory, embodied_carbon_factors)
|
593 |
+
st.session_state.project_data["embodied_energy"]["results"] = results
|
594 |
+
st.success("Embodied carbon calculated successfully.")
|
595 |
+
logger.info("Embodied carbon calculated.")
|
596 |
+
st.rerun() # Refresh to show results
|
597 |
+
except Exception as e:
|
598 |
+
st.error(f"Error calculating embodied carbon: {e}")
|
599 |
+
logger.error(f"Error calculating embodied carbon: {e}", exc_info=True)
|
600 |
+
st.session_state.project_data["embodied_energy"]["results"] = None
|
601 |
+
|
602 |
+
# Display embodied carbon results if available
|
603 |
+
results = st.session_state.project_data["embodied_energy"].get("results")
|
604 |
+
if results:
|
605 |
+
display_embodied_carbon_results(results)
|
606 |
+
|
607 |
+
def display_embodied_carbon_results(results: Dict[str, Any]):
|
608 |
+
"""Display the embodied carbon calculation results."""
|
609 |
+
st.subheader("Embodied Carbon Summary")
|
610 |
+
|
611 |
+
# Get building info for normalization
|
612 |
+
building_info = st.session_state.project_data["building_info"]
|
613 |
+
floor_area = building_info.get("floor_area", 0)
|
614 |
+
|
615 |
+
col1, col2 = st.columns(2)
|
616 |
+
|
617 |
+
with col1:
|
618 |
+
st.metric(
|
619 |
+
"Total Initial Embodied Carbon",
|
620 |
+
f"{results['total_initial_embodied_carbon']:.1f} tonnes CO2e",
|
621 |
+
help="Total embodied carbon from initial construction."
|
622 |
+
)
|
623 |
+
|
624 |
+
with col2:
|
625 |
+
st.metric(
|
626 |
+
"Total Lifecycle Embodied Carbon",
|
627 |
+
f"{results['total_lifecycle_embodied_carbon']:.1f} tonnes CO2e",
|
628 |
+
help=f"Total embodied carbon over {YEARS_FOR_ANALYSIS} years including replacements."
|
629 |
+
)
|
630 |
+
|
631 |
+
# Display normalized metrics
|
632 |
+
col1, col2 = st.columns(2)
|
633 |
+
|
634 |
+
with col1:
|
635 |
+
st.metric(
|
636 |
+
"Initial Embodied Carbon Intensity",
|
637 |
+
f"{results['initial_embodied_carbon_intensity']:.1f} kg CO2e/m²",
|
638 |
+
help="Initial embodied carbon per square meter of floor area."
|
639 |
+
)
|
640 |
+
|
641 |
+
with col2:
|
642 |
+
st.metric(
|
643 |
+
"Lifecycle Embodied Carbon Intensity",
|
644 |
+
f"{results['lifecycle_embodied_carbon_intensity']:.1f} kg CO2e/m²",
|
645 |
+
help=f"Lifecycle embodied carbon per square meter of floor area over {YEARS_FOR_ANALYSIS} years."
|
646 |
+
)
|
647 |
+
|
648 |
+
# Display embodied carbon breakdown by category
|
649 |
+
st.subheader("Embodied Carbon by Category")
|
650 |
+
|
651 |
+
# Create pie chart of initial embodied carbon by category
|
652 |
+
fig_initial = px.pie(
|
653 |
+
values=list(results["initial_embodied_carbon_by_category"].values()),
|
654 |
+
names=list(results["initial_embodied_carbon_by_category"].keys()),
|
655 |
+
title="Initial Embodied Carbon by Category"
|
656 |
+
)
|
657 |
+
st.plotly_chart(fig_initial, use_container_width=True)
|
658 |
+
|
659 |
+
# Create pie chart of lifecycle embodied carbon by category
|
660 |
+
fig_lifecycle = px.pie(
|
661 |
+
values=list(results["lifecycle_embodied_carbon_by_category"].values()),
|
662 |
+
names=list(results["lifecycle_embodied_carbon_by_category"].keys()),
|
663 |
+
title=f"Lifecycle Embodied Carbon by Category ({YEARS_FOR_ANALYSIS} years)"
|
664 |
+
)
|
665 |
+
st.plotly_chart(fig_lifecycle, use_container_width=True)
|
666 |
+
|
667 |
+
# Display embodied carbon breakdown by material
|
668 |
+
st.subheader("Top 10 Materials by Embodied Carbon")
|
669 |
+
|
670 |
+
# Create bar chart of top 10 materials by initial embodied carbon
|
671 |
+
top_materials_df = pd.DataFrame({
|
672 |
+
"Material": list(results["initial_embodied_carbon_by_material"].keys()),
|
673 |
+
"Initial Embodied Carbon (tonnes CO2e)": list(results["initial_embodied_carbon_by_material"].values())
|
674 |
+
})
|
675 |
+
|
676 |
+
top_materials_df = top_materials_df.sort_values(
|
677 |
+
by="Initial Embodied Carbon (tonnes CO2e)",
|
678 |
+
ascending=False
|
679 |
+
).head(10)
|
680 |
+
|
681 |
+
fig_top_materials = px.bar(
|
682 |
+
top_materials_df,
|
683 |
+
x="Material",
|
684 |
+
y="Initial Embodied Carbon (tonnes CO2e)",
|
685 |
+
title="Top 10 Materials by Initial Embodied Carbon"
|
686 |
+
)
|
687 |
+
st.plotly_chart(fig_top_materials, use_container_width=True)
|
688 |
+
|
689 |
+
# Display embodied carbon over time
|
690 |
+
st.subheader("Embodied Carbon Over Time")
|
691 |
+
|
692 |
+
# Create line chart of cumulative embodied carbon over time
|
693 |
+
years = list(range(0, YEARS_FOR_ANALYSIS + 1, 5))
|
694 |
+
cumulative_carbon = [results["cumulative_embodied_carbon"].get(str(year), 0) for year in years]
|
695 |
+
|
696 |
+
fig_over_time = px.line(
|
697 |
+
x=years,
|
698 |
+
y=cumulative_carbon,
|
699 |
+
title="Cumulative Embodied Carbon Over Time",
|
700 |
+
labels={"x": "Year", "y": "Cumulative Embodied Carbon (tonnes CO2e)"}
|
701 |
+
)
|
702 |
+
st.plotly_chart(fig_over_time, use_container_width=True)
|
703 |
+
|
704 |
+
def display_carbon_payback_tab():
|
705 |
+
"""Display the carbon payback analysis tab."""
|
706 |
+
st.header("Carbon Payback Analysis")
|
707 |
+
|
708 |
+
# Check if embodied carbon results are available
|
709 |
+
embodied_results = st.session_state.project_data["embodied_energy"].get("results")
|
710 |
+
if not embodied_results:
|
711 |
+
st.info("Please calculate embodied carbon in the Embodied Carbon tab.")
|
712 |
+
return
|
713 |
+
|
714 |
+
# Check if operational carbon results are available
|
715 |
+
if "building_energy" not in st.session_state.project_data or not st.session_state.project_data["building_energy"].get("results"):
|
716 |
+
st.info("Please complete the Building Energy analysis to calculate operational carbon.")
|
717 |
+
return
|
718 |
+
|
719 |
+
# Check if renewable energy results are available
|
720 |
+
renewable_results = None
|
721 |
+
if "renewable_energy" in st.session_state.project_data and st.session_state.project_data["renewable_energy"].get("results"):
|
722 |
+
renewable_results = st.session_state.project_data["renewable_energy"]["results"]
|
723 |
+
|
724 |
+
# Get operational carbon from building energy results
|
725 |
+
building_energy_results = st.session_state.project_data["building_energy"]["results"]
|
726 |
+
annual_operational_carbon = building_energy_results["annual_carbon_emissions"] # tonnes CO2e/year
|
727 |
+
|
728 |
+
# Calculate carbon payback
|
729 |
+
lifecycle_embodied_carbon = embodied_results["total_lifecycle_embodied_carbon"] # tonnes CO2e
|
730 |
+
|
731 |
+
# Display carbon comparison
|
732 |
+
st.subheader("Embodied vs. Operational Carbon")
|
733 |
+
|
734 |
+
col1, col2 = st.columns(2)
|
735 |
+
|
736 |
+
with col1:
|
737 |
+
st.metric(
|
738 |
+
"Lifecycle Embodied Carbon",
|
739 |
+
f"{lifecycle_embodied_carbon:.1f} tonnes CO2e",
|
740 |
+
help=f"Total embodied carbon over {YEARS_FOR_ANALYSIS} years including replacements."
|
741 |
+
)
|
742 |
+
|
743 |
+
with col2:
|
744 |
+
st.metric(
|
745 |
+
"Annual Operational Carbon",
|
746 |
+
f"{annual_operational_carbon:.1f} tonnes CO2e/year",
|
747 |
+
help="Annual carbon emissions from building operation."
|
748 |
+
)
|
749 |
+
|
750 |
+
# Calculate total operational carbon over building lifecycle
|
751 |
+
total_operational_carbon = annual_operational_carbon * YEARS_FOR_ANALYSIS
|
752 |
+
|
753 |
+
# Calculate total lifecycle carbon
|
754 |
+
total_lifecycle_carbon = lifecycle_embodied_carbon + total_operational_carbon
|
755 |
+
|
756 |
+
# Display total lifecycle carbon
|
757 |
+
st.metric(
|
758 |
+
f"Total Lifecycle Carbon ({YEARS_FOR_ANALYSIS} years)",
|
759 |
+
f"{total_lifecycle_carbon:.1f} tonnes CO2e",
|
760 |
+
help=f"Total carbon emissions over {YEARS_FOR_ANALYSIS} years (embodied + operational)."
|
761 |
+
)
|
762 |
+
|
763 |
+
# Create pie chart of embodied vs. operational carbon
|
764 |
+
carbon_breakdown = {
|
765 |
+
"Embodied Carbon": lifecycle_embodied_carbon,
|
766 |
+
"Operational Carbon": total_operational_carbon
|
767 |
+
}
|
768 |
+
|
769 |
+
fig_breakdown = px.pie(
|
770 |
+
values=list(carbon_breakdown.values()),
|
771 |
+
names=list(carbon_breakdown.keys()),
|
772 |
+
title=f"Lifecycle Carbon Breakdown ({YEARS_FOR_ANALYSIS} years)"
|
773 |
+
)
|
774 |
+
st.plotly_chart(fig_breakdown, use_container_width=True)
|
775 |
+
|
776 |
+
# Calculate carbon payback period
|
777 |
+
st.subheader("Carbon Payback Analysis")
|
778 |
+
|
779 |
+
# If renewable energy results are available, calculate carbon savings
|
780 |
+
if renewable_results:
|
781 |
+
# Get annual PV generation
|
782 |
+
annual_pv_generation = renewable_results["annual_pv_generation"] # kWh
|
783 |
+
|
784 |
+
# Calculate carbon savings from PV
|
785 |
+
electricity_carbon_intensity = building_energy_results["energy_rates"]["electricity"]["carbon_intensity"] # kg CO2e/kWh
|
786 |
+
annual_carbon_savings = (annual_pv_generation * electricity_carbon_intensity) / 1000 # tonnes CO2e/year
|
787 |
+
|
788 |
+
# Calculate carbon payback period
|
789 |
+
initial_embodied_carbon = embodied_results["total_initial_embodied_carbon"] # tonnes CO2e
|
790 |
+
|
791 |
+
if annual_carbon_savings > 0:
|
792 |
+
carbon_payback_period = initial_embodied_carbon / annual_carbon_savings
|
793 |
+
|
794 |
+
st.metric(
|
795 |
+
"Carbon Payback Period",
|
796 |
+
f"{carbon_payback_period:.1f} years",
|
797 |
+
help="Years required for operational carbon savings to offset initial embodied carbon."
|
798 |
+
)
|
799 |
+
|
800 |
+
# Create carbon payback chart
|
801 |
+
years = list(range(0, min(int(carbon_payback_period * 2), YEARS_FOR_ANALYSIS) + 1))
|
802 |
+
embodied_carbon = [initial_embodied_carbon] * len(years)
|
803 |
+
carbon_savings = [year * annual_carbon_savings for year in years]
|
804 |
+
|
805 |
+
fig_payback = go.Figure()
|
806 |
+
|
807 |
+
fig_payback.add_trace(go.Scatter(
|
808 |
+
x=years,
|
809 |
+
y=embodied_carbon,
|
810 |
+
mode="lines",
|
811 |
+
name="Initial Embodied Carbon"
|
812 |
+
))
|
813 |
+
|
814 |
+
fig_payback.add_trace(go.Scatter(
|
815 |
+
x=years,
|
816 |
+
y=carbon_savings,
|
817 |
+
mode="lines",
|
818 |
+
name="Cumulative Carbon Savings"
|
819 |
+
))
|
820 |
+
|
821 |
+
fig_payback.update_layout(
|
822 |
+
title="Carbon Payback Analysis",
|
823 |
+
xaxis_title="Year",
|
824 |
+
yaxis_title="Carbon (tonnes CO2e)"
|
825 |
+
)
|
826 |
+
|
827 |
+
st.plotly_chart(fig_payback, use_container_width=True)
|
828 |
+
else:
|
829 |
+
st.warning("No carbon savings from renewable energy. Carbon payback period is infinite.")
|
830 |
+
else:
|
831 |
+
st.info("Please calculate PV generation in the Renewable Energy section for carbon payback analysis.")
|
832 |
+
|
833 |
+
# Display carbon reduction strategies
|
834 |
+
st.subheader("Carbon Reduction Strategies")
|
835 |
+
|
836 |
+
# Create a DataFrame of potential strategies
|
837 |
+
strategies_data = {
|
838 |
+
"Strategy": [
|
839 |
+
"Use low-carbon materials",
|
840 |
+
"Optimize structural design",
|
841 |
+
"Increase renewable energy capacity",
|
842 |
+
"Improve building envelope",
|
843 |
+
"Extend material lifespans"
|
844 |
+
],
|
845 |
+
"Potential Reduction": [
|
846 |
+
"20-30% embodied carbon",
|
847 |
+
"15-25% embodied carbon",
|
848 |
+
"50-100% operational carbon",
|
849 |
+
"20-40% operational carbon",
|
850 |
+
"10-20% lifecycle embodied carbon"
|
851 |
+
],
|
852 |
+
"Implementation Difficulty": [
|
853 |
+
"Medium",
|
854 |
+
"High",
|
855 |
+
"Medium",
|
856 |
+
"Medium",
|
857 |
+
"Low"
|
858 |
+
]
|
859 |
+
}
|
860 |
+
|
861 |
+
strategies_df = pd.DataFrame(strategies_data)
|
862 |
+
st.table(strategies_df)
|
863 |
+
|
864 |
+
def calculate_embodied_carbon(material_inventory: List[Dict[str, Any]], embodied_carbon_factors: Dict[str, float]) -> Dict[str, Any]:
|
865 |
+
"""
|
866 |
+
Calculate embodied carbon based on material inventory and carbon factors.
|
867 |
+
|
868 |
+
Args:
|
869 |
+
material_inventory: List of material items with quantities
|
870 |
+
embodied_carbon_factors: Dictionary of embodied carbon factors by material
|
871 |
+
|
872 |
+
Returns:
|
873 |
+
Dictionary containing embodied carbon results
|
874 |
+
"""
|
875 |
+
logger.info("Starting embodied carbon calculations...")
|
876 |
+
|
877 |
+
# Initialize results
|
878 |
+
initial_embodied_carbon_by_material = {}
|
879 |
+
initial_embodied_carbon_by_category = {}
|
880 |
+
lifecycle_embodied_carbon_by_category = {}
|
881 |
+
cumulative_embodied_carbon = {"0": 0}
|
882 |
+
|
883 |
+
# Calculate embodied carbon for each material
|
884 |
+
for item in material_inventory:
|
885 |
+
material_name = item["material_name"]
|
886 |
+
category = item["material_category"]
|
887 |
+
quantity = item["quantity"] # kg
|
888 |
+
replacement_cycle = item["replacement_cycle"] # years
|
889 |
+
|
890 |
+
# Get embodied carbon factor
|
891 |
+
carbon_factor = embodied_carbon_factors.get(material_name, 0) # kg CO2e/kg
|
892 |
+
|
893 |
+
# Calculate initial embodied carbon (tonnes CO2e)
|
894 |
+
initial_carbon = (quantity * carbon_factor) / 1000
|
895 |
+
|
896 |
+
# Add to material totals
|
897 |
+
if material_name in initial_embodied_carbon_by_material:
|
898 |
+
initial_embodied_carbon_by_material[material_name] += initial_carbon
|
899 |
+
else:
|
900 |
+
initial_embodied_carbon_by_material[material_name] = initial_carbon
|
901 |
+
|
902 |
+
# Add to category totals
|
903 |
+
if category in initial_embodied_carbon_by_category:
|
904 |
+
initial_embodied_carbon_by_category[category] += initial_carbon
|
905 |
+
else:
|
906 |
+
initial_embodied_carbon_by_category[category] = initial_carbon
|
907 |
+
|
908 |
+
# Calculate lifecycle embodied carbon with replacements
|
909 |
+
num_replacements = YEARS_FOR_ANALYSIS // replacement_cycle
|
910 |
+
lifecycle_carbon = initial_carbon * (num_replacements + 1) # +1 for initial installation
|
911 |
+
|
912 |
+
# Add to lifecycle category totals
|
913 |
+
if category in lifecycle_embodied_carbon_by_category:
|
914 |
+
lifecycle_embodied_carbon_by_category[category] += lifecycle_carbon
|
915 |
+
else:
|
916 |
+
lifecycle_embodied_carbon_by_category[category] = lifecycle_carbon
|
917 |
+
|
918 |
+
# Add to cumulative carbon over time
|
919 |
+
cumulative_embodied_carbon["0"] += initial_carbon
|
920 |
+
|
921 |
+
for year in range(replacement_cycle, YEARS_FOR_ANALYSIS + 1, replacement_cycle):
|
922 |
+
year_str = str(year)
|
923 |
+
if year_str not in cumulative_embodied_carbon:
|
924 |
+
cumulative_embodied_carbon[year_str] = cumulative_embodied_carbon[str(year - replacement_cycle)]
|
925 |
+
|
926 |
+
cumulative_embodied_carbon[year_str] += initial_carbon
|
927 |
+
|
928 |
+
# Calculate totals
|
929 |
+
total_initial_embodied_carbon = sum(initial_embodied_carbon_by_material.values())
|
930 |
+
total_lifecycle_embodied_carbon = sum(lifecycle_embodied_carbon_by_category.values())
|
931 |
+
|
932 |
+
# Get building info for normalization
|
933 |
+
building_info = st.session_state.project_data["building_info"]
|
934 |
+
floor_area = building_info.get("floor_area", 0)
|
935 |
+
|
936 |
+
# Calculate normalized metrics
|
937 |
+
initial_embodied_carbon_intensity = (total_initial_embodied_carbon * 1000) / floor_area if floor_area > 0 else 0
|
938 |
+
lifecycle_embodied_carbon_intensity = (total_lifecycle_embodied_carbon * 1000) / floor_area if floor_area > 0 else 0
|
939 |
+
|
940 |
+
# Compile results
|
941 |
+
results = {
|
942 |
+
"initial_embodied_carbon_by_material": initial_embodied_carbon_by_material,
|
943 |
+
"initial_embodied_carbon_by_category": initial_embodied_carbon_by_category,
|
944 |
+
"lifecycle_embodied_carbon_by_category": lifecycle_embodied_carbon_by_category,
|
945 |
+
"cumulative_embodied_carbon": cumulative_embodied_carbon,
|
946 |
+
"total_initial_embodied_carbon": total_initial_embodied_carbon,
|
947 |
+
"total_lifecycle_embodied_carbon": total_lifecycle_embodied_carbon,
|
948 |
+
"initial_embodied_carbon_intensity": initial_embodied_carbon_intensity,
|
949 |
+
"lifecycle_embodied_carbon_intensity": lifecycle_embodied_carbon_intensity,
|
950 |
+
"calculation_timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
951 |
+
}
|
952 |
+
|
953 |
+
logger.info("Embodied carbon calculations completed.")
|
954 |
+
return results
|
955 |
+
|
956 |
+
def display_embodied_energy_help():
|
957 |
+
"""
|
958 |
+
Display help information for the embodied energy page.
|
959 |
+
"""
|
960 |
+
st.markdown("""
|
961 |
+
### Embodied Energy Analysis Help
|
962 |
+
|
963 |
+
This section calculates the embodied carbon of building materials, analyzes lifecycle carbon emissions, and evaluates carbon payback periods.
|
964 |
+
|
965 |
+
**Key Concepts:**
|
966 |
+
|
967 |
+
* **Embodied Carbon**: Greenhouse gas emissions associated with materials throughout their lifecycle (extraction, manufacturing, transportation, installation, replacement, and disposal).
|
968 |
+
* **Initial Embodied Carbon**: Carbon emissions from the initial construction of the building.
|
969 |
+
* **Lifecycle Embodied Carbon**: Total carbon emissions over the building's lifespan, including initial construction and material replacements.
|
970 |
+
* **Carbon Intensity**: Embodied carbon per unit area (kg CO2e/m²).
|
971 |
+
* **Carbon Payback Period**: Time required for operational carbon savings (e.g., from renewable energy) to offset the embodied carbon.
|
972 |
+
|
973 |
+
**Workflow:**
|
974 |
+
|
975 |
+
1. **Material Inventory Tab**:
|
976 |
+
* Review the automatically generated material inventory based on building components.
|
977 |
+
* Add or modify materials as needed.
|
978 |
+
* View material quantity summary by category.
|
979 |
+
|
980 |
+
2. **Embodied Carbon Tab**:
|
981 |
+
* Adjust embodied carbon factors for different materials if needed.
|
982 |
+
* Calculate embodied carbon based on the material inventory.
|
983 |
+
* Analyze embodied carbon breakdown by category and material.
|
984 |
+
* View embodied carbon accumulation over the building's lifespan.
|
985 |
+
|
986 |
+
3. **Carbon Payback Tab**:
|
987 |
+
* Compare embodied carbon with operational carbon.
|
988 |
+
* Analyze the carbon payback period if renewable energy is implemented.
|
989 |
+
* Explore carbon reduction strategies.
|
990 |
+
|
991 |
+
**Important:**
|
992 |
+
|
993 |
+
* Accurate material quantities are crucial for embodied carbon calculations.
|
994 |
+
* Embodied carbon factors vary by region and manufacturing process.
|
995 |
+
* The standard analysis period is 60 years, but different materials have different replacement cycles.
|
996 |
+
* Carbon payback analysis requires both embodied carbon and operational carbon data.
|
997 |
+
""")
|
app/hvac_loads.py
ADDED
@@ -0,0 +1,683 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
HVAC Load Calculator - HVAC Loads Module
|
3 |
+
|
4 |
+
This module performs the core cooling and heating load calculations based on the
|
5 |
+
ASHRAE methodology. It integrates data from climate, components, and internal loads
|
6 |
+
modules to determine the building's thermal loads.
|
7 |
+
|
8 |
+
Developed by: Dr Majed Abuseif, Deakin University
|
9 |
+
© 2025
|
10 |
+
"""
|
11 |
+
|
12 |
+
import streamlit as st
|
13 |
+
import pandas as pd
|
14 |
+
import numpy as np
|
15 |
+
import json
|
16 |
+
import logging
|
17 |
+
import uuid
|
18 |
+
import plotly.graph_objects as go
|
19 |
+
import plotly.express as px
|
20 |
+
from typing import Dict, List, Any, Optional, Tuple, Union
|
21 |
+
|
22 |
+
# Configure logging
|
23 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
24 |
+
logger = logging.getLogger(__name__)
|
25 |
+
|
26 |
+
# Constants
|
27 |
+
STEFAN_BOLTZMANN = 5.67e-8 # W/m²·K⁴
|
28 |
+
ABSORPTIVITY_DEFAULT = 0.7 # Default surface absorptivity
|
29 |
+
EMISSIVITY_DEFAULT = 0.9 # Default surface emissivity
|
30 |
+
SKY_TEMP_FACTOR = 0.0552 # Factor for sky temperature calculation
|
31 |
+
AIR_DENSITY = 1.2 # kg/m³
|
32 |
+
AIR_SPECIFIC_HEAT = 1005 # J/kg·K
|
33 |
+
LATENT_HEAT_VAPORIZATION = 2260000 # J/kg
|
34 |
+
|
35 |
+
def display_hvac_loads_page():
|
36 |
+
"""
|
37 |
+
Display the HVAC loads calculation page.
|
38 |
+
This is the main function called by main.py when the HVAC Loads page is selected.
|
39 |
+
"""
|
40 |
+
st.title("HVAC Load Calculations")
|
41 |
+
|
42 |
+
# Display help information in an expandable section
|
43 |
+
with st.expander("Help & Information"):
|
44 |
+
display_hvac_loads_help()
|
45 |
+
|
46 |
+
# Check if necessary data is available
|
47 |
+
if not check_prerequisites():
|
48 |
+
st.warning("Please complete all previous steps (Building Info, Climate, Materials, Construction, Components, Internal Loads) before calculating HVAC loads.")
|
49 |
+
return
|
50 |
+
|
51 |
+
# Calculation trigger
|
52 |
+
if st.button("Calculate HVAC Loads", key="calculate_hvac_loads"):
|
53 |
+
try:
|
54 |
+
results = calculate_loads()
|
55 |
+
st.session_state.project_data["hvac_loads"] = results
|
56 |
+
st.success("HVAC loads calculated successfully.")
|
57 |
+
logger.info("HVAC loads calculated.")
|
58 |
+
except Exception as e:
|
59 |
+
st.error(f"Error calculating HVAC loads: {e}")
|
60 |
+
logger.error(f"Error calculating HVAC loads: {e}", exc_info=True)
|
61 |
+
st.session_state.project_data["hvac_loads"] = None
|
62 |
+
|
63 |
+
# Display results if available
|
64 |
+
if "hvac_loads" in st.session_state.project_data and st.session_state.project_data["hvac_loads"]:
|
65 |
+
display_load_results(st.session_state.project_data["hvac_loads"])
|
66 |
+
else:
|
67 |
+
st.info("Click the button above to calculate HVAC loads.")
|
68 |
+
|
69 |
+
# Navigation buttons
|
70 |
+
col1, col2 = st.columns(2)
|
71 |
+
|
72 |
+
with col1:
|
73 |
+
if st.button("Back to Internal Loads", key="back_to_internal_loads"):
|
74 |
+
st.session_state.current_page = "Internal Loads"
|
75 |
+
st.rerun()
|
76 |
+
|
77 |
+
with col2:
|
78 |
+
if st.button("Continue to Building Energy", key="continue_to_building_energy"):
|
79 |
+
st.session_state.current_page = "Building Energy"
|
80 |
+
st.rerun()
|
81 |
+
|
82 |
+
def check_prerequisites() -> bool:
|
83 |
+
"""Check if all prerequisite data is available in session state."""
|
84 |
+
required_keys = [
|
85 |
+
"building_info",
|
86 |
+
"climate_data",
|
87 |
+
"materials",
|
88 |
+
"constructions",
|
89 |
+
"components",
|
90 |
+
"internal_loads"
|
91 |
+
]
|
92 |
+
|
93 |
+
for key in required_keys:
|
94 |
+
if key not in st.session_state.project_data or not st.session_state.project_data[key]:
|
95 |
+
logger.warning(f"Prerequisite check failed: Missing data for '{key}'.")
|
96 |
+
return False
|
97 |
+
|
98 |
+
# Specific checks within components
|
99 |
+
components = st.session_state.project_data["components"]
|
100 |
+
if not components.get("walls") and not components.get("roofs") and not components.get("floors"):
|
101 |
+
logger.warning("Prerequisite check failed: No opaque components defined.")
|
102 |
+
return False
|
103 |
+
|
104 |
+
# Specific checks within climate data
|
105 |
+
climate = st.session_state.project_data["climate_data"]
|
106 |
+
if not climate.get("hourly_data") or not climate.get("design_conditions"):
|
107 |
+
logger.warning("Prerequisite check failed: Climate data not processed.")
|
108 |
+
return False
|
109 |
+
|
110 |
+
return True
|
111 |
+
|
112 |
+
def calculate_loads() -> Dict[str, Any]:
|
113 |
+
"""
|
114 |
+
Perform the main HVAC load calculations.
|
115 |
+
|
116 |
+
Returns:
|
117 |
+
Dictionary containing calculation results.
|
118 |
+
"""
|
119 |
+
logger.info("Starting HVAC load calculations...")
|
120 |
+
|
121 |
+
# Get data from session state
|
122 |
+
building_info = st.session_state.project_data["building_info"]
|
123 |
+
climate_data = st.session_state.project_data["climate_data"]
|
124 |
+
materials = get_available_materials()
|
125 |
+
constructions = get_available_constructions()
|
126 |
+
fenestrations = get_available_fenestrations()
|
127 |
+
components = st.session_state.project_data["components"]
|
128 |
+
internal_loads = st.session_state.project_data["internal_loads"]
|
129 |
+
|
130 |
+
# Get design conditions
|
131 |
+
design_conditions = climate_data["design_conditions"]
|
132 |
+
hourly_data = pd.DataFrame(climate_data["hourly_data"])
|
133 |
+
|
134 |
+
# --- Solar Calculations --- #
|
135 |
+
logger.info("Calculating solar geometry...")
|
136 |
+
latitude = climate_data["location"]["latitude"]
|
137 |
+
longitude = climate_data["location"]["longitude"]
|
138 |
+
timezone = climate_data["location"]["timezone"]
|
139 |
+
building_orientation_angle = building_info["orientation_angle"]
|
140 |
+
|
141 |
+
# Calculate solar angles for every hour of the year
|
142 |
+
solar_angles = calculate_solar_angles(latitude, longitude, timezone, hourly_data.index)
|
143 |
+
hourly_data = pd.concat([hourly_data, solar_angles], axis=1)
|
144 |
+
|
145 |
+
# --- Heat Transfer Calculations --- #
|
146 |
+
logger.info("Calculating heat transfer through components...")
|
147 |
+
cooling_loads = {
|
148 |
+
"opaque_conduction": np.zeros(8760),
|
149 |
+
"fenestration_conduction": np.zeros(8760),
|
150 |
+
"fenestration_solar": np.zeros(8760),
|
151 |
+
"infiltration": np.zeros(8760),
|
152 |
+
"ventilation": np.zeros(8760),
|
153 |
+
"internal_sensible": np.zeros(8760),
|
154 |
+
"internal_latent": np.zeros(8760)
|
155 |
+
}
|
156 |
+
heating_loads = {
|
157 |
+
"opaque_conduction": np.zeros(8760),
|
158 |
+
"fenestration_conduction": np.zeros(8760),
|
159 |
+
"infiltration": np.zeros(8760),
|
160 |
+
"ventilation": np.zeros(8760)
|
161 |
+
}
|
162 |
+
|
163 |
+
# 1. Opaque Surfaces (Walls, Roofs, Floors)
|
164 |
+
for comp_type in ["walls", "roofs", "floors"]:
|
165 |
+
for comp in components.get(comp_type, []):
|
166 |
+
logger.debug(f"Calculating loads for {comp_type}: {comp['name']}")
|
167 |
+
construction = constructions[comp['construction']]
|
168 |
+
u_value = construction['u_value']
|
169 |
+
area = comp['area']
|
170 |
+
tilt = comp['tilt']
|
171 |
+
orientation = comp['orientation']
|
172 |
+
|
173 |
+
# Calculate surface azimuth
|
174 |
+
surface_azimuth = calculate_surface_azimuth(orientation, building_orientation_angle)
|
175 |
+
|
176 |
+
# Calculate sol-air temperature
|
177 |
+
sol_air_temp = calculate_sol_air_temperature(
|
178 |
+
hourly_data["Dry Bulb Temperature"],
|
179 |
+
hourly_data["Direct Normal Radiation"],
|
180 |
+
hourly_data["Diffuse Horizontal Radiation"],
|
181 |
+
hourly_data["solar_altitude"],
|
182 |
+
hourly_data["solar_azimuth"],
|
183 |
+
tilt,
|
184 |
+
surface_azimuth,
|
185 |
+
absorptivity=ABSORPTIVITY_DEFAULT, # TODO: Get from material/construction
|
186 |
+
emissivity=EMISSIVITY_DEFAULT, # TODO: Get from material/construction
|
187 |
+
h_o=20.0 # TODO: Calculate based on wind speed
|
188 |
+
)
|
189 |
+
|
190 |
+
# Calculate conduction heat gain/loss
|
191 |
+
delta_t_cooling = sol_air_temp - design_conditions["cooling_indoor_temp"]
|
192 |
+
delta_t_heating = design_conditions["heating_indoor_temp"] - hourly_data["Dry Bulb Temperature"] # Use outdoor air temp for heating loss
|
193 |
+
|
194 |
+
conduction_gain = u_value * area * delta_t_cooling
|
195 |
+
conduction_loss = u_value * area * delta_t_heating
|
196 |
+
|
197 |
+
cooling_loads["opaque_conduction"] += np.maximum(0, conduction_gain) # Only gain for cooling
|
198 |
+
heating_loads["opaque_conduction"] += np.maximum(0, conduction_loss) # Only loss for heating
|
199 |
+
|
200 |
+
# 2. Fenestration (Windows, Doors, Skylights)
|
201 |
+
for comp_type in ["windows", "doors", "skylights"]:
|
202 |
+
for comp in components.get(comp_type, []):
|
203 |
+
logger.debug(f"Calculating loads for {comp_type}: {comp['name']}")
|
204 |
+
fenestration = fenestrations[comp['fenestration']]
|
205 |
+
u_value = fenestration['u_value']
|
206 |
+
shgc = fenestration['shgc']
|
207 |
+
area = comp['area']
|
208 |
+
tilt = comp['tilt']
|
209 |
+
orientation = comp['orientation']
|
210 |
+
|
211 |
+
# Calculate surface azimuth
|
212 |
+
surface_azimuth = calculate_surface_azimuth(orientation, building_orientation_angle)
|
213 |
+
|
214 |
+
# Calculate incident solar radiation on the surface
|
215 |
+
incident_solar = calculate_incident_solar(
|
216 |
+
hourly_data["Direct Normal Radiation"],
|
217 |
+
hourly_data["Diffuse Horizontal Radiation"],
|
218 |
+
hourly_data["solar_altitude"],
|
219 |
+
hourly_data["solar_azimuth"],
|
220 |
+
tilt,
|
221 |
+
surface_azimuth
|
222 |
+
)
|
223 |
+
|
224 |
+
# Calculate conduction heat gain/loss
|
225 |
+
delta_t_cooling = hourly_data["Dry Bulb Temperature"] - design_conditions["cooling_indoor_temp"]
|
226 |
+
delta_t_heating = design_conditions["heating_indoor_temp"] - hourly_data["Dry Bulb Temperature"]
|
227 |
+
|
228 |
+
conduction_gain = u_value * area * delta_t_cooling
|
229 |
+
conduction_loss = u_value * area * delta_t_heating
|
230 |
+
|
231 |
+
cooling_loads["fenestration_conduction"] += np.maximum(0, conduction_gain)
|
232 |
+
heating_loads["fenestration_conduction"] += np.maximum(0, conduction_loss)
|
233 |
+
|
234 |
+
# Calculate solar heat gain
|
235 |
+
solar_gain = shgc * area * incident_solar
|
236 |
+
cooling_loads["fenestration_solar"] += solar_gain
|
237 |
+
|
238 |
+
# 3. Infiltration
|
239 |
+
logger.info("Calculating infiltration loads...")
|
240 |
+
# Simple ACH method for now - TODO: Implement more detailed method (e.g., LBL model)
|
241 |
+
volume = building_info["floor_area"] * building_info["building_height"]
|
242 |
+
ach = 0.5 # Air changes per hour (typical value, needs refinement)
|
243 |
+
infiltration_rate_m3s = volume * ach / 3600.0
|
244 |
+
|
245 |
+
delta_t_cooling_infil = hourly_data["Dry Bulb Temperature"] - design_conditions["cooling_indoor_temp"]
|
246 |
+
delta_t_heating_infil = design_conditions["heating_indoor_temp"] - hourly_data["Dry Bulb Temperature"]
|
247 |
+
|
248 |
+
infiltration_sensible_gain = AIR_DENSITY * AIR_SPECIFIC_HEAT * infiltration_rate_m3s * delta_t_cooling_infil
|
249 |
+
infiltration_sensible_loss = AIR_DENSITY * AIR_SPECIFIC_HEAT * infiltration_rate_m3s * delta_t_heating_infil
|
250 |
+
|
251 |
+
cooling_loads["infiltration"] += np.maximum(0, infiltration_sensible_gain)
|
252 |
+
heating_loads["infiltration"] += np.maximum(0, infiltration_sensible_loss)
|
253 |
+
|
254 |
+
# TODO: Add latent infiltration load calculation
|
255 |
+
|
256 |
+
# 4. Ventilation
|
257 |
+
logger.info("Calculating ventilation loads...")
|
258 |
+
ventilation_rate_lps = building_info["ventilation_rate"] # L/s per person or L/s per m² - needs clarification
|
259 |
+
# Assuming L/s per m² for now
|
260 |
+
ventilation_rate_m3s = ventilation_rate_lps * building_info["floor_area"] / 1000.0
|
261 |
+
|
262 |
+
delta_t_cooling_vent = hourly_data["Dry Bulb Temperature"] - design_conditions["cooling_indoor_temp"]
|
263 |
+
delta_t_heating_vent = design_conditions["heating_indoor_temp"] - hourly_data["Dry Bulb Temperature"]
|
264 |
+
|
265 |
+
ventilation_sensible_gain = AIR_DENSITY * AIR_SPECIFIC_HEAT * ventilation_rate_m3s * delta_t_cooling_vent
|
266 |
+
ventilation_sensible_loss = AIR_DENSITY * AIR_SPECIFIC_HEAT * ventilation_rate_m3s * delta_t_heating_vent
|
267 |
+
|
268 |
+
cooling_loads["ventilation"] += np.maximum(0, ventilation_sensible_gain)
|
269 |
+
heating_loads["ventilation"] += np.maximum(0, ventilation_sensible_loss)
|
270 |
+
|
271 |
+
# TODO: Add latent ventilation load calculation
|
272 |
+
|
273 |
+
# 5. Internal Loads
|
274 |
+
logger.info("Calculating internal loads...")
|
275 |
+
internal_sensible, internal_latent = calculate_hourly_internal_loads(internal_loads)
|
276 |
+
cooling_loads["internal_sensible"] = internal_sensible
|
277 |
+
cooling_loads["internal_latent"] = internal_latent
|
278 |
+
|
279 |
+
# --- Total Loads --- #
|
280 |
+
logger.info("Summing up loads...")
|
281 |
+
total_cooling_sensible = (
|
282 |
+
cooling_loads["opaque_conduction"] +
|
283 |
+
cooling_loads["fenestration_conduction"] +
|
284 |
+
cooling_loads["fenestration_solar"] +
|
285 |
+
cooling_loads["infiltration"] +
|
286 |
+
cooling_loads["ventilation"] +
|
287 |
+
cooling_loads["internal_sensible"]
|
288 |
+
)
|
289 |
+
total_cooling_latent = cooling_loads["internal_latent"] # Add infiltration/ventilation latent later
|
290 |
+
total_cooling = total_cooling_sensible + total_cooling_latent
|
291 |
+
|
292 |
+
# Heating loads are losses, internal gains reduce heating need
|
293 |
+
total_heating_loss = (
|
294 |
+
heating_loads["opaque_conduction"] +
|
295 |
+
heating_loads["fenestration_conduction"] +
|
296 |
+
heating_loads["infiltration"] +
|
297 |
+
heating_loads["ventilation"]
|
298 |
+
)
|
299 |
+
# Consider internal sensible gains as reducing heating load
|
300 |
+
total_heating_needed = np.maximum(0, total_heating_loss - cooling_loads["internal_sensible"])
|
301 |
+
|
302 |
+
# --- Peak Loads --- #
|
303 |
+
logger.info("Calculating peak loads...")
|
304 |
+
peak_cooling_sensible = np.max(total_cooling_sensible)
|
305 |
+
peak_cooling_latent = np.max(total_cooling_latent)
|
306 |
+
peak_cooling_total = np.max(total_cooling)
|
307 |
+
peak_heating = np.max(total_heating_needed)
|
308 |
+
|
309 |
+
peak_cooling_sensible_hour = np.argmax(total_cooling_sensible)
|
310 |
+
peak_cooling_latent_hour = np.argmax(total_cooling_latent)
|
311 |
+
peak_cooling_total_hour = np.argmax(total_cooling)
|
312 |
+
peak_heating_hour = np.argmax(total_heating_needed)
|
313 |
+
|
314 |
+
results = {
|
315 |
+
"hourly_cooling_sensible": total_cooling_sensible.tolist(),
|
316 |
+
"hourly_cooling_latent": total_cooling_latent.tolist(),
|
317 |
+
"hourly_cooling_total": total_cooling.tolist(),
|
318 |
+
"hourly_heating": total_heating_needed.tolist(),
|
319 |
+
"cooling_load_components": {k: v.tolist() for k, v in cooling_loads.items()},
|
320 |
+
"heating_load_components": {k: v.tolist() for k, v in heating_loads.items()},
|
321 |
+
"peak_cooling_sensible": peak_cooling_sensible,
|
322 |
+
"peak_cooling_latent": peak_cooling_latent,
|
323 |
+
"peak_cooling_total": peak_cooling_total,
|
324 |
+
"peak_heating": peak_heating,
|
325 |
+
"peak_cooling_sensible_hour": int(peak_cooling_sensible_hour),
|
326 |
+
"peak_cooling_latent_hour": int(peak_cooling_latent_hour),
|
327 |
+
"peak_cooling_total_hour": int(peak_cooling_total_hour),
|
328 |
+
"peak_heating_hour": int(peak_heating_hour)
|
329 |
+
}
|
330 |
+
|
331 |
+
logger.info("HVAC load calculations completed.")
|
332 |
+
return results
|
333 |
+
|
334 |
+
def calculate_solar_angles(latitude: float, longitude: float, timezone: float, index: pd.DatetimeIndex) -> pd.DataFrame:
|
335 |
+
"""
|
336 |
+
Calculate solar altitude and azimuth angles for given location and time.
|
337 |
+
Uses formulas from Duffie & Beckman, Solar Engineering of Thermal Processes.
|
338 |
+
|
339 |
+
Args:
|
340 |
+
latitude: Latitude in degrees
|
341 |
+
longitude: Longitude in degrees (East positive)
|
342 |
+
timezone: Timezone offset from UTC in hours
|
343 |
+
index: Pandas DatetimeIndex for the hours of the year
|
344 |
+
|
345 |
+
Returns:
|
346 |
+
DataFrame with solar altitude and azimuth angles in degrees.
|
347 |
+
"""
|
348 |
+
# Convert angles to radians
|
349 |
+
lat_rad = np.radians(latitude)
|
350 |
+
|
351 |
+
# Day of year
|
352 |
+
day_of_year = index.dayofyear
|
353 |
+
|
354 |
+
# Equation of time (simplified)
|
355 |
+
b = 2 * np.pi * (day_of_year - 81) / 364
|
356 |
+
eot = 9.87 * np.sin(2 * b) - 7.53 * np.cos(b) - 1.5 * np.sin(b) # minutes
|
357 |
+
|
358 |
+
# Local Standard Time Meridian (LSTM)
|
359 |
+
lstm = 15 * timezone
|
360 |
+
|
361 |
+
# Time Correction Factor (TC)
|
362 |
+
tc = 4 * (longitude - lstm) + eot # minutes
|
363 |
+
|
364 |
+
# Local Solar Time (LST)
|
365 |
+
local_time_hour = index.hour + index.minute / 60.0
|
366 |
+
lst = local_time_hour + tc / 60.0
|
367 |
+
|
368 |
+
# Hour Angle (HRA)
|
369 |
+
hra = 15 * (lst - 12) # degrees
|
370 |
+
hra_rad = np.radians(hra)
|
371 |
+
|
372 |
+
# Declination Angle
|
373 |
+
declination_rad = np.radians(23.45 * np.sin(2 * np.pi * (284 + day_of_year) / 365))
|
374 |
+
|
375 |
+
# Solar Altitude (alpha)
|
376 |
+
sin_alpha = np.sin(lat_rad) * np.sin(declination_rad) + \
|
377 |
+
np.cos(lat_rad) * np.cos(declination_rad) * np.cos(hra_rad)
|
378 |
+
solar_altitude_rad = np.arcsin(np.clip(sin_alpha, -1, 1))
|
379 |
+
solar_altitude_deg = np.degrees(solar_altitude_rad)
|
380 |
+
|
381 |
+
# Solar Azimuth (gamma_s) - measured from South, positive West
|
382 |
+
cos_gamma_s = (np.sin(solar_altitude_rad) * np.sin(lat_rad) - np.sin(declination_rad)) / \
|
383 |
+
(np.cos(solar_altitude_rad) * np.cos(lat_rad))
|
384 |
+
solar_azimuth_rad = np.arccos(np.clip(cos_gamma_s, -1, 1))
|
385 |
+
solar_azimuth_deg = np.degrees(solar_azimuth_rad)
|
386 |
+
# Adjust azimuth based on hour angle
|
387 |
+
solar_azimuth_deg = np.where(hra > 0, solar_azimuth_deg, 360 - solar_azimuth_deg)
|
388 |
+
# Convert to azimuth from North, positive East (common convention)
|
389 |
+
solar_azimuth_deg = (solar_azimuth_deg + 180) % 360
|
390 |
+
|
391 |
+
return pd.DataFrame({
|
392 |
+
"solar_altitude": solar_altitude_deg,
|
393 |
+
"solar_azimuth": solar_azimuth_deg
|
394 |
+
}, index=index)
|
395 |
+
|
396 |
+
def calculate_surface_azimuth(orientation: str, building_orientation_angle: float) -> float:
|
397 |
+
"""
|
398 |
+
Calculate the actual surface azimuth based on orientation label and building rotation.
|
399 |
+
Azimuth: 0=N, 90=E, 180=S, 270=W
|
400 |
+
|
401 |
+
Args:
|
402 |
+
orientation: Orientation label (e.g., "A (North)", "B (South)")
|
403 |
+
building_orientation_angle: Building rotation angle from North (degrees, positive East)
|
404 |
+
|
405 |
+
Returns:
|
406 |
+
Surface azimuth in degrees.
|
407 |
+
"""
|
408 |
+
base_azimuth = {
|
409 |
+
"A (North)": 0.0,
|
410 |
+
"B (South)": 180.0,
|
411 |
+
"C (East)": 90.0,
|
412 |
+
"D (West)": 270.0,
|
413 |
+
"Horizontal": 0.0 # Azimuth doesn't matter for horizontal
|
414 |
+
}.get(orientation, 0.0)
|
415 |
+
|
416 |
+
# Adjust for building rotation
|
417 |
+
surface_azimuth = (base_azimuth + building_orientation_angle) % 360
|
418 |
+
return surface_azimuth
|
419 |
+
|
420 |
+
def calculate_incident_solar(dnr: pd.Series, dhr: pd.Series, solar_altitude: pd.Series, solar_azimuth: pd.Series, tilt: float, surface_azimuth: float) -> pd.Series:
|
421 |
+
"""
|
422 |
+
Calculate total incident solar radiation on a tilted surface.
|
423 |
+
Uses Perez model for diffuse radiation is simplified here.
|
424 |
+
|
425 |
+
Args:
|
426 |
+
dnr: Direct Normal Radiation (W/m²)
|
427 |
+
dhr: Diffuse Horizontal Radiation (W/m²)
|
428 |
+
solar_altitude: Solar altitude angle (degrees)
|
429 |
+
solar_azimuth: Solar azimuth angle (degrees, 0=N, positive E)
|
430 |
+
tilt: Surface tilt angle from horizontal (degrees)
|
431 |
+
surface_azimuth: Surface azimuth angle (degrees, 0=N, positive E)
|
432 |
+
|
433 |
+
Returns:
|
434 |
+
Total incident solar radiation on the surface (W/m²).
|
435 |
+
"""
|
436 |
+
# Convert angles to radians
|
437 |
+
alt_rad = np.radians(solar_altitude)
|
438 |
+
az_rad = np.radians(solar_azimuth)
|
439 |
+
tilt_rad = np.radians(tilt)
|
440 |
+
surf_az_rad = np.radians(surface_azimuth)
|
441 |
+
|
442 |
+
# Angle of Incidence (theta)
|
443 |
+
cos_theta = np.cos(alt_rad) * np.sin(tilt_rad) * np.cos(az_rad - surf_az_rad) + \
|
444 |
+
np.sin(alt_rad) * np.cos(tilt_rad)
|
445 |
+
cos_theta = np.maximum(0, cos_theta) # Radiation only incident if cos_theta > 0
|
446 |
+
|
447 |
+
# Direct radiation component on tilted surface
|
448 |
+
direct_tilted = dnr * cos_theta
|
449 |
+
|
450 |
+
# Diffuse radiation component (simplified isotropic sky model)
|
451 |
+
# TODO: Implement Perez model or Hay-Davies model for better accuracy
|
452 |
+
sky_diffuse_tilted = dhr * (1 + np.cos(tilt_rad)) / 2
|
453 |
+
|
454 |
+
# Ground reflected component (simplified)
|
455 |
+
albedo = 0.2 # Typical ground reflectance
|
456 |
+
ground_reflected_tilted = (dnr * np.sin(alt_rad) + dhr) * albedo * (1 - np.cos(tilt_rad)) / 2
|
457 |
+
|
458 |
+
# Total incident solar radiation
|
459 |
+
total_incident = direct_tilted + sky_diffuse_tilted + ground_reflected_tilted
|
460 |
+
return total_incident
|
461 |
+
|
462 |
+
def calculate_sol_air_temperature(t_oa: pd.Series, dnr: pd.Series, dhr: pd.Series, solar_altitude: pd.Series, solar_azimuth: pd.Series, tilt: float, surface_azimuth: float, absorptivity: float, emissivity: float, h_o: float) -> pd.Series:
|
463 |
+
"""
|
464 |
+
Calculate Sol-Air Temperature.
|
465 |
+
|
466 |
+
Args:
|
467 |
+
t_oa: Outdoor air temperature (°C)
|
468 |
+
dnr, dhr, solar_altitude, solar_azimuth: Solar data
|
469 |
+
tilt, surface_azimuth: Surface orientation
|
470 |
+
absorptivity: Surface solar absorptivity
|
471 |
+
emissivity: Surface thermal emissivity
|
472 |
+
h_o: Outside surface heat transfer coefficient (W/m²·K)
|
473 |
+
|
474 |
+
Returns:
|
475 |
+
Sol-air temperature (°C).
|
476 |
+
"""
|
477 |
+
# Calculate incident solar radiation
|
478 |
+
i_total = calculate_incident_solar(dnr, dhr, solar_altitude, solar_azimuth, tilt, surface_azimuth)
|
479 |
+
|
480 |
+
# Calculate sky temperature (simplified from Berdahl & Martin)
|
481 |
+
t_sky = t_oa * (SKY_TEMP_FACTOR * hourly_data["Dew Point Temperature"]**0.25)**0.25 # Approximation
|
482 |
+
|
483 |
+
# Longwave radiation exchange term
|
484 |
+
delta_r = STEFAN_BOLTZMANN * emissivity * ((t_oa + 273.15)**4 - (t_sky + 273.15)**4) / h_o
|
485 |
+
|
486 |
+
# Sol-air temperature
|
487 |
+
t_sol_air = t_oa + (absorptivity * i_total / h_o) - delta_r
|
488 |
+
return t_sol_air
|
489 |
+
|
490 |
+
def calculate_hourly_internal_loads(internal_loads_data: Dict[str, List[Dict[str, Any]]]) -> Tuple[np.ndarray, np.ndarray]:
|
491 |
+
"""
|
492 |
+
Calculate total hourly sensible and latent internal loads.
|
493 |
+
|
494 |
+
Args:
|
495 |
+
internal_loads_data: Dictionary containing lists of loads for each type.
|
496 |
+
|
497 |
+
Returns:
|
498 |
+
Tuple of (hourly_sensible_loads, hourly_latent_loads) as numpy arrays.
|
499 |
+
"""
|
500 |
+
hourly_sensible = np.zeros(8760)
|
501 |
+
hourly_latent = np.zeros(8760)
|
502 |
+
|
503 |
+
# Process each load type
|
504 |
+
for load_type, loads in internal_loads_data.items():
|
505 |
+
for load in loads:
|
506 |
+
total_load = load["total"]
|
507 |
+
schedule_type = load["schedule_type"]
|
508 |
+
|
509 |
+
# Get schedule multipliers (assuming 365 days, repeating weekly pattern)
|
510 |
+
schedule_multipliers = np.zeros(8760)
|
511 |
+
if schedule_type == "Custom" and "custom_schedule" in load:
|
512 |
+
weekday_schedule = load["custom_schedule"]["Weekday"]
|
513 |
+
weekend_schedule = load["custom_schedule"]["Weekend"]
|
514 |
+
elif schedule_type in DEFAULT_SCHEDULES:
|
515 |
+
weekday_schedule = DEFAULT_SCHEDULES[schedule_type]["Weekday"]
|
516 |
+
weekend_schedule = DEFAULT_SCHEDULES[schedule_type]["Weekend"]
|
517 |
+
else: # Continuous
|
518 |
+
weekday_schedule = [1.0] * 24
|
519 |
+
weekend_schedule = [1.0] * 24
|
520 |
+
|
521 |
+
# Apply schedule to 8760 hours (assuming standard year)
|
522 |
+
# TODO: Use actual date index for proper weekday/weekend assignment
|
523 |
+
for hour in range(8760):
|
524 |
+
day_of_week = (hour // 24) % 7 # Simple approximation (0=Mon, 6=Sun)
|
525 |
+
hour_of_day = hour % 24
|
526 |
+
if day_of_week < 5: # Weekday
|
527 |
+
schedule_multipliers[hour] = weekday_schedule[hour_of_day]
|
528 |
+
else: # Weekend
|
529 |
+
schedule_multipliers[hour] = weekend_schedule[hour_of_day]
|
530 |
+
|
531 |
+
# Calculate hourly load components
|
532 |
+
if load_type == "occupancy":
|
533 |
+
sensible_fraction = load["sensible_fraction"]
|
534 |
+
latent_fraction = load["latent_fraction"]
|
535 |
+
hourly_sensible += total_load * sensible_fraction * schedule_multipliers
|
536 |
+
hourly_latent += total_load * latent_fraction * schedule_multipliers
|
537 |
+
else: # Lighting, Equipment, Other (assumed fully sensible)
|
538 |
+
hourly_sensible += total_load * schedule_multipliers
|
539 |
+
|
540 |
+
return hourly_sensible, hourly_latent
|
541 |
+
|
542 |
+
def display_load_results(results: Dict[str, Any]):
|
543 |
+
"""
|
544 |
+
Display the calculated HVAC load results.
|
545 |
+
|
546 |
+
Args:
|
547 |
+
results: Dictionary containing calculation results.
|
548 |
+
"""
|
549 |
+
st.subheader("Peak Load Summary")
|
550 |
+
|
551 |
+
col1, col2, col3 = st.columns(3)
|
552 |
+
with col1:
|
553 |
+
st.metric("Peak Cooling (Total)", f"{results['peak_cooling_total'] / 1000:.1f} kW", help=f"Occurred at hour {results['peak_cooling_total_hour']}")
|
554 |
+
st.metric("Peak Cooling (Sensible)", f"{results['peak_cooling_sensible'] / 1000:.1f} kW", help=f"Occurred at hour {results['peak_cooling_sensible_hour']}")
|
555 |
+
with col2:
|
556 |
+
st.metric("Peak Heating", f"{results['peak_heating'] / 1000:.1f} kW", help=f"Occurred at hour {results['peak_heating_hour']}")
|
557 |
+
st.metric("Peak Cooling (Latent)", f"{results['peak_cooling_latent'] / 1000:.1f} kW", help=f"Occurred at hour {results['peak_cooling_latent_hour']}")
|
558 |
+
|
559 |
+
st.subheader("Hourly Load Profiles")
|
560 |
+
|
561 |
+
# Create DataFrame for plotting
|
562 |
+
hourly_df = pd.DataFrame({
|
563 |
+
"Hour": range(8760),
|
564 |
+
"Cooling (Sensible)": results["hourly_cooling_sensible"],
|
565 |
+
"Cooling (Latent)": results["hourly_cooling_latent"],
|
566 |
+
"Cooling (Total)": results["hourly_cooling_total"],
|
567 |
+
"Heating": results["hourly_heating"]
|
568 |
+
})
|
569 |
+
|
570 |
+
# Plot cooling loads
|
571 |
+
fig_cooling = go.Figure()
|
572 |
+
fig_cooling.add_trace(go.Scatter(x=hourly_df["Hour"], y=hourly_df["Cooling (Sensible)"], name="Sensible Cooling", stackgroup='one'))
|
573 |
+
fig_cooling.add_trace(go.Scatter(x=hourly_df["Hour"], y=hourly_df["Cooling (Latent)"], name="Latent Cooling", stackgroup='one'))
|
574 |
+
fig_cooling.update_layout(title="Hourly Cooling Load Profile", xaxis_title="Hour of Year", yaxis_title="Load (W)", height=400)
|
575 |
+
st.plotly_chart(fig_cooling, use_container_width=True)
|
576 |
+
|
577 |
+
# Plot heating loads
|
578 |
+
fig_heating = go.Figure()
|
579 |
+
fig_heating.add_trace(go.Scatter(x=hourly_df["Hour"], y=hourly_df["Heating"], name="Heating Load", line=dict(color='red')))
|
580 |
+
fig_heating.update_layout(title="Hourly Heating Load Profile", xaxis_title="Hour of Year", yaxis_title="Load (W)", height=400)
|
581 |
+
st.plotly_chart(fig_heating, use_container_width=True)
|
582 |
+
|
583 |
+
st.subheader("Load Components at Peak Cooling Hour")
|
584 |
+
peak_hour = results["peak_cooling_total_hour"]
|
585 |
+
|
586 |
+
cooling_components = results["cooling_load_components"]
|
587 |
+
peak_cooling_data = {
|
588 |
+
"Component": list(cooling_components.keys()),
|
589 |
+
"Load (W)": [cooling_components[k][peak_hour] for k in cooling_components]
|
590 |
+
}
|
591 |
+
peak_cooling_df = pd.DataFrame(peak_cooling_data)
|
592 |
+
peak_cooling_df = peak_cooling_df[peak_cooling_df["Load (W)"] > 0] # Show only positive contributions
|
593 |
+
|
594 |
+
fig_peak_cooling = px.pie(
|
595 |
+
peak_cooling_df,
|
596 |
+
values="Load (W)",
|
597 |
+
names="Component",
|
598 |
+
title=f"Cooling Load Breakdown at Peak Hour ({peak_hour})"
|
599 |
+
)
|
600 |
+
st.plotly_chart(fig_peak_cooling, use_container_width=True)
|
601 |
+
|
602 |
+
st.subheader("Load Components at Peak Heating Hour")
|
603 |
+
peak_hour_heating = results["peak_heating_hour"]
|
604 |
+
|
605 |
+
heating_components = results["heating_load_components"]
|
606 |
+
internal_gains_at_peak = results["cooling_load_components"]["internal_sensible"][peak_hour_heating]
|
607 |
+
|
608 |
+
peak_heating_data = {
|
609 |
+
"Component": list(heating_components.keys()) + ["Internal Gains Offset"],
|
610 |
+
"Load (W)": [heating_components[k][peak_hour_heating] for k in heating_components] + [-internal_gains_at_peak] # Show gains as negative
|
611 |
+
}
|
612 |
+
peak_heating_df = pd.DataFrame(peak_heating_data)
|
613 |
+
peak_heating_df = peak_heating_df[peak_heating_df["Load (W)"] != 0] # Show only non-zero contributions
|
614 |
+
|
615 |
+
fig_peak_heating = px.pie(
|
616 |
+
peak_heating_df,
|
617 |
+
values="Load (W)",
|
618 |
+
names="Component",
|
619 |
+
title=f"Heating Load Breakdown at Peak Hour ({peak_hour_heating})"
|
620 |
+
)
|
621 |
+
st.plotly_chart(fig_peak_heating, use_container_width=True)
|
622 |
+
|
623 |
+
# Helper functions to get data (assuming they exist in other modules or session state)
|
624 |
+
def get_available_materials() -> Dict[str, Any]:
|
625 |
+
# Placeholder - should retrieve from materials_library state
|
626 |
+
mats = {}
|
627 |
+
if "materials" in st.session_state.project_data:
|
628 |
+
mats.update(st.session_state.project_data["materials"].get("library", {}))
|
629 |
+
mats.update(st.session_state.project_data["materials"].get("project", {}))
|
630 |
+
return mats
|
631 |
+
|
632 |
+
def get_available_constructions() -> Dict[str, Any]:
|
633 |
+
# Placeholder - should retrieve from construction state
|
634 |
+
consts = {}
|
635 |
+
if "constructions" in st.session_state.project_data:
|
636 |
+
consts.update(st.session_state.project_data["constructions"].get("library", {}))
|
637 |
+
consts.update(st.session_state.project_data["constructions"].get("project", {}))
|
638 |
+
return consts
|
639 |
+
|
640 |
+
def get_available_fenestrations() -> Dict[str, Any]:
|
641 |
+
# Placeholder - should retrieve from materials_library state
|
642 |
+
fens = {}
|
643 |
+
if "fenestrations" in st.session_state.project_data:
|
644 |
+
fens.update(st.session_state.project_data["fenestrations"].get("library", {}))
|
645 |
+
fens.update(st.session_state.project_data["fenestrations"].get("project", {}))
|
646 |
+
return fens
|
647 |
+
|
648 |
+
def display_hvac_loads_help():
|
649 |
+
"""
|
650 |
+
Display help information for the HVAC loads page.
|
651 |
+
"""
|
652 |
+
st.markdown("""
|
653 |
+
### HVAC Loads Help
|
654 |
+
|
655 |
+
This section calculates the building's heating and cooling loads based on the information provided in the previous steps.
|
656 |
+
|
657 |
+
**Calculation Process:**
|
658 |
+
|
659 |
+
1. **Heat Transfer**: Calculates heat gains and losses through the building envelope (walls, roofs, floors, windows, doors, skylights) considering conduction and solar radiation.
|
660 |
+
2. **Infiltration & Ventilation**: Calculates loads due to air exchange with the outside.
|
661 |
+
3. **Internal Loads**: Incorporates heat gains from occupants, lighting, and equipment.
|
662 |
+
4. **Summation**: Combines all heat gains and losses to determine the net hourly cooling and heating loads.
|
663 |
+
|
664 |
+
**Results:**
|
665 |
+
|
666 |
+
* **Peak Loads**: Shows the maximum calculated cooling and heating loads required to size HVAC equipment.
|
667 |
+
* **Hourly Profiles**: Displays graphs of the calculated loads for every hour of the year.
|
668 |
+
* **Load Components**: Shows the breakdown of loads by source (e.g., conduction, solar, internal) at the peak hours.
|
669 |
+
|
670 |
+
**Workflow:**
|
671 |
+
|
672 |
+
1. Ensure all previous sections (Building Info, Climate, Materials, Construction, Components, Internal Loads) are complete and accurate.
|
673 |
+
2. Click the "Calculate HVAC Loads" button.
|
674 |
+
3. Review the peak load summary and hourly profiles.
|
675 |
+
4. Analyze the load component breakdowns to understand the main drivers of heating and cooling needs.
|
676 |
+
5. Continue to the Building Energy section to simulate energy consumption based on these loads.
|
677 |
+
|
678 |
+
**Important:**
|
679 |
+
|
680 |
+
* The accuracy of these calculations depends heavily on the quality of the input data.
|
681 |
+
* Calculations are based on the ASHRAE methodology.
|
682 |
+
* Review the results carefully before proceeding.
|
683 |
+
""")
|
app/internal_loads.py
ADDED
@@ -0,0 +1,883 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
HVAC Load Calculator - Internal Loads Module
|
3 |
+
|
4 |
+
This module handles the internal loads functionality of the HVAC Load Calculator application,
|
5 |
+
allowing users to define occupancy, lighting, equipment, and other internal heat gains.
|
6 |
+
It provides schedule-based load profiles and integrates with building information.
|
7 |
+
|
8 |
+
Developed by: Dr Majed Abuseif, Deakin University
|
9 |
+
© 2025
|
10 |
+
"""
|
11 |
+
|
12 |
+
import streamlit as st
|
13 |
+
import pandas as pd
|
14 |
+
import numpy as np
|
15 |
+
import json
|
16 |
+
import logging
|
17 |
+
import uuid
|
18 |
+
import plotly.graph_objects as go
|
19 |
+
import plotly.express as px
|
20 |
+
from typing import Dict, List, Any, Optional, Tuple, Union
|
21 |
+
|
22 |
+
# Configure logging
|
23 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
24 |
+
logger = logging.getLogger(__name__)
|
25 |
+
|
26 |
+
# Define constants
|
27 |
+
LOAD_TYPES = ["Occupancy", "Lighting", "Equipment", "Other"]
|
28 |
+
SCHEDULE_TYPES = ["Continuous", "Day/Night", "Custom"]
|
29 |
+
DAYS_OF_WEEK = ["Weekday", "Weekend"]
|
30 |
+
|
31 |
+
# Default occupancy densities by building type (m² per person)
|
32 |
+
DEFAULT_OCCUPANCY_DENSITIES = {
|
33 |
+
"Office": 10.0,
|
34 |
+
"Retail": 5.0,
|
35 |
+
"Residential": 20.0,
|
36 |
+
"Educational": 4.0,
|
37 |
+
"Healthcare": 8.0,
|
38 |
+
"Industrial": 15.0,
|
39 |
+
"Hospitality": 3.0,
|
40 |
+
"Other": 10.0
|
41 |
+
}
|
42 |
+
|
43 |
+
# Default lighting power densities by building type (W/m²)
|
44 |
+
DEFAULT_LIGHTING_DENSITIES = {
|
45 |
+
"Office": 12.0,
|
46 |
+
"Retail": 18.0,
|
47 |
+
"Residential": 8.0,
|
48 |
+
"Educational": 15.0,
|
49 |
+
"Healthcare": 15.0,
|
50 |
+
"Industrial": 13.0,
|
51 |
+
"Hospitality": 14.0,
|
52 |
+
"Other": 12.0
|
53 |
+
}
|
54 |
+
|
55 |
+
# Default equipment power densities by building type (W/m²)
|
56 |
+
DEFAULT_EQUIPMENT_DENSITIES = {
|
57 |
+
"Office": 15.0,
|
58 |
+
"Retail": 10.0,
|
59 |
+
"Residential": 5.0,
|
60 |
+
"Educational": 8.0,
|
61 |
+
"Healthcare": 20.0,
|
62 |
+
"Industrial": 25.0,
|
63 |
+
"Hospitality": 10.0,
|
64 |
+
"Other": 12.0
|
65 |
+
}
|
66 |
+
|
67 |
+
# Default schedules
|
68 |
+
DEFAULT_SCHEDULES = {
|
69 |
+
"Continuous": {
|
70 |
+
"Weekday": [1.0] * 24,
|
71 |
+
"Weekend": [1.0] * 24
|
72 |
+
},
|
73 |
+
"Day/Night": {
|
74 |
+
"Weekday": [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.5, 0.8, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.8, 0.6, 0.5, 0.5, 0.4, 0.4, 0.3, 0.3],
|
75 |
+
"Weekend": [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.4, 0.5, 0.6, 0.7, 0.7, 0.7, 0.7, 0.6, 0.5, 0.5, 0.4, 0.4, 0.4, 0.3, 0.3, 0.3, 0.3]
|
76 |
+
}
|
77 |
+
}
|
78 |
+
|
79 |
+
def display_internal_loads_page():
|
80 |
+
"""
|
81 |
+
Display the internal loads page.
|
82 |
+
This is the main function called by main.py when the Internal Loads page is selected.
|
83 |
+
"""
|
84 |
+
st.title("Internal Loads")
|
85 |
+
|
86 |
+
# Display help information in an expandable section
|
87 |
+
with st.expander("Help & Information"):
|
88 |
+
display_internal_loads_help()
|
89 |
+
|
90 |
+
# Initialize internal loads in session state if not present
|
91 |
+
initialize_internal_loads()
|
92 |
+
|
93 |
+
# Create tabs for different load types
|
94 |
+
tabs = st.tabs(LOAD_TYPES)
|
95 |
+
|
96 |
+
for i, load_type in enumerate(LOAD_TYPES):
|
97 |
+
with tabs[i]:
|
98 |
+
display_load_tab(load_type)
|
99 |
+
|
100 |
+
# Summary tab
|
101 |
+
st.markdown("---")
|
102 |
+
st.subheader("Internal Loads Summary")
|
103 |
+
display_loads_summary()
|
104 |
+
|
105 |
+
# Navigation buttons
|
106 |
+
col1, col2 = st.columns(2)
|
107 |
+
|
108 |
+
with col1:
|
109 |
+
if st.button("Back to Building Components", key="back_to_components"):
|
110 |
+
st.session_state.current_page = "Building Components"
|
111 |
+
st.rerun()
|
112 |
+
|
113 |
+
with col2:
|
114 |
+
if st.button("Continue to HVAC Loads", key="continue_to_hvac_loads"):
|
115 |
+
st.session_state.current_page = "HVAC Loads"
|
116 |
+
st.rerun()
|
117 |
+
|
118 |
+
def initialize_internal_loads():
|
119 |
+
"""Initialize internal loads in session state if not present."""
|
120 |
+
if "internal_loads" not in st.session_state.project_data:
|
121 |
+
st.session_state.project_data["internal_loads"] = {
|
122 |
+
"occupancy": [],
|
123 |
+
"lighting": [],
|
124 |
+
"equipment": [],
|
125 |
+
"other": []
|
126 |
+
}
|
127 |
+
|
128 |
+
# Initialize load editor state
|
129 |
+
if "load_editor" not in st.session_state:
|
130 |
+
st.session_state.load_editor = {
|
131 |
+
"type": LOAD_TYPES[0],
|
132 |
+
"name": "",
|
133 |
+
"zone": "Whole Building",
|
134 |
+
"area": get_building_area(),
|
135 |
+
"density": 0.0,
|
136 |
+
"total": 0.0,
|
137 |
+
"sensible_fraction": 0.7,
|
138 |
+
"latent_fraction": 0.3,
|
139 |
+
"radiative_fraction": 0.4,
|
140 |
+
"convective_fraction": 0.6,
|
141 |
+
"schedule_type": SCHEDULE_TYPES[0],
|
142 |
+
"custom_schedule": {
|
143 |
+
"Weekday": [0.0] * 24,
|
144 |
+
"Weekend": [0.0] * 24
|
145 |
+
},
|
146 |
+
"edit_mode": False,
|
147 |
+
"original_id": ""
|
148 |
+
}
|
149 |
+
|
150 |
+
# Initialize custom zones if not present
|
151 |
+
if "custom_zones" not in st.session_state.project_data:
|
152 |
+
st.session_state.project_data["custom_zones"] = ["Whole Building"]
|
153 |
+
|
154 |
+
def display_load_tab(load_type: str):
|
155 |
+
"""
|
156 |
+
Display the content for a specific load type tab.
|
157 |
+
|
158 |
+
Args:
|
159 |
+
load_type: The type of load (e.g., "Occupancy", "Lighting")
|
160 |
+
"""
|
161 |
+
st.subheader(f"{load_type} Loads")
|
162 |
+
|
163 |
+
# Get loads of this type
|
164 |
+
load_key = load_type.lower()
|
165 |
+
loads = st.session_state.project_data["internal_loads"].get(load_key, [])
|
166 |
+
|
167 |
+
# Display existing loads
|
168 |
+
if loads:
|
169 |
+
display_load_list(load_type, loads)
|
170 |
+
else:
|
171 |
+
st.info(f"No {load_type.lower()} loads added yet. Use the editor below to add loads.")
|
172 |
+
|
173 |
+
# Load Editor
|
174 |
+
st.markdown("---")
|
175 |
+
st.subheader(f"{load_type} Load Editor")
|
176 |
+
display_load_editor(load_type)
|
177 |
+
|
178 |
+
def display_load_list(load_type: str, loads: List[Dict[str, Any]]):
|
179 |
+
"""
|
180 |
+
Display the list of existing loads for a given type.
|
181 |
+
|
182 |
+
Args:
|
183 |
+
load_type: The type of load
|
184 |
+
loads: List of load dictionaries
|
185 |
+
"""
|
186 |
+
# Create a DataFrame for display
|
187 |
+
data = []
|
188 |
+
for i, load in enumerate(loads):
|
189 |
+
record = {
|
190 |
+
"#": i + 1,
|
191 |
+
"Name": load["name"],
|
192 |
+
"Zone": load["zone"],
|
193 |
+
"Area (m²)": load["area"],
|
194 |
+
"Total (W)": load["total"],
|
195 |
+
"Density (W/m²)": load["density"],
|
196 |
+
"Schedule": load["schedule_type"]
|
197 |
+
}
|
198 |
+
|
199 |
+
if load_type == "Occupancy":
|
200 |
+
record["Sensible (W)"] = load["total"] * load["sensible_fraction"]
|
201 |
+
record["Latent (W)"] = load["total"] * load["latent_fraction"]
|
202 |
+
else:
|
203 |
+
record["Radiative (W)"] = load["total"] * load["radiative_fraction"]
|
204 |
+
record["Convective (W)"] = load["total"] * load["convective_fraction"]
|
205 |
+
|
206 |
+
data.append(record)
|
207 |
+
|
208 |
+
df = pd.DataFrame(data)
|
209 |
+
st.dataframe(df, use_container_width=True, hide_index=True)
|
210 |
+
|
211 |
+
# Edit and delete options
|
212 |
+
col1, col2 = st.columns(2)
|
213 |
+
|
214 |
+
with col1:
|
215 |
+
selected_index = st.selectbox(
|
216 |
+
f"Select {load_type} Load # to Edit",
|
217 |
+
range(1, len(loads) + 1),
|
218 |
+
key=f"edit_{load_type}_selector"
|
219 |
+
)
|
220 |
+
|
221 |
+
if st.button(f"Edit {load_type} Load", key=f"edit_{load_type}_button"):
|
222 |
+
# Load data into editor
|
223 |
+
load_data = loads[selected_index - 1]
|
224 |
+
st.session_state.load_editor = {
|
225 |
+
"type": load_type,
|
226 |
+
"name": load_data["name"],
|
227 |
+
"zone": load_data["zone"],
|
228 |
+
"area": load_data["area"],
|
229 |
+
"density": load_data["density"],
|
230 |
+
"total": load_data["total"],
|
231 |
+
"sensible_fraction": load_data.get("sensible_fraction", 0.7),
|
232 |
+
"latent_fraction": load_data.get("latent_fraction", 0.3),
|
233 |
+
"radiative_fraction": load_data.get("radiative_fraction", 0.4),
|
234 |
+
"convective_fraction": load_data.get("convective_fraction", 0.6),
|
235 |
+
"schedule_type": load_data["schedule_type"],
|
236 |
+
"custom_schedule": load_data.get("custom_schedule", {
|
237 |
+
"Weekday": [0.0] * 24,
|
238 |
+
"Weekend": [0.0] * 24
|
239 |
+
}),
|
240 |
+
"edit_mode": True,
|
241 |
+
"original_id": load_data["id"]
|
242 |
+
}
|
243 |
+
st.success(f"{load_type} Load '{load_data['name']}' loaded for editing.")
|
244 |
+
st.rerun()
|
245 |
+
|
246 |
+
with col2:
|
247 |
+
selected_index_delete = st.selectbox(
|
248 |
+
f"Select {load_type} Load # to Delete",
|
249 |
+
range(1, len(loads) + 1),
|
250 |
+
key=f"delete_{load_type}_selector"
|
251 |
+
)
|
252 |
+
|
253 |
+
if st.button(f"Delete {load_type} Load", key=f"delete_{load_type}_button"):
|
254 |
+
# Delete load
|
255 |
+
load_key = load_type.lower()
|
256 |
+
deleted_load = st.session_state.project_data["internal_loads"][load_key].pop(selected_index_delete - 1)
|
257 |
+
st.success(f"{load_type} Load '{deleted_load['name']}' deleted.")
|
258 |
+
logger.info(f"Deleted {load_type} Load '{deleted_load['name']}'")
|
259 |
+
st.rerun()
|
260 |
+
|
261 |
+
def display_load_editor(load_type: str):
|
262 |
+
"""
|
263 |
+
Display the editor form for a specific load type.
|
264 |
+
|
265 |
+
Args:
|
266 |
+
load_type: The type of load
|
267 |
+
"""
|
268 |
+
# Check if the editor is currently set to this load type
|
269 |
+
if st.session_state.load_editor["type"] != load_type and not st.session_state.load_editor["edit_mode"]:
|
270 |
+
reset_load_editor(load_type)
|
271 |
+
|
272 |
+
# Get building information
|
273 |
+
building_area = get_building_area()
|
274 |
+
building_type = get_building_type()
|
275 |
+
|
276 |
+
with st.form(f"{load_type}_editor_form"):
|
277 |
+
# Load name
|
278 |
+
name = st.text_input(
|
279 |
+
"Load Name",
|
280 |
+
value=st.session_state.load_editor["name"],
|
281 |
+
help="Enter a unique name for this load."
|
282 |
+
)
|
283 |
+
|
284 |
+
# Create columns for layout
|
285 |
+
col1, col2 = st.columns(2)
|
286 |
+
|
287 |
+
with col1:
|
288 |
+
# Zone selection
|
289 |
+
zones = st.session_state.project_data["custom_zones"]
|
290 |
+
zone = st.selectbox(
|
291 |
+
"Zone",
|
292 |
+
zones,
|
293 |
+
index=zones.index(st.session_state.load_editor["zone"]) if st.session_state.load_editor["zone"] in zones else 0,
|
294 |
+
help="Select the zone for this load."
|
295 |
+
)
|
296 |
+
|
297 |
+
# Area
|
298 |
+
area = st.number_input(
|
299 |
+
"Area (m²)",
|
300 |
+
min_value=0.1,
|
301 |
+
max_value=float(building_area),
|
302 |
+
value=min(float(st.session_state.load_editor["area"]), float(building_area)),
|
303 |
+
format="%.2f",
|
304 |
+
help="Floor area affected by this load."
|
305 |
+
)
|
306 |
+
|
307 |
+
with col2:
|
308 |
+
# Density or total selection
|
309 |
+
input_mode = st.radio(
|
310 |
+
"Input Mode",
|
311 |
+
["Density (W/m²)", "Total Load (W)"],
|
312 |
+
horizontal=True,
|
313 |
+
help="Choose whether to input load density or total load."
|
314 |
+
)
|
315 |
+
|
316 |
+
if input_mode == "Density (W/m²)":
|
317 |
+
# Set default density based on building type if not in edit mode
|
318 |
+
default_density = 0.0
|
319 |
+
if not st.session_state.load_editor["edit_mode"]:
|
320 |
+
if load_type == "Occupancy":
|
321 |
+
# For occupancy, we need to convert from m²/person to W/m²
|
322 |
+
people_density = 1.0 / DEFAULT_OCCUPANCY_DENSITIES.get(building_type, 10.0)
|
323 |
+
default_density = people_density * 115.0 # 115W per person (sensible + latent)
|
324 |
+
elif load_type == "Lighting":
|
325 |
+
default_density = DEFAULT_LIGHTING_DENSITIES.get(building_type, 12.0)
|
326 |
+
elif load_type == "Equipment":
|
327 |
+
default_density = DEFAULT_EQUIPMENT_DENSITIES.get(building_type, 15.0)
|
328 |
+
else: # Other
|
329 |
+
default_density = 5.0
|
330 |
+
else:
|
331 |
+
default_density = st.session_state.load_editor["density"]
|
332 |
+
|
333 |
+
density = st.number_input(
|
334 |
+
f"{load_type} Density (W/m²)",
|
335 |
+
min_value=0.0,
|
336 |
+
max_value=1000.0,
|
337 |
+
value=default_density,
|
338 |
+
format="%.2f",
|
339 |
+
help=f"Power density for {load_type.lower()}."
|
340 |
+
)
|
341 |
+
total = density * area
|
342 |
+
else:
|
343 |
+
# Total load input
|
344 |
+
default_total = st.session_state.load_editor["total"] if st.session_state.load_editor["edit_mode"] else 0.0
|
345 |
+
total = st.number_input(
|
346 |
+
f"Total {load_type} Load (W)",
|
347 |
+
min_value=0.0,
|
348 |
+
max_value=1000000.0,
|
349 |
+
value=default_total,
|
350 |
+
format="%.1f",
|
351 |
+
help=f"Total power for {load_type.lower()}."
|
352 |
+
)
|
353 |
+
density = total / area if area > 0 else 0.0
|
354 |
+
|
355 |
+
# Load-specific properties
|
356 |
+
st.subheader("Load Properties")
|
357 |
+
|
358 |
+
if load_type == "Occupancy":
|
359 |
+
col1, col2 = st.columns(2)
|
360 |
+
|
361 |
+
with col1:
|
362 |
+
sensible_fraction = st.slider(
|
363 |
+
"Sensible Heat Fraction",
|
364 |
+
min_value=0.0,
|
365 |
+
max_value=1.0,
|
366 |
+
value=float(st.session_state.load_editor["sensible_fraction"]),
|
367 |
+
format="%.2f",
|
368 |
+
help="Fraction of heat that is sensible (affects air temperature)."
|
369 |
+
)
|
370 |
+
|
371 |
+
with col2:
|
372 |
+
latent_fraction = st.slider(
|
373 |
+
"Latent Heat Fraction",
|
374 |
+
min_value=0.0,
|
375 |
+
max_value=1.0,
|
376 |
+
value=float(st.session_state.load_editor["latent_fraction"]),
|
377 |
+
format="%.2f",
|
378 |
+
help="Fraction of heat that is latent (affects humidity)."
|
379 |
+
)
|
380 |
+
|
381 |
+
# Ensure fractions sum to 1.0
|
382 |
+
if abs(sensible_fraction + latent_fraction - 1.0) > 0.01:
|
383 |
+
st.warning("Sensible and latent fractions should sum to 1.0. Values will be normalized.")
|
384 |
+
total_fraction = sensible_fraction + latent_fraction
|
385 |
+
if total_fraction > 0:
|
386 |
+
sensible_fraction = sensible_fraction / total_fraction
|
387 |
+
latent_fraction = latent_fraction / total_fraction
|
388 |
+
else:
|
389 |
+
sensible_fraction = 0.7
|
390 |
+
latent_fraction = 0.3
|
391 |
+
|
392 |
+
radiative_fraction = 0.0
|
393 |
+
convective_fraction = 0.0
|
394 |
+
else:
|
395 |
+
col1, col2 = st.columns(2)
|
396 |
+
|
397 |
+
with col1:
|
398 |
+
radiative_fraction = st.slider(
|
399 |
+
"Radiative Heat Fraction",
|
400 |
+
min_value=0.0,
|
401 |
+
max_value=1.0,
|
402 |
+
value=float(st.session_state.load_editor["radiative_fraction"]),
|
403 |
+
format="%.2f",
|
404 |
+
help="Fraction of heat that is radiative (affects surface temperatures)."
|
405 |
+
)
|
406 |
+
|
407 |
+
with col2:
|
408 |
+
convective_fraction = st.slider(
|
409 |
+
"Convective Heat Fraction",
|
410 |
+
min_value=0.0,
|
411 |
+
max_value=1.0,
|
412 |
+
value=float(st.session_state.load_editor["convective_fraction"]),
|
413 |
+
format="%.2f",
|
414 |
+
help="Fraction of heat that is convective (affects air temperature)."
|
415 |
+
)
|
416 |
+
|
417 |
+
# Ensure fractions sum to 1.0
|
418 |
+
if abs(radiative_fraction + convective_fraction - 1.0) > 0.01:
|
419 |
+
st.warning("Radiative and convective fractions should sum to 1.0. Values will be normalized.")
|
420 |
+
total_fraction = radiative_fraction + convective_fraction
|
421 |
+
if total_fraction > 0:
|
422 |
+
radiative_fraction = radiative_fraction / total_fraction
|
423 |
+
convective_fraction = convective_fraction / total_fraction
|
424 |
+
else:
|
425 |
+
radiative_fraction = 0.4
|
426 |
+
convective_fraction = 0.6
|
427 |
+
|
428 |
+
sensible_fraction = 1.0
|
429 |
+
latent_fraction = 0.0
|
430 |
+
|
431 |
+
# Schedule
|
432 |
+
st.subheader("Schedule")
|
433 |
+
|
434 |
+
schedule_type = st.selectbox(
|
435 |
+
"Schedule Type",
|
436 |
+
SCHEDULE_TYPES,
|
437 |
+
index=SCHEDULE_TYPES.index(st.session_state.load_editor["schedule_type"]) if st.session_state.load_editor["schedule_type"] in SCHEDULE_TYPES else 0,
|
438 |
+
help="Select the schedule type for this load."
|
439 |
+
)
|
440 |
+
|
441 |
+
if schedule_type == "Custom":
|
442 |
+
st.write("Define custom schedule for each day type:")
|
443 |
+
|
444 |
+
# Initialize custom schedule if not present
|
445 |
+
if "custom_schedule" not in st.session_state.load_editor or not st.session_state.load_editor["custom_schedule"]:
|
446 |
+
st.session_state.load_editor["custom_schedule"] = {
|
447 |
+
"Weekday": [0.0] * 24,
|
448 |
+
"Weekend": [0.0] * 24
|
449 |
+
}
|
450 |
+
|
451 |
+
# Create tabs for day types
|
452 |
+
day_tabs = st.tabs(DAYS_OF_WEEK)
|
453 |
+
|
454 |
+
for i, day_type in enumerate(DAYS_OF_WEEK):
|
455 |
+
with day_tabs[i]:
|
456 |
+
# Get current schedule values
|
457 |
+
current_values = st.session_state.load_editor["custom_schedule"].get(day_type, [0.0] * 24)
|
458 |
+
|
459 |
+
# Create sliders for each hour
|
460 |
+
for hour in range(0, 24, 3):
|
461 |
+
cols = st.columns(3)
|
462 |
+
for j in range(3):
|
463 |
+
if hour + j < 24:
|
464 |
+
with cols[j]:
|
465 |
+
hour_label = f"{hour + j:02d}:00 - {hour + j + 1:02d}:00"
|
466 |
+
current_values[hour + j] = st.slider(
|
467 |
+
hour_label,
|
468 |
+
min_value=0.0,
|
469 |
+
max_value=1.0,
|
470 |
+
value=float(current_values[hour + j]),
|
471 |
+
format="%.2f",
|
472 |
+
key=f"schedule_{day_type}_{hour + j}"
|
473 |
+
)
|
474 |
+
|
475 |
+
# Update custom schedule
|
476 |
+
st.session_state.load_editor["custom_schedule"][day_type] = current_values
|
477 |
+
|
478 |
+
# Display schedule as a chart
|
479 |
+
fig = px.bar(
|
480 |
+
x=list(range(24)),
|
481 |
+
y=current_values,
|
482 |
+
labels={"x": "Hour", "y": "Load Factor"},
|
483 |
+
title=f"{day_type} Schedule"
|
484 |
+
)
|
485 |
+
fig.update_layout(height=300)
|
486 |
+
st.plotly_chart(fig, use_container_width=True)
|
487 |
+
else:
|
488 |
+
# Display predefined schedule
|
489 |
+
if schedule_type in DEFAULT_SCHEDULES:
|
490 |
+
schedule_data = DEFAULT_SCHEDULES[schedule_type]
|
491 |
+
|
492 |
+
# Create tabs for day types
|
493 |
+
day_tabs = st.tabs(DAYS_OF_WEEK)
|
494 |
+
|
495 |
+
for i, day_type in enumerate(DAYS_OF_WEEK):
|
496 |
+
with day_tabs[i]:
|
497 |
+
# Display schedule as a chart
|
498 |
+
fig = px.bar(
|
499 |
+
x=list(range(24)),
|
500 |
+
y=schedule_data[day_type],
|
501 |
+
labels={"x": "Hour", "y": "Load Factor"},
|
502 |
+
title=f"{day_type} Schedule"
|
503 |
+
)
|
504 |
+
fig.update_layout(height=300)
|
505 |
+
st.plotly_chart(fig, use_container_width=True)
|
506 |
+
|
507 |
+
# Form submission buttons
|
508 |
+
col1, col2 = st.columns(2)
|
509 |
+
|
510 |
+
with col1:
|
511 |
+
submit_button = st.form_submit_button("Save Load")
|
512 |
+
|
513 |
+
with col2:
|
514 |
+
clear_button = st.form_submit_button("Clear Form")
|
515 |
+
|
516 |
+
# Handle form submission
|
517 |
+
if submit_button:
|
518 |
+
# Validate inputs
|
519 |
+
validation_errors = validate_load(
|
520 |
+
load_type, name, zone, area, density, total,
|
521 |
+
sensible_fraction, latent_fraction, radiative_fraction, convective_fraction,
|
522 |
+
schedule_type, st.session_state.load_editor["custom_schedule"] if schedule_type == "Custom" else None,
|
523 |
+
st.session_state.load_editor["edit_mode"], st.session_state.load_editor["original_id"]
|
524 |
+
)
|
525 |
+
|
526 |
+
if validation_errors:
|
527 |
+
# Display validation errors
|
528 |
+
for error in validation_errors:
|
529 |
+
st.error(error)
|
530 |
+
else:
|
531 |
+
# Create load data
|
532 |
+
load_data = {
|
533 |
+
"id": st.session_state.load_editor["original_id"] if st.session_state.load_editor["edit_mode"] else str(uuid.uuid4()),
|
534 |
+
"name": name,
|
535 |
+
"type": load_type,
|
536 |
+
"zone": zone,
|
537 |
+
"area": area,
|
538 |
+
"density": density,
|
539 |
+
"total": total,
|
540 |
+
"schedule_type": schedule_type
|
541 |
+
}
|
542 |
+
|
543 |
+
# Add load-specific properties
|
544 |
+
if load_type == "Occupancy":
|
545 |
+
load_data["sensible_fraction"] = sensible_fraction
|
546 |
+
load_data["latent_fraction"] = latent_fraction
|
547 |
+
else:
|
548 |
+
load_data["radiative_fraction"] = radiative_fraction
|
549 |
+
load_data["convective_fraction"] = convective_fraction
|
550 |
+
|
551 |
+
# Add custom schedule if applicable
|
552 |
+
if schedule_type == "Custom":
|
553 |
+
load_data["custom_schedule"] = st.session_state.load_editor["custom_schedule"]
|
554 |
+
|
555 |
+
# Handle edit mode
|
556 |
+
load_key = load_type.lower()
|
557 |
+
if st.session_state.load_editor["edit_mode"]:
|
558 |
+
# Find and update the load
|
559 |
+
loads = st.session_state.project_data["internal_loads"][load_key]
|
560 |
+
for i, load in enumerate(loads):
|
561 |
+
if load["id"] == st.session_state.load_editor["original_id"]:
|
562 |
+
loads[i] = load_data
|
563 |
+
break
|
564 |
+
st.success(f"{load_type} Load '{name}' updated successfully.")
|
565 |
+
logger.info(f"Updated {load_type} Load '{name}'")
|
566 |
+
else:
|
567 |
+
# Add new load
|
568 |
+
st.session_state.project_data["internal_loads"][load_key].append(load_data)
|
569 |
+
st.success(f"{load_type} Load '{name}' added successfully.")
|
570 |
+
logger.info(f"Added new {load_type} Load '{name}'")
|
571 |
+
|
572 |
+
# Reset editor
|
573 |
+
reset_load_editor(load_type)
|
574 |
+
st.rerun()
|
575 |
+
|
576 |
+
# Handle clear button
|
577 |
+
if clear_button:
|
578 |
+
reset_load_editor(load_type)
|
579 |
+
st.rerun()
|
580 |
+
|
581 |
+
def display_loads_summary():
|
582 |
+
"""Display a summary of all internal loads."""
|
583 |
+
# Get all loads
|
584 |
+
all_loads = []
|
585 |
+
for load_type in LOAD_TYPES:
|
586 |
+
load_key = load_type.lower()
|
587 |
+
loads = st.session_state.project_data["internal_loads"].get(load_key, [])
|
588 |
+
for load in loads:
|
589 |
+
all_loads.append({
|
590 |
+
"Type": load_type,
|
591 |
+
"Name": load["name"],
|
592 |
+
"Zone": load["zone"],
|
593 |
+
"Total (W)": load["total"]
|
594 |
+
})
|
595 |
+
|
596 |
+
if all_loads:
|
597 |
+
# Create a DataFrame for display
|
598 |
+
df = pd.DataFrame(all_loads)
|
599 |
+
|
600 |
+
# Calculate totals by type
|
601 |
+
totals_by_type = df.groupby("Type")["Total (W)"].sum().reset_index()
|
602 |
+
|
603 |
+
# Display summary table
|
604 |
+
st.subheader("Total Internal Loads by Type")
|
605 |
+
st.dataframe(totals_by_type, use_container_width=True, hide_index=True)
|
606 |
+
|
607 |
+
# Display pie chart
|
608 |
+
fig = px.pie(
|
609 |
+
totals_by_type,
|
610 |
+
values="Total (W)",
|
611 |
+
names="Type",
|
612 |
+
title="Internal Loads Distribution"
|
613 |
+
)
|
614 |
+
st.plotly_chart(fig, use_container_width=True)
|
615 |
+
|
616 |
+
# Calculate peak load profiles
|
617 |
+
st.subheader("Peak Load Profiles")
|
618 |
+
|
619 |
+
# Create tabs for day types
|
620 |
+
day_tabs = st.tabs(DAYS_OF_WEEK)
|
621 |
+
|
622 |
+
for i, day_type in enumerate(DAYS_OF_WEEK):
|
623 |
+
with day_tabs[i]:
|
624 |
+
# Calculate hourly profiles for each load type
|
625 |
+
hourly_data = calculate_hourly_profiles(day_type)
|
626 |
+
|
627 |
+
if hourly_data:
|
628 |
+
# Create a stacked area chart
|
629 |
+
fig = go.Figure()
|
630 |
+
|
631 |
+
for load_type in LOAD_TYPES:
|
632 |
+
if load_type in hourly_data:
|
633 |
+
fig.add_trace(go.Scatter(
|
634 |
+
x=list(range(24)),
|
635 |
+
y=hourly_data[load_type],
|
636 |
+
mode='lines',
|
637 |
+
name=load_type,
|
638 |
+
stackgroup='one',
|
639 |
+
line=dict(width=0.5)
|
640 |
+
))
|
641 |
+
|
642 |
+
fig.update_layout(
|
643 |
+
title=f"{day_type} Hourly Load Profile",
|
644 |
+
xaxis_title="Hour",
|
645 |
+
yaxis_title="Load (W)",
|
646 |
+
legend_title="Load Type",
|
647 |
+
height=400
|
648 |
+
)
|
649 |
+
|
650 |
+
st.plotly_chart(fig, use_container_width=True)
|
651 |
+
else:
|
652 |
+
st.info("No load profiles available. Add loads to see hourly profiles.")
|
653 |
+
else:
|
654 |
+
st.info("No internal loads defined yet. Add loads in the tabs above to see a summary.")
|
655 |
+
|
656 |
+
def calculate_hourly_profiles(day_type: str) -> Dict[str, List[float]]:
|
657 |
+
"""
|
658 |
+
Calculate hourly load profiles for each load type.
|
659 |
+
|
660 |
+
Args:
|
661 |
+
day_type: Day type ("Weekday" or "Weekend")
|
662 |
+
|
663 |
+
Returns:
|
664 |
+
Dictionary of load type to hourly values
|
665 |
+
"""
|
666 |
+
hourly_data = {}
|
667 |
+
|
668 |
+
for load_type in LOAD_TYPES:
|
669 |
+
load_key = load_type.lower()
|
670 |
+
loads = st.session_state.project_data["internal_loads"].get(load_key, [])
|
671 |
+
|
672 |
+
if loads:
|
673 |
+
hourly_values = [0.0] * 24
|
674 |
+
|
675 |
+
for load in loads:
|
676 |
+
# Get schedule values
|
677 |
+
schedule_values = []
|
678 |
+
|
679 |
+
if load["schedule_type"] == "Custom" and "custom_schedule" in load:
|
680 |
+
schedule_values = load["custom_schedule"].get(day_type, [0.0] * 24)
|
681 |
+
elif load["schedule_type"] in DEFAULT_SCHEDULES:
|
682 |
+
schedule_values = DEFAULT_SCHEDULES[load["schedule_type"]].get(day_type, [0.0] * 24)
|
683 |
+
else:
|
684 |
+
schedule_values = [1.0] * 24
|
685 |
+
|
686 |
+
# Apply schedule to load
|
687 |
+
for hour in range(24):
|
688 |
+
hourly_values[hour] += load["total"] * schedule_values[hour]
|
689 |
+
|
690 |
+
hourly_data[load_type] = hourly_values
|
691 |
+
|
692 |
+
return hourly_data
|
693 |
+
|
694 |
+
def get_building_area() -> float:
|
695 |
+
"""
|
696 |
+
Get the total floor area from building information.
|
697 |
+
|
698 |
+
Returns:
|
699 |
+
Total floor area in m²
|
700 |
+
"""
|
701 |
+
if "building_info" in st.session_state.project_data and "floor_area" in st.session_state.project_data["building_info"]:
|
702 |
+
return st.session_state.project_data["building_info"]["floor_area"]
|
703 |
+
return 100.0 # Default value
|
704 |
+
|
705 |
+
def get_building_type() -> str:
|
706 |
+
"""
|
707 |
+
Get the building type from building information.
|
708 |
+
|
709 |
+
Returns:
|
710 |
+
Building type
|
711 |
+
"""
|
712 |
+
if "building_info" in st.session_state.project_data and "building_type" in st.session_state.project_data["building_info"]:
|
713 |
+
return st.session_state.project_data["building_info"]["building_type"]
|
714 |
+
return "Other" # Default value
|
715 |
+
|
716 |
+
def validate_load(
|
717 |
+
load_type: str, name: str, zone: str, area: float, density: float, total: float,
|
718 |
+
sensible_fraction: float, latent_fraction: float, radiative_fraction: float, convective_fraction: float,
|
719 |
+
schedule_type: str, custom_schedule: Optional[Dict[str, List[float]]], edit_mode: bool, original_id: str
|
720 |
+
) -> List[str]:
|
721 |
+
"""
|
722 |
+
Validate load inputs.
|
723 |
+
|
724 |
+
Args:
|
725 |
+
load_type: Type of load
|
726 |
+
name: Load name
|
727 |
+
zone: Zone name
|
728 |
+
area: Floor area
|
729 |
+
density: Power density
|
730 |
+
total: Total power
|
731 |
+
sensible_fraction: Sensible heat fraction
|
732 |
+
latent_fraction: Latent heat fraction
|
733 |
+
radiative_fraction: Radiative heat fraction
|
734 |
+
convective_fraction: Convective heat fraction
|
735 |
+
schedule_type: Schedule type
|
736 |
+
custom_schedule: Custom schedule data
|
737 |
+
edit_mode: Whether in edit mode
|
738 |
+
original_id: Original ID if in edit mode
|
739 |
+
|
740 |
+
Returns:
|
741 |
+
List of validation error messages, empty if all inputs are valid
|
742 |
+
"""
|
743 |
+
errors = []
|
744 |
+
|
745 |
+
# Validate name
|
746 |
+
if not name or name.strip() == "":
|
747 |
+
errors.append("Load name is required.")
|
748 |
+
|
749 |
+
# Check for name uniqueness within the same load type
|
750 |
+
load_key = load_type.lower()
|
751 |
+
loads = st.session_state.project_data["internal_loads"].get(load_key, [])
|
752 |
+
|
753 |
+
for load in loads:
|
754 |
+
if load["name"] == name and (not edit_mode or load["id"] != original_id):
|
755 |
+
errors.append(f"{load_type} Load name '{name}' already exists.")
|
756 |
+
break
|
757 |
+
|
758 |
+
# Validate zone
|
759 |
+
if not zone:
|
760 |
+
errors.append("Zone selection is required.")
|
761 |
+
|
762 |
+
# Validate area
|
763 |
+
if area <= 0:
|
764 |
+
errors.append("Area must be greater than zero.")
|
765 |
+
|
766 |
+
# Validate density and total
|
767 |
+
if density <= 0 and total <= 0:
|
768 |
+
errors.append("Either density or total load must be greater than zero.")
|
769 |
+
|
770 |
+
# Validate fractions
|
771 |
+
if load_type == "Occupancy":
|
772 |
+
if abs(sensible_fraction + latent_fraction - 1.0) > 0.01:
|
773 |
+
errors.append("Sensible and latent fractions should sum to 1.0.")
|
774 |
+
else:
|
775 |
+
if abs(radiative_fraction + convective_fraction - 1.0) > 0.01:
|
776 |
+
errors.append("Radiative and convective fractions should sum to 1.0.")
|
777 |
+
|
778 |
+
# Validate schedule
|
779 |
+
if schedule_type == "Custom":
|
780 |
+
if not custom_schedule:
|
781 |
+
errors.append("Custom schedule data is missing.")
|
782 |
+
else:
|
783 |
+
for day_type in DAYS_OF_WEEK:
|
784 |
+
if day_type not in custom_schedule or len(custom_schedule[day_type]) != 24:
|
785 |
+
errors.append(f"Custom schedule for {day_type} must have 24 hourly values.")
|
786 |
+
|
787 |
+
return errors
|
788 |
+
|
789 |
+
def reset_load_editor(load_type: str):
|
790 |
+
"""
|
791 |
+
Reset the load editor to default values for the given type.
|
792 |
+
|
793 |
+
Args:
|
794 |
+
load_type: The type of load
|
795 |
+
"""
|
796 |
+
# Get building information
|
797 |
+
building_area = get_building_area()
|
798 |
+
building_type = get_building_type()
|
799 |
+
|
800 |
+
# Set default density based on building type
|
801 |
+
default_density = 0.0
|
802 |
+
if load_type == "Occupancy":
|
803 |
+
# For occupancy, we need to convert from m²/person to W/m²
|
804 |
+
people_density = 1.0 / DEFAULT_OCCUPANCY_DENSITIES.get(building_type, 10.0)
|
805 |
+
default_density = people_density * 115.0 # 115W per person (sensible + latent)
|
806 |
+
elif load_type == "Lighting":
|
807 |
+
default_density = DEFAULT_LIGHTING_DENSITIES.get(building_type, 12.0)
|
808 |
+
elif load_type == "Equipment":
|
809 |
+
default_density = DEFAULT_EQUIPMENT_DENSITIES.get(building_type, 15.0)
|
810 |
+
else: # Other
|
811 |
+
default_density = 5.0
|
812 |
+
|
813 |
+
st.session_state.load_editor = {
|
814 |
+
"type": load_type,
|
815 |
+
"name": "",
|
816 |
+
"zone": "Whole Building",
|
817 |
+
"area": building_area,
|
818 |
+
"density": default_density,
|
819 |
+
"total": default_density * building_area,
|
820 |
+
"sensible_fraction": 0.7,
|
821 |
+
"latent_fraction": 0.3,
|
822 |
+
"radiative_fraction": 0.4,
|
823 |
+
"convective_fraction": 0.6,
|
824 |
+
"schedule_type": SCHEDULE_TYPES[0],
|
825 |
+
"custom_schedule": {
|
826 |
+
"Weekday": [0.0] * 24,
|
827 |
+
"Weekend": [0.0] * 24
|
828 |
+
},
|
829 |
+
"edit_mode": False,
|
830 |
+
"original_id": ""
|
831 |
+
}
|
832 |
+
|
833 |
+
def display_internal_loads_help():
|
834 |
+
"""
|
835 |
+
Display help information for the internal loads page.
|
836 |
+
"""
|
837 |
+
st.markdown("""
|
838 |
+
### Internal Loads Help
|
839 |
+
|
840 |
+
This section allows you to define the internal heat gains in your building from occupants, lighting, equipment, and other sources.
|
841 |
+
|
842 |
+
**Key Concepts:**
|
843 |
+
|
844 |
+
* **Internal Loads**: Heat gains inside the building that contribute to cooling loads and reduce heating loads.
|
845 |
+
* **Occupancy Loads**: Heat generated by people, including both sensible heat (affects air temperature) and latent heat (affects humidity).
|
846 |
+
* **Lighting Loads**: Heat generated by lighting fixtures, primarily as a combination of radiative and convective heat.
|
847 |
+
* **Equipment Loads**: Heat generated by appliances, computers, and other electrical equipment.
|
848 |
+
* **Schedules**: Time-based patterns that define when loads are active throughout the day.
|
849 |
+
|
850 |
+
**Load Properties:**
|
851 |
+
|
852 |
+
* **Density**: Power per unit area (W/m²).
|
853 |
+
* **Total**: Total power for the load (W).
|
854 |
+
* **Sensible Heat**: Heat that directly affects air temperature.
|
855 |
+
* **Latent Heat**: Heat that affects humidity (moisture in the air).
|
856 |
+
* **Radiative Heat**: Heat transferred by radiation to surfaces.
|
857 |
+
* **Convective Heat**: Heat transferred directly to the air.
|
858 |
+
|
859 |
+
**Schedules:**
|
860 |
+
|
861 |
+
* **Continuous**: Load is constant throughout the day.
|
862 |
+
* **Day/Night**: Load varies between day and night hours.
|
863 |
+
* **Custom**: Define your own hourly schedule for weekdays and weekends.
|
864 |
+
|
865 |
+
**Workflow:**
|
866 |
+
|
867 |
+
1. Select the tab for the load type you want to define (e.g., "Occupancy").
|
868 |
+
2. Use the editor to add new loads:
|
869 |
+
* Give the load a unique name.
|
870 |
+
* Select the zone it applies to.
|
871 |
+
* Enter the area and either the density or total load.
|
872 |
+
* Set the appropriate heat fractions.
|
873 |
+
* Choose or define a schedule.
|
874 |
+
3. Save the load.
|
875 |
+
4. Repeat for all internal load sources.
|
876 |
+
5. Review the summary to see the total internal loads and hourly profiles.
|
877 |
+
|
878 |
+
**Important:**
|
879 |
+
|
880 |
+
* Internal loads are a significant factor in cooling load calculations.
|
881 |
+
* Accurate schedules are essential for proper load calculations.
|
882 |
+
* The summary section shows the combined effect of all internal loads.
|
883 |
+
""")
|
app/materials_cost.py
ADDED
@@ -0,0 +1,889 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
HVAC Load Calculator - Materials Cost Module
|
3 |
+
|
4 |
+
This module handles the cost calculations for building materials,
|
5 |
+
lifecycle cost analysis including initial, replacement, and maintenance costs,
|
6 |
+
cost optimization recommendations, and economic payback period analysis.
|
7 |
+
|
8 |
+
Developed by: Dr Majed Abuseif, Deakin University
|
9 |
+
© 2025
|
10 |
+
"""
|
11 |
+
|
12 |
+
import streamlit as st
|
13 |
+
import pandas as pd
|
14 |
+
import numpy as np
|
15 |
+
import json
|
16 |
+
import logging
|
17 |
+
import plotly.graph_objects as go
|
18 |
+
import plotly.express as px
|
19 |
+
from typing import Dict, List, Any, Optional, Tuple, Union
|
20 |
+
from datetime import datetime
|
21 |
+
|
22 |
+
# Configure logging
|
23 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
24 |
+
logger = logging.getLogger(__name__)
|
25 |
+
|
26 |
+
# Constants
|
27 |
+
YEARS_FOR_ANALYSIS = 60 # Standard building lifecycle for cost analysis
|
28 |
+
DISCOUNT_RATE = 0.03 # Default discount rate for NPV calculations (3%)
|
29 |
+
INFLATION_RATE = 0.025 # Default inflation rate (2.5%)
|
30 |
+
|
31 |
+
# Default material costs ($/kg)
|
32 |
+
DEFAULT_MATERIAL_COSTS = {
|
33 |
+
"Concrete": 0.12,
|
34 |
+
"Steel": 2.50,
|
35 |
+
"Timber": 1.80,
|
36 |
+
"Brick": 0.45,
|
37 |
+
"Glass": 3.20,
|
38 |
+
"Aluminum": 4.50,
|
39 |
+
"Insulation (mineral wool)": 2.80,
|
40 |
+
"Insulation (EPS)": 3.50,
|
41 |
+
"Gypsum board": 1.20,
|
42 |
+
"Carpet": 15.00,
|
43 |
+
"Ceramic tile": 8.50,
|
44 |
+
"PVC": 3.80,
|
45 |
+
"Paint": 12.00,
|
46 |
+
"HVAC equipment": 25.00,
|
47 |
+
"Electrical equipment": 35.00,
|
48 |
+
"Plumbing fixtures": 18.00
|
49 |
+
}
|
50 |
+
|
51 |
+
# Default labor costs ($/kg for installation)
|
52 |
+
DEFAULT_LABOR_COSTS = {
|
53 |
+
"Concrete": 0.08,
|
54 |
+
"Steel": 1.20,
|
55 |
+
"Timber": 0.90,
|
56 |
+
"Brick": 0.35,
|
57 |
+
"Glass": 2.50,
|
58 |
+
"Aluminum": 1.80,
|
59 |
+
"Insulation (mineral wool)": 1.50,
|
60 |
+
"Insulation (EPS)": 1.20,
|
61 |
+
"Gypsum board": 2.00,
|
62 |
+
"Carpet": 8.00,
|
63 |
+
"Ceramic tile": 12.00,
|
64 |
+
"PVC": 2.50,
|
65 |
+
"Paint": 8.00,
|
66 |
+
"HVAC equipment": 15.00,
|
67 |
+
"Electrical equipment": 20.00,
|
68 |
+
"Plumbing fixtures": 12.00
|
69 |
+
}
|
70 |
+
|
71 |
+
def display_materials_cost_page():
|
72 |
+
"""
|
73 |
+
Display the materials cost page.
|
74 |
+
This is the main function called by main.py when the Materials Cost page is selected.
|
75 |
+
"""
|
76 |
+
st.title("Materials Cost Analysis")
|
77 |
+
|
78 |
+
# Display help information in an expandable section
|
79 |
+
with st.expander("Help & Information"):
|
80 |
+
display_materials_cost_help()
|
81 |
+
|
82 |
+
# Check if embodied energy has been calculated (for material inventory)
|
83 |
+
if "embodied_energy" not in st.session_state.project_data or not st.session_state.project_data["embodied_energy"].get("material_inventory"):
|
84 |
+
st.warning("Please complete the Embodied Energy analysis to generate material inventory before proceeding to Materials Cost analysis.")
|
85 |
+
|
86 |
+
# Navigation buttons
|
87 |
+
col1, col2 = st.columns(2)
|
88 |
+
with col1:
|
89 |
+
if st.button("Back to Embodied Energy", key="back_to_embodied_energy_mc"):
|
90 |
+
st.session_state.current_page = "Embodied Energy"
|
91 |
+
st.rerun()
|
92 |
+
return
|
93 |
+
|
94 |
+
# Initialize materials cost data if not present
|
95 |
+
initialize_materials_cost_data()
|
96 |
+
|
97 |
+
# Create tabs for different aspects of materials cost analysis
|
98 |
+
tabs = st.tabs(["Cost Parameters", "Cost Analysis", "Lifecycle Costs", "Cost Optimization"])
|
99 |
+
|
100 |
+
with tabs[0]:
|
101 |
+
display_cost_parameters_tab()
|
102 |
+
|
103 |
+
with tabs[1]:
|
104 |
+
display_cost_analysis_tab()
|
105 |
+
|
106 |
+
with tabs[2]:
|
107 |
+
display_lifecycle_costs_tab()
|
108 |
+
|
109 |
+
with tabs[3]:
|
110 |
+
display_cost_optimization_tab()
|
111 |
+
|
112 |
+
# Navigation buttons
|
113 |
+
col1, col2 = st.columns(2)
|
114 |
+
|
115 |
+
with col1:
|
116 |
+
if st.button("Back to Embodied Energy", key="back_to_embodied_energy"):
|
117 |
+
st.session_state.current_page = "Embodied Energy"
|
118 |
+
st.rerun()
|
119 |
+
|
120 |
+
with col2:
|
121 |
+
st.info("This is the final module in the HVAC Load Calculator.")
|
122 |
+
|
123 |
+
def initialize_materials_cost_data():
|
124 |
+
"""Initialize materials cost data in session state if not present."""
|
125 |
+
if "materials_cost" not in st.session_state.project_data:
|
126 |
+
st.session_state.project_data["materials_cost"] = {
|
127 |
+
"material_costs": DEFAULT_MATERIAL_COSTS.copy(),
|
128 |
+
"labor_costs": DEFAULT_LABOR_COSTS.copy(),
|
129 |
+
"economic_parameters": {
|
130 |
+
"discount_rate": DISCOUNT_RATE,
|
131 |
+
"inflation_rate": INFLATION_RATE,
|
132 |
+
"maintenance_rate": 0.02, # 2% of initial cost annually
|
133 |
+
"energy_cost_escalation": 0.03 # 3% annual energy cost increase
|
134 |
+
},
|
135 |
+
"results": None
|
136 |
+
}
|
137 |
+
|
138 |
+
def display_cost_parameters_tab():
|
139 |
+
"""Display the cost parameters configuration tab."""
|
140 |
+
st.header("Cost Parameters Configuration")
|
141 |
+
|
142 |
+
# Get current cost data
|
143 |
+
material_costs = st.session_state.project_data["materials_cost"]["material_costs"]
|
144 |
+
labor_costs = st.session_state.project_data["materials_cost"]["labor_costs"]
|
145 |
+
economic_params = st.session_state.project_data["materials_cost"]["economic_parameters"]
|
146 |
+
|
147 |
+
# Economic parameters
|
148 |
+
st.subheader("Economic Parameters")
|
149 |
+
|
150 |
+
col1, col2 = st.columns(2)
|
151 |
+
|
152 |
+
with col1:
|
153 |
+
discount_rate = st.number_input(
|
154 |
+
"Discount Rate",
|
155 |
+
min_value=0.01,
|
156 |
+
max_value=0.15,
|
157 |
+
value=float(economic_params["discount_rate"]),
|
158 |
+
step=0.005,
|
159 |
+
format="%.3f",
|
160 |
+
help="Annual discount rate for net present value calculations."
|
161 |
+
)
|
162 |
+
|
163 |
+
maintenance_rate = st.number_input(
|
164 |
+
"Annual Maintenance Rate",
|
165 |
+
min_value=0.005,
|
166 |
+
max_value=0.1,
|
167 |
+
value=float(economic_params["maintenance_rate"]),
|
168 |
+
step=0.005,
|
169 |
+
format="%.3f",
|
170 |
+
help="Annual maintenance cost as a fraction of initial cost."
|
171 |
+
)
|
172 |
+
|
173 |
+
with col2:
|
174 |
+
inflation_rate = st.number_input(
|
175 |
+
"Inflation Rate",
|
176 |
+
min_value=0.01,
|
177 |
+
max_value=0.1,
|
178 |
+
value=float(economic_params["inflation_rate"]),
|
179 |
+
step=0.005,
|
180 |
+
format="%.3f",
|
181 |
+
help="Annual inflation rate for cost escalation."
|
182 |
+
)
|
183 |
+
|
184 |
+
energy_cost_escalation = st.number_input(
|
185 |
+
"Energy Cost Escalation Rate",
|
186 |
+
min_value=0.01,
|
187 |
+
max_value=0.1,
|
188 |
+
value=float(economic_params["energy_cost_escalation"]),
|
189 |
+
step=0.005,
|
190 |
+
format="%.3f",
|
191 |
+
help="Annual energy cost escalation rate."
|
192 |
+
)
|
193 |
+
|
194 |
+
# Update economic parameters
|
195 |
+
economic_params.update({
|
196 |
+
"discount_rate": discount_rate,
|
197 |
+
"inflation_rate": inflation_rate,
|
198 |
+
"maintenance_rate": maintenance_rate,
|
199 |
+
"energy_cost_escalation": energy_cost_escalation
|
200 |
+
})
|
201 |
+
|
202 |
+
# Material costs
|
203 |
+
st.subheader("Material Costs")
|
204 |
+
|
205 |
+
# Create a DataFrame for display and editing
|
206 |
+
cost_df = pd.DataFrame({
|
207 |
+
"Material": list(material_costs.keys()),
|
208 |
+
"Material Cost ($/kg)": list(material_costs.values()),
|
209 |
+
"Labor Cost ($/kg)": [labor_costs.get(material, 0) for material in material_costs.keys()]
|
210 |
+
})
|
211 |
+
|
212 |
+
# Display the costs table
|
213 |
+
edited_cost_df = st.data_editor(
|
214 |
+
cost_df,
|
215 |
+
column_config={
|
216 |
+
"Material": st.column_config.TextColumn("Material"),
|
217 |
+
"Material Cost ($/kg)": st.column_config.NumberColumn(
|
218 |
+
"Material Cost ($/kg)",
|
219 |
+
min_value=0.0,
|
220 |
+
max_value=100.0,
|
221 |
+
step=0.01,
|
222 |
+
format="%.2f"
|
223 |
+
),
|
224 |
+
"Labor Cost ($/kg)": st.column_config.NumberColumn(
|
225 |
+
"Labor Cost ($/kg)",
|
226 |
+
min_value=0.0,
|
227 |
+
max_value=50.0,
|
228 |
+
step=0.01,
|
229 |
+
format="%.2f"
|
230 |
+
)
|
231 |
+
},
|
232 |
+
use_container_width=True,
|
233 |
+
key="material_costs_editor"
|
234 |
+
)
|
235 |
+
|
236 |
+
# Update material and labor costs if edited
|
237 |
+
if not cost_df.equals(edited_cost_df):
|
238 |
+
updated_material_costs = dict(zip(edited_cost_df["Material"], edited_cost_df["Material Cost ($/kg)"]))
|
239 |
+
updated_labor_costs = dict(zip(edited_cost_df["Material"], edited_cost_df["Labor Cost ($/kg)"]))
|
240 |
+
|
241 |
+
st.session_state.project_data["materials_cost"]["material_costs"] = updated_material_costs
|
242 |
+
st.session_state.project_data["materials_cost"]["labor_costs"] = updated_labor_costs
|
243 |
+
|
244 |
+
material_costs = updated_material_costs
|
245 |
+
labor_costs = updated_labor_costs
|
246 |
+
|
247 |
+
# Calculate costs button
|
248 |
+
if st.button("Calculate Material Costs", key="calculate_material_costs"):
|
249 |
+
try:
|
250 |
+
results = calculate_material_costs()
|
251 |
+
st.session_state.project_data["materials_cost"]["results"] = results
|
252 |
+
st.success("Material costs calculated successfully.")
|
253 |
+
logger.info("Material costs calculated.")
|
254 |
+
st.rerun() # Refresh to show results
|
255 |
+
except Exception as e:
|
256 |
+
st.error(f"Error calculating material costs: {e}")
|
257 |
+
logger.error(f"Error calculating material costs: {e}", exc_info=True)
|
258 |
+
st.session_state.project_data["materials_cost"]["results"] = None
|
259 |
+
|
260 |
+
def display_cost_analysis_tab():
|
261 |
+
"""Display the cost analysis tab."""
|
262 |
+
st.header("Cost Analysis")
|
263 |
+
|
264 |
+
# Check if results are available
|
265 |
+
results = st.session_state.project_data["materials_cost"].get("results")
|
266 |
+
if not results:
|
267 |
+
st.info("Please calculate material costs using the Cost Parameters tab.")
|
268 |
+
return
|
269 |
+
|
270 |
+
# Display cost summary
|
271 |
+
st.subheader("Cost Summary")
|
272 |
+
|
273 |
+
# Get building info for normalization
|
274 |
+
building_info = st.session_state.project_data["building_info"]
|
275 |
+
floor_area = building_info.get("floor_area", 0)
|
276 |
+
|
277 |
+
col1, col2 = st.columns(2)
|
278 |
+
|
279 |
+
with col1:
|
280 |
+
st.metric(
|
281 |
+
"Total Initial Material Cost",
|
282 |
+
f"${results['total_initial_material_cost']:,.0f}",
|
283 |
+
help="Total cost of materials for initial construction."
|
284 |
+
)
|
285 |
+
|
286 |
+
with col2:
|
287 |
+
st.metric(
|
288 |
+
"Total Initial Labor Cost",
|
289 |
+
f"${results['total_initial_labor_cost']:,.0f}",
|
290 |
+
help="Total labor cost for initial construction."
|
291 |
+
)
|
292 |
+
|
293 |
+
col1, col2 = st.columns(2)
|
294 |
+
|
295 |
+
with col1:
|
296 |
+
st.metric(
|
297 |
+
"Total Initial Construction Cost",
|
298 |
+
f"${results['total_initial_construction_cost']:,.0f}",
|
299 |
+
help="Total initial construction cost (materials + labor)."
|
300 |
+
)
|
301 |
+
|
302 |
+
with col2:
|
303 |
+
st.metric(
|
304 |
+
"Construction Cost per Area",
|
305 |
+
f"${results['construction_cost_per_area']:,.0f}/m²",
|
306 |
+
help="Initial construction cost per square meter of floor area."
|
307 |
+
)
|
308 |
+
|
309 |
+
# Display cost breakdown by category
|
310 |
+
st.subheader("Cost Breakdown by Category")
|
311 |
+
|
312 |
+
# Create pie chart of initial costs by category
|
313 |
+
fig_material_category = px.pie(
|
314 |
+
values=list(results["initial_material_cost_by_category"].values()),
|
315 |
+
names=list(results["initial_material_cost_by_category"].keys()),
|
316 |
+
title="Initial Material Cost by Category"
|
317 |
+
)
|
318 |
+
st.plotly_chart(fig_material_category, use_container_width=True)
|
319 |
+
|
320 |
+
# Create pie chart of initial labor costs by category
|
321 |
+
fig_labor_category = px.pie(
|
322 |
+
values=list(results["initial_labor_cost_by_category"].values()),
|
323 |
+
names=list(results["initial_labor_cost_by_category"].keys()),
|
324 |
+
title="Initial Labor Cost by Category"
|
325 |
+
)
|
326 |
+
st.plotly_chart(fig_labor_category, use_container_width=True)
|
327 |
+
|
328 |
+
# Display cost breakdown by material
|
329 |
+
st.subheader("Top 10 Materials by Cost")
|
330 |
+
|
331 |
+
# Create bar chart of top 10 materials by total initial cost
|
332 |
+
material_total_costs = {}
|
333 |
+
for material, material_cost in results["initial_material_cost_by_material"].items():
|
334 |
+
labor_cost = results["initial_labor_cost_by_material"].get(material, 0)
|
335 |
+
material_total_costs[material] = material_cost + labor_cost
|
336 |
+
|
337 |
+
top_materials_df = pd.DataFrame({
|
338 |
+
"Material": list(material_total_costs.keys()),
|
339 |
+
"Total Initial Cost ($)": list(material_total_costs.values())
|
340 |
+
})
|
341 |
+
|
342 |
+
top_materials_df = top_materials_df.sort_values(
|
343 |
+
by="Total Initial Cost ($)",
|
344 |
+
ascending=False
|
345 |
+
).head(10)
|
346 |
+
|
347 |
+
fig_top_materials = px.bar(
|
348 |
+
top_materials_df,
|
349 |
+
x="Material",
|
350 |
+
y="Total Initial Cost ($)",
|
351 |
+
title="Top 10 Materials by Total Initial Cost"
|
352 |
+
)
|
353 |
+
st.plotly_chart(fig_top_materials, use_container_width=True)
|
354 |
+
|
355 |
+
def display_lifecycle_costs_tab():
|
356 |
+
"""Display the lifecycle costs analysis tab."""
|
357 |
+
st.header("Lifecycle Cost Analysis")
|
358 |
+
|
359 |
+
# Check if results are available
|
360 |
+
results = st.session_state.project_data["materials_cost"].get("results")
|
361 |
+
if not results:
|
362 |
+
st.info("Please calculate material costs using the Cost Parameters tab.")
|
363 |
+
return
|
364 |
+
|
365 |
+
# Display lifecycle cost summary
|
366 |
+
st.subheader("Lifecycle Cost Summary")
|
367 |
+
|
368 |
+
col1, col2 = st.columns(2)
|
369 |
+
|
370 |
+
with col1:
|
371 |
+
st.metric(
|
372 |
+
f"Total Lifecycle Material Cost ({YEARS_FOR_ANALYSIS} years)",
|
373 |
+
f"${results['total_lifecycle_material_cost']:,.0f}",
|
374 |
+
help=f"Total material cost over {YEARS_FOR_ANALYSIS} years including replacements."
|
375 |
+
)
|
376 |
+
|
377 |
+
with col2:
|
378 |
+
st.metric(
|
379 |
+
f"Total Lifecycle Construction Cost ({YEARS_FOR_ANALYSIS} years)",
|
380 |
+
f"${results['total_lifecycle_construction_cost']:,.0f}",
|
381 |
+
help=f"Total construction cost over {YEARS_FOR_ANALYSIS} years including materials, labor, and maintenance."
|
382 |
+
)
|
383 |
+
|
384 |
+
# Display net present value
|
385 |
+
col1, col2 = st.columns(2)
|
386 |
+
|
387 |
+
with col1:
|
388 |
+
st.metric(
|
389 |
+
"Net Present Value (NPV)",
|
390 |
+
f"${results['net_present_value']:,.0f}",
|
391 |
+
help="Net present value of all lifecycle costs."
|
392 |
+
)
|
393 |
+
|
394 |
+
with col2:
|
395 |
+
st.metric(
|
396 |
+
"Annualized Cost",
|
397 |
+
f"${results['annualized_cost']:,.0f}/year",
|
398 |
+
help=f"Average annual cost over {YEARS_FOR_ANALYSIS} years."
|
399 |
+
)
|
400 |
+
|
401 |
+
# Display lifecycle cost breakdown
|
402 |
+
st.subheader("Lifecycle Cost Breakdown")
|
403 |
+
|
404 |
+
# Create pie chart of lifecycle costs
|
405 |
+
lifecycle_components = {
|
406 |
+
"Initial Materials": results["total_initial_material_cost"],
|
407 |
+
"Initial Labor": results["total_initial_labor_cost"],
|
408 |
+
"Replacement Materials": results["total_lifecycle_material_cost"] - results["total_initial_material_cost"],
|
409 |
+
"Replacement Labor": results["total_lifecycle_labor_cost"] - results["total_initial_labor_cost"],
|
410 |
+
"Maintenance": results["total_maintenance_cost"]
|
411 |
+
}
|
412 |
+
|
413 |
+
fig_lifecycle = px.pie(
|
414 |
+
values=list(lifecycle_components.values()),
|
415 |
+
names=list(lifecycle_components.keys()),
|
416 |
+
title=f"Lifecycle Cost Breakdown ({YEARS_FOR_ANALYSIS} years)"
|
417 |
+
)
|
418 |
+
st.plotly_chart(fig_lifecycle, use_container_width=True)
|
419 |
+
|
420 |
+
# Display cost over time
|
421 |
+
st.subheader("Cost Accumulation Over Time")
|
422 |
+
|
423 |
+
# Create line chart of cumulative costs over time
|
424 |
+
years = list(range(0, YEARS_FOR_ANALYSIS + 1, 5))
|
425 |
+
cumulative_costs = [results["cumulative_costs"].get(str(year), 0) for year in years]
|
426 |
+
|
427 |
+
fig_over_time = px.line(
|
428 |
+
x=years,
|
429 |
+
y=cumulative_costs,
|
430 |
+
title="Cumulative Costs Over Time",
|
431 |
+
labels={"x": "Year", "y": "Cumulative Cost ($)"}
|
432 |
+
)
|
433 |
+
st.plotly_chart(fig_over_time, use_container_width=True)
|
434 |
+
|
435 |
+
# Compare with energy costs if available
|
436 |
+
if "building_energy" in st.session_state.project_data and st.session_state.project_data["building_energy"].get("results"):
|
437 |
+
st.subheader("Construction vs. Energy Costs")
|
438 |
+
|
439 |
+
building_energy_results = st.session_state.project_data["building_energy"]["results"]
|
440 |
+
annual_energy_cost = building_energy_results["annual_energy_cost"]
|
441 |
+
|
442 |
+
# Calculate total energy cost over lifecycle
|
443 |
+
economic_params = st.session_state.project_data["materials_cost"]["economic_parameters"]
|
444 |
+
energy_escalation = economic_params["energy_cost_escalation"]
|
445 |
+
discount_rate = economic_params["discount_rate"]
|
446 |
+
|
447 |
+
total_energy_cost = 0
|
448 |
+
for year in range(1, YEARS_FOR_ANALYSIS + 1):
|
449 |
+
escalated_cost = annual_energy_cost * ((1 + energy_escalation) ** year)
|
450 |
+
discounted_cost = escalated_cost / ((1 + discount_rate) ** year)
|
451 |
+
total_energy_cost += discounted_cost
|
452 |
+
|
453 |
+
# Create comparison chart
|
454 |
+
cost_comparison = {
|
455 |
+
"Construction (Lifecycle)": results["net_present_value"],
|
456 |
+
"Energy (Lifecycle)": total_energy_cost
|
457 |
+
}
|
458 |
+
|
459 |
+
fig_comparison = px.bar(
|
460 |
+
x=list(cost_comparison.keys()),
|
461 |
+
y=list(cost_comparison.values()),
|
462 |
+
title=f"Lifecycle Cost Comparison ({YEARS_FOR_ANALYSIS} years, NPV)",
|
463 |
+
labels={"x": "Cost Category", "y": "Net Present Value ($)"}
|
464 |
+
)
|
465 |
+
st.plotly_chart(fig_comparison, use_container_width=True)
|
466 |
+
|
467 |
+
# Display comparison metrics
|
468 |
+
col1, col2 = st.columns(2)
|
469 |
+
|
470 |
+
with col1:
|
471 |
+
st.metric(
|
472 |
+
"Construction NPV",
|
473 |
+
f"${results['net_present_value']:,.0f}",
|
474 |
+
help="Net present value of construction costs."
|
475 |
+
)
|
476 |
+
|
477 |
+
with col2:
|
478 |
+
st.metric(
|
479 |
+
"Energy NPV",
|
480 |
+
f"${total_energy_cost:,.0f}",
|
481 |
+
help="Net present value of energy costs."
|
482 |
+
)
|
483 |
+
|
484 |
+
def display_cost_optimization_tab():
|
485 |
+
"""Display the cost optimization recommendations tab."""
|
486 |
+
st.header("Cost Optimization")
|
487 |
+
|
488 |
+
# Check if results are available
|
489 |
+
results = st.session_state.project_data["materials_cost"].get("results")
|
490 |
+
if not results:
|
491 |
+
st.info("Please calculate material costs using the Cost Parameters tab.")
|
492 |
+
return
|
493 |
+
|
494 |
+
# Display cost optimization opportunities
|
495 |
+
st.subheader("Cost Optimization Opportunities")
|
496 |
+
|
497 |
+
# Identify high-cost materials
|
498 |
+
material_total_costs = {}
|
499 |
+
for material, material_cost in results["initial_material_cost_by_material"].items():
|
500 |
+
labor_cost = results["initial_labor_cost_by_material"].get(material, 0)
|
501 |
+
material_total_costs[material] = material_cost + labor_cost
|
502 |
+
|
503 |
+
# Sort materials by cost
|
504 |
+
sorted_materials = sorted(material_total_costs.items(), key=lambda x: x[1], reverse=True)
|
505 |
+
|
506 |
+
# Create optimization recommendations
|
507 |
+
optimization_data = []
|
508 |
+
|
509 |
+
for i, (material, cost) in enumerate(sorted_materials[:10]): # Top 10 materials
|
510 |
+
# Calculate potential savings
|
511 |
+
potential_savings_low = cost * 0.1 # 10% savings
|
512 |
+
potential_savings_high = cost * 0.3 # 30% savings
|
513 |
+
|
514 |
+
# Determine optimization strategy
|
515 |
+
if "Concrete" in material:
|
516 |
+
strategy = "Use high-performance concrete, optimize mix design"
|
517 |
+
difficulty = "Medium"
|
518 |
+
elif "Steel" in material:
|
519 |
+
strategy = "Optimize structural design, use high-strength steel"
|
520 |
+
difficulty = "High"
|
521 |
+
elif "Timber" in material:
|
522 |
+
strategy = "Use engineered wood products, optimize sizing"
|
523 |
+
difficulty = "Medium"
|
524 |
+
elif "Glass" in material:
|
525 |
+
strategy = "Optimize window sizing, use high-performance glazing"
|
526 |
+
difficulty = "Medium"
|
527 |
+
elif "Aluminum" in material:
|
528 |
+
strategy = "Optimize frame design, consider alternative materials"
|
529 |
+
difficulty = "Medium"
|
530 |
+
elif "Insulation" in material:
|
531 |
+
strategy = "Optimize insulation thickness, use cost-effective materials"
|
532 |
+
difficulty = "Low"
|
533 |
+
elif "equipment" in material:
|
534 |
+
strategy = "Right-size equipment, consider high-efficiency options"
|
535 |
+
difficulty = "Medium"
|
536 |
+
else:
|
537 |
+
strategy = "Value engineering, alternative materials"
|
538 |
+
difficulty = "Medium"
|
539 |
+
|
540 |
+
optimization_data.append({
|
541 |
+
"Rank": i + 1,
|
542 |
+
"Material": material,
|
543 |
+
"Current Cost": f"${cost:,.0f}",
|
544 |
+
"Potential Savings (10-30%)": f"${potential_savings_low:,.0f} - ${potential_savings_high:,.0f}",
|
545 |
+
"Optimization Strategy": strategy,
|
546 |
+
"Implementation Difficulty": difficulty
|
547 |
+
})
|
548 |
+
|
549 |
+
# Display optimization table
|
550 |
+
optimization_df = pd.DataFrame(optimization_data)
|
551 |
+
st.table(optimization_df)
|
552 |
+
|
553 |
+
# Display cost reduction scenarios
|
554 |
+
st.subheader("Cost Reduction Scenarios")
|
555 |
+
|
556 |
+
# Calculate different scenarios
|
557 |
+
current_cost = results["total_initial_construction_cost"]
|
558 |
+
|
559 |
+
scenarios = {
|
560 |
+
"Current Design": current_cost,
|
561 |
+
"5% Cost Reduction": current_cost * 0.95,
|
562 |
+
"10% Cost Reduction": current_cost * 0.90,
|
563 |
+
"15% Cost Reduction": current_cost * 0.85,
|
564 |
+
"20% Cost Reduction": current_cost * 0.80
|
565 |
+
}
|
566 |
+
|
567 |
+
# Create scenario comparison chart
|
568 |
+
fig_scenarios = px.bar(
|
569 |
+
x=list(scenarios.keys()),
|
570 |
+
y=list(scenarios.values()),
|
571 |
+
title="Cost Reduction Scenarios",
|
572 |
+
labels={"x": "Scenario", "y": "Initial Construction Cost ($)"}
|
573 |
+
)
|
574 |
+
st.plotly_chart(fig_scenarios, use_container_width=True)
|
575 |
+
|
576 |
+
# Display payback analysis if renewable energy is available
|
577 |
+
if "renewable_energy" in st.session_state.project_data and st.session_state.project_data["renewable_energy"].get("results"):
|
578 |
+
st.subheader("Economic Payback Analysis")
|
579 |
+
|
580 |
+
# Get renewable energy results
|
581 |
+
renewable_results = st.session_state.project_data["renewable_energy"]["results"]
|
582 |
+
building_energy_results = st.session_state.project_data["building_energy"]["results"]
|
583 |
+
|
584 |
+
# Calculate PV system cost
|
585 |
+
pv_system = st.session_state.project_data["renewable_energy"]["pv_system"]
|
586 |
+
pv_capacity = pv_system["system_capacity_kw"]
|
587 |
+
pv_cost_per_kw = pv_system["cost_per_kw"]
|
588 |
+
pv_system_cost = pv_capacity * pv_cost_per_kw
|
589 |
+
|
590 |
+
# Calculate annual energy savings
|
591 |
+
annual_pv_generation = renewable_results["annual_pv_generation"] # kWh
|
592 |
+
electricity_rate = building_energy_results["energy_rates"]["electricity"]["rate"] # $/kWh
|
593 |
+
annual_energy_savings = annual_pv_generation * electricity_rate
|
594 |
+
|
595 |
+
# Calculate simple payback period
|
596 |
+
if annual_energy_savings > 0:
|
597 |
+
simple_payback = pv_system_cost / annual_energy_savings
|
598 |
+
|
599 |
+
st.metric(
|
600 |
+
"PV System Simple Payback Period",
|
601 |
+
f"{simple_payback:.1f} years",
|
602 |
+
help="Years required for energy savings to offset PV system cost."
|
603 |
+
)
|
604 |
+
|
605 |
+
# Create payback chart
|
606 |
+
years = list(range(0, min(int(simple_payback * 2), 30) + 1))
|
607 |
+
pv_cost = [pv_system_cost] * len(years)
|
608 |
+
energy_savings = [year * annual_energy_savings for year in years]
|
609 |
+
|
610 |
+
fig_payback = go.Figure()
|
611 |
+
|
612 |
+
fig_payback.add_trace(go.Scatter(
|
613 |
+
x=years,
|
614 |
+
y=pv_cost,
|
615 |
+
mode="lines",
|
616 |
+
name="PV System Cost"
|
617 |
+
))
|
618 |
+
|
619 |
+
fig_payback.add_trace(go.Scatter(
|
620 |
+
x=years,
|
621 |
+
y=energy_savings,
|
622 |
+
mode="lines",
|
623 |
+
name="Cumulative Energy Savings"
|
624 |
+
))
|
625 |
+
|
626 |
+
fig_payback.update_layout(
|
627 |
+
title="Economic Payback Analysis",
|
628 |
+
xaxis_title="Year",
|
629 |
+
yaxis_title="Cost/Savings ($)"
|
630 |
+
)
|
631 |
+
|
632 |
+
st.plotly_chart(fig_payback, use_container_width=True)
|
633 |
+
else:
|
634 |
+
st.warning("No energy savings calculated. Cannot determine payback period.")
|
635 |
+
|
636 |
+
# Display general cost optimization strategies
|
637 |
+
st.subheader("General Cost Optimization Strategies")
|
638 |
+
|
639 |
+
strategies_data = {
|
640 |
+
"Strategy": [
|
641 |
+
"Value Engineering",
|
642 |
+
"Design Optimization",
|
643 |
+
"Material Substitution",
|
644 |
+
"Bulk Purchasing",
|
645 |
+
"Construction Sequencing",
|
646 |
+
"Energy Efficiency Measures",
|
647 |
+
"Lifecycle Cost Analysis",
|
648 |
+
"Standardization"
|
649 |
+
],
|
650 |
+
"Potential Savings": [
|
651 |
+
"10-20%",
|
652 |
+
"5-15%",
|
653 |
+
"5-25%",
|
654 |
+
"2-8%",
|
655 |
+
"3-10%",
|
656 |
+
"Long-term savings",
|
657 |
+
"Optimize total cost",
|
658 |
+
"5-15%"
|
659 |
+
],
|
660 |
+
"Implementation Phase": [
|
661 |
+
"Design",
|
662 |
+
"Design",
|
663 |
+
"Design/Procurement",
|
664 |
+
"Procurement",
|
665 |
+
"Construction",
|
666 |
+
"Design",
|
667 |
+
"Design",
|
668 |
+
"Design"
|
669 |
+
]
|
670 |
+
}
|
671 |
+
|
672 |
+
strategies_df = pd.DataFrame(strategies_data)
|
673 |
+
st.table(strategies_df)
|
674 |
+
|
675 |
+
def calculate_material_costs() -> Dict[str, Any]:
|
676 |
+
"""
|
677 |
+
Calculate material costs based on material inventory and cost parameters.
|
678 |
+
|
679 |
+
Returns:
|
680 |
+
Dictionary containing material cost results.
|
681 |
+
"""
|
682 |
+
logger.info("Starting material cost calculations...")
|
683 |
+
|
684 |
+
# Get required data
|
685 |
+
material_inventory = st.session_state.project_data["embodied_energy"]["material_inventory"]
|
686 |
+
material_costs = st.session_state.project_data["materials_cost"]["material_costs"]
|
687 |
+
labor_costs = st.session_state.project_data["materials_cost"]["labor_costs"]
|
688 |
+
economic_params = st.session_state.project_data["materials_cost"]["economic_parameters"]
|
689 |
+
|
690 |
+
# Get economic parameters
|
691 |
+
discount_rate = economic_params["discount_rate"]
|
692 |
+
inflation_rate = economic_params["inflation_rate"]
|
693 |
+
maintenance_rate = economic_params["maintenance_rate"]
|
694 |
+
|
695 |
+
# Initialize results
|
696 |
+
initial_material_cost_by_material = {}
|
697 |
+
initial_labor_cost_by_material = {}
|
698 |
+
initial_material_cost_by_category = {}
|
699 |
+
initial_labor_cost_by_category = {}
|
700 |
+
lifecycle_material_cost_by_category = {}
|
701 |
+
lifecycle_labor_cost_by_category = {}
|
702 |
+
cumulative_costs = {"0": 0}
|
703 |
+
|
704 |
+
# Calculate costs for each material
|
705 |
+
for item in material_inventory:
|
706 |
+
material_name = item["material_name"]
|
707 |
+
category = item["material_category"]
|
708 |
+
quantity = item["quantity"] # kg
|
709 |
+
replacement_cycle = item["replacement_cycle"] # years
|
710 |
+
|
711 |
+
# Get cost factors
|
712 |
+
material_cost_per_kg = material_costs.get(material_name, 0) # $/kg
|
713 |
+
labor_cost_per_kg = labor_costs.get(material_name, 0) # $/kg
|
714 |
+
|
715 |
+
# Calculate initial costs
|
716 |
+
initial_material_cost = quantity * material_cost_per_kg
|
717 |
+
initial_labor_cost = quantity * labor_cost_per_kg
|
718 |
+
|
719 |
+
# Add to material totals
|
720 |
+
if material_name in initial_material_cost_by_material:
|
721 |
+
initial_material_cost_by_material[material_name] += initial_material_cost
|
722 |
+
initial_labor_cost_by_material[material_name] += initial_labor_cost
|
723 |
+
else:
|
724 |
+
initial_material_cost_by_material[material_name] = initial_material_cost
|
725 |
+
initial_labor_cost_by_material[material_name] = initial_labor_cost
|
726 |
+
|
727 |
+
# Add to category totals
|
728 |
+
if category in initial_material_cost_by_category:
|
729 |
+
initial_material_cost_by_category[category] += initial_material_cost
|
730 |
+
initial_labor_cost_by_category[category] += initial_labor_cost
|
731 |
+
else:
|
732 |
+
initial_material_cost_by_category[category] = initial_material_cost
|
733 |
+
initial_labor_cost_by_category[category] = initial_labor_cost
|
734 |
+
|
735 |
+
# Calculate lifecycle costs with replacements and inflation
|
736 |
+
num_replacements = YEARS_FOR_ANALYSIS // replacement_cycle
|
737 |
+
|
738 |
+
# Initial cost
|
739 |
+
lifecycle_material_cost = initial_material_cost
|
740 |
+
lifecycle_labor_cost = initial_labor_cost
|
741 |
+
|
742 |
+
# Replacement costs
|
743 |
+
for replacement in range(1, num_replacements + 1):
|
744 |
+
replacement_year = replacement * replacement_cycle
|
745 |
+
|
746 |
+
# Apply inflation to replacement costs
|
747 |
+
inflated_material_cost = initial_material_cost * ((1 + inflation_rate) ** replacement_year)
|
748 |
+
inflated_labor_cost = initial_labor_cost * ((1 + inflation_rate) ** replacement_year)
|
749 |
+
|
750 |
+
# Discount to present value
|
751 |
+
discounted_material_cost = inflated_material_cost / ((1 + discount_rate) ** replacement_year)
|
752 |
+
discounted_labor_cost = inflated_labor_cost / ((1 + discount_rate) ** replacement_year)
|
753 |
+
|
754 |
+
lifecycle_material_cost += discounted_material_cost
|
755 |
+
lifecycle_labor_cost += discounted_labor_cost
|
756 |
+
|
757 |
+
# Add to lifecycle category totals
|
758 |
+
if category in lifecycle_material_cost_by_category:
|
759 |
+
lifecycle_material_cost_by_category[category] += lifecycle_material_cost
|
760 |
+
lifecycle_labor_cost_by_category[category] += lifecycle_labor_cost
|
761 |
+
else:
|
762 |
+
lifecycle_material_cost_by_category[category] = lifecycle_material_cost
|
763 |
+
lifecycle_labor_cost_by_category[category] = lifecycle_labor_cost
|
764 |
+
|
765 |
+
# Add to cumulative costs over time
|
766 |
+
cumulative_costs["0"] += initial_material_cost + initial_labor_cost
|
767 |
+
|
768 |
+
for replacement in range(1, num_replacements + 1):
|
769 |
+
replacement_year = replacement * replacement_cycle
|
770 |
+
year_str = str(replacement_year)
|
771 |
+
|
772 |
+
if year_str not in cumulative_costs:
|
773 |
+
# Find the previous year's cumulative cost
|
774 |
+
prev_year = replacement_year - replacement_cycle
|
775 |
+
cumulative_costs[year_str] = cumulative_costs[str(prev_year)]
|
776 |
+
|
777 |
+
# Add replacement cost (not discounted for cumulative display)
|
778 |
+
inflated_material_cost = initial_material_cost * ((1 + inflation_rate) ** replacement_year)
|
779 |
+
inflated_labor_cost = initial_labor_cost * ((1 + inflation_rate) ** replacement_year)
|
780 |
+
|
781 |
+
cumulative_costs[year_str] += inflated_material_cost + inflated_labor_cost
|
782 |
+
|
783 |
+
# Calculate totals
|
784 |
+
total_initial_material_cost = sum(initial_material_cost_by_material.values())
|
785 |
+
total_initial_labor_cost = sum(initial_labor_cost_by_material.values())
|
786 |
+
total_initial_construction_cost = total_initial_material_cost + total_initial_labor_cost
|
787 |
+
|
788 |
+
total_lifecycle_material_cost = sum(lifecycle_material_cost_by_category.values())
|
789 |
+
total_lifecycle_labor_cost = sum(lifecycle_labor_cost_by_category.values())
|
790 |
+
|
791 |
+
# Calculate maintenance costs
|
792 |
+
total_maintenance_cost = 0
|
793 |
+
for year in range(1, YEARS_FOR_ANALYSIS + 1):
|
794 |
+
annual_maintenance = total_initial_construction_cost * maintenance_rate
|
795 |
+
inflated_maintenance = annual_maintenance * ((1 + inflation_rate) ** year)
|
796 |
+
discounted_maintenance = inflated_maintenance / ((1 + discount_rate) ** year)
|
797 |
+
total_maintenance_cost += discounted_maintenance
|
798 |
+
|
799 |
+
total_lifecycle_construction_cost = total_lifecycle_material_cost + total_lifecycle_labor_cost + total_maintenance_cost
|
800 |
+
|
801 |
+
# Calculate net present value
|
802 |
+
net_present_value = total_lifecycle_construction_cost
|
803 |
+
|
804 |
+
# Calculate annualized cost
|
805 |
+
# Using capital recovery factor
|
806 |
+
capital_recovery_factor = (discount_rate * ((1 + discount_rate) ** YEARS_FOR_ANALYSIS)) / (((1 + discount_rate) ** YEARS_FOR_ANALYSIS) - 1)
|
807 |
+
annualized_cost = net_present_value * capital_recovery_factor
|
808 |
+
|
809 |
+
# Get building info for normalization
|
810 |
+
building_info = st.session_state.project_data["building_info"]
|
811 |
+
floor_area = building_info.get("floor_area", 0)
|
812 |
+
|
813 |
+
# Calculate normalized metrics
|
814 |
+
construction_cost_per_area = total_initial_construction_cost / floor_area if floor_area > 0 else 0
|
815 |
+
|
816 |
+
# Compile results
|
817 |
+
results = {
|
818 |
+
"initial_material_cost_by_material": initial_material_cost_by_material,
|
819 |
+
"initial_labor_cost_by_material": initial_labor_cost_by_material,
|
820 |
+
"initial_material_cost_by_category": initial_material_cost_by_category,
|
821 |
+
"initial_labor_cost_by_category": initial_labor_cost_by_category,
|
822 |
+
"lifecycle_material_cost_by_category": lifecycle_material_cost_by_category,
|
823 |
+
"lifecycle_labor_cost_by_category": lifecycle_labor_cost_by_category,
|
824 |
+
"cumulative_costs": cumulative_costs,
|
825 |
+
"total_initial_material_cost": total_initial_material_cost,
|
826 |
+
"total_initial_labor_cost": total_initial_labor_cost,
|
827 |
+
"total_initial_construction_cost": total_initial_construction_cost,
|
828 |
+
"total_lifecycle_material_cost": total_lifecycle_material_cost,
|
829 |
+
"total_lifecycle_labor_cost": total_lifecycle_labor_cost,
|
830 |
+
"total_maintenance_cost": total_maintenance_cost,
|
831 |
+
"total_lifecycle_construction_cost": total_lifecycle_construction_cost,
|
832 |
+
"net_present_value": net_present_value,
|
833 |
+
"annualized_cost": annualized_cost,
|
834 |
+
"construction_cost_per_area": construction_cost_per_area,
|
835 |
+
"calculation_timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
836 |
+
}
|
837 |
+
|
838 |
+
logger.info("Material cost calculations completed.")
|
839 |
+
return results
|
840 |
+
|
841 |
+
def display_materials_cost_help():
|
842 |
+
"""
|
843 |
+
Display help information for the materials cost page.
|
844 |
+
"""
|
845 |
+
st.markdown("""
|
846 |
+
### Materials Cost Analysis Help
|
847 |
+
|
848 |
+
This section calculates the costs associated with building materials, including initial construction costs, lifecycle costs, and economic optimization opportunities.
|
849 |
+
|
850 |
+
**Key Concepts:**
|
851 |
+
|
852 |
+
* **Material Costs**: Direct costs of materials ($/kg).
|
853 |
+
* **Labor Costs**: Installation labor costs ($/kg).
|
854 |
+
* **Lifecycle Costs**: Total costs over the building's lifespan including initial construction, replacements, and maintenance.
|
855 |
+
* **Net Present Value (NPV)**: Present value of all future costs, accounting for discount rate.
|
856 |
+
* **Discount Rate**: Interest rate used to calculate present value of future costs.
|
857 |
+
* **Inflation Rate**: Annual rate of cost increase.
|
858 |
+
* **Payback Period**: Time required for savings to offset initial investment.
|
859 |
+
|
860 |
+
**Workflow:**
|
861 |
+
|
862 |
+
1. **Cost Parameters Tab**:
|
863 |
+
* Configure economic parameters (discount rate, inflation rate, maintenance rate).
|
864 |
+
* Adjust material and labor costs for different materials.
|
865 |
+
* Click "Calculate Material Costs" to perform the analysis.
|
866 |
+
|
867 |
+
2. **Cost Analysis Tab**:
|
868 |
+
* Review initial construction costs and cost breakdown.
|
869 |
+
* Analyze costs by material category and individual materials.
|
870 |
+
* Identify the most expensive materials.
|
871 |
+
|
872 |
+
3. **Lifecycle Costs Tab**:
|
873 |
+
* Review total lifecycle costs including replacements and maintenance.
|
874 |
+
* Analyze cost accumulation over time.
|
875 |
+
* Compare construction costs with energy costs.
|
876 |
+
|
877 |
+
4. **Cost Optimization Tab**:
|
878 |
+
* Identify cost optimization opportunities.
|
879 |
+
* Review cost reduction scenarios.
|
880 |
+
* Analyze economic payback of renewable energy systems.
|
881 |
+
* Explore general cost optimization strategies.
|
882 |
+
|
883 |
+
**Important:**
|
884 |
+
|
885 |
+
* Accurate material quantities from the Embodied Energy module are essential.
|
886 |
+
* Cost data should reflect local market conditions.
|
887 |
+
* Lifecycle cost analysis helps identify the most cost-effective solutions.
|
888 |
+
* Consider both initial costs and long-term operational costs in decision-making.
|
889 |
+
""")
|
app/materials_library.py
ADDED
@@ -0,0 +1,1215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
HVAC Load Calculator - Material Library Module
|
3 |
+
|
4 |
+
This module handles the material library functionality of the HVAC Load Calculator application,
|
5 |
+
allowing users to manage building materials and fenestrations (windows, doors, skylights).
|
6 |
+
It provides both predefined library materials and the ability to create custom materials.
|
7 |
+
|
8 |
+
Developed by: Dr Majed Abuseif, Deakin University
|
9 |
+
© 2025
|
10 |
+
"""
|
11 |
+
|
12 |
+
import streamlit as st
|
13 |
+
import pandas as pd
|
14 |
+
import numpy as np
|
15 |
+
import json
|
16 |
+
import logging
|
17 |
+
import uuid
|
18 |
+
from typing import Dict, List, Any, Optional, Tuple, Union
|
19 |
+
|
20 |
+
# Configure logging
|
21 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
22 |
+
logger = logging.getLogger(__name__)
|
23 |
+
|
24 |
+
# Define constants
|
25 |
+
MATERIAL_CATEGORIES = [
|
26 |
+
"Finishing Materials",
|
27 |
+
"Structural Materials",
|
28 |
+
"Sub-Structural Materials",
|
29 |
+
"Insulation",
|
30 |
+
"Custom"
|
31 |
+
]
|
32 |
+
|
33 |
+
FENESTRATION_TYPES = [
|
34 |
+
"Window",
|
35 |
+
"Door",
|
36 |
+
"Skylight",
|
37 |
+
"Custom"
|
38 |
+
]
|
39 |
+
|
40 |
+
# Default library materials
|
41 |
+
DEFAULT_MATERIALS = {
|
42 |
+
"Brick": {
|
43 |
+
"category": "Structural Materials",
|
44 |
+
"thermal_conductivity": 0.72,
|
45 |
+
"density": 1920.0,
|
46 |
+
"specific_heat": 840.0,
|
47 |
+
"thickness_range": [0.05, 0.3],
|
48 |
+
"default_thickness": 0.1,
|
49 |
+
"embodied_carbon": 240.0, # kg CO2e/m³
|
50 |
+
"cost": 180.0 # USD/m³
|
51 |
+
},
|
52 |
+
"Concrete": {
|
53 |
+
"category": "Structural Materials",
|
54 |
+
"thermal_conductivity": 1.4,
|
55 |
+
"density": 2300.0,
|
56 |
+
"specific_heat": 880.0,
|
57 |
+
"thickness_range": [0.05, 0.5],
|
58 |
+
"default_thickness": 0.15,
|
59 |
+
"embodied_carbon": 320.0,
|
60 |
+
"cost": 120.0
|
61 |
+
},
|
62 |
+
"Gypsum Board": {
|
63 |
+
"category": "Finishing Materials",
|
64 |
+
"thermal_conductivity": 0.25,
|
65 |
+
"density": 900.0,
|
66 |
+
"specific_heat": 1000.0,
|
67 |
+
"thickness_range": [0.01, 0.05],
|
68 |
+
"default_thickness": 0.0125,
|
69 |
+
"embodied_carbon": 120.0,
|
70 |
+
"cost": 90.0
|
71 |
+
},
|
72 |
+
"Mineral Wool": {
|
73 |
+
"category": "Insulation",
|
74 |
+
"thermal_conductivity": 0.04,
|
75 |
+
"density": 30.0,
|
76 |
+
"specific_heat": 840.0,
|
77 |
+
"thickness_range": [0.025, 0.3],
|
78 |
+
"default_thickness": 0.1,
|
79 |
+
"embodied_carbon": 45.0,
|
80 |
+
"cost": 70.0
|
81 |
+
},
|
82 |
+
"EPS Insulation": {
|
83 |
+
"category": "Insulation",
|
84 |
+
"thermal_conductivity": 0.035,
|
85 |
+
"density": 25.0,
|
86 |
+
"specific_heat": 1400.0,
|
87 |
+
"thickness_range": [0.025, 0.3],
|
88 |
+
"default_thickness": 0.1,
|
89 |
+
"embodied_carbon": 88.0,
|
90 |
+
"cost": 65.0
|
91 |
+
},
|
92 |
+
"Wood (Pine)": {
|
93 |
+
"category": "Structural Materials",
|
94 |
+
"thermal_conductivity": 0.14,
|
95 |
+
"density": 550.0,
|
96 |
+
"specific_heat": 1600.0,
|
97 |
+
"thickness_range": [0.01, 0.3],
|
98 |
+
"default_thickness": 0.05,
|
99 |
+
"embodied_carbon": 110.0,
|
100 |
+
"cost": 250.0
|
101 |
+
},
|
102 |
+
"Steel": {
|
103 |
+
"category": "Structural Materials",
|
104 |
+
"thermal_conductivity": 50.0,
|
105 |
+
"density": 7800.0,
|
106 |
+
"specific_heat": 450.0,
|
107 |
+
"thickness_range": [0.001, 0.05],
|
108 |
+
"default_thickness": 0.005,
|
109 |
+
"embodied_carbon": 1750.0,
|
110 |
+
"cost": 800.0
|
111 |
+
},
|
112 |
+
"Aluminum": {
|
113 |
+
"category": "Structural Materials",
|
114 |
+
"thermal_conductivity": 230.0,
|
115 |
+
"density": 2700.0,
|
116 |
+
"specific_heat": 880.0,
|
117 |
+
"thickness_range": [0.001, 0.05],
|
118 |
+
"default_thickness": 0.003,
|
119 |
+
"embodied_carbon": 8500.0,
|
120 |
+
"cost": 1200.0
|
121 |
+
},
|
122 |
+
"Glass Fiber Insulation": {
|
123 |
+
"category": "Insulation",
|
124 |
+
"thermal_conductivity": 0.035,
|
125 |
+
"density": 12.0,
|
126 |
+
"specific_heat": 840.0,
|
127 |
+
"thickness_range": [0.025, 0.3],
|
128 |
+
"default_thickness": 0.1,
|
129 |
+
"embodied_carbon": 28.0,
|
130 |
+
"cost": 45.0
|
131 |
+
},
|
132 |
+
"Ceramic Tile": {
|
133 |
+
"category": "Finishing Materials",
|
134 |
+
"thermal_conductivity": 1.3,
|
135 |
+
"density": 2300.0,
|
136 |
+
"specific_heat": 840.0,
|
137 |
+
"thickness_range": [0.005, 0.025],
|
138 |
+
"default_thickness": 0.01,
|
139 |
+
"embodied_carbon": 650.0,
|
140 |
+
"cost": 280.0
|
141 |
+
},
|
142 |
+
"Carpet": {
|
143 |
+
"category": "Finishing Materials",
|
144 |
+
"thermal_conductivity": 0.06,
|
145 |
+
"density": 200.0,
|
146 |
+
"specific_heat": 1300.0,
|
147 |
+
"thickness_range": [0.005, 0.02],
|
148 |
+
"default_thickness": 0.01,
|
149 |
+
"embodied_carbon": 40.0,
|
150 |
+
"cost": 150.0
|
151 |
+
},
|
152 |
+
"Plywood": {
|
153 |
+
"category": "Sub-Structural Materials",
|
154 |
+
"thermal_conductivity": 0.13,
|
155 |
+
"density": 560.0,
|
156 |
+
"specific_heat": 1400.0,
|
157 |
+
"thickness_range": [0.006, 0.025],
|
158 |
+
"default_thickness": 0.012,
|
159 |
+
"embodied_carbon": 350.0,
|
160 |
+
"cost": 180.0
|
161 |
+
},
|
162 |
+
"Concrete Block": {
|
163 |
+
"category": "Structural Materials",
|
164 |
+
"thermal_conductivity": 0.51,
|
165 |
+
"density": 1400.0,
|
166 |
+
"specific_heat": 1000.0,
|
167 |
+
"thickness_range": [0.1, 0.3],
|
168 |
+
"default_thickness": 0.2,
|
169 |
+
"embodied_carbon": 260.0,
|
170 |
+
"cost": 140.0
|
171 |
+
},
|
172 |
+
"Stone (Granite)": {
|
173 |
+
"category": "Finishing Materials",
|
174 |
+
"thermal_conductivity": 2.8,
|
175 |
+
"density": 2600.0,
|
176 |
+
"specific_heat": 790.0,
|
177 |
+
"thickness_range": [0.01, 0.1],
|
178 |
+
"default_thickness": 0.03,
|
179 |
+
"embodied_carbon": 170.0,
|
180 |
+
"cost": 450.0
|
181 |
+
},
|
182 |
+
"Polyurethane Insulation": {
|
183 |
+
"category": "Insulation",
|
184 |
+
"thermal_conductivity": 0.025,
|
185 |
+
"density": 30.0,
|
186 |
+
"specific_heat": 1400.0,
|
187 |
+
"thickness_range": [0.025, 0.2],
|
188 |
+
"default_thickness": 0.075,
|
189 |
+
"embodied_carbon": 102.0,
|
190 |
+
"cost": 95.0
|
191 |
+
}
|
192 |
+
}
|
193 |
+
|
194 |
+
# Default library fenestrations
|
195 |
+
DEFAULT_FENESTRATIONS = {
|
196 |
+
"Single Glazing": {
|
197 |
+
"type": "Window",
|
198 |
+
"u_value": 5.8,
|
199 |
+
"shgc": 0.86,
|
200 |
+
"visible_transmittance": 0.9,
|
201 |
+
"thickness": 0.006,
|
202 |
+
"embodied_carbon": 800.0, # kg CO2e/m²
|
203 |
+
"cost": 120.0 # USD/m²
|
204 |
+
},
|
205 |
+
"Double Glazing (Air)": {
|
206 |
+
"type": "Window",
|
207 |
+
"u_value": 2.8,
|
208 |
+
"shgc": 0.72,
|
209 |
+
"visible_transmittance": 0.78,
|
210 |
+
"thickness": 0.024,
|
211 |
+
"embodied_carbon": 950.0,
|
212 |
+
"cost": 180.0
|
213 |
+
},
|
214 |
+
"Double Glazing (Argon)": {
|
215 |
+
"type": "Window",
|
216 |
+
"u_value": 1.4,
|
217 |
+
"shgc": 0.7,
|
218 |
+
"visible_transmittance": 0.76,
|
219 |
+
"thickness": 0.024,
|
220 |
+
"embodied_carbon": 980.0,
|
221 |
+
"cost": 220.0
|
222 |
+
},
|
223 |
+
"Triple Glazing": {
|
224 |
+
"type": "Window",
|
225 |
+
"u_value": 0.8,
|
226 |
+
"shgc": 0.5,
|
227 |
+
"visible_transmittance": 0.68,
|
228 |
+
"thickness": 0.036,
|
229 |
+
"embodied_carbon": 1100.0,
|
230 |
+
"cost": 320.0
|
231 |
+
},
|
232 |
+
"Low-E Double Glazing": {
|
233 |
+
"type": "Window",
|
234 |
+
"u_value": 1.8,
|
235 |
+
"shgc": 0.4,
|
236 |
+
"visible_transmittance": 0.74,
|
237 |
+
"thickness": 0.024,
|
238 |
+
"embodied_carbon": 1050.0,
|
239 |
+
"cost": 250.0
|
240 |
+
},
|
241 |
+
"Wooden Door": {
|
242 |
+
"type": "Door",
|
243 |
+
"u_value": 2.2,
|
244 |
+
"shgc": 0.0,
|
245 |
+
"visible_transmittance": 0.0,
|
246 |
+
"thickness": 0.04,
|
247 |
+
"embodied_carbon": 400.0,
|
248 |
+
"cost": 280.0
|
249 |
+
},
|
250 |
+
"Steel Door": {
|
251 |
+
"type": "Door",
|
252 |
+
"u_value": 3.1,
|
253 |
+
"shgc": 0.0,
|
254 |
+
"visible_transmittance": 0.0,
|
255 |
+
"thickness": 0.035,
|
256 |
+
"embodied_carbon": 1200.0,
|
257 |
+
"cost": 350.0
|
258 |
+
},
|
259 |
+
"Glass Door": {
|
260 |
+
"type": "Door",
|
261 |
+
"u_value": 3.8,
|
262 |
+
"shgc": 0.6,
|
263 |
+
"visible_transmittance": 0.7,
|
264 |
+
"thickness": 0.01,
|
265 |
+
"embodied_carbon": 900.0,
|
266 |
+
"cost": 420.0
|
267 |
+
},
|
268 |
+
"Skylight (Double Glazed)": {
|
269 |
+
"type": "Skylight",
|
270 |
+
"u_value": 3.0,
|
271 |
+
"shgc": 0.65,
|
272 |
+
"visible_transmittance": 0.75,
|
273 |
+
"thickness": 0.024,
|
274 |
+
"embodied_carbon": 1050.0,
|
275 |
+
"cost": 380.0
|
276 |
+
},
|
277 |
+
"Skylight (Low-E)": {
|
278 |
+
"type": "Skylight",
|
279 |
+
"u_value": 2.0,
|
280 |
+
"shgc": 0.35,
|
281 |
+
"visible_transmittance": 0.7,
|
282 |
+
"thickness": 0.024,
|
283 |
+
"embodied_carbon": 1150.0,
|
284 |
+
"cost": 450.0
|
285 |
+
}
|
286 |
+
}
|
287 |
+
|
288 |
+
def display_materials_page():
|
289 |
+
"""
|
290 |
+
Display the material library page.
|
291 |
+
This is the main function called by main.py when the Material Library page is selected.
|
292 |
+
"""
|
293 |
+
st.title("Material Library")
|
294 |
+
|
295 |
+
# Display help information in an expandable section
|
296 |
+
with st.expander("Help & Information"):
|
297 |
+
display_materials_help()
|
298 |
+
|
299 |
+
# Create tabs for materials and fenestrations
|
300 |
+
tab1, tab2 = st.tabs(["Materials", "Fenestrations"])
|
301 |
+
|
302 |
+
# Materials tab
|
303 |
+
with tab1:
|
304 |
+
display_materials_tab()
|
305 |
+
|
306 |
+
# Fenestrations tab
|
307 |
+
with tab2:
|
308 |
+
display_fenestrations_tab()
|
309 |
+
|
310 |
+
# Navigation buttons
|
311 |
+
col1, col2 = st.columns(2)
|
312 |
+
|
313 |
+
with col1:
|
314 |
+
if st.button("Back to Climate Data", key="back_to_climate"):
|
315 |
+
st.session_state.current_page = "Climate Data"
|
316 |
+
st.rerun()
|
317 |
+
|
318 |
+
with col2:
|
319 |
+
if st.button("Continue to Construction", key="continue_to_construction"):
|
320 |
+
st.session_state.current_page = "Construction"
|
321 |
+
st.rerun()
|
322 |
+
|
323 |
+
def display_materials_tab():
|
324 |
+
"""Display the materials tab content."""
|
325 |
+
# Initialize materials in session state if not present
|
326 |
+
initialize_materials()
|
327 |
+
|
328 |
+
# Create columns for library and project materials
|
329 |
+
col1, col2 = st.columns(2)
|
330 |
+
|
331 |
+
# Library Materials
|
332 |
+
with col1:
|
333 |
+
st.subheader("Library Materials")
|
334 |
+
display_library_materials()
|
335 |
+
|
336 |
+
# Project Materials
|
337 |
+
with col2:
|
338 |
+
st.subheader("Project Materials")
|
339 |
+
display_project_materials()
|
340 |
+
|
341 |
+
# Material Editor
|
342 |
+
st.markdown("---")
|
343 |
+
st.subheader("Material Editor")
|
344 |
+
display_material_editor()
|
345 |
+
|
346 |
+
def display_fenestrations_tab():
|
347 |
+
"""Display the fenestrations tab content."""
|
348 |
+
# Initialize fenestrations in session state if not present
|
349 |
+
initialize_fenestrations()
|
350 |
+
|
351 |
+
# Create columns for library and project fenestrations
|
352 |
+
col1, col2 = st.columns(2)
|
353 |
+
|
354 |
+
# Library Fenestrations
|
355 |
+
with col1:
|
356 |
+
st.subheader("Library Fenestrations")
|
357 |
+
display_library_fenestrations()
|
358 |
+
|
359 |
+
# Project Fenestrations
|
360 |
+
with col2:
|
361 |
+
st.subheader("Project Fenestrations")
|
362 |
+
display_project_fenestrations()
|
363 |
+
|
364 |
+
# Fenestration Editor
|
365 |
+
st.markdown("---")
|
366 |
+
st.subheader("Fenestration Editor")
|
367 |
+
display_fenestration_editor()
|
368 |
+
|
369 |
+
def initialize_materials():
|
370 |
+
"""Initialize materials in session state if not present."""
|
371 |
+
if "materials" not in st.session_state.project_data:
|
372 |
+
st.session_state.project_data["materials"] = {
|
373 |
+
"library": {},
|
374 |
+
"project": {}
|
375 |
+
}
|
376 |
+
|
377 |
+
# Initialize library materials if empty
|
378 |
+
if not st.session_state.project_data["materials"]["library"]:
|
379 |
+
st.session_state.project_data["materials"]["library"] = DEFAULT_MATERIALS.copy()
|
380 |
+
|
381 |
+
# Initialize material editor state
|
382 |
+
if "material_editor" not in st.session_state:
|
383 |
+
st.session_state.material_editor = {
|
384 |
+
"name": "",
|
385 |
+
"category": MATERIAL_CATEGORIES[0],
|
386 |
+
"thermal_conductivity": 0.5,
|
387 |
+
"density": 1000.0,
|
388 |
+
"specific_heat": 1000.0,
|
389 |
+
"thickness_range": [0.01, 0.2],
|
390 |
+
"default_thickness": 0.05,
|
391 |
+
"embodied_carbon": 100.0,
|
392 |
+
"cost": 100.0,
|
393 |
+
"edit_mode": False,
|
394 |
+
"original_name": ""
|
395 |
+
}
|
396 |
+
|
397 |
+
def initialize_fenestrations():
|
398 |
+
"""Initialize fenestrations in session state if not present."""
|
399 |
+
if "fenestrations" not in st.session_state.project_data:
|
400 |
+
st.session_state.project_data["fenestrations"] = {
|
401 |
+
"library": {},
|
402 |
+
"project": {}
|
403 |
+
}
|
404 |
+
|
405 |
+
# Initialize library fenestrations if empty
|
406 |
+
if not st.session_state.project_data["fenestrations"]["library"]:
|
407 |
+
st.session_state.project_data["fenestrations"]["library"] = DEFAULT_FENESTRATIONS.copy()
|
408 |
+
|
409 |
+
# Initialize fenestration editor state
|
410 |
+
if "fenestration_editor" not in st.session_state:
|
411 |
+
st.session_state.fenestration_editor = {
|
412 |
+
"name": "",
|
413 |
+
"type": FENESTRATION_TYPES[0],
|
414 |
+
"u_value": 2.8,
|
415 |
+
"shgc": 0.7,
|
416 |
+
"visible_transmittance": 0.8,
|
417 |
+
"thickness": 0.024,
|
418 |
+
"embodied_carbon": 900.0,
|
419 |
+
"cost": 200.0,
|
420 |
+
"edit_mode": False,
|
421 |
+
"original_name": ""
|
422 |
+
}
|
423 |
+
|
424 |
+
def display_library_materials():
|
425 |
+
"""Display the library materials section."""
|
426 |
+
# Filter options
|
427 |
+
category_filter = st.selectbox(
|
428 |
+
"Filter by Category",
|
429 |
+
["All"] + MATERIAL_CATEGORIES,
|
430 |
+
key="library_material_category_filter"
|
431 |
+
)
|
432 |
+
|
433 |
+
# Get library materials
|
434 |
+
library_materials = st.session_state.project_data["materials"]["library"]
|
435 |
+
|
436 |
+
# Apply filter
|
437 |
+
if category_filter != "All":
|
438 |
+
filtered_materials = {
|
439 |
+
name: props for name, props in library_materials.items()
|
440 |
+
if props["category"] == category_filter
|
441 |
+
}
|
442 |
+
else:
|
443 |
+
filtered_materials = library_materials
|
444 |
+
|
445 |
+
# Display materials in a table
|
446 |
+
if filtered_materials:
|
447 |
+
# Create a DataFrame for display
|
448 |
+
data = []
|
449 |
+
for name, props in filtered_materials.items():
|
450 |
+
data.append({
|
451 |
+
"Name": name,
|
452 |
+
"Category": props["category"],
|
453 |
+
"Thermal Conductivity (W/m·K)": props["thermal_conductivity"],
|
454 |
+
"Density (kg/m³)": props["density"],
|
455 |
+
"Specific Heat (J/kg·K)": props["specific_heat"]
|
456 |
+
})
|
457 |
+
|
458 |
+
df = pd.DataFrame(data)
|
459 |
+
st.dataframe(df, use_container_width=True, hide_index=True)
|
460 |
+
|
461 |
+
# Add to project button
|
462 |
+
selected_material = st.selectbox(
|
463 |
+
"Select Material to Add to Project",
|
464 |
+
list(filtered_materials.keys()),
|
465 |
+
key="library_material_selector"
|
466 |
+
)
|
467 |
+
|
468 |
+
if st.button("Add to Project", key="add_library_material_to_project"):
|
469 |
+
# Check if material already exists in project
|
470 |
+
if selected_material in st.session_state.project_data["materials"]["project"]:
|
471 |
+
st.warning(f"Material '{selected_material}' already exists in your project.")
|
472 |
+
else:
|
473 |
+
# Add to project materials
|
474 |
+
st.session_state.project_data["materials"]["project"][selected_material] = \
|
475 |
+
st.session_state.project_data["materials"]["library"][selected_material].copy()
|
476 |
+
st.success(f"Material '{selected_material}' added to your project.")
|
477 |
+
logger.info(f"Added library material '{selected_material}' to project")
|
478 |
+
else:
|
479 |
+
st.info("No materials found in the selected category.")
|
480 |
+
|
481 |
+
def display_project_materials():
|
482 |
+
"""Display the project materials section."""
|
483 |
+
# Get project materials
|
484 |
+
project_materials = st.session_state.project_data["materials"]["project"]
|
485 |
+
|
486 |
+
if project_materials:
|
487 |
+
# Create a DataFrame for display
|
488 |
+
data = []
|
489 |
+
for name, props in project_materials.items():
|
490 |
+
data.append({
|
491 |
+
"Name": name,
|
492 |
+
"Category": props["category"],
|
493 |
+
"Thermal Conductivity (W/m·K)": props["thermal_conductivity"],
|
494 |
+
"Density (kg/m³)": props["density"],
|
495 |
+
"Specific Heat (J/kg·K)": props["specific_heat"]
|
496 |
+
})
|
497 |
+
|
498 |
+
df = pd.DataFrame(data)
|
499 |
+
st.dataframe(df, use_container_width=True, hide_index=True)
|
500 |
+
|
501 |
+
# Edit and delete options
|
502 |
+
col1, col2 = st.columns(2)
|
503 |
+
|
504 |
+
with col1:
|
505 |
+
selected_material = st.selectbox(
|
506 |
+
"Select Material to Edit",
|
507 |
+
list(project_materials.keys()),
|
508 |
+
key="project_material_edit_selector"
|
509 |
+
)
|
510 |
+
|
511 |
+
if st.button("Edit Material", key="edit_project_material"):
|
512 |
+
# Load material data into editor
|
513 |
+
material_data = project_materials[selected_material]
|
514 |
+
st.session_state.material_editor = {
|
515 |
+
"name": selected_material,
|
516 |
+
"category": material_data["category"],
|
517 |
+
"thermal_conductivity": material_data["thermal_conductivity"],
|
518 |
+
"density": material_data["density"],
|
519 |
+
"specific_heat": material_data["specific_heat"],
|
520 |
+
"thickness_range": material_data["thickness_range"],
|
521 |
+
"default_thickness": material_data["default_thickness"],
|
522 |
+
"embodied_carbon": material_data["embodied_carbon"],
|
523 |
+
"cost": material_data["cost"],
|
524 |
+
"edit_mode": True,
|
525 |
+
"original_name": selected_material
|
526 |
+
}
|
527 |
+
st.success(f"Material '{selected_material}' loaded for editing.")
|
528 |
+
|
529 |
+
with col2:
|
530 |
+
selected_material_delete = st.selectbox(
|
531 |
+
"Select Material to Delete",
|
532 |
+
list(project_materials.keys()),
|
533 |
+
key="project_material_delete_selector"
|
534 |
+
)
|
535 |
+
|
536 |
+
if st.button("Delete Material", key="delete_project_material"):
|
537 |
+
# Check if material is in use
|
538 |
+
is_in_use = check_material_in_use(selected_material_delete)
|
539 |
+
|
540 |
+
if is_in_use:
|
541 |
+
st.error(f"Cannot delete material '{selected_material_delete}' because it is in use in constructions.")
|
542 |
+
else:
|
543 |
+
# Delete material
|
544 |
+
del st.session_state.project_data["materials"]["project"][selected_material_delete]
|
545 |
+
st.success(f"Material '{selected_material_delete}' deleted from your project.")
|
546 |
+
logger.info(f"Deleted material '{selected_material_delete}' from project")
|
547 |
+
else:
|
548 |
+
st.info("No materials in your project. Add materials from the library or create custom materials.")
|
549 |
+
|
550 |
+
def display_material_editor():
|
551 |
+
"""Display the material editor form."""
|
552 |
+
with st.form("material_editor_form"):
|
553 |
+
# Material name
|
554 |
+
name = st.text_input(
|
555 |
+
"Material Name",
|
556 |
+
value=st.session_state.material_editor["name"],
|
557 |
+
help="Enter a unique name for the material."
|
558 |
+
)
|
559 |
+
|
560 |
+
# Create two columns for layout
|
561 |
+
col1, col2 = st.columns(2)
|
562 |
+
|
563 |
+
with col1:
|
564 |
+
# Category
|
565 |
+
category = st.selectbox(
|
566 |
+
"Category",
|
567 |
+
MATERIAL_CATEGORIES,
|
568 |
+
index=MATERIAL_CATEGORIES.index(st.session_state.material_editor["category"]) if st.session_state.material_editor["category"] in MATERIAL_CATEGORIES else 0,
|
569 |
+
help="Select the material category."
|
570 |
+
)
|
571 |
+
|
572 |
+
# Thermal conductivity
|
573 |
+
thermal_conductivity = st.number_input(
|
574 |
+
"Thermal Conductivity (W/m·K)",
|
575 |
+
min_value=0.001,
|
576 |
+
max_value=1000.0,
|
577 |
+
value=float(st.session_state.material_editor["thermal_conductivity"]),
|
578 |
+
format="%.3f",
|
579 |
+
help="Thermal conductivity in W/m·K. Lower values indicate better insulation."
|
580 |
+
)
|
581 |
+
|
582 |
+
# Density
|
583 |
+
density = st.number_input(
|
584 |
+
"Density (kg/m³)",
|
585 |
+
min_value=1.0,
|
586 |
+
max_value=20000.0,
|
587 |
+
value=float(st.session_state.material_editor["density"]),
|
588 |
+
format="%.1f",
|
589 |
+
help="Material density in kg/m³."
|
590 |
+
)
|
591 |
+
|
592 |
+
with col2:
|
593 |
+
# Specific heat
|
594 |
+
specific_heat = st.number_input(
|
595 |
+
"Specific Heat (J/kg·K)",
|
596 |
+
min_value=100.0,
|
597 |
+
max_value=10000.0,
|
598 |
+
value=float(st.session_state.material_editor["specific_heat"]),
|
599 |
+
format="%.1f",
|
600 |
+
help="Specific heat capacity in J/kg·K. Higher values indicate better thermal mass."
|
601 |
+
)
|
602 |
+
|
603 |
+
# Thickness range
|
604 |
+
min_thickness, max_thickness = st.session_state.material_editor["thickness_range"]
|
605 |
+
thickness_range = st.slider(
|
606 |
+
"Thickness Range (m)",
|
607 |
+
min_value=0.001,
|
608 |
+
max_value=0.5,
|
609 |
+
value=(float(min_thickness), float(max_thickness)),
|
610 |
+
format="%.3f",
|
611 |
+
help="Minimum and maximum thickness range for this material in meters."
|
612 |
+
)
|
613 |
+
|
614 |
+
# Default thickness
|
615 |
+
default_thickness = st.number_input(
|
616 |
+
"Default Thickness (m)",
|
617 |
+
min_value=float(thickness_range[0]),
|
618 |
+
max_value=float(thickness_range[1]),
|
619 |
+
value=min(max(float(st.session_state.material_editor["default_thickness"]), float(thickness_range[0])), float(thickness_range[1])),
|
620 |
+
format="%.3f",
|
621 |
+
help="Default thickness for this material in meters."
|
622 |
+
)
|
623 |
+
|
624 |
+
# Additional properties for embodied carbon and cost
|
625 |
+
st.subheader("Additional Properties")
|
626 |
+
col1, col2 = st.columns(2)
|
627 |
+
|
628 |
+
with col1:
|
629 |
+
# Embodied carbon
|
630 |
+
embodied_carbon = st.number_input(
|
631 |
+
"Embodied Carbon (kg CO₂e/m³)",
|
632 |
+
min_value=0.0,
|
633 |
+
max_value=10000.0,
|
634 |
+
value=float(st.session_state.material_editor["embodied_carbon"]),
|
635 |
+
format="%.1f",
|
636 |
+
help="Embodied carbon in kg CO₂e per cubic meter."
|
637 |
+
)
|
638 |
+
|
639 |
+
with col2:
|
640 |
+
# Cost
|
641 |
+
cost = st.number_input(
|
642 |
+
"Cost (USD/m³)",
|
643 |
+
min_value=0.0,
|
644 |
+
max_value=10000.0,
|
645 |
+
value=float(st.session_state.material_editor["cost"]),
|
646 |
+
format="%.1f",
|
647 |
+
help="Material cost in USD per cubic meter."
|
648 |
+
)
|
649 |
+
|
650 |
+
# Form submission buttons
|
651 |
+
col1, col2 = st.columns(2)
|
652 |
+
|
653 |
+
with col1:
|
654 |
+
submit_button = st.form_submit_button("Save Material")
|
655 |
+
|
656 |
+
with col2:
|
657 |
+
clear_button = st.form_submit_button("Clear Form")
|
658 |
+
|
659 |
+
# Handle form submission
|
660 |
+
if submit_button:
|
661 |
+
# Validate inputs
|
662 |
+
validation_errors = validate_material(
|
663 |
+
name, category, thermal_conductivity, density, specific_heat,
|
664 |
+
thickness_range, default_thickness, embodied_carbon, cost,
|
665 |
+
st.session_state.material_editor["edit_mode"], st.session_state.material_editor["original_name"]
|
666 |
+
)
|
667 |
+
|
668 |
+
if validation_errors:
|
669 |
+
# Display validation errors
|
670 |
+
for error in validation_errors:
|
671 |
+
st.error(error)
|
672 |
+
else:
|
673 |
+
# Create material data
|
674 |
+
material_data = {
|
675 |
+
"category": category,
|
676 |
+
"thermal_conductivity": thermal_conductivity,
|
677 |
+
"density": density,
|
678 |
+
"specific_heat": specific_heat,
|
679 |
+
"thickness_range": list(thickness_range),
|
680 |
+
"default_thickness": default_thickness,
|
681 |
+
"embodied_carbon": embodied_carbon,
|
682 |
+
"cost": cost
|
683 |
+
}
|
684 |
+
|
685 |
+
# Handle edit mode
|
686 |
+
if st.session_state.material_editor["edit_mode"]:
|
687 |
+
original_name = st.session_state.material_editor["original_name"]
|
688 |
+
|
689 |
+
# If name changed, delete old entry and create new one
|
690 |
+
if original_name != name:
|
691 |
+
del st.session_state.project_data["materials"]["project"][original_name]
|
692 |
+
|
693 |
+
# Update material
|
694 |
+
st.session_state.project_data["materials"]["project"][name] = material_data
|
695 |
+
st.success(f"Material '{name}' updated successfully.")
|
696 |
+
logger.info(f"Updated material '{name}' in project")
|
697 |
+
else:
|
698 |
+
# Add new material
|
699 |
+
st.session_state.project_data["materials"]["project"][name] = material_data
|
700 |
+
st.success(f"Material '{name}' added to your project.")
|
701 |
+
logger.info(f"Added new material '{name}' to project")
|
702 |
+
|
703 |
+
# Reset editor
|
704 |
+
reset_material_editor()
|
705 |
+
|
706 |
+
# Handle clear button
|
707 |
+
if clear_button:
|
708 |
+
reset_material_editor()
|
709 |
+
st.rerun()
|
710 |
+
|
711 |
+
def display_library_fenestrations():
|
712 |
+
"""Display the library fenestrations section."""
|
713 |
+
# Filter options
|
714 |
+
type_filter = st.selectbox(
|
715 |
+
"Filter by Type",
|
716 |
+
["All"] + FENESTRATION_TYPES,
|
717 |
+
key="library_fenestration_type_filter"
|
718 |
+
)
|
719 |
+
|
720 |
+
# Get library fenestrations
|
721 |
+
library_fenestrations = st.session_state.project_data["fenestrations"]["library"]
|
722 |
+
|
723 |
+
# Apply filter
|
724 |
+
if type_filter != "All":
|
725 |
+
filtered_fenestrations = {
|
726 |
+
name: props for name, props in library_fenestrations.items()
|
727 |
+
if props["type"] == type_filter
|
728 |
+
}
|
729 |
+
else:
|
730 |
+
filtered_fenestrations = library_fenestrations
|
731 |
+
|
732 |
+
# Display fenestrations in a table
|
733 |
+
if filtered_fenestrations:
|
734 |
+
# Create a DataFrame for display
|
735 |
+
data = []
|
736 |
+
for name, props in filtered_fenestrations.items():
|
737 |
+
data.append({
|
738 |
+
"Name": name,
|
739 |
+
"Type": props["type"],
|
740 |
+
"U-Value (W/m²·K)": props["u_value"],
|
741 |
+
"SHGC": props["shgc"],
|
742 |
+
"Visible Transmittance": props["visible_transmittance"]
|
743 |
+
})
|
744 |
+
|
745 |
+
df = pd.DataFrame(data)
|
746 |
+
st.dataframe(df, use_container_width=True, hide_index=True)
|
747 |
+
|
748 |
+
# Add to project button
|
749 |
+
selected_fenestration = st.selectbox(
|
750 |
+
"Select Fenestration to Add to Project",
|
751 |
+
list(filtered_fenestrations.keys()),
|
752 |
+
key="library_fenestration_selector"
|
753 |
+
)
|
754 |
+
|
755 |
+
if st.button("Add to Project", key="add_library_fenestration_to_project"):
|
756 |
+
# Check if fenestration already exists in project
|
757 |
+
if selected_fenestration in st.session_state.project_data["fenestrations"]["project"]:
|
758 |
+
st.warning(f"Fenestration '{selected_fenestration}' already exists in your project.")
|
759 |
+
else:
|
760 |
+
# Add to project fenestrations
|
761 |
+
st.session_state.project_data["fenestrations"]["project"][selected_fenestration] = \
|
762 |
+
st.session_state.project_data["fenestrations"]["library"][selected_fenestration].copy()
|
763 |
+
st.success(f"Fenestration '{selected_fenestration}' added to your project.")
|
764 |
+
logger.info(f"Added library fenestration '{selected_fenestration}' to project")
|
765 |
+
else:
|
766 |
+
st.info("No fenestrations found in the selected type.")
|
767 |
+
|
768 |
+
def display_project_fenestrations():
|
769 |
+
"""Display the project fenestrations section."""
|
770 |
+
# Get project fenestrations
|
771 |
+
project_fenestrations = st.session_state.project_data["fenestrations"]["project"]
|
772 |
+
|
773 |
+
if project_fenestrations:
|
774 |
+
# Create a DataFrame for display
|
775 |
+
data = []
|
776 |
+
for name, props in project_fenestrations.items():
|
777 |
+
data.append({
|
778 |
+
"Name": name,
|
779 |
+
"Type": props["type"],
|
780 |
+
"U-Value (W/m²·K)": props["u_value"],
|
781 |
+
"SHGC": props["shgc"],
|
782 |
+
"Visible Transmittance": props["visible_transmittance"]
|
783 |
+
})
|
784 |
+
|
785 |
+
df = pd.DataFrame(data)
|
786 |
+
st.dataframe(df, use_container_width=True, hide_index=True)
|
787 |
+
|
788 |
+
# Edit and delete options
|
789 |
+
col1, col2 = st.columns(2)
|
790 |
+
|
791 |
+
with col1:
|
792 |
+
selected_fenestration = st.selectbox(
|
793 |
+
"Select Fenestration to Edit",
|
794 |
+
list(project_fenestrations.keys()),
|
795 |
+
key="project_fenestration_edit_selector"
|
796 |
+
)
|
797 |
+
|
798 |
+
if st.button("Edit Fenestration", key="edit_project_fenestration"):
|
799 |
+
# Load fenestration data into editor
|
800 |
+
fenestration_data = project_fenestrations[selected_fenestration]
|
801 |
+
st.session_state.fenestration_editor = {
|
802 |
+
"name": selected_fenestration,
|
803 |
+
"type": fenestration_data["type"],
|
804 |
+
"u_value": fenestration_data["u_value"],
|
805 |
+
"shgc": fenestration_data["shgc"],
|
806 |
+
"visible_transmittance": fenestration_data["visible_transmittance"],
|
807 |
+
"thickness": fenestration_data["thickness"],
|
808 |
+
"embodied_carbon": fenestration_data["embodied_carbon"],
|
809 |
+
"cost": fenestration_data["cost"],
|
810 |
+
"edit_mode": True,
|
811 |
+
"original_name": selected_fenestration
|
812 |
+
}
|
813 |
+
st.success(f"Fenestration '{selected_fenestration}' loaded for editing.")
|
814 |
+
|
815 |
+
with col2:
|
816 |
+
selected_fenestration_delete = st.selectbox(
|
817 |
+
"Select Fenestration to Delete",
|
818 |
+
list(project_fenestrations.keys()),
|
819 |
+
key="project_fenestration_delete_selector"
|
820 |
+
)
|
821 |
+
|
822 |
+
if st.button("Delete Fenestration", key="delete_project_fenestration"):
|
823 |
+
# Check if fenestration is in use
|
824 |
+
is_in_use = check_fenestration_in_use(selected_fenestration_delete)
|
825 |
+
|
826 |
+
if is_in_use:
|
827 |
+
st.error(f"Cannot delete fenestration '{selected_fenestration_delete}' because it is in use in components.")
|
828 |
+
else:
|
829 |
+
# Delete fenestration
|
830 |
+
del st.session_state.project_data["fenestrations"]["project"][selected_fenestration_delete]
|
831 |
+
st.success(f"Fenestration '{selected_fenestration_delete}' deleted from your project.")
|
832 |
+
logger.info(f"Deleted fenestration '{selected_fenestration_delete}' from project")
|
833 |
+
else:
|
834 |
+
st.info("No fenestrations in your project. Add fenestrations from the library or create custom fenestrations.")
|
835 |
+
|
836 |
+
def display_fenestration_editor():
|
837 |
+
"""Display the fenestration editor form."""
|
838 |
+
with st.form("fenestration_editor_form"):
|
839 |
+
# Fenestration name
|
840 |
+
name = st.text_input(
|
841 |
+
"Fenestration Name",
|
842 |
+
value=st.session_state.fenestration_editor["name"],
|
843 |
+
help="Enter a unique name for the fenestration."
|
844 |
+
)
|
845 |
+
|
846 |
+
# Create two columns for layout
|
847 |
+
col1, col2 = st.columns(2)
|
848 |
+
|
849 |
+
with col1:
|
850 |
+
# Type
|
851 |
+
fenestration_type = st.selectbox(
|
852 |
+
"Type",
|
853 |
+
FENESTRATION_TYPES,
|
854 |
+
index=FENESTRATION_TYPES.index(st.session_state.fenestration_editor["type"]) if st.session_state.fenestration_editor["type"] in FENESTRATION_TYPES else 0,
|
855 |
+
help="Select the fenestration type."
|
856 |
+
)
|
857 |
+
|
858 |
+
# U-value
|
859 |
+
u_value = st.number_input(
|
860 |
+
"U-Value (W/m²·K)",
|
861 |
+
min_value=0.1,
|
862 |
+
max_value=10.0,
|
863 |
+
value=float(st.session_state.fenestration_editor["u_value"]),
|
864 |
+
format="%.2f",
|
865 |
+
help="U-value in W/m²·K. Lower values indicate better insulation."
|
866 |
+
)
|
867 |
+
|
868 |
+
# SHGC
|
869 |
+
shgc = st.number_input(
|
870 |
+
"Solar Heat Gain Coefficient (SHGC)",
|
871 |
+
min_value=0.0,
|
872 |
+
max_value=1.0,
|
873 |
+
value=float(st.session_state.fenestration_editor["shgc"]),
|
874 |
+
format="%.2f",
|
875 |
+
help="Solar Heat Gain Coefficient (0-1). Lower values indicate less solar heat transmission."
|
876 |
+
)
|
877 |
+
|
878 |
+
with col2:
|
879 |
+
# Visible transmittance
|
880 |
+
visible_transmittance = st.number_input(
|
881 |
+
"Visible Transmittance",
|
882 |
+
min_value=0.0,
|
883 |
+
max_value=1.0,
|
884 |
+
value=float(st.session_state.fenestration_editor["visible_transmittance"]),
|
885 |
+
format="%.2f",
|
886 |
+
help="Visible Transmittance (0-1). Higher values indicate more visible light transmission."
|
887 |
+
)
|
888 |
+
|
889 |
+
# Thickness
|
890 |
+
thickness = st.number_input(
|
891 |
+
"Thickness (m)",
|
892 |
+
min_value=0.001,
|
893 |
+
max_value=0.1,
|
894 |
+
value=float(st.session_state.fenestration_editor["thickness"]),
|
895 |
+
format="%.3f",
|
896 |
+
help="Thickness in meters."
|
897 |
+
)
|
898 |
+
|
899 |
+
# Additional properties for embodied carbon and cost
|
900 |
+
st.subheader("Additional Properties")
|
901 |
+
col1, col2 = st.columns(2)
|
902 |
+
|
903 |
+
with col1:
|
904 |
+
# Embodied carbon
|
905 |
+
embodied_carbon = st.number_input(
|
906 |
+
"Embodied Carbon (kg CO₂e/m²)",
|
907 |
+
min_value=0.0,
|
908 |
+
max_value=5000.0,
|
909 |
+
value=float(st.session_state.fenestration_editor["embodied_carbon"]),
|
910 |
+
format="%.1f",
|
911 |
+
help="Embodied carbon in kg CO₂e per square meter."
|
912 |
+
)
|
913 |
+
|
914 |
+
with col2:
|
915 |
+
# Cost
|
916 |
+
cost = st.number_input(
|
917 |
+
"Cost (USD/m²)",
|
918 |
+
min_value=0.0,
|
919 |
+
max_value=5000.0,
|
920 |
+
value=float(st.session_state.fenestration_editor["cost"]),
|
921 |
+
format="%.1f",
|
922 |
+
help="Fenestration cost in USD per square meter."
|
923 |
+
)
|
924 |
+
|
925 |
+
# Form submission buttons
|
926 |
+
col1, col2 = st.columns(2)
|
927 |
+
|
928 |
+
with col1:
|
929 |
+
submit_button = st.form_submit_button("Save Fenestration")
|
930 |
+
|
931 |
+
with col2:
|
932 |
+
clear_button = st.form_submit_button("Clear Form")
|
933 |
+
|
934 |
+
# Handle form submission
|
935 |
+
if submit_button:
|
936 |
+
# Validate inputs
|
937 |
+
validation_errors = validate_fenestration(
|
938 |
+
name, fenestration_type, u_value, shgc, visible_transmittance, thickness,
|
939 |
+
embodied_carbon, cost, st.session_state.fenestration_editor["edit_mode"],
|
940 |
+
st.session_state.fenestration_editor["original_name"]
|
941 |
+
)
|
942 |
+
|
943 |
+
if validation_errors:
|
944 |
+
# Display validation errors
|
945 |
+
for error in validation_errors:
|
946 |
+
st.error(error)
|
947 |
+
else:
|
948 |
+
# Create fenestration data
|
949 |
+
fenestration_data = {
|
950 |
+
"type": fenestration_type,
|
951 |
+
"u_value": u_value,
|
952 |
+
"shgc": shgc,
|
953 |
+
"visible_transmittance": visible_transmittance,
|
954 |
+
"thickness": thickness,
|
955 |
+
"embodied_carbon": embodied_carbon,
|
956 |
+
"cost": cost
|
957 |
+
}
|
958 |
+
|
959 |
+
# Handle edit mode
|
960 |
+
if st.session_state.fenestration_editor["edit_mode"]:
|
961 |
+
original_name = st.session_state.fenestration_editor["original_name"]
|
962 |
+
|
963 |
+
# If name changed, delete old entry and create new one
|
964 |
+
if original_name != name:
|
965 |
+
del st.session_state.project_data["fenestrations"]["project"][original_name]
|
966 |
+
|
967 |
+
# Update fenestration
|
968 |
+
st.session_state.project_data["fenestrations"]["project"][name] = fenestration_data
|
969 |
+
st.success(f"Fenestration '{name}' updated successfully.")
|
970 |
+
logger.info(f"Updated fenestration '{name}' in project")
|
971 |
+
else:
|
972 |
+
# Add new fenestration
|
973 |
+
st.session_state.project_data["fenestrations"]["project"][name] = fenestration_data
|
974 |
+
st.success(f"Fenestration '{name}' added to your project.")
|
975 |
+
logger.info(f"Added new fenestration '{name}' to project")
|
976 |
+
|
977 |
+
# Reset editor
|
978 |
+
reset_fenestration_editor()
|
979 |
+
|
980 |
+
# Handle clear button
|
981 |
+
if clear_button:
|
982 |
+
reset_fenestration_editor()
|
983 |
+
st.rerun()
|
984 |
+
|
985 |
+
def validate_material(
|
986 |
+
name: str, category: str, thermal_conductivity: float, density: float,
|
987 |
+
specific_heat: float, thickness_range: Tuple[float, float], default_thickness: float,
|
988 |
+
embodied_carbon: float, cost: float, edit_mode: bool, original_name: str
|
989 |
+
) -> List[str]:
|
990 |
+
"""
|
991 |
+
Validate material inputs.
|
992 |
+
|
993 |
+
Args:
|
994 |
+
name: Material name
|
995 |
+
category: Material category
|
996 |
+
thermal_conductivity: Thermal conductivity in W/m·K
|
997 |
+
density: Density in kg/m³
|
998 |
+
specific_heat: Specific heat in J/kg·K
|
999 |
+
thickness_range: Tuple of (min_thickness, max_thickness) in meters
|
1000 |
+
default_thickness: Default thickness in meters
|
1001 |
+
embodied_carbon: Embodied carbon in kg CO₂e/m³
|
1002 |
+
cost: Cost in USD/m³
|
1003 |
+
edit_mode: Whether in edit mode
|
1004 |
+
original_name: Original name if in edit mode
|
1005 |
+
|
1006 |
+
Returns:
|
1007 |
+
List of validation error messages, empty if all inputs are valid
|
1008 |
+
"""
|
1009 |
+
errors = []
|
1010 |
+
|
1011 |
+
# Validate name
|
1012 |
+
if not name or name.strip() == "":
|
1013 |
+
errors.append("Material name is required.")
|
1014 |
+
|
1015 |
+
# Check for name uniqueness if not in edit mode or if name changed
|
1016 |
+
if not edit_mode or (edit_mode and name != original_name):
|
1017 |
+
if name in st.session_state.project_data["materials"]["project"]:
|
1018 |
+
errors.append(f"Material name '{name}' already exists in your project.")
|
1019 |
+
|
1020 |
+
# Validate category
|
1021 |
+
if category not in MATERIAL_CATEGORIES:
|
1022 |
+
errors.append("Please select a valid material category.")
|
1023 |
+
|
1024 |
+
# Validate thermal conductivity
|
1025 |
+
if thermal_conductivity <= 0:
|
1026 |
+
errors.append("Thermal conductivity must be greater than zero.")
|
1027 |
+
|
1028 |
+
# Validate density
|
1029 |
+
if density <= 0:
|
1030 |
+
errors.append("Density must be greater than zero.")
|
1031 |
+
|
1032 |
+
# Validate specific heat
|
1033 |
+
if specific_heat <= 0:
|
1034 |
+
errors.append("Specific heat must be greater than zero.")
|
1035 |
+
|
1036 |
+
# Validate thickness range
|
1037 |
+
if thickness_range[0] <= 0:
|
1038 |
+
errors.append("Minimum thickness must be greater than zero.")
|
1039 |
+
if thickness_range[0] >= thickness_range[1]:
|
1040 |
+
errors.append("Maximum thickness must be greater than minimum thickness.")
|
1041 |
+
|
1042 |
+
# Validate default thickness
|
1043 |
+
if default_thickness < thickness_range[0] or default_thickness > thickness_range[1]:
|
1044 |
+
errors.append("Default thickness must be within the thickness range.")
|
1045 |
+
|
1046 |
+
# Validate embodied carbon
|
1047 |
+
if embodied_carbon < 0:
|
1048 |
+
errors.append("Embodied carbon cannot be negative.")
|
1049 |
+
|
1050 |
+
# Validate cost
|
1051 |
+
if cost < 0:
|
1052 |
+
errors.append("Cost cannot be negative.")
|
1053 |
+
|
1054 |
+
return errors
|
1055 |
+
|
1056 |
+
def validate_fenestration(
|
1057 |
+
name: str, fenestration_type: str, u_value: float, shgc: float,
|
1058 |
+
visible_transmittance: float, thickness: float, embodied_carbon: float,
|
1059 |
+
cost: float, edit_mode: bool, original_name: str
|
1060 |
+
) -> List[str]:
|
1061 |
+
"""
|
1062 |
+
Validate fenestration inputs.
|
1063 |
+
|
1064 |
+
Args:
|
1065 |
+
name: Fenestration name
|
1066 |
+
fenestration_type: Fenestration type
|
1067 |
+
u_value: U-value in W/m²·K
|
1068 |
+
shgc: Solar Heat Gain Coefficient (0-1)
|
1069 |
+
visible_transmittance: Visible Transmittance (0-1)
|
1070 |
+
thickness: Thickness in meters
|
1071 |
+
embodied_carbon: Embodied carbon in kg CO₂e/m²
|
1072 |
+
cost: Cost in USD/m²
|
1073 |
+
edit_mode: Whether in edit mode
|
1074 |
+
original_name: Original name if in edit mode
|
1075 |
+
|
1076 |
+
Returns:
|
1077 |
+
List of validation error messages, empty if all inputs are valid
|
1078 |
+
"""
|
1079 |
+
errors = []
|
1080 |
+
|
1081 |
+
# Validate name
|
1082 |
+
if not name or name.strip() == "":
|
1083 |
+
errors.append("Fenestration name is required.")
|
1084 |
+
|
1085 |
+
# Check for name uniqueness if not in edit mode or if name changed
|
1086 |
+
if not edit_mode or (edit_mode and name != original_name):
|
1087 |
+
if name in st.session_state.project_data["fenestrations"]["project"]:
|
1088 |
+
errors.append(f"Fenestration name '{name}' already exists in your project.")
|
1089 |
+
|
1090 |
+
# Validate type
|
1091 |
+
if fenestration_type not in FENESTRATION_TYPES:
|
1092 |
+
errors.append("Please select a valid fenestration type.")
|
1093 |
+
|
1094 |
+
# Validate u_value
|
1095 |
+
if u_value <= 0:
|
1096 |
+
errors.append("U-value must be greater than zero.")
|
1097 |
+
|
1098 |
+
# Validate shgc
|
1099 |
+
if shgc < 0 or shgc > 1:
|
1100 |
+
errors.append("SHGC must be between 0 and 1.")
|
1101 |
+
|
1102 |
+
# Validate visible transmittance
|
1103 |
+
if visible_transmittance < 0 or visible_transmittance > 1:
|
1104 |
+
errors.append("Visible transmittance must be between 0 and 1.")
|
1105 |
+
|
1106 |
+
# Validate thickness
|
1107 |
+
if thickness <= 0:
|
1108 |
+
errors.append("Thickness must be greater than zero.")
|
1109 |
+
|
1110 |
+
# Validate embodied carbon
|
1111 |
+
if embodied_carbon < 0:
|
1112 |
+
errors.append("Embodied carbon cannot be negative.")
|
1113 |
+
|
1114 |
+
# Validate cost
|
1115 |
+
if cost < 0:
|
1116 |
+
errors.append("Cost cannot be negative.")
|
1117 |
+
|
1118 |
+
return errors
|
1119 |
+
|
1120 |
+
def reset_material_editor():
|
1121 |
+
"""Reset the material editor to default values."""
|
1122 |
+
st.session_state.material_editor = {
|
1123 |
+
"name": "",
|
1124 |
+
"category": MATERIAL_CATEGORIES[0],
|
1125 |
+
"thermal_conductivity": 0.5,
|
1126 |
+
"density": 1000.0,
|
1127 |
+
"specific_heat": 1000.0,
|
1128 |
+
"thickness_range": [0.01, 0.2],
|
1129 |
+
"default_thickness": 0.05,
|
1130 |
+
"embodied_carbon": 100.0,
|
1131 |
+
"cost": 100.0,
|
1132 |
+
"edit_mode": False,
|
1133 |
+
"original_name": ""
|
1134 |
+
}
|
1135 |
+
|
1136 |
+
def reset_fenestration_editor():
|
1137 |
+
"""Reset the fenestration editor to default values."""
|
1138 |
+
st.session_state.fenestration_editor = {
|
1139 |
+
"name": "",
|
1140 |
+
"type": FENESTRATION_TYPES[0],
|
1141 |
+
"u_value": 2.8,
|
1142 |
+
"shgc": 0.7,
|
1143 |
+
"visible_transmittance": 0.8,
|
1144 |
+
"thickness": 0.024,
|
1145 |
+
"embodied_carbon": 900.0,
|
1146 |
+
"cost": 200.0,
|
1147 |
+
"edit_mode": False,
|
1148 |
+
"original_name": ""
|
1149 |
+
}
|
1150 |
+
|
1151 |
+
def check_material_in_use(material_name: str) -> bool:
|
1152 |
+
"""
|
1153 |
+
Check if a material is in use in any constructions.
|
1154 |
+
|
1155 |
+
Args:
|
1156 |
+
material_name: Name of the material to check
|
1157 |
+
|
1158 |
+
Returns:
|
1159 |
+
True if the material is in use, False otherwise
|
1160 |
+
"""
|
1161 |
+
# This is a placeholder function that will be implemented when constructions are added
|
1162 |
+
# For now, we'll assume materials are not in use
|
1163 |
+
return False
|
1164 |
+
|
1165 |
+
def check_fenestration_in_use(fenestration_name: str) -> bool:
|
1166 |
+
"""
|
1167 |
+
Check if a fenestration is in use in any components.
|
1168 |
+
|
1169 |
+
Args:
|
1170 |
+
fenestration_name: Name of the fenestration to check
|
1171 |
+
|
1172 |
+
Returns:
|
1173 |
+
True if the fenestration is in use, False otherwise
|
1174 |
+
"""
|
1175 |
+
# This is a placeholder function that will be implemented when components are added
|
1176 |
+
# For now, we'll assume fenestrations are not in use
|
1177 |
+
return False
|
1178 |
+
|
1179 |
+
def display_materials_help():
|
1180 |
+
"""Display help information for the material library page."""
|
1181 |
+
st.markdown("""
|
1182 |
+
### Material Library Help
|
1183 |
+
|
1184 |
+
This section allows you to manage building materials and fenestrations for your project.
|
1185 |
+
|
1186 |
+
**Materials Tab:**
|
1187 |
+
|
1188 |
+
* **Library Materials**: Pre-defined materials with standard thermal properties.
|
1189 |
+
* **Project Materials**: Materials you've added to your project from the library or created custom.
|
1190 |
+
* **Material Editor**: Create new materials or edit existing ones in your project.
|
1191 |
+
|
1192 |
+
**Fenestrations Tab:**
|
1193 |
+
|
1194 |
+
* **Library Fenestrations**: Pre-defined windows, doors, and skylights with standard thermal properties.
|
1195 |
+
* **Project Fenestrations**: Fenestrations you've added to your project from the library or created custom.
|
1196 |
+
* **Fenestration Editor**: Create new fenestrations or edit existing ones in your project.
|
1197 |
+
|
1198 |
+
**Key Properties:**
|
1199 |
+
|
1200 |
+
* **Thermal Conductivity (W/m·K)**: Rate of heat transfer through a material. Lower values indicate better insulation.
|
1201 |
+
* **Density (kg/m³)**: Mass per unit volume.
|
1202 |
+
* **Specific Heat (J/kg·K)**: Energy required to raise the temperature of 1 kg by 1 K. Higher values indicate better thermal mass.
|
1203 |
+
* **U-Value (W/m²·K)**: Overall heat transfer coefficient for fenestrations. Lower values indicate better insulation.
|
1204 |
+
* **SHGC**: Solar Heat Gain Coefficient (0-1). Fraction of incident solar radiation that enters through a fenestration.
|
1205 |
+
* **Visible Transmittance**: Fraction of visible light that passes through a fenestration.
|
1206 |
+
* **Embodied Carbon**: Carbon emissions associated with material production, measured in kg CO₂e per unit volume or area.
|
1207 |
+
* **Cost**: Material cost in USD per unit volume or area.
|
1208 |
+
|
1209 |
+
**Workflow:**
|
1210 |
+
|
1211 |
+
1. Browse the library materials and fenestrations
|
1212 |
+
2. Add items to your project or create custom ones
|
1213 |
+
3. Edit properties as needed for your specific project
|
1214 |
+
4. Continue to the Construction page to create assemblies using these materials
|
1215 |
+
""")
|
app/renewable_energy.py
ADDED
@@ -0,0 +1,626 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
HVAC Load Calculator - Renewable Energy Module
|
3 |
+
|
4 |
+
This module handles the renewable energy system sizing and simulation, focusing on
|
5 |
+
solar PV performance modeling, energy offset calculations, and zero energy building analysis.
|
6 |
+
|
7 |
+
Developed by: Dr Majed Abuseif, Deakin University
|
8 |
+
© 2025
|
9 |
+
"""
|
10 |
+
|
11 |
+
import streamlit as st
|
12 |
+
import pandas as pd
|
13 |
+
import numpy as np
|
14 |
+
import json
|
15 |
+
import logging
|
16 |
+
import plotly.graph_objects as go
|
17 |
+
import plotly.express as px
|
18 |
+
from typing import Dict, List, Any, Optional, Tuple, Union
|
19 |
+
from datetime import datetime
|
20 |
+
|
21 |
+
# Configure logging
|
22 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
23 |
+
logger = logging.getLogger(__name__)
|
24 |
+
|
25 |
+
# Constants
|
26 |
+
MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
27 |
+
DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] # Non-leap year
|
28 |
+
HOURS_IN_YEAR = 8760
|
29 |
+
|
30 |
+
# Default PV system parameters
|
31 |
+
DEFAULT_PV_SYSTEM = {
|
32 |
+
"system_capacity_kw": 5.0, # kWp
|
33 |
+
"module_efficiency": 0.20, # 20%
|
34 |
+
"inverter_efficiency": 0.96, # 96%
|
35 |
+
"system_losses": 0.14, # 14% (wiring, soiling, temperature, etc.)
|
36 |
+
"tilt_angle": 20.0, # degrees
|
37 |
+
"azimuth_angle": 180.0, # degrees (0=N, 180=S)
|
38 |
+
"cost_per_kw": 1500, # $/kWp
|
39 |
+
"lifespan_years": 25,
|
40 |
+
"maintenance_cost_ratio": 0.01 # Annual maintenance as fraction of installation cost
|
41 |
+
}
|
42 |
+
|
43 |
+
def display_renewable_energy_page():
|
44 |
+
"""
|
45 |
+
Display the renewable energy page.
|
46 |
+
This is the main function called by main.py when the Renewable Energy page is selected.
|
47 |
+
"""
|
48 |
+
st.title("Renewable Energy Systems")
|
49 |
+
|
50 |
+
# Display help information in an expandable section
|
51 |
+
with st.expander("Help & Information"):
|
52 |
+
display_renewable_energy_help()
|
53 |
+
|
54 |
+
# Check if building energy has been calculated
|
55 |
+
if "building_energy" not in st.session_state.project_data or not st.session_state.project_data["building_energy"].get("results"):
|
56 |
+
st.warning("Please complete the Building Energy analysis before proceeding to Renewable Energy systems.")
|
57 |
+
|
58 |
+
# Navigation buttons
|
59 |
+
col1, col2 = st.columns(2)
|
60 |
+
with col1:
|
61 |
+
if st.button("Back to Building Energy", key="back_to_building_energy_re"):
|
62 |
+
st.session_state.current_page = "Building Energy"
|
63 |
+
st.rerun()
|
64 |
+
return
|
65 |
+
|
66 |
+
# Initialize renewable energy data if not present
|
67 |
+
initialize_renewable_energy_data()
|
68 |
+
|
69 |
+
# Create tabs for different aspects of renewable energy analysis
|
70 |
+
tabs = st.tabs(["Solar PV System", "Energy Offset & Net Zero"])
|
71 |
+
|
72 |
+
with tabs[0]:
|
73 |
+
display_solar_pv_system_tab()
|
74 |
+
|
75 |
+
with tabs[1]:
|
76 |
+
display_energy_offset_tab()
|
77 |
+
|
78 |
+
# Navigation buttons
|
79 |
+
col1, col2 = st.columns(2)
|
80 |
+
|
81 |
+
with col1:
|
82 |
+
if st.button("Back to Building Energy", key="back_to_building_energy_re"):
|
83 |
+
st.session_state.current_page = "Building Energy"
|
84 |
+
st.rerun()
|
85 |
+
|
86 |
+
with col2:
|
87 |
+
if st.button("Continue to Embodied Energy", key="continue_to_embodied_energy"):
|
88 |
+
st.session_state.current_page = "Embodied Energy"
|
89 |
+
st.rerun()
|
90 |
+
|
91 |
+
def initialize_renewable_energy_data():
|
92 |
+
"""Initialize renewable energy data in session state if not present."""
|
93 |
+
if "renewable_energy" not in st.session_state.project_data:
|
94 |
+
st.session_state.project_data["renewable_energy"] = {
|
95 |
+
"pv_system": DEFAULT_PV_SYSTEM.copy(),
|
96 |
+
"results": None
|
97 |
+
}
|
98 |
+
|
99 |
+
def display_solar_pv_system_tab():
|
100 |
+
"""
|
101 |
+
Display the Solar PV system configuration and analysis tab.
|
102 |
+
"""
|
103 |
+
st.header("Solar Photovoltaic (PV) System")
|
104 |
+
|
105 |
+
# Get current PV system data
|
106 |
+
pv_system = st.session_state.project_data["renewable_energy"]["pv_system"]
|
107 |
+
|
108 |
+
# System parameters
|
109 |
+
st.subheader("PV System Parameters")
|
110 |
+
|
111 |
+
col1, col2 = st.columns(2)
|
112 |
+
|
113 |
+
with col1:
|
114 |
+
system_capacity_kw = st.number_input(
|
115 |
+
"System Capacity (kWp)",
|
116 |
+
min_value=0.1,
|
117 |
+
max_value=1000.0,
|
118 |
+
value=float(pv_system["system_capacity_kw"]),
|
119 |
+
step=0.1,
|
120 |
+
format="%.1f",
|
121 |
+
help="Peak DC power output of the PV array."
|
122 |
+
)
|
123 |
+
|
124 |
+
module_efficiency = st.number_input(
|
125 |
+
"Module Efficiency",
|
126 |
+
min_value=0.10,
|
127 |
+
max_value=0.30,
|
128 |
+
value=float(pv_system["module_efficiency"]),
|
129 |
+
step=0.01,
|
130 |
+
format="%.2f",
|
131 |
+
help="Efficiency of the PV modules (e.g., 0.20 for 20%)."
|
132 |
+
)
|
133 |
+
|
134 |
+
tilt_angle = st.number_input(
|
135 |
+
"Tilt Angle (degrees)",
|
136 |
+
min_value=0.0,
|
137 |
+
max_value=90.0,
|
138 |
+
value=float(pv_system["tilt_angle"]),
|
139 |
+
step=1.0,
|
140 |
+
format="%.1f",
|
141 |
+
help="Angle of the PV panels relative to horizontal."
|
142 |
+
)
|
143 |
+
|
144 |
+
with col2:
|
145 |
+
inverter_efficiency = st.number_input(
|
146 |
+
"Inverter Efficiency",
|
147 |
+
min_value=0.80,
|
148 |
+
max_value=0.99,
|
149 |
+
value=float(pv_system["inverter_efficiency"]),
|
150 |
+
step=0.01,
|
151 |
+
format="%.2f",
|
152 |
+
help="Efficiency of the PV inverter."
|
153 |
+
)
|
154 |
+
|
155 |
+
system_losses = st.number_input(
|
156 |
+
"System Losses",
|
157 |
+
min_value=0.05,
|
158 |
+
max_value=0.30,
|
159 |
+
value=float(pv_system["system_losses"]),
|
160 |
+
step=0.01,
|
161 |
+
format="%.2f",
|
162 |
+
help="Overall system losses (wiring, soiling, temperature, etc.)."
|
163 |
+
)
|
164 |
+
|
165 |
+
azimuth_angle = st.number_input(
|
166 |
+
"Azimuth Angle (degrees)",
|
167 |
+
min_value=0.0,
|
168 |
+
max_value=359.9,
|
169 |
+
value=float(pv_system["azimuth_angle"]),
|
170 |
+
step=1.0,
|
171 |
+
format="%.1f",
|
172 |
+
help="Orientation of the PV panels (0=North, 90=East, 180=South, 270=West)."
|
173 |
+
)
|
174 |
+
|
175 |
+
# System cost parameters
|
176 |
+
st.subheader("PV System Cost Parameters")
|
177 |
+
|
178 |
+
col1, col2, col3 = st.columns(3)
|
179 |
+
|
180 |
+
with col1:
|
181 |
+
cost_per_kw = st.number_input(
|
182 |
+
"Installation Cost ($/kWp)",
|
183 |
+
min_value=500.0,
|
184 |
+
max_value=5000.0,
|
185 |
+
value=float(pv_system["cost_per_kw"]),
|
186 |
+
step=50.0,
|
187 |
+
format="%.0f",
|
188 |
+
help="Installation cost per kWp of PV system capacity."
|
189 |
+
)
|
190 |
+
|
191 |
+
with col2:
|
192 |
+
lifespan_years = st.number_input(
|
193 |
+
"System Lifespan (years)",
|
194 |
+
min_value=10,
|
195 |
+
max_value=40,
|
196 |
+
value=int(pv_system["lifespan_years"]),
|
197 |
+
step=1,
|
198 |
+
help="Expected lifespan of the PV system in years."
|
199 |
+
)
|
200 |
+
|
201 |
+
with col3:
|
202 |
+
maintenance_cost_ratio = st.number_input(
|
203 |
+
"Annual Maintenance Cost Ratio",
|
204 |
+
min_value=0.001,
|
205 |
+
max_value=0.05,
|
206 |
+
value=float(pv_system["maintenance_cost_ratio"]),
|
207 |
+
step=0.001,
|
208 |
+
format="%.3f",
|
209 |
+
help="Annual maintenance cost as a fraction of installation cost."
|
210 |
+
)
|
211 |
+
|
212 |
+
# Update PV system data
|
213 |
+
pv_system.update({
|
214 |
+
"system_capacity_kw": system_capacity_kw,
|
215 |
+
"module_efficiency": module_efficiency,
|
216 |
+
"inverter_efficiency": inverter_efficiency,
|
217 |
+
"system_losses": system_losses,
|
218 |
+
"tilt_angle": tilt_angle,
|
219 |
+
"azimuth_angle": azimuth_angle,
|
220 |
+
"cost_per_kw": cost_per_kw,
|
221 |
+
"lifespan_years": lifespan_years,
|
222 |
+
"maintenance_cost_ratio": maintenance_cost_ratio
|
223 |
+
})
|
224 |
+
|
225 |
+
# Calculate PV generation button
|
226 |
+
if st.button("Calculate PV Generation", key="calculate_pv_generation"):
|
227 |
+
try:
|
228 |
+
results = calculate_pv_generation()
|
229 |
+
st.session_state.project_data["renewable_energy"]["results"] = results
|
230 |
+
st.success("PV generation calculated successfully.")
|
231 |
+
logger.info("PV generation calculated.")
|
232 |
+
st.rerun() # Refresh to show results
|
233 |
+
except Exception as e:
|
234 |
+
st.error(f"Error calculating PV generation: {e}")
|
235 |
+
logger.error(f"Error calculating PV generation: {e}", exc_info=True)
|
236 |
+
st.session_state.project_data["renewable_energy"]["results"] = None
|
237 |
+
|
238 |
+
# Display PV generation results if available
|
239 |
+
results = st.session_state.project_data["renewable_energy"].get("results")
|
240 |
+
if results:
|
241 |
+
display_pv_generation_results(results)
|
242 |
+
|
243 |
+
def display_pv_generation_results(results: Dict[str, Any]):
|
244 |
+
"""Display the PV generation calculation results."""
|
245 |
+
st.subheader("PV Generation Summary")
|
246 |
+
|
247 |
+
col1, col2 = st.columns(2)
|
248 |
+
|
249 |
+
with col1:
|
250 |
+
st.metric(
|
251 |
+
"Annual PV Generation",
|
252 |
+
f"{results['annual_pv_generation'] / 1000:.1f} MWh",
|
253 |
+
help="Total annual electricity generated by the PV system."
|
254 |
+
)
|
255 |
+
|
256 |
+
with col2:
|
257 |
+
st.metric(
|
258 |
+
"Capacity Factor",
|
259 |
+
f"{results['capacity_factor'] * 100:.1f}%",
|
260 |
+
help="Ratio of actual energy generated to maximum possible generation."
|
261 |
+
)
|
262 |
+
|
263 |
+
# Display monthly PV generation
|
264 |
+
st.subheader("Monthly PV Generation")
|
265 |
+
|
266 |
+
# Create bar chart of monthly generation
|
267 |
+
monthly_data = {
|
268 |
+
"Month": MONTHS,
|
269 |
+
"PV Generation (kWh)": results["monthly_pv_generation"]
|
270 |
+
}
|
271 |
+
|
272 |
+
monthly_df = pd.DataFrame(monthly_data)
|
273 |
+
|
274 |
+
fig_monthly = px.bar(
|
275 |
+
monthly_df,
|
276 |
+
x="Month",
|
277 |
+
y="PV Generation (kWh)",
|
278 |
+
title="Monthly PV Generation"
|
279 |
+
)
|
280 |
+
st.plotly_chart(fig_monthly, use_container_width=True)
|
281 |
+
|
282 |
+
# Display hourly PV generation profile for a typical day
|
283 |
+
st.subheader("Hourly PV Generation Profile")
|
284 |
+
|
285 |
+
# Allow user to select month for hourly profile
|
286 |
+
selected_month = st.selectbox(
|
287 |
+
"Select Month for Hourly Profile",
|
288 |
+
MONTHS,
|
289 |
+
index=6, # Default to July
|
290 |
+
key="pv_hourly_month_selector",
|
291 |
+
help="Select a month to view the typical daily PV generation profile."
|
292 |
+
)
|
293 |
+
|
294 |
+
month_index = MONTHS.index(selected_month)
|
295 |
+
|
296 |
+
# Get hourly data for the selected month
|
297 |
+
month_start_hour = sum(DAYS_IN_MONTH[:month_index]) * 24
|
298 |
+
month_hours = DAYS_IN_MONTH[month_index] * 24
|
299 |
+
|
300 |
+
# Calculate average hourly profile for the month
|
301 |
+
hourly_profile = calculate_average_daily_profile(
|
302 |
+
results["hourly_pv_generation"][month_start_hour:month_start_hour + month_hours],
|
303 |
+
DAYS_IN_MONTH[month_index]
|
304 |
+
)
|
305 |
+
|
306 |
+
# Create hourly profile chart
|
307 |
+
fig_hourly = px.line(
|
308 |
+
x=list(range(24)),
|
309 |
+
y=hourly_profile,
|
310 |
+
title=f"Average Daily PV Generation Profile for {selected_month}",
|
311 |
+
labels={"x": "Hour of Day", "y": "PV Generation (kWh)"}
|
312 |
+
)
|
313 |
+
st.plotly_chart(fig_hourly, use_container_width=True)
|
314 |
+
|
315 |
+
def display_energy_offset_tab():
|
316 |
+
"""Display the energy offset and net-zero analysis tab."""
|
317 |
+
st.header("Energy Offset & Net-Zero Analysis")
|
318 |
+
|
319 |
+
# Check if PV results are available
|
320 |
+
pv_results = st.session_state.project_data["renewable_energy"].get("results")
|
321 |
+
if not pv_results:
|
322 |
+
st.info("Please calculate PV generation using the Solar PV System tab.")
|
323 |
+
return
|
324 |
+
|
325 |
+
# Get building energy consumption data
|
326 |
+
building_energy_results = st.session_state.project_data["building_energy"]["results"]
|
327 |
+
|
328 |
+
# Calculate energy offset
|
329 |
+
annual_consumption = building_energy_results["annual_total_energy"]
|
330 |
+
annual_generation = pv_results["annual_pv_generation"]
|
331 |
+
energy_offset_percentage = (annual_generation / annual_consumption) * 100 if annual_consumption > 0 else 0
|
332 |
+
|
333 |
+
# Display offset summary
|
334 |
+
st.subheader("Energy Offset Summary")
|
335 |
+
|
336 |
+
col1, col2 = st.columns(2)
|
337 |
+
|
338 |
+
with col1:
|
339 |
+
st.metric(
|
340 |
+
"Annual Energy Consumption",
|
341 |
+
f"{annual_consumption / 1000:.1f} MWh",
|
342 |
+
help="Total annual energy consumed by the building."
|
343 |
+
)
|
344 |
+
|
345 |
+
with col2:
|
346 |
+
st.metric(
|
347 |
+
"Annual PV Generation",
|
348 |
+
f"{annual_generation / 1000:.1f} MWh",
|
349 |
+
help="Total annual electricity generated by the PV system."
|
350 |
+
)
|
351 |
+
|
352 |
+
st.metric(
|
353 |
+
"Energy Offset Percentage",
|
354 |
+
f"{energy_offset_percentage:.1f}%",
|
355 |
+
help="Percentage of building energy consumption offset by PV generation."
|
356 |
+
)
|
357 |
+
|
358 |
+
# Display net energy balance
|
359 |
+
st.subheader("Net Energy Balance")
|
360 |
+
|
361 |
+
# Calculate hourly net energy
|
362 |
+
hourly_consumption = np.array(building_energy_results["hourly_total_energy"])
|
363 |
+
hourly_generation = np.array(pv_results["hourly_pv_generation"])
|
364 |
+
hourly_net_energy = hourly_generation - hourly_consumption # Positive = export, Negative = import
|
365 |
+
|
366 |
+
# Calculate monthly net energy
|
367 |
+
monthly_net_energy = calculate_monthly_totals(hourly_net_energy)
|
368 |
+
|
369 |
+
# Create bar chart of monthly net energy
|
370 |
+
monthly_net_df = pd.DataFrame({
|
371 |
+
"Month": MONTHS,
|
372 |
+
"Net Energy (kWh)": monthly_net_energy
|
373 |
+
})
|
374 |
+
|
375 |
+
fig_monthly_net = px.bar(
|
376 |
+
monthly_net_df,
|
377 |
+
x="Month",
|
378 |
+
y="Net Energy (kWh)",
|
379 |
+
title="Monthly Net Energy Balance (PV Generation - Consumption)",
|
380 |
+
color="Net Energy (kWh)",
|
381 |
+
color_continuous_scale=px.colors.diverging.RdBu
|
382 |
+
)
|
383 |
+
st.plotly_chart(fig_monthly_net, use_container_width=True)
|
384 |
+
|
385 |
+
# Display net-zero analysis
|
386 |
+
st.subheader("Net-Zero Energy Analysis")
|
387 |
+
|
388 |
+
if energy_offset_percentage >= 100:
|
389 |
+
st.success("Congratulations! This building achieves Net-Zero Energy based on annual generation vs. consumption.")
|
390 |
+
else:
|
391 |
+
st.warning("This building does not achieve Net-Zero Energy based on annual generation vs. consumption.")
|
392 |
+
|
393 |
+
# Calculate additional PV capacity needed for net-zero
|
394 |
+
shortfall_kwh = annual_consumption - annual_generation
|
395 |
+
if shortfall_kwh > 0:
|
396 |
+
# Estimate additional capacity based on current system performance
|
397 |
+
current_capacity_kw = st.session_state.project_data["renewable_energy"]["pv_system"]["system_capacity_kw"]
|
398 |
+
kwh_per_kw = annual_generation / current_capacity_kw if current_capacity_kw > 0 else 0
|
399 |
+
additional_capacity_kw = shortfall_kwh / kwh_per_kw if kwh_per_kw > 0 else float("inf")
|
400 |
+
|
401 |
+
st.info(f"Estimated additional PV capacity needed for Net-Zero: {additional_capacity_kw:.1f} kWp")
|
402 |
+
|
403 |
+
# Display self-consumption and grid interaction
|
404 |
+
st.subheader("Self-Consumption & Grid Interaction")
|
405 |
+
|
406 |
+
# Calculate self-consumption and grid export/import
|
407 |
+
hourly_self_consumption = np.minimum(hourly_consumption, hourly_generation)
|
408 |
+
hourly_grid_export = np.maximum(0, hourly_generation - hourly_consumption)
|
409 |
+
hourly_grid_import = np.maximum(0, hourly_consumption - hourly_generation)
|
410 |
+
|
411 |
+
# Calculate annual totals
|
412 |
+
annual_self_consumption = sum(hourly_self_consumption)
|
413 |
+
annual_grid_export = sum(hourly_grid_export)
|
414 |
+
annual_grid_import = sum(hourly_grid_import)
|
415 |
+
|
416 |
+
# Calculate self-consumption rate and self-sufficiency rate
|
417 |
+
self_consumption_rate = (annual_self_consumption / annual_generation) * 100 if annual_generation > 0 else 0
|
418 |
+
self_sufficiency_rate = (annual_self_consumption / annual_consumption) * 100 if annual_consumption > 0 else 0
|
419 |
+
|
420 |
+
col1, col2, col3 = st.columns(3)
|
421 |
+
|
422 |
+
with col1:
|
423 |
+
st.metric("Self-Consumption Rate", f"{self_consumption_rate:.1f}%", help="Percentage of PV generation consumed on-site.")
|
424 |
+
with col2:
|
425 |
+
st.metric("Self-Sufficiency Rate", f"{self_sufficiency_rate:.1f}%", help="Percentage of building energy demand met by on-site PV.")
|
426 |
+
|
427 |
+
# Create pie chart of energy flows
|
428 |
+
energy_flows = {
|
429 |
+
"Self-Consumed PV": annual_self_consumption,
|
430 |
+
"Exported to Grid": annual_grid_export,
|
431 |
+
"Imported from Grid": annual_grid_import
|
432 |
+
}
|
433 |
+
|
434 |
+
fig_flows = px.pie(
|
435 |
+
values=list(energy_flows.values()),
|
436 |
+
names=list(energy_flows.keys()),
|
437 |
+
title="Annual Energy Flows"
|
438 |
+
)
|
439 |
+
st.plotly_chart(fig_flows, use_container_width=True)
|
440 |
+
|
441 |
+
def calculate_pv_generation() -> Dict[str, Any]:
|
442 |
+
"""
|
443 |
+
Calculate PV system energy generation.
|
444 |
+
|
445 |
+
Returns:
|
446 |
+
Dictionary containing PV generation results.
|
447 |
+
"""
|
448 |
+
logger.info("Starting PV generation calculations...")
|
449 |
+
|
450 |
+
# Get required data
|
451 |
+
climate_data = st.session_state.project_data["climate_data"]
|
452 |
+
pv_system = st.session_state.project_data["renewable_energy"]["pv_system"]
|
453 |
+
|
454 |
+
# Get hourly solar radiation data
|
455 |
+
hourly_weather = pd.DataFrame(climate_data["hourly_data"])
|
456 |
+
hourly_solar_angles = pd.DataFrame(climate_data["solar_angles"]) # Assuming this is stored from HVAC loads
|
457 |
+
|
458 |
+
# Combine weather and solar angles
|
459 |
+
hourly_data = pd.concat([hourly_weather, hourly_solar_angles], axis=1)
|
460 |
+
|
461 |
+
# Get PV system parameters
|
462 |
+
capacity_kw = pv_system["system_capacity_kw"]
|
463 |
+
module_efficiency = pv_system["module_efficiency"]
|
464 |
+
inverter_efficiency = pv_system["inverter_efficiency"]
|
465 |
+
system_losses = pv_system["system_losses"]
|
466 |
+
tilt_angle = pv_system["tilt_angle"]
|
467 |
+
azimuth_angle = pv_system["azimuth_angle"]
|
468 |
+
|
469 |
+
# Calculate incident solar radiation on PV panels (W/m²)
|
470 |
+
# Using the same function from hvac_loads.py (ensure it is accessible or duplicated)
|
471 |
+
incident_solar_pv = calculate_incident_solar_on_surface(
|
472 |
+
hourly_data["Direct Normal Radiation"],
|
473 |
+
hourly_data["Diffuse Horizontal Radiation"],
|
474 |
+
hourly_data["solar_altitude"],
|
475 |
+
hourly_data["solar_azimuth"],
|
476 |
+
tilt_angle,
|
477 |
+
azimuth_angle
|
478 |
+
)
|
479 |
+
|
480 |
+
# Calculate PV array area (m²)
|
481 |
+
# Assuming standard test conditions (STC) irradiance of 1000 W/m²
|
482 |
+
array_area = (capacity_kw * 1000) / (1000 * module_efficiency)
|
483 |
+
|
484 |
+
# Calculate DC power output from PV array (W)
|
485 |
+
dc_power = incident_solar_pv * array_area * module_efficiency
|
486 |
+
|
487 |
+
# Calculate AC power output after inverter and system losses (W)
|
488 |
+
ac_power = dc_power * inverter_efficiency * (1 - system_losses)
|
489 |
+
|
490 |
+
# Ensure AC power does not exceed system capacity (kW -> W)
|
491 |
+
ac_power = np.minimum(ac_power, capacity_kw * 1000)
|
492 |
+
|
493 |
+
# Convert hourly AC power from W to kWh
|
494 |
+
hourly_pv_generation = ac_power / 1000
|
495 |
+
|
496 |
+
# Calculate monthly PV generation
|
497 |
+
monthly_pv_generation = calculate_monthly_totals(hourly_pv_generation)
|
498 |
+
|
499 |
+
# Calculate annual PV generation
|
500 |
+
annual_pv_generation = sum(monthly_pv_generation)
|
501 |
+
|
502 |
+
# Calculate capacity factor
|
503 |
+
max_possible_generation = capacity_kw * HOURS_IN_YEAR
|
504 |
+
capacity_factor = annual_pv_generation / max_possible_generation if max_possible_generation > 0 else 0
|
505 |
+
|
506 |
+
# Compile results
|
507 |
+
results = {
|
508 |
+
"hourly_pv_generation": hourly_pv_generation.tolist(),
|
509 |
+
"monthly_pv_generation": monthly_pv_generation,
|
510 |
+
"annual_pv_generation": annual_pv_generation,
|
511 |
+
"capacity_factor": capacity_factor,
|
512 |
+
"calculation_timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
513 |
+
}
|
514 |
+
|
515 |
+
logger.info("PV generation calculations completed.")
|
516 |
+
return results
|
517 |
+
|
518 |
+
# Helper function (can be moved to a utility module later)
|
519 |
+
def calculate_incident_solar_on_surface(dnr: pd.Series, dhr: pd.Series, solar_altitude: pd.Series, solar_azimuth: pd.Series, tilt: float, surface_azimuth: float) -> pd.Series:
|
520 |
+
"""
|
521 |
+
Calculate total incident solar radiation on a tilted surface.
|
522 |
+
(Copied from hvac_loads.py for now, consider refactoring to a shared utility)
|
523 |
+
"""
|
524 |
+
# Convert angles to radians
|
525 |
+
alt_rad = np.radians(solar_altitude)
|
526 |
+
az_rad = np.radians(solar_azimuth)
|
527 |
+
tilt_rad = np.radians(tilt)
|
528 |
+
surf_az_rad = np.radians(surface_azimuth)
|
529 |
+
|
530 |
+
# Angle of Incidence (theta)
|
531 |
+
cos_theta = np.cos(alt_rad) * np.sin(tilt_rad) * np.cos(az_rad - surf_az_rad) + \
|
532 |
+
np.sin(alt_rad) * np.cos(tilt_rad)
|
533 |
+
cos_theta = np.maximum(0, cos_theta) # Radiation only incident if cos_theta > 0
|
534 |
+
|
535 |
+
# Direct radiation component on tilted surface
|
536 |
+
direct_tilted = dnr * cos_theta
|
537 |
+
|
538 |
+
# Diffuse radiation component (simplified isotropic sky model)
|
539 |
+
sky_diffuse_tilted = dhr * (1 + np.cos(tilt_rad)) / 2
|
540 |
+
|
541 |
+
# Ground reflected component (simplified)
|
542 |
+
albedo = 0.2 # Typical ground reflectance
|
543 |
+
ground_reflected_tilted = (dnr * np.sin(alt_rad) + dhr) * albedo * (1 - np.cos(tilt_rad)) / 2
|
544 |
+
|
545 |
+
# Total incident solar radiation
|
546 |
+
total_incident = direct_tilted + sky_diffuse_tilted + ground_reflected_tilted
|
547 |
+
return total_incident
|
548 |
+
|
549 |
+
# Helper function (can be moved to a utility module later)
|
550 |
+
def calculate_average_daily_profile(hourly_data: List[float], days: int) -> List[float]:
|
551 |
+
"""
|
552 |
+
Calculate average daily profile from hourly data for a month.
|
553 |
+
(Copied from building_energy.py for now, consider refactoring to a shared utility)
|
554 |
+
"""
|
555 |
+
daily_profile = [0.0] * 24
|
556 |
+
hourly_data_np = np.array(hourly_data)
|
557 |
+
|
558 |
+
for hour in range(len(hourly_data_np)):
|
559 |
+
hour_of_day = hour % 24
|
560 |
+
daily_profile[hour_of_day] += hourly_data_np[hour]
|
561 |
+
|
562 |
+
# Calculate averages
|
563 |
+
for i in range(24):
|
564 |
+
daily_profile[i] /= days
|
565 |
+
|
566 |
+
return daily_profile
|
567 |
+
|
568 |
+
# Helper function (can be moved to a utility module later)
|
569 |
+
def calculate_monthly_totals(hourly_data: np.ndarray) -> List[float]:
|
570 |
+
"""
|
571 |
+
Calculate monthly totals from hourly data.
|
572 |
+
(Copied from building_energy.py for now, consider refactoring to a shared utility)
|
573 |
+
"""
|
574 |
+
monthly_totals = []
|
575 |
+
hour_index = 0
|
576 |
+
|
577 |
+
for days_in_month_count in DAYS_IN_MONTH:
|
578 |
+
hours_in_month = days_in_month_count * 24
|
579 |
+
month_total = sum(hourly_data[hour_index:hour_index + hours_in_month])
|
580 |
+
monthly_totals.append(month_total)
|
581 |
+
hour_index += hours_in_month
|
582 |
+
|
583 |
+
return monthly_totals
|
584 |
+
|
585 |
+
def display_renewable_energy_help():
|
586 |
+
"""
|
587 |
+
Display help information for the renewable energy page.
|
588 |
+
"""
|
589 |
+
st.markdown("""
|
590 |
+
### Renewable Energy Systems Help
|
591 |
+
|
592 |
+
This section allows you to model renewable energy systems, primarily Solar Photovoltaics (PV), and analyze their impact on the building\s energy balance.
|
593 |
+
|
594 |
+
**Key Concepts:**
|
595 |
+
|
596 |
+
* **Solar PV System**: Converts sunlight into electricity.
|
597 |
+
* **System Capacity (kWp)**: Peak power output of the PV array under standard test conditions.
|
598 |
+
* **Module Efficiency**: Percentage of sunlight converted to DC electricity by the PV modules.
|
599 |
+
* **Inverter Efficiency**: Percentage of DC power converted to AC power by the inverter.
|
600 |
+
* **System Losses**: Reductions in PV output due to factors like wiring, soiling, temperature, and shading.
|
601 |
+
* **Tilt & Azimuth Angles**: Orientation of the PV panels, affecting solar energy capture.
|
602 |
+
* **Energy Offset**: Percentage of the building\s energy consumption met by on-site renewable generation.
|
603 |
+
* **Net-Zero Energy**: A building that produces as much energy as it consumes over a year.
|
604 |
+
* **Self-Consumption**: Portion of PV-generated electricity used directly by the building.
|
605 |
+
* **Grid Interaction**: Exchange of electricity with the utility grid (import and export).
|
606 |
+
|
607 |
+
**Workflow:**
|
608 |
+
|
609 |
+
1. **Solar PV System Tab**:
|
610 |
+
* Configure the parameters of your PV system (capacity, efficiency, orientation, costs).
|
611 |
+
* Click "Calculate PV Generation" to simulate the system\s output based on climate data.
|
612 |
+
* Review the PV generation summary, monthly profiles, and typical daily output.
|
613 |
+
|
614 |
+
2. **Energy Offset & Net Zero Tab**:
|
615 |
+
* Analyze how the PV generation offsets the building\s energy consumption (calculated in the Building Energy section).
|
616 |
+
* View the net energy balance (generation vs. consumption) on a monthly basis.
|
617 |
+
* Assess if the building achieves Net-Zero Energy status.
|
618 |
+
* Examine self-consumption rates and grid interaction patterns.
|
619 |
+
|
620 |
+
**Important:**
|
621 |
+
|
622 |
+
* Accurate climate data (especially solar radiation) is crucial for PV modeling.
|
623 |
+
* System losses can significantly impact actual PV generation.
|
624 |
+
* Net-Zero calculations here are based on annual energy balance; consider hourly dynamics for more detailed analysis.
|
625 |
+
* This module focuses on PV; other renewable technologies may require different modeling approaches.
|
626 |
+
""")
|