mabuseif commited on
Commit
5b298ee
·
verified ·
1 Parent(s): dc18ae3

Update app/hvac_loads.py

Browse files
Files changed (1) hide show
  1. app/hvac_loads.py +925 -28
app/hvac_loads.py CHANGED
@@ -8,43 +8,940 @@ modules to determine the building's thermal loads.
8
  Developed by: Dr Majed Abuseif, Deakin University
9
  © 2025
10
  """
 
11
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- def render_ui():
14
- """Render the Streamlit UI for location information, simulation periods, and action buttons."""
15
- st.header("HVAC Load Calculator")
16
-
17
- # Location Information
18
- with st.expander("Location Information", expanded=True):
19
- col1, col2 = st.columns(2)
20
- with col1:
21
- st.text_input("City", key="city")
22
- st.number_input("Latitude (°)", min_value=-90.0, max_value=90.0, value=0.0, step=0.1, key="latitude")
23
- st.number_input("Longitude (°)", min_value=-180.0, max_value=180.0, value=0.0, step=0.1, key="longitude")
24
- with col2:
25
- st.number_input("Time Zone", min_value=-12.0, max_value=14.0, value=0.0, step=0.5, key="time_zone")
26
- st.number_input("Ground Reflectivity", min_value=0.0, max_value=1.0, value=0.2, step=0.01, key="ground_reflectivity")
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # Simulation Period
29
- with st.expander("Simulation Period", expanded=True):
30
- sim_type = st.selectbox("Simulation Type", ["Full Year", "From-to", "HDD", "CDD"], key="sim_type")
31
- if sim_type == "From-to":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  col1, col2 = st.columns(2)
33
  with col1:
34
- st.date_input("Start Date", key="start_date")
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  with col2:
36
- st.date_input("End Date", key="end_date")
37
- elif sim_type in ["HDD", "CDD"]:
38
- st.number_input("Base Temperature (°C)", value=18.3 if sim_type == "HDD" else 23.9, key="base_temp")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- # Action Buttons
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  col1, col2, col3 = st.columns(3)
42
  with col1:
43
- st.button("Calculate", key="calculate_button")
 
44
  with col2:
45
- st.button("Save", key="save_button")
46
- with col3:
47
- st.button("Reset", key="reset_button")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- if __name__ == "__main__":
50
- render_ui()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from datetime import datetime
22
+ import math
23
+ from collections import defaultdict
24
+
25
+ # Configure logging
26
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
27
+ logger = logging.getLogger(__name__)
28
 
29
+ # Constants
30
+ STEFAN_BOLTZMANN = 5.67e-8 # W/m²·K⁴
31
+ ABSORPTIVITY_DEFAULT = 0.7 # Default surface absorptivity
32
+ EMISSIVITY_DEFAULT = 0.9 # Default surface emissivity
33
+ SKY_TEMP_FACTOR = 0.0552 # Factor for sky temperature calculation
34
+ AIR_DENSITY = 1.2 # kg/m³
35
+ AIR_SPECIFIC_HEAT = 1005 # J/kg·K
36
+ LATENT_HEAT_VAPORIZATION = 2260000 # J/kg
 
 
 
 
 
 
37
 
38
+ def display_hvac_loads_page():
39
+ """
40
+ Display the HVAC loads calculation page.
41
+ This is the main function called by main.py when the HVAC Loads page is selected.
42
+ """
43
+ st.title("HVAC Load Calculations")
44
+
45
+ # Display help information in an expandable section
46
+ with st.expander("Help & Information"):
47
+ display_hvac_loads_help()
48
+
49
+ # Check if necessary data is available
50
+ if not check_prerequisites():
51
+ st.warning("Please complete all previous steps (Building Info, Climate, Materials, Construction, Components, Internal Loads) before calculating HVAC loads.")
52
+ return
53
+
54
+ # --- User Input for Simulation Parameters --- #
55
+ st.subheader("Simulation Parameters")
56
+
57
  # Simulation Period
58
+ with st.expander("Simulation Period"):
59
+ sim_period_type = st.selectbox(
60
+ "Simulation Period Type",
61
+ ["Full Year", "From-to", "HDD", "CDD"],
62
+ key="sim_period_type"
63
+ )
64
+ sim_period = {"type": sim_period_type}
65
+
66
+ if sim_period_type == "From-to":
67
+ col1, col2 = st.columns(2)
68
+ with col1:
69
+ start_date = st.date_input("Start Date", value=datetime(2025, 1, 1))
70
+ with col2:
71
+ end_date = st.date_input("End Date", value=datetime(2025, 12, 31))
72
+ sim_period["start_date"] = start_date
73
+ sim_period["end_date"] = end_date
74
+ elif sim_period_type in ["HDD", "CDD"]:
75
+ base_temp = st.number_input(
76
+ f"Base Temperature (°C) for {sim_period_type}",
77
+ value=18.3 if sim_period_type == "HDD" else 23.9,
78
+ min_value=0.0,
79
+ max_value=40.0,
80
+ step=0.1
81
+ )
82
+ sim_period["base_temp"] = base_temp
83
+
84
+ # Indoor Conditions
85
+ with st.expander("Indoor Conditions"):
86
+ indoor_cond_type = st.selectbox(
87
+ "Indoor Conditions Type",
88
+ ["Fixed", "Time-varying", "Adaptive"],
89
+ key="indoor_conditions_type"
90
+ )
91
+ indoor_conditions = {"type": indoor_cond_type}
92
+
93
+ if indoor_cond_type == "Fixed":
94
  col1, col2 = st.columns(2)
95
  with col1:
96
+ cooling_temp = st.number_input(
97
+ "Cooling Setpoint Temperature (°C)",
98
+ value=24.0,
99
+ min_value=15.0,
100
+ max_value=30.0,
101
+ step=0.5
102
+ )
103
+ cooling_rh = st.number_input(
104
+ "Cooling Setpoint Relative Humidity (%)",
105
+ value=50.0,
106
+ min_value=30.0,
107
+ max_value=70.0,
108
+ step=5.0
109
+ )
110
  with col2:
111
+ heating_temp = st.number_input(
112
+ "Heating Setpoint Temperature (°C)",
113
+ value=22.0,
114
+ min_value=15.0,
115
+ max_value=30.0,
116
+ step=0.5
117
+ )
118
+ heating_rh = st.number_input(
119
+ "Heating Setpoint Relative Humidity (%)",
120
+ value=50.0,
121
+ min_value=30.0,
122
+ max_value=70.0,
123
+ step=5.0
124
+ )
125
+ indoor_conditions["cooling_setpoint"] = {"temperature": cooling_temp, "rh": cooling_rh}
126
+ indoor_conditions["heating_setpoint"] = {"temperature": heating_temp, "rh": heating_rh}
127
+ elif indoor_cond_type == "Time-varying":
128
+ st.write("Define Hourly Schedule")
129
+ schedule = []
130
+ for hour in range(24):
131
+ with st.container():
132
+ col1, col2, col3 = st.columns([1, 2, 2])
133
+ with col1:
134
+ st.write(f"Hour {hour}:00")
135
+ with col2:
136
+ temp = st.number_input(
137
+ "Temperature (°C)",
138
+ value=24.0,
139
+ min_value=15.0,
140
+ max_value=30.0,
141
+ step=0.5,
142
+ key=f"schedule_temp_{hour}"
143
+ )
144
+ with col3:
145
+ rh = st.number_input(
146
+ "Relative Humidity (%)",
147
+ value=50.0,
148
+ min_value=30.0,
149
+ max_value=70.0,
150
+ step=5.0,
151
+ key=f"schedule_rh_{hour}"
152
+ )
153
+ schedule.append({"hour": hour, "temperature": temp, "rh": rh})
154
+ indoor_conditions["schedule"] = schedule
155
+
156
+ # Operating Hours
157
+ with st.expander("HVAC Operating Hours"):
158
+ num_periods = st.number_input(
159
+ "Number of Operating Periods",
160
+ value=1,
161
+ min_value=1,
162
+ max_value=5,
163
+ step=1
164
+ )
165
+ operating_periods = []
166
+ for i in range(int(num_periods)):
167
+ with st.container():
168
+ st.write(f"Operating Period {i+1}")
169
+ col1, col2 = st.columns(2)
170
+ with col1:
171
+ start_hour = st.number_input(
172
+ "Start Hour",
173
+ value=8,
174
+ min_value=0,
175
+ max_value=23,
176
+ step=1,
177
+ key=f"start_hour_{i}"
178
+ )
179
+ with col2:
180
+ end_hour = st.number_input(
181
+ "End Hour",
182
+ value=18,
183
+ min_value=0,
184
+ max_value=23,
185
+ step=1,
186
+ key=f"end_hour_{i}"
187
+ )
188
+ operating_periods.append({"start": start_hour, "end": end_hour})
189
+ hvac_settings = {"operating_hours": operating_periods}
190
+
191
+ # Calculation trigger
192
+ if st.button("Calculate HVAC Loads", key="calculate_hvac_loads"):
193
+ try:
194
+ results = calculate_loads(
195
+ sim_period=sim_period,
196
+ indoor_conditions=indoor_conditions,
197
+ hvac_settings=hvac_settings
198
+ )
199
+ st.session_state.project_data["hvac_loads"] = results
200
+ st.success("HVAC loads calculated successfully.")
201
+ logger.info("HVAC loads calculated.")
202
+ except Exception as e:
203
+ st.error(f"Error calculating HVAC loads: {e}")
204
+ logger.error(f"Error calculating HVAC loads: {e}", exc_info=True)
205
+ st.session_state.project_data["hvac_loads"] = None
206
+
207
+ # Display results if available
208
+ if "hvac_loads" in st.session_state.project_data and st.session_state.project_data["hvac_loads"]:
209
+ display_load_results(st.session_state.project_data["hvac_loads"])
210
+ else:
211
+ st.info("Click the button above to calculate HVAC loads.")
212
+
213
+ # Navigation buttons
214
+ col1, col2 = st.columns(2)
215
+
216
+ with col1:
217
+ if st.button("Back to Internal Loads", key="back_to_internal_loads"):
218
+ st.session_state.current_page = "Internal Loads"
219
+ st.rerun()
220
+
221
+ with col2:
222
+ if st.button("Continue to Building Energy", key="continue_to_building_energy"):
223
+ st.session_state.current_page = "Building Energy"
224
+ st.rerun()
225
+
226
+ def check_prerequisites() -> bool:
227
+ """Check if all prerequisite data is available in session state."""
228
+ required_keys = [
229
+ "building_info",
230
+ "climate_data",
231
+ "materials",
232
+ "constructions",
233
+ "components",
234
+ "internal_loads"
235
+ ]
236
+
237
+ for key in required_keys:
238
+ if key not in st.session_state.project_data or not st.session_state.project_data[key]:
239
+ logger.warning(f"Prerequisite check failed: Missing data for '{key}'.")
240
+ return False
241
+
242
+ # Specific checks within components
243
+ components = st.session_state.project_data["components"]
244
+ if not components.get("walls") and not components.get("roofs") and not components.get("floors"):
245
+ logger.warning("Prerequisite check failed: No opaque components defined.")
246
+ return False
247
+
248
+ # Specific checks within climate data
249
+ climate = st.session_state.project_data["climate_data"]
250
+ if not climate.get("hourly_data") or not climate.get("design_conditions"):
251
+ logger.warning("Prerequisite check failed: Climate data not processed.")
252
+ return False
253
+
254
+ return True
255
+
256
+ def filter_hourly_data(hourly_data: List[Dict], sim_period: Dict, climate_data: Dict) -> List[Dict]:
257
+ """Filter hourly data based on simulation period, ignoring year."""
258
+ if sim_period["type"] == "Full Year":
259
+ return hourly_data
260
+ filtered_data = []
261
+ if sim_period["type"] == "From-to":
262
+ start_month = sim_period["start_date"].month
263
+ start_day = sim_period["start_date"].day
264
+ end_month = sim_period["end_date"].month
265
+ end_day = sim_period["end_date"].day
266
+ for data in hourly_data:
267
+ month, day = data["month"], data["day"]
268
+ if (month > start_month or (month == start_month and day >= start_day)) and \
269
+ (month < end_month or (month == end_month and day <= end_day)):
270
+ filtered_data.append(data)
271
+ elif sim_period["type"] in ["HDD", "CDD"]:
272
+ base_temp = sim_period.get("base_temp", 18.3 if sim_period["type"] == "HDD" else 23.9)
273
+ for data in hourly_data:
274
+ temp = data["dry_bulb"]
275
+ if (sim_period["type"] == "HDD" and temp < base_temp) or (sim_period["type"] == "CDD" and temp > base_temp):
276
+ filtered_data.append(data)
277
+ return filtered_data
278
+
279
+ def get_indoor_conditions(indoor_conditions: Dict, hour: int, outdoor_temp: float) -> Dict:
280
+ """Determine indoor conditions based on user settings."""
281
+ if indoor_conditions["type"] == "Fixed":
282
+ mode = "none" if abs(outdoor_temp - 18) < 0.01 else "cooling" if outdoor_temp > 18 else "heating"
283
+ if mode == "cooling":
284
+ return {
285
+ "temperature": indoor_conditions.get("cooling_setpoint", {}).get("temperature", 24.0),
286
+ "rh": indoor_conditions.get("cooling_setpoint", {}).get("rh", 50.0)
287
+ }
288
+ elif mode == "heating":
289
+ return {
290
+ "temperature": indoor_conditions.get("heating_setpoint", {}).get("temperature", 22.0),
291
+ "rh": indoor_conditions.get("heating_setpoint", {}).get("rh", 50.0)
292
+ }
293
+ else:
294
+ return {"temperature": 24.0, "rh": 50.0}
295
+ elif indoor_conditions["type"] == "Time-varying":
296
+ schedule = indoor_conditions.get("schedule", [])
297
+ if schedule:
298
+ hour_idx = hour % 24
299
+ for entry in schedule:
300
+ if entry["hour"] == hour_idx:
301
+ return {"temperature": entry["temperature"], "rh": entry["rh"]}
302
+ return {"temperature": 24.0, "rh": 50.0}
303
+ else: # Adaptive
304
+ if 10 <= outdoor_temp <= 33.5:
305
+ return {"temperature": 0.31 * outdoor_temp + 17.8, "rh": 50.0}
306
+ return {"temperature": 24.0, "rh": 50.0}
307
 
308
+ def calculate_loads(sim_period: Dict, indoor_conditions: Dict, hvac_settings: Dict) -> Dict[str, Any]:
309
+ """
310
+ Perform the main HVAC load calculations with user-defined filters and temperature threshold.
311
+
312
+ Args:
313
+ sim_period: Simulation period settings.
314
+ indoor_conditions: Indoor conditions settings.
315
+ hvac_settings: HVAC operating hours settings.
316
+
317
+ Returns:
318
+ Dictionary containing calculation results.
319
+ """
320
+ logger.info("Starting HVAC load calculations...")
321
+
322
+ # Get data from session state
323
+ building_info = st.session_state.project_data["building_info"]
324
+ climate_data = st.session_state.project_data["climate_data"]
325
+ materials = get_available_materials()
326
+ constructions = get_available_constructions()
327
+ fenestrations = get_available_fenestrations()
328
+ components = st.session_state.project_data["components"]
329
+ internal_loads = st.session_state.project_data["internal_loads"]
330
+
331
+ # Get design conditions
332
+ design_conditions = climate_data["design_conditions"]
333
+ hourly_data_list = climate_data["hourly_data"]
334
+
335
+ # Filter hourly data based on simulation period
336
+ filtered_hourly_data = filter_hourly_data(hourly_data_list, sim_period, climate_data)
337
+ hourly_data_df = pd.DataFrame(filtered_hourly_data)
338
+ if hourly_data_df.empty:
339
+ logger.warning("No hourly data available after filtering.")
340
+ return {}
341
+
342
+ # --- Solar Calculations --- #
343
+ logger.info("Calculating solar geometry...")
344
+ latitude = climate_data["location"]["latitude"]
345
+ longitude = climate_data["location"]["longitude"]
346
+ timezone = climate_data["location"]["timezone"]
347
+ building_orientation_angle = building_info["orientation_angle"]
348
+
349
+ # Calculate solar angles for filtered hours
350
+ solar_angles = calculate_solar_angles(latitude, longitude, timezone, hourly_data_df.index)
351
+ hourly_data_df = pd.concat([hourly_data_df, solar_angles], axis=1)
352
+
353
+ # Initialize load arrays
354
+ num_hours = len(filtered_hourly_data)
355
+ cooling_loads = {
356
+ "opaque_conduction": np.zeros(num_hours),
357
+ "fenestration_conduction": np.zeros(num_hours),
358
+ "fenestration_solar": np.zeros(num_hours),
359
+ "infiltration": np.zeros(num_hours),
360
+ "ventilation": np.zeros(num_hours),
361
+ "internal_sensible": np.zeros(num_hours),
362
+ "internal_latent": np.zeros(num_hours)
363
+ }
364
+ heating_loads = {
365
+ "opaque_conduction": np.zeros(num_hours),
366
+ "fenestration_conduction": np.zeros(num_hours),
367
+ "infiltration": np.zeros(num_hours),
368
+ "ventilation": np.zeros(num_hours)
369
+ }
370
+
371
+ # --- Load Calculations --- #
372
+ operating_periods = hvac_settings.get("operating_hours", [{"start": 8, "end": 18}])
373
+ area = building_info["floor_area"]
374
+
375
+ for idx, hour_data in enumerate(filtered_hourly_data):
376
+ hour = hour_data["hour"]
377
+ outdoor_temp = hour_data["dry_bulb"]
378
+ indoor_cond = get_indoor_conditions(indoor_conditions, hour, outdoor_temp)
379
+ indoor_temp = indoor_cond["temperature"]
380
+
381
+ # Check if hour is within operating periods
382
+ is_operating = False
383
+ for period in operating_periods:
384
+ start_hour = period.get("start", 8)
385
+ end_hour = period.get("end", 18)
386
+ if start_hour <= hour % 24 <= end_hour:
387
+ is_operating = True
388
+ break
389
+
390
+ # Determine mode based on temperature threshold (18°C)
391
+ mode = "none" if abs(outdoor_temp - 18) < 0.01 else "cooling" if outdoor_temp > 18 else "heating"
392
+
393
+ if not is_operating:
394
+ continue
395
+
396
+ # 1. Opaque Surfaces (Walls, Roofs, Floors)
397
+ for comp_type in ["walls", "roofs", "floors"]:
398
+ for comp in components.get(comp_type, []):
399
+ logger.debug(f"Calculating loads for {comp_type}: {comp['name']}")
400
+ construction = constructions[comp['construction']]
401
+ u_value = construction['u_value']
402
+ area = comp['area']
403
+ tilt = comp['tilt']
404
+ orientation = comp['orientation']
405
+
406
+ # Calculate surface azimuth
407
+ surface_azimuth = calculate_surface_azimuth(orientation, building_orientation_angle)
408
+
409
+ # Calculate sol-air temperature
410
+ sol_air_temp = calculate_sol_air_temperature(
411
+ pd.Series([outdoor_temp], index=[idx]),
412
+ pd.Series([hour_data.get("direct_normal_radiation", 0)], index=[idx]),
413
+ pd.Series([hour_data.get("diffuse_horizontal_radiation", 0)], index=[idx]),
414
+ pd.Series([hourly_data_df.loc[idx, "solar_altitude"]], index=[idx]),
415
+ pd.Series([hourly_data_df.loc[idx, "solar_azimuth"]], index=[idx]),
416
+ tilt,
417
+ surface_azimuth,
418
+ absorptivity=ABSORPTIVITY_DEFAULT,
419
+ emissivity=EMISSIVITY_DEFAULT,
420
+ h_o=20.0
421
+ )[0]
422
+
423
+ # Calculate conduction heat gain/loss
424
+ if mode == "cooling":
425
+ delta_t = sol_air_temp - indoor_temp
426
+ if delta_t > 0:
427
+ cooling_loads["opaque_conduction"][idx] += u_value * area * delta_t
428
+ elif mode == "heating":
429
+ delta_t = indoor_temp - outdoor_temp
430
+ if delta_t > 0:
431
+ heating_loads["opaque_conduction"][idx] += u_value * area * delta_t
432
+
433
+ # 2. Fenestration (Windows, Doors, Skylights)
434
+ for comp_type in ["windows", "doors", "skylights"]:
435
+ for comp in components.get(comp_type, []):
436
+ logger.debug(f"Calculating loads for {comp_type}: {comp['name']}")
437
+ fenestration = fenestrations[comp['fenestration']]
438
+ u_value = fenestration['u_value']
439
+ shgc = fenestration['shgc']
440
+ area = comp['area']
441
+ tilt = comp['tilt']
442
+ orientation = comp['orientation']
443
+
444
+ # Calculate surface azimuth
445
+ surface_azimuth = calculate_surface_azimuth(orientation, building_orientation_angle)
446
+
447
+ # Calculate incident solar radiation
448
+ incident_solar = calculate_incident_solar(
449
+ pd.Series([hour_data.get("direct_normal_radiation", 0)], index=[idx]),
450
+ pd.Series([hour_data.get("diffuse_horizontal_radiation", 0)], index=[idx]),
451
+ pd.Series([hourly_data_df.loc[idx, "solar_altitude"]], index=[idx]),
452
+ pd.Series([hourly_data_df.loc[idx, "solar_azimuth"]], index=[idx]),
453
+ tilt,
454
+ surface_azimuth
455
+ )[0]
456
+
457
+ # Calculate conduction heat gain/loss
458
+ if mode == "cooling":
459
+ delta_t = outdoor_temp - indoor_temp
460
+ if delta_t > 0:
461
+ cooling_loads["fenestration_conduction"][idx] += u_value * area * delta_t
462
+ elif mode == "heating":
463
+ delta_t = indoor_temp - outdoor_temp
464
+ if delta_t > 0:
465
+ heating_loads["fenestration_conduction"][idx] += u_value * area * delta_t
466
+
467
+ # Calculate solar heat gain
468
+ if mode == "cooling":
469
+ solar_gain = shgc * area * incident_solar
470
+ cooling_loads["fenestration_solar"][idx] += solar_gain
471
+
472
+ # 3. Infiltration
473
+ if mode in ["cooling", "heating"]:
474
+ logger.info("Calculating infiltration loads...")
475
+ volume = building_info["floor_area"] * building_info["building_height"]
476
+ ach = 0.5
477
+ infiltration_rate_m3s = volume * ach / 3600.0
478
+
479
+ delta_t = outdoor_temp - indoor_temp if mode == "cooling" else indoor_temp - outdoor_temp
480
+ if delta_t > 0:
481
+ infiltration_sensible = AIR_DENSITY * AIR_SPECIFIC_HEAT * infiltration_rate_m3s * delta_t
482
+ if mode == "cooling":
483
+ cooling_loads["infiltration"][idx] += infiltration_sensible
484
+ else:
485
+ heating_loads["infiltration"][idx] += infiltration_sensible
486
+
487
+ # 4. Ventilation
488
+ if mode in ["cooling", "heating"]:
489
+ logger.info("Calculating ventilation loads...")
490
+ ventilation_rate_lps = building_info["ventilation_rate"]
491
+ ventilation_rate_m3s = ventilation_rate_lps * building_info["floor_area"] / 1000.0
492
+
493
+ delta_t = outdoor_temp - indoor_temp if mode == "cooling" else indoor_temp - outdoor_temp
494
+ if delta_t > 0:
495
+ ventilation_sensible = AIR_DENSITY * AIR_SPECIFIC_HEAT * ventilation_rate_m3s * delta_t
496
+ if mode == "cooling":
497
+ cooling_loads["ventilation"][idx] += ventilation_sensible
498
+ else:
499
+ heating_loads["ventilation"][idx] += ventilation_sensible
500
+
501
+ # 5. Internal Loads
502
+ if mode == "cooling":
503
+ logger.info("Calculating internal loads...")
504
+ internal_sensible, internal_latent = calculate_hourly_internal_loads(internal_loads)
505
+ cooling_loads["internal_sensible"][idx] = internal_sensible[idx % 8760]
506
+ cooling_loads["internal_latent"][idx] = internal_latent[idx % 8760]
507
+
508
+ # --- Total Loads --- #
509
+ logger.info("Summing up loads...")
510
+ total_cooling_sensible = (
511
+ cooling_loads["opaque_conduction"] +
512
+ cooling_loads["fenestration_conduction"] +
513
+ cooling_loads["fenestration_solar"] +
514
+ cooling_loads["infiltration"] +
515
+ cooling_loads["ventilation"] +
516
+ cooling_loads["internal_sensible"]
517
+ )
518
+ total_cooling_latent = cooling_loads["internal_latent"]
519
+ total_cooling = total_cooling_sensible + total_cooling_latent
520
+
521
+ total_heating_loss = (
522
+ heating_loads["opaque_conduction"] +
523
+ heating_loads["fenestration_conduction"] +
524
+ heating_loads["infiltration"] +
525
+ heating_loads["ventilation"]
526
+ )
527
+ total_heating_needed = np.maximum(0, total_heating_loss - cooling_loads["internal_sensible"])
528
+
529
+ # --- Daily Control --- #
530
+ loads_by_day = defaultdict(list)
531
+ for idx, hour_data in enumerate(filtered_hourly_data):
532
+ day_key = (hour_data["month"], hour_data["day"])
533
+ loads_by_day[day_key].append({
534
+ "hour": hour_data["hour"],
535
+ "month": hour_data["month"],
536
+ "day": hour_data["day"],
537
+ "total_cooling": total_cooling[idx],
538
+ "total_heating": total_heating_needed[idx],
539
+ "cooling_components": {k: v[idx] for k, v in cooling_loads.items()},
540
+ "heating_components": {k: v[idx] for k, v in heating_loads.items()}
541
+ })
542
+
543
+ final_loads = []
544
+ for day_key, day_loads in loads_by_day.items():
545
+ cooling_hours = sum(1 for load in day_loads if load["total_cooling"] > 0)
546
+ heating_hours = sum(1 for load in day_loads if load["total_heating"] > 0)
547
+ for load in day_loads:
548
+ if cooling_hours > heating_hours:
549
+ load["total_heating"] = 0
550
+ elif heating_hours > cooling_hours:
551
+ load["total_cooling"] = 0
552
+ else:
553
+ load["total_cooling"] = 0
554
+ load["total_heating"] = 0
555
+ final_loads.append(load)
556
+
557
+ # --- Peak Loads --- #
558
+ logger.info("Calculating peak loads...")
559
+ hourly_cooling_sensible = np.array([load["cooling_components"]["opaque_conduction"] +
560
+ load["cooling_components"]["fenestration_conduction"] +
561
+ load["cooling_components"]["fenestration_solar"] +
562
+ load["cooling_components"]["infiltration"] +
563
+ load["cooling_components"]["ventilation"] +
564
+ load["cooling_components"]["internal_sensible"]
565
+ for load in final_loads])
566
+ hourly_cooling_latent = np.array([load["cooling_components"]["internal_latent"] for load in final_loads])
567
+ hourly_cooling_total = hourly_cooling_sensible + hourly_cooling_latent
568
+ hourly_heating = np.array([load["total_heating"] for load in final_loads])
569
+
570
+ peak_cooling_sensible = np.max(hourly_cooling_sensible) if len(hourly_cooling_sensible) > 0 else 0
571
+ peak_cooling_latent = np.max(hourly_cooling_latent) if len(hourly_cooling_latent) > 0 else 0
572
+ peak_cooling_total = np.max(hourly_cooling_total) if len(hourly_cooling_total) > 0 else 0
573
+ peak_heating = np.max(hourly_heating) if len(hourly_heating) > 0 else 0
574
+
575
+ peak_cooling_sensible_hour = np.argmax(hourly_cooling_sensible) if len(hourly_cooling_sensible) > 0 else 0
576
+ peak_cooling_latent_hour = np.argmax(hourly_cooling_latent) if len(hourly_cooling_latent) > 0 else 0
577
+ peak_cooling_total_hour = np.argmax(hourly_cooling_total) if len(hourly_cooling_total) > 0 else 0
578
+ peak_heating_hour = np.argmax(hourly_heating) if len(hourly_heating) > 0 else 0
579
+
580
+ results = {
581
+ "hourly_cooling_sensible": hourly_cooling_sensible.tolist(),
582
+ "hourly_cooling_latent": hourly_cooling_latent.tolist(),
583
+ "hourly_cooling_total": hourly_cooling_total.tolist(),
584
+ "hourly_heating": hourly_heating.tolist(),
585
+ "cooling_load_components": {k: [load["cooling_components"][k] for load in final_loads] for k in cooling_loads},
586
+ "heating_load_components": {k: [load["heating_components"][k] for load in final_loads] for k in heating_loads},
587
+ "peak_cooling_sensible": peak_cooling_sensible,
588
+ "peak_cooling_latent": peak_cooling_latent,
589
+ "peak_cooling_total": peak_cooling_total,
590
+ "peak_heating": peak_heating,
591
+ "peak_cooling_sensible_hour": int(peak_cooling_sensible_hour),
592
+ "peak_cooling_latent_hour": int(peak_cooling_latent_hour),
593
+ "peak_cooling_total_hour": int(peak_cooling_total_hour),
594
+ "peak_heating_hour": int(peak_heating_hour),
595
+ "filtered_hours": [{"month": load["month"], "day": load["day"], "hour": load["hour"]} for load in final_loads]
596
+ }
597
+
598
+ logger.info("HVAC load calculations completed.")
599
+ return results
600
+
601
+ def calculate_solar_angles(latitude: float, longitude: float, timezone: float, index: pd.Index) -> pd.DataFrame:
602
+ """
603
+ Calculate solar altitude and azimuth angles for given location and time.
604
+ Uses formulas from Duffie & Beckman, Solar Engineering of Thermal Processes.
605
+
606
+ Args:
607
+ latitude: Latitude in degrees
608
+ longitude: Longitude in degrees (East positive)
609
+ timezone: Timezone offset from UTC in hours
610
+ index: Pandas Index for the hours
611
+
612
+ Returns:
613
+ DataFrame with solar altitude and azimuth angles in degrees.
614
+ """
615
+ # Convert angles to radians
616
+ lat_rad = np.radians(latitude)
617
+
618
+ # Day of year
619
+ day_of_year = pd.Series(index).apply(lambda x: pd.Timestamp(x).dayofyear)
620
+
621
+ # Equation of time (simplified)
622
+ b = 2 * np.pi * (day_of_year - 81) / 364
623
+ eot = 9.87 * np.sin(2 * b) - 7.53 * np.cos(b) - 1.5 * np.sin(b) # minutes
624
+
625
+ # Local Standard Time Meridian (LSTM)
626
+ lstm = 15 * timezone
627
+
628
+ # Time Correction Factor (TC)
629
+ tc = 4 * (longitude - lstm) + eot # minutes
630
+
631
+ # Local Solar Time (LST)
632
+ local_time_hour = pd.Series(index).apply(lambda x: pd.Timestamp(x).hour + pd.Timestamp(x).minute / 60.0)
633
+ lst = local_time_hour + tc / 60.0
634
+
635
+ # Hour Angle (HRA)
636
+ hra = 15 * (lst - 12) # degrees
637
+ hra_rad = np.radians(hra)
638
+
639
+ # Declination Angle
640
+ declination_rad = np.radians(23.45 * np.sin(2 * np.pi * (284 + day_of_year) / 365))
641
+
642
+ # Solar Altitude (alpha)
643
+ sin_alpha = np.sin(lat_rad) * np.sin(declination_rad) + \
644
+ np.cos(lat_rad) * np.cos(declination_rad) * np.cos(hra_rad)
645
+ solar_altitude_rad = np.arcsin(np.clip(sin_alpha, -1, 1))
646
+ solar_altitude_deg = np.degrees(solar_altitude_rad)
647
+
648
+ # Solar Azimuth (gamma_s) - measured from South, positive West
649
+ cos_gamma_s = (np.sin(solar_altitude_rad) * np.sin(lat_rad) - np.sin(declination_rad)) / \
650
+ (np.cos(solar_altitude_rad) * np.cos(lat_rad))
651
+ solar_azimuth_rad = np.arccos(np.clip(cos_gamma_s, -1, 1))
652
+ solar_azimuth_deg = np.degrees(solar_azimuth_rad)
653
+ # Adjust azimuth based on hour angle
654
+ solar_azimuth_deg = np.where(hra > 0, solar_azimuth_deg, 360 - solar_azimuth_deg)
655
+ # Convert to azimuth from North, positive East
656
+ solar_azimuth_deg = (solar_azimuth_deg + 180) % 360
657
+
658
+ return pd.DataFrame({
659
+ "solar_altitude": solar_altitude_deg,
660
+ "solar_azimuth": solar_azimuth_deg
661
+ }, index=index)
662
+
663
+ def calculate_surface_azimuth(orientation: str, building_orientation_angle: float) -> float:
664
+ """
665
+ Calculate the actual surface azimuth based on orientation label and building rotation.
666
+ Azimuth: 0=N, 90=E, 180=S, 270=W
667
+
668
+ Args:
669
+ orientation: Orientation label (e.g., "A (North)", "B (South)")
670
+ building_orientation_angle: Building rotation angle from North (degrees, positive East)
671
+
672
+ Returns:
673
+ Surface azimuth in degrees.
674
+ """
675
+ base_azimuth = {
676
+ "A (North)": 0.0,
677
+ "B (South)": 180.0,
678
+ "C (East)": 90.0,
679
+ "D (West)": 270.0,
680
+ "Horizontal": 0.0
681
+ }.get(orientation, 0.0)
682
+
683
+ # Adjust for building rotation
684
+ surface_azimuth = (base_azimuth + building_orientation_angle) % 360
685
+ return surface_azimuth
686
+
687
+ 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:
688
+ """
689
+ Calculate total incident solar radiation on a tilted surface.
690
+
691
+ Args:
692
+ dnr: Direct Normal Radiation (W/m²)
693
+ dhr: Diffuse Horizontal Radiation (W/m²)
694
+ solar_altitude: Solar altitude angle (degrees)
695
+ solar_azimuth: Solar azimuth angle (degrees, 0=N, positive E)
696
+ tilt: Surface tilt angle from horizontal (degrees)
697
+ surface_azimuth: Surface azimuth angle (degrees, 0=N, positive E)
698
+
699
+ Returns:
700
+ Total incident solar radiation on the surface (W/m²).
701
+ """
702
+ # Convert angles to radians
703
+ alt_rad = np.radians(solar_altitude)
704
+ az_rad = np.radians(solar_azimuth)
705
+ tilt_rad = np.radians(tilt)
706
+ surf_az_rad = np.radians(surface_azimuth)
707
+
708
+ # Angle of Incidence (theta)
709
+ cos_theta = np.cos(alt_rad) * np.sin(tilt_rad) * np.cos(az_rad - surf_az_rad) + \
710
+ np.sin(alt_rad) * np.cos(tilt_rad)
711
+ cos_theta = np.maximum(0, cos_theta)
712
+
713
+ # Direct radiation component
714
+ direct_tilted = dnr * cos_theta
715
+
716
+ # Diffuse radiation component (simplified isotropic sky model)
717
+ sky_diffuse_tilted = dhr * (1 + np.cos(tilt_rad)) / 2
718
+
719
+ # Ground reflected component
720
+ albedo = 0.2
721
+ ground_reflected_tilted = (dnr * np.sin(alt_rad) + dhr) * albedo * (1 - np.cos(tilt_rad)) / 2
722
+
723
+ # Total incident solar radiation
724
+ total_incident = direct_tilted + sky_diffuse_tilted + ground_reflected_tilted
725
+ return total_incident
726
+
727
+ 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:
728
+ """
729
+ Calculate Sol-Air Temperature.
730
+
731
+ Args:
732
+ t_oa: Outdoor air temperature (°C)
733
+ dnr, dhr, solar_altitude, solar_azimuth: Solar data
734
+ tilt, surface_azimuth: Surface orientation
735
+ absorptivity: Surface solar absorptivity
736
+ emissivity: Surface thermal emissivity
737
+ h_o: Outside surface heat transfer coefficient (W/m²·K)
738
+
739
+ Returns:
740
+ Sol-air temperature (°C).
741
+ """
742
+ # Calculate incident solar radiation
743
+ i_total = calculate_incident_solar(dnr, dhr, solar_altitude, solar_azimuth, tilt, surface_azimuth)
744
+
745
+ # Calculate sky temperature
746
+ t_sky = t_oa * (SKY_TEMP_FACTOR * t_oa**0.25)**0.25
747
+
748
+ # Longwave radiation exchange term
749
+ delta_r = STEFAN_BOLTZMANN * emissivity * ((t_oa + 273.15)**4 - (t_sky + 273.15)**4) / h_o
750
+
751
+ # Sol-air temperature
752
+ t_sol_air = t_oa + (absorptivity * i_total / h_o) - delta_r
753
+ return t_sol_air
754
+
755
+ def calculate_hourly_internal_loads(internal_loads_data: Dict[str, List[Dict[str, Any]]]) -> Tuple[np.ndarray, np.ndarray]:
756
+ """
757
+ Calculate total hourly sensible and latent internal loads.
758
+
759
+ Args:
760
+ internal_loads_data: Dictionary containing lists of loads for each type.
761
+
762
+ Returns:
763
+ Tuple of (hourly_sensible_loads, hourly_latent_loads) as numpy arrays.
764
+ """
765
+ hourly_sensible = np.zeros(8760)
766
+ hourly_latent = np.zeros(8760)
767
+
768
+ # Process each load type
769
+ for load_type, loads in internal_loads_data.items():
770
+ for load in loads:
771
+ total_load = load["total"]
772
+ schedule_type = load["schedule_type"]
773
+
774
+ # Get schedule multipliers
775
+ schedule_multipliers = np.zeros(8760)
776
+ if schedule_type == "Custom" and "custom_schedule" in load:
777
+ weekday_schedule = load["custom_schedule"]["Weekday"]
778
+ weekend_schedule = load["custom_schedule"]["Weekend"]
779
+ elif schedule_type in DEFAULT_SCHEDULES:
780
+ weekday_schedule = DEFAULT_SCHEDULES[schedule_type]["Weekday"]
781
+ weekend_schedule = DEFAULT_SCHEDULES[schedule_type]["Weekend"]
782
+ else:
783
+ weekday_schedule = [1.0] * 24
784
+ weekend_schedule = [1.0] * 24
785
+
786
+ for hour in range(8760):
787
+ day_of_week = (hour // 24) % 7
788
+ hour_of_day = hour % 24
789
+ if day_of_week < 5:
790
+ schedule_multipliers[hour] = weekday_schedule[hour_of_day]
791
+ else:
792
+ schedule_multipliers[hour] = weekend_schedule[hour_of_day]
793
+
794
+ if load_type == "occupancy":
795
+ sensible_fraction = load["sensible_fraction"]
796
+ latent_fraction = load["latent_fraction"]
797
+ hourly_sensible += total_load * sensible_fraction * schedule_multipliers
798
+ hourly_latent += total_load * latent_fraction * schedule_multipliers
799
+ else:
800
+ hourly_sensible += total_load * schedule_multipliers
801
+
802
+ return hourly_sensible, hourly_latent
803
+
804
+ def display_load_results(results: Dict[str, Any]):
805
+ """
806
+ Display the calculated HVAC load results.
807
+
808
+ Args:
809
+ results: Dictionary containing calculation results.
810
+ """
811
+ st.subheader("Peak Load Summary")
812
+
813
  col1, col2, col3 = st.columns(3)
814
  with col1:
815
+ st.metric("Peak Cooling (Total)", f"{results['peak_cooling_total'] / 1000:.1f} kW", help=f"Occurred at hour {results['peak_cooling_total_hour']}")
816
+ st.metric("Peak Cooling (Sensible)", f"{results['peak_cooling_sensible'] / 1000:.1f} kW", help=f"Occurred at hour {results['peak_cooling_sensible_hour']}")
817
  with col2:
818
+ st.metric("Peak Heating", f"{results['peak_heating'] / 1000:.1f} kW", help=f"Occurred at hour {results['peak_heating_hour']}")
819
+ st.metric("Peak Cooling (Latent)", f"{results['peak_cooling_latent'] / 1000:.1f} kW", help=f"Occurred at hour {results['peak_cooling_latent_hour']}")
820
+
821
+ st.subheader("Hourly Load Profiles")
822
+
823
+ # Create DataFrame for plotting
824
+ hourly_df = pd.DataFrame({
825
+ "Hour": range(len(results["hourly_cooling_total"])),
826
+ "Cooling (Sensible)": results["hourly_cooling_sensible"],
827
+ "Cooling (Latent)": results["hourly_cooling_latent"],
828
+ "Cooling (Total)": results["hourly_cooling_total"],
829
+ "Heating": results["hourly_heating"]
830
+ })
831
+
832
+ # Plot cooling loads
833
+ fig_cooling = go.Figure()
834
+ fig_cooling.add_trace(go.Scatter(x=hourly_df["Hour"], y=hourly_df["Cooling (Sensible)"], name="Sensible Cooling", stackgroup='one'))
835
+ fig_cooling.add_trace(go.Scatter(x=hourly_df["Hour"], y=hourly_df["Cooling (Latent)"], name="Latent Cooling", stackgroup='one'))
836
+ fig_cooling.update_layout(title="Hourly Cooling Load Profile", xaxis_title="Hour Index", yaxis_title="Load (W)", height=400)
837
+ st.plotly_chart(fig_cooling, use_container_width=True)
838
+
839
+ # Plot heating loads
840
+ fig_heating = go.Figure()
841
+ fig_heating.add_trace(go.Scatter(x=hourly_df["Hour"], y=hourly_df["Heating"], name="Heating Load", line=dict(color='red')))
842
+ fig_heating.update_layout(title="Hourly Heating Load Profile", xaxis_title="Hour Index", yaxis_title="Load (W)", height=400)
843
+ st.plotly_chart(fig_heating, use_container_width=True)
844
+
845
+ st.subheader("Load Components at Peak Cooling Hour")
846
+ peak_hour = results["peak_cooling_total_hour"]
847
+
848
+ cooling_components = results["cooling_load_components"]
849
+ peak_cooling_data = {
850
+ "Component": list(cooling_components.keys()),
851
+ "Load (W)": [cooling_components[k][peak_hour] for k in cooling_components]
852
+ }
853
+ peak_cooling_df = pd.DataFrame(peak_cooling_data)
854
+ peak_cooling_df = peak_cooling_df[peak_cooling_df["Load (W)"] > 0]
855
+
856
+ fig_peak_cooling = px.pie(
857
+ peak_cooling_df,
858
+ values="Load (W)",
859
+ names="Component",
860
+ title=f"Cooling Load Breakdown at Peak Hour ({peak_hour})"
861
+ )
862
+ st.plotly_chart(fig_peak_cooling, use_container_width=True)
863
+
864
+ st.subheader("Load Components at Peak Heating Hour")
865
+ peak_hour_heating = results["peak_heating_hour"]
866
+
867
+ heating_components = results["heating_load_components"]
868
+ internal_gains_at_peak = results["cooling_load_components"]["internal_sensible"][peak_hour_heating]
869
+
870
+ peak_heating_data = {
871
+ "Component": list(heating_components.keys()) + ["Internal Gains Offset"],
872
+ "Load (W)": [heating_components[k][peak_hour_heating] for k in heating_components] + [-internal_gains_at_peak]
873
+ }
874
+ peak_heating_df = pd.DataFrame(peak_heating_data)
875
+ peak_heating_df = peak_heating_df[peak_heating_df["Load (W)"] != 0]
876
+
877
+ fig_peak_heating = px.pie(
878
+ peak_heating_df,
879
+ values="Load (W)",
880
+ names="Component",
881
+ title=f"Heating Load Breakdown at Peak Hour ({peak_hour_heating})"
882
+ )
883
+ st.plotly_chart(fig_peak_heating, use_container_width=True)
884
+
885
+ def get_available_materials() -> Dict[str, Any]:
886
+ mats = {}
887
+ if "materials" in st.session_state.project_data:
888
+ mats.update(st.session_state.project_data["materials"].get("library", {}))
889
+ mats.update(st.session_state.project_data["materials"].get("project", {}))
890
+ return mats
891
+
892
+ def get_available_constructions() -> Dict[str, Any]:
893
+ consts = {}
894
+ if "constructions" in st.session_state.project_data:
895
+ consts.update(st.session_state.project_data["constructions"].get("library", {}))
896
+ consts.update(st.session_state.project_data["constructions"].get("project", {}))
897
+ return consts
898
+
899
+ def get_available_fenestrations() -> Dict[str, Any]:
900
+ fens = {}
901
+ if "fenestrations" in st.session_state.project_data:
902
+ fens.update(st.session_state.project_data["fenestrations"].get("library", {}))
903
+ fens.update(st.session_state.project_data["fenestrations"].get("project", {}))
904
+ return fens
905
 
906
+ def display_hvac_loads_help():
907
+ """
908
+ Display help information for the HVAC loads page.
909
+ """
910
+ st.markdown("""
911
+ ### HVAC Loads Help
912
+
913
+ This section calculates the building's heating and cooling loads based on the information provided in the previous steps.
914
+
915
+ **Calculation Process:**
916
+
917
+ 1. **Heat Transfer**: Calculates heat gains and losses through the building envelope (walls, roofs, floors, windows, doors, skylights) considering conduction and solar radiation.
918
+ 2. **Infiltration & Ventilation**: Calculates loads due to air exchange with the outside.
919
+ 3. **Internal Loads**: Incorporates heat gains from occupants, lighting, and equipment.
920
+ 4. **Summation**: Combines all heat gains and losses to determine the net hourly cooling and heating loads.
921
+
922
+ **Simulation Parameters:**
923
+
924
+ * **Simulation Period**: Choose 'Full Year', a specific date range ('From-to'), or degree-day methods ('HDD' or 'CDD').
925
+ * **Indoor Conditions**: Set fixed setpoints, time-varying schedules, or adaptive comfort temperatures.
926
+ * **Operating Hours**: Define when the HVAC system operates.
927
+
928
+ **Results:**
929
+
930
+ * **Peak Loads**: Shows the maximum calculated cooling and heating loads.
931
+ * **Hourly Profiles**: Displays graphs of loads for the selected period.
932
+ * **Load Components**: Shows the breakdown of loads at peak hours.
933
+
934
+ **Workflow:**
935
+
936
+ 1. Complete all previous sections.
937
+ 2. Configure simulation parameters.
938
+ 3. Click "Calculate HVAC Loads".
939
+ 4. Review results.
940
+ 5. Proceed to Building Energy section.
941
+
942
+ **Important:**
943
+
944
+ * Calculations follow ASHRAE methodology.
945
+ * Input data quality affects accuracy.
946
+ * Review results carefully.
947
+ """)