mabuseif commited on
Commit
dc18ae3
·
verified ·
1 Parent(s): 95c7797

Update app/hvac_loads.py

Browse files
Files changed (1) hide show
  1. app/hvac_loads.py +36 -659
app/hvac_loads.py CHANGED
@@ -8,666 +8,43 @@ modules to determine the building's thermal loads.
8
  Developed by: Dr Majed Abuseif, Deakin University
9
  © 2025
10
  """
11
-
12
- import numpy as np
13
- import pandas as pd
14
- from typing import Dict, List, Optional, NamedTuple, Any, Tuple
15
- from enum import Enum
16
  import streamlit as st
17
- from app.materials_library import MaterialLibrary, Material, GlazingMaterial
18
- # from app.construction import Construction
19
- from datetime import datetime
20
- from collections import defaultdict
21
- import logging
22
- import math
23
- # from utils.ctf_calculations import CTFCalculator, ComponentType, CTFCoefficients
24
-
25
- # Configure logging
26
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
27
- logger = logging.getLogger(__name__)
28
-
29
- class TFMCalculations:
30
- # Solar calculation constants (from solar.py)
31
- SHGC_COEFFICIENTS = {
32
- "Single Clear": [0.1, -0.0, 0.0, -0.0, 0.0, 0.87],
33
- "Single Tinted": [0.12, -0.0, 0.0, -0.0, 0.8, -0.0],
34
- "Double Clear": [0.14, -0.0, 0.0, -0.0, 0.78, -0.0],
35
- "Double Low-E": [0.2, -0.0, 0.0, 0.7, 0.0, -0.0],
36
- "Double Tinted": [0.15, -0.0, 0.0, -0.0, 0.65, -0.0],
37
- "Double Low-E with Argon": [0.18, -0.0, 0.0, 0.68, 0.0, -0.0],
38
- "Single Low-E Reflective": [0.22, -0.0, 0.0, 0.6, 0.0, -0.0],
39
- "Double Reflective": [0.24, -0.0, 0.0, 0.58, 0.0, -0.0],
40
- "Electrochromic": [0.25, -0.0, 0.5, -0.0, 0.0, -0.0]
41
- }
42
-
43
- GLAZING_TYPE_MAPPING = {
44
- "Single Clear 3mm": "Single Clear",
45
- "Single Clear 6mm": "Single Clear",
46
- "Single Tinted 6mm": "Single Tinted",
47
- "Double Clear 6mm/13mm Air": "Double Clear",
48
- "Double Low-E 6mm/13mm Air": "Double Low-E",
49
- "Double Tinted 6mm/13mm Air": "Double Tinted",
50
- "Double Low-E 6mm/13mm Argon": "Double Low-E with Argon",
51
- "Single Low-E Reflective 6mm": "Single Low-E Reflective",
52
- "Double Reflective 6mm/13mm Air": "Double Reflective",
53
- "Electrochromic 6mm/13mm Air": "Electrochromic"
54
- }
55
-
56
- @staticmethod
57
- def calculate_conduction_load(component, outdoor_temp: float, indoor_temp: float, hour: int, mode: str = "none") -> tuple[float, float]:
58
- """Calculate conduction load for heating and cooling in kW based on mode."""
59
- if mode == "none":
60
- return 0, 0
61
- delta_t = outdoor_temp - indoor_temp
62
- if mode == "cooling" and delta_t <= 0:
63
- return 0, 0
64
- if mode == "heating" and delta_t >= 0:
65
- return 0, 0
66
-
67
- # Get CTF coefficients using CTFCalculator
68
- ctf = CTFCalculator.calculate_ctf_coefficients(component)
69
-
70
- # Initialize history terms (simplified: assume steady-state history for demonstration)
71
- load = component.u_value * component.area * delta_t
72
- for i in range(len(ctf.Y)):
73
- load += component.area * ctf.Y[i] * (outdoor_temp - indoor_temp) * np.exp(-i * 3600 / 3600)
74
- load -= component.area * ctf.Z[i] * (outdoor_temp - indoor_temp) * np.exp(-i * 3600 / 3600)
75
- cooling_load = load / 1000 if mode == "cooling" else 0
76
- heating_load = -load / 1000 if mode == "heating" else 0
77
- return cooling_load, heating_load
78
-
79
- @staticmethod
80
- def day_of_year(month: int, day: int, year: int) -> int:
81
- """Calculate day of the year (n) from month, day, and year, accounting for leap years."""
82
- days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
83
- if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0):
84
- days_in_month[1] = 29
85
- return sum(days_in_month[:month-1]) + day
86
-
87
- @staticmethod
88
- def equation_of_time(n: int) -> float:
89
- """Calculate Equation of Time (EOT) in minutes using Spencer's formula."""
90
- B = (n - 1) * 360 / 365
91
- B_rad = math.radians(B)
92
- EOT = 229.2 * (0.000075 + 0.001868 * math.cos(B_rad) - 0.032077 * math.sin(B_rad) -
93
- 0.014615 * math.cos(2 * B_rad) - 0.04089 * math.sin(2 * B_rad))
94
- return EOT
95
-
96
- @staticmethod
97
- def calculate_dynamic_shgc(glazing_type: str, cos_theta: float) -> float:
98
- """Calculate dynamic SHGC based on incidence angle."""
99
- if glazing_type not in TFMCalculations.SHGC_COEFFICIENTS:
100
- logger.warning(f"Unknown glazing type '{glazing_type}'. Using default SHGC coefficients for Single Clear.")
101
- glazing_type = "Single Clear"
102
-
103
- c = TFMCalculations.SHGC_COEFFICIENTS[glazing_type]
104
- f_cos_theta = (c[0] + c[1] * cos_theta + c[2] * cos_theta**2 +
105
- c[3] * cos_theta**3 + c[4] * cos_theta**4 + c[5] * cos_theta**5)
106
- return f_cos_theta
107
-
108
- @staticmethod
109
- def get_surface_parameters(component: Any, building_info: Dict, material_library: MaterialLibrary,
110
- project_materials: Dict, project_constructions: Dict,
111
- project_fenestrations: Dict) -> Tuple[float, float, float, Optional[float], float]:
112
- """
113
- Determine surface parameters (tilt, azimuth, h_o, emissivity, absorptivity) for a component.
114
- Uses MaterialLibrary for materials/fenestrations and Construction for walls/roofs/floors.
115
- """
116
- component_name = getattr(component, 'name', 'unnamed_component')
117
-
118
- # Initialize default values
119
- surface_tilt = 90.0 # Default vertical for walls, windows
120
- surface_azimuth = 0.0 # Default north-facing
121
- h_o = 17.0 # Default exterior convection coefficient
122
- emissivity = 0.9 # Default for opaque components
123
- absorptivity = 0.6 # Default
124
-
125
- try:
126
- # Set component-specific defaults based on type
127
- if component.component_type == ComponentType.ROOF:
128
- surface_tilt = getattr(component, 'tilt', 0.0)
129
- h_o = 23.0
130
- surface_azimuth = getattr(component, 'rotation', 0.0)
131
- logger.debug(f"Roof component {component_name}: using rotation={surface_azimuth}, tilt={surface_tilt}")
132
-
133
- elif component.component_type == ComponentType.SKYLIGHT:
134
- surface_tilt = getattr(component, 'tilt', 0.0)
135
- h_o = 23.0
136
- surface_azimuth = getattr(component, 'rotation', 0.0)
137
- logger.debug(f"Skylight component {component_name}: using rotation={surface_azimuth}, tilt={surface_tilt}")
138
-
139
- elif component.component_type == ComponentType.FLOOR:
140
- surface_tilt = 180.0
141
- h_o = 17.0
142
- surface_azimuth = 0.0
143
- logger.debug(f"Floor component {component_name}: using default azimuth={surface_azimuth}, tilt={surface_tilt}")
144
-
145
- else: # WALL, WINDOW
146
- surface_tilt = 90.0
147
- h_o = 17.0
148
- elevation = getattr(component, 'elevation', None)
149
- if not elevation:
150
- logger.warning(f"Component {component_name} ({component.component_type.value}) is missing 'elevation' field. Using default azimuth=0.")
151
- surface_azimuth = 0.0
152
- else:
153
- elevation_angles = {
154
- "A": building_info.get("orientation_angle", 0.0),
155
- "B": (building_info.get("orientation_angle", 0.0) + 90.0) % 360,
156
- "C": (building_info.get("orientation_angle", 0.0) + 180.0) % 360,
157
- "D": (building_info.get("orientation_angle", 0.0) + 270.0) % 360
158
- }
159
- if elevation not in elevation_angles:
160
- logger.warning(f"Invalid elevation '{elevation}' for component {component_name}. Using default azimuth=0.")
161
- surface_azimuth = 0.0
162
- else:
163
- surface_azimuth = (elevation_angles[elevation] + getattr(component, 'rotation', 0.0)) % 360
164
- logger.debug(f"Component {component_name}: elevation={elevation}, total_azimuth={surface_azimuth}, tilt={surface_tilt}")
165
-
166
- # Fetch material properties
167
- if component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]:
168
- construction = getattr(component, 'construction', None)
169
- if not construction:
170
- logger.warning(f"No construction defined for {component_name} ({component.component_type.value}). Using defaults: absorptivity=0.6, emissivity=0.9.")
171
- else:
172
- construction_obj = project_constructions.get(construction) or material_library.library_constructions.get(construction)
173
- if not construction_obj or not construction_obj.layers:
174
- logger.warning(f"Construction not found or empty for {component_name}. Using defaults.")
175
- else:
176
- absorptivity = getattr(construction_obj, 'absorptivity', 0.6)
177
- emissivity = getattr(construction_obj, 'emissivity', 0.9)
178
- logger.debug(f"Using construction for {component_name}: absorptivity={absorptivity}, emissivity={emissivity}")
179
-
180
- elif component.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
181
- fenestration = getattr(component, 'fenestration', None)
182
- if not fenestration:
183
- logger.warning(f"No fenestration defined for {component_name} ({component.component_type.value}). Using default SHGC=0.7, h_o={h_o}.")
184
- shgc = 0.7
185
- else:
186
- fenestration_obj = project_fenestrations.get(fenestration) or material_library.library_fenestrations.get(fenestration)
187
- if not fenestration_obj:
188
- logger.warning(f"Fenestration not found for {component_name}. Using default SHGC=0.7.")
189
- shgc = 0.7
190
- else:
191
- shgc = getattr(fenestration_obj, 'shgc', 0.7)
192
- h_o = getattr(fenestration_obj, 'h_o', h_o)
193
- logger.debug(f"Using fenestration for {component_name}: shgc={shgc}, h_o={h_o}")
194
- emissivity = None # Not used for fenestration
195
-
196
- except Exception as e:
197
- logger.error(f"Error retrieving surface parameters for {component_name}: {str(e)}")
198
- if component.component_type == ComponentType.ROOF:
199
- surface_tilt = 0.0
200
- h_o = 23.0
201
- surface_azimuth = 0.0
202
- elif component.component_type == ComponentType.SKYLIGHT:
203
- surface_tilt = 0.0
204
- h_o = 23.0
205
- surface_azimuth = 0.0
206
- elif component.component_type == ComponentType.FLOOR:
207
- surface_tilt = 180.0
208
- h_o = 17.0
209
- surface_azimuth = 0.0
210
- else:
211
- surface_tilt = 90.0
212
- h_o = 17.0
213
- surface_azimuth = 0.0
214
- absorptivity = 0.6 if component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR] else 0.0
215
- emissivity = 0.9 if component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR] else None
216
-
217
- logger.info(f"Final surface parameters for {component_name}: tilt={surface_tilt:.1f}, azimuth={surface_azimuth:.1f}, h_o={h_o:.1f}")
218
- return surface_tilt, surface_azimuth, h_o, emissivity, absorptivity
219
-
220
- @staticmethod
221
- def calculate_solar_load(component, hourly_data: Dict, hour: int, building_orientation: float, mode: str = "none") -> float:
222
- """Calculate solar load in kW (cooling only) using ASHRAE-compliant solar calculations."""
223
- if mode != "cooling":
224
- return 0
225
- if component.component_type == ComponentType.FLOOR:
226
- return 0
227
-
228
- component_name = getattr(component, 'name', 'unnamed_component')
229
-
230
- try:
231
- material_library = st.session_state.project_data.get("material_library", MaterialLibrary())
232
- project_materials = st.session_state.project_data.get("materials", {}).get("project", {})
233
- project_constructions = st.session_state.project_data.get("constructions", {}).get("project", {})
234
- project_fenestrations = st.session_state.project_data.get("fenestrations", {}).get("project", {})
235
-
236
- climate_data = st.session_state.project_data.get("climate_data", {})
237
- latitude = climate_data.get("latitude", 0.0)
238
- longitude = climate_data.get("longitude", 0.0)
239
- timezone = climate_data.get("time_zone", 0.0)
240
- ground_reflectivity = climate_data.get("ground_reflectivity", 0.2)
241
-
242
- if not -90 <= latitude <= 90:
243
- logger.warning(f"Invalid latitude {latitude} for {component_name}. Using default 0.0.")
244
- latitude = 0.0
245
- if not -180 <= longitude <= 180:
246
- logger.warning(f"Invalid longitude {longitude} for {component_name}. Using default 0.0.")
247
- longitude = 0.0
248
- if not -12 <= timezone <= 14:
249
- logger.warning(f"Invalid timezone {timezone} for {component_name}. Using default 0.0.")
250
- timezone = 0.0
251
- if not 0 <= ground_reflectivity <= 1:
252
- logger.warning(f"Invalid ground_reflectivity {ground_reflectivity} for {component_name}. Using default 0.2.")
253
- ground_reflectivity = 0.2
254
-
255
- required_fields = ["month", "day", "hour", "global_horizontal_radiation", "direct_normal_radiation",
256
- "diffuse_horizontal_radiation", "dry_bulb"]
257
- if not all(field in hourly_data for field in required_fields):
258
- logger.warning(f"Missing required fields in hourly_data for hour {hour} for {component_name}: {hourly_data}")
259
- return 0
260
-
261
- if hourly_data["global_horizontal_radiation"] <= 0:
262
- logger.info(f"No solar load for hour {hour} due to GHI={hourly_data['global_horizontal_radiation']} for {component_name}")
263
- return 0
264
-
265
- month = hourly_data["month"]
266
- day = hourly_data["day"]
267
- hour = hourly_data["hour"]
268
- ghi = hourly_data["global_horizontal_radiation"]
269
- dni = hourly_data.get("direct_normal_radiation", ghi * 0.7)
270
- dhi = hourly_data.get("diffuse_horizontal_radiation", ghi * 0.3)
271
- outdoor_temp = hourly_data["dry_bulb"]
272
-
273
- if ghi < 0 or dni < 0 or dhi < 0:
274
- logger.error(f"Negative radiation values for {month}/{day}/{hour} for {component_name}")
275
- raise ValueError(f"Negative radiation values for {month}/{day}/{hour}")
276
-
277
- logger.info(f"Processing solar for {month}/{day}/{hour} with GHI={ghi}, DNI={dni}, DHI={dhi}, "
278
- f"dry_bulb={outdoor_temp} for {component_name}")
279
-
280
- year = 2025
281
- n = TFMCalculations.day_of_year(month, day, year)
282
- EOT = TFMCalculations.equation_of_time(n)
283
- lambda_std = 15 * timezone
284
- standard_time = hour - 1 + 0.5
285
- LST = standard_time + (4 * (lambda_std - longitude) + EOT) / 60
286
-
287
- delta = 23.45 * math.sin(math.radians(360 / 365 * (284 + n)))
288
- hra = 15 * (LST - 12)
289
- phi = math.radians(latitude)
290
- delta_rad = math.radians(delta)
291
- hra_rad = math.radians(hra)
292
-
293
- sin_alpha = math.sin(phi) * math.sin(delta_rad) + math.cos(phi) * math.cos(delta_rad) * math.cos(hra_rad)
294
- alpha = math.degrees(math.asin(sin_alpha))
295
-
296
- if abs(math.cos(math.radians(alpha))) < 0.01:
297
- azimuth = 0
298
- else:
299
- sin_az = math.cos(delta_rad) * math.sin(hra_rad) / math.cos(math.radians(alpha))
300
- cos_az = (sin_alpha * math.sin(phi) - math.sin(delta_rad)) / (math.cos(math.radians(alpha)) * math.cos(phi))
301
- azimuth = math.degrees(math.atan2(sin_az, cos_az))
302
- if hra > 0:
303
- azimuth = 360 - azimuth if azimuth > 0 else -azimuth
304
-
305
- logger.info(f"Solar angles for {month}/{day}/{hour}: declination={delta:.2f}, LST={LST:.2f}, "
306
- f"HRA={hra:.2f}, altitude={alpha:.2f}, azimuth={azimuth:.2f} for {component_name}")
307
-
308
- building_info = {"orientation_angle": building_orientation}
309
- surface_tilt, surface_azimuth, h_o, emissivity, absorptivity = \
310
- TFMCalculations.get_surface_parameters(
311
- component, building_info, material_library, project_materials,
312
- project_constructions, project_fenestrations
313
- )
314
-
315
- alpha_rad = math.radians(alpha)
316
- surface_tilt_rad = math.radians(surface_tilt)
317
- azimuth_rad = math.radians(azimuth)
318
- surface_azimuth_rad = math.radians(surface_azimuth)
319
-
320
- cos_theta = (math.sin(alpha_rad) * math.cos(surface_tilt_rad) +
321
- math.cos(alpha_rad) * math.sin(surface_tilt_rad) *
322
- math.cos(azimuth_rad - surface_azimuth_rad))
323
- cos_theta = max(min(cos_theta, 1.0), 0.0)
324
-
325
- logger.info(f"Component {component_name} at {month}/{day}/{hour}: "
326
- f"surface_tilt={surface_tilt:.2f}, surface_azimuth={surface_azimuth:.2f}, "
327
- f"cos_theta={cos_theta:.4f}")
328
-
329
- view_factor = (1 - math.cos(surface_tilt_rad)) / 2
330
- ground_reflected = ground_reflectivity * ghi * view_factor
331
-
332
- if cos_theta > 0:
333
- I_t = dni * cos_theta + dhi + ground_reflected
334
- else:
335
- I_t = dhi + ground_reflected
336
-
337
- solar_heat_gain = 0.0
338
-
339
- if component.component_type in [ComponentType.WINDOW, ComponentType.SKYLIGHT]:
340
- fenestration = getattr(component, 'fenestration', None)
341
- shgc = 0.7
342
- if fenestration:
343
- fenestration_obj = project_fenestrations.get(fenestration) or material_library.library_fenestrations.get(fenestration)
344
- if fenestration_obj:
345
- shgc = getattr(fenestration_obj, 'shgc', 0.7)
346
- h_o = getattr(fenestration_obj, 'h_o', h_o)
347
- else:
348
- logger.warning(f"Fenestration not found for {component_name}. Using default SHGC=0.7.")
349
-
350
- glazing_type = TFMCalculations.GLAZING_TYPE_MAPPING.get(fenestration, "Single Clear")
351
- iac = getattr(component, 'shading_coefficient', 1.0)
352
- shgc_dynamic = shgc * TFMCalculations.calculate_dynamic_shgc(glazing_type, cos_theta)
353
- solar_heat_gain = component.area * shgc_dynamic * I_t * iac / 1000
354
-
355
- logger.info(f"Fenestration solar heat gain for {component_name} at {month}/{day}/{hour}: "
356
- f"{solar_heat_gain:.4f} kW (area={component.area}, shgc_dynamic={shgc_dynamic:.4f}, "
357
- f"I_t={I_t:.2f}, iac={iac})")
358
-
359
- elif component.component_type in [ComponentType.WALL, ComponentType.ROOF, ComponentType.FLOOR]:
360
- surface_resistance = 1/h_o
361
- solar_heat_gain = component.area * absorptivity * I_t * surface_resistance / 1000
362
-
363
- logger.info(f"Opaque surface solar heat gain for {component_name} at {month}/{day}/{hour}: "
364
- f"{solar_heat_gain:.4f} kW (area={component.area}, absorptivity={absorptivity:.2f}, "
365
- f"I_t={I_t:.2f}, surface_resistance={surface_resistance:.4f})")
366
-
367
- return solar_heat_gain
368
-
369
- except Exception as e:
370
- logger.error(f"Error calculating solar load for {component_name} at hour {hour}: {str(e)}")
371
- return 0
372
-
373
- @staticmethod
374
- def calculate_internal_load(internal_loads: Dict, hour: int, operation_hours: int, area: float) -> float:
375
- """Calculate total internal load in kW."""
376
- total_load = 0
377
- for group in internal_loads.get("people", []):
378
- activity_data = group["activity_data"]
379
- sensible = (activity_data["sensible_min_w"] + activity_data["sensible_max_w"]) / 2
380
- latent = (activity_data["latent_min_w"] + activity_data["latent_max_w"]) / 2
381
- load_per_person = sensible + latent
382
- total_load += group["num_people"] * load_per_person * group.get("diversity_factor", 1.0)
383
- for light in internal_loads.get("lighting", []):
384
- lpd = light["lpd"]
385
- lighting_operating_hours = light.get("operating_hours", operation_hours)
386
- fraction = min(lighting_operating_hours, operation_hours) / operation_hours if operation_hours > 0 else 0
387
- lighting_load = lpd * light["area"] * fraction
388
- total_load += lighting_load
389
- for equip in internal_loads.get("equipment", []):
390
- equipment_load = equip["equipment_load"] * equip["area"]
391
- total_load += equipment_load
392
- return total_load / 1000
393
-
394
- @staticmethod
395
- def calculate_ventilation_load(internal_loads: Dict, outdoor_temp: float, indoor_temp: float, area: float, building_info: Dict, mode: str = "none") -> tuple[float, float]:
396
- """Calculate ventilation load for heating and cooling in kW based on mode."""
397
- if mode == "none":
398
- return 0, 0
399
- total_ventilation_flow = 0
400
- num_people = sum(group["num_people"] for group in internal_loads.get("people", []))
401
- for ventilation in internal_loads.get("ventilation", []):
402
- space_rate = ventilation.get("design_flow_rate", 0.3) # L/s/m²
403
- total_ventilation_flow += space_rate * ventilation["area"] / 1000 # m³/s
404
- if not internal_loads.get("ventilation"):
405
- total_ventilation_flow = 0.3 * area / 1000 + 2.5 * num_people / 1000 # Default rates
406
- air_density = 1.2
407
- specific_heat = 1000
408
- delta_t = outdoor_temp - indoor_temp
409
- if mode == "cooling" and delta_t <= 0:
410
- return 0, 0
411
- if mode == "heating" and delta_t >= 0:
412
- return 0, 0
413
- load = total_ventilation_flow * air_density * specific_heat * delta_t / 1000
414
- cooling_load = load if mode == "cooling" else 0
415
- heating_load = -load if mode == "heating" else 0
416
- return cooling_load, heating_load
417
-
418
- @staticmethod
419
- def calculate_infiltration_load(internal_loads: Dict, outdoor_temp: float, indoor_temp: float, area: float, building_info: Dict, mode: str = "none") -> tuple[float, float]:
420
- """Calculate infiltration load for heating and cooling in kW based on mode."""
421
- if mode == "none":
422
- return 0, 0
423
- total_infiltration_flow = 0
424
- building_height = building_info.get("building_height", 3.0)
425
- volume = area * building_height
426
- air_density = 1.2
427
- specific_heat = 1000
428
- delta_t = outdoor_temp - indoor_temp
429
- if mode == "cooling" and delta_t <= 0:
430
- return 0, 0
431
- if mode == "heating" and delta_t >= 0:
432
- return 0, 0
433
- for infiltration in internal_loads.get("infiltration", []):
434
- method = infiltration.get("system_type", "AirChanges/Hour")
435
- if method == "AirChanges/Hour":
436
- ach = infiltration.get("design_flow_rate", 0.5)
437
- total_infiltration_flow += ach * volume / 3600
438
- elif method == "Crack Flow":
439
- ela = infiltration.get("effective_air_leakage_area", 0.0001) / 10000 # cm² to m²
440
- wind_speed = 4.0
441
- total_infiltration_flow += ela * infiltration["area"] * wind_speed / 2
442
- else: # Flow Equation
443
- c = infiltration.get("flow_coefficient", 0.0001)
444
- n = infiltration.get("pressure_exponent", 0.65)
445
- delta_t_abs = abs(delta_t)
446
- total_infiltration_flow += c * (delta_t_abs ** n) * infiltration["area"] / 3600
447
- if not internal_loads.get("infiltration"):
448
- total_infiltration_flow = 0.5 * volume / 3600 # Default ACH
449
- load = total_infiltration_flow * air_density * specific_heat * delta_t / 1000
450
- cooling_load = load if mode == "cooling" else 0
451
- heating_load = -load if mode == "heating" else 0
452
- return cooling_load, heating_load
453
-
454
- @staticmethod
455
- def get_adaptive_comfort_temp(outdoor_temp: float) -> float:
456
- """Calculate adaptive comfort temperature per ASHRAE 55."""
457
- if 10 <= outdoor_temp <= 33.5:
458
- return 0.31 * outdoor_temp + 17.8
459
- return 24.0
460
-
461
- @staticmethod
462
- def filter_hourly_data(hourly_data: List[Dict], sim_period: Dict, climate_data: Dict) -> List[Dict]:
463
- """Filter hourly data based on simulation period, ignoring year."""
464
- if sim_period.get("type") == "Full Year":
465
- return hourly_data
466
- filtered_data = []
467
- if sim_period.get("type") == "From-to":
468
- start_month = sim_period["start_date"].month
469
- start_day = sim_period["start_date"].day
470
- end_month = sim_period["end_date"].month
471
- end_day = sim_period["end_date"].day
472
- for data in hourly_data:
473
- month, day = data["month"], data["day"]
474
- if (month > start_month or (month == start_month and day >= start_day)) and \
475
- (month < end_month or (month == end_month and day <= end_day)):
476
- filtered_data.append(data)
477
- elif sim_period.get("type") in ["HDD", "CDD"]:
478
- base_temp = sim_period.get("base_temp", 18.3 if sim_period["type"] == "HDD" else 23.9)
479
- for data in hourly_data:
480
- temp = data["dry_bulb"]
481
- if (sim_period["type"] == "HDD" and temp < base_temp) or (sim_period["type"] == "CDD" and temp > base_temp):
482
- filtered_data.append(data)
483
- return filtered_data
484
-
485
- @staticmethod
486
- def get_indoor_conditions(indoor_conditions: Dict, hour: int, outdoor_temp: float, building_info: Dict) -> Dict:
487
- """Determine indoor conditions based on user settings and building_info."""
488
- winter_temp = building_info.get("winter_indoor_design_temp", 20.0)
489
- summer_temp = building_info.get("summer_indoor_design_temp", 24.0)
490
- winter_rh = building_info.get("winter_indoor_design_rh", 50.0)
491
- summer_rh = building_info.get("summer_indoor_design_rh", 50.0)
492
-
493
- if indoor_conditions.get("type") == "Fixed":
494
- mode = "none" if abs(outdoor_temp - winter_temp) < 0.01 else "cooling" if outdoor_temp > summer_temp else "heating"
495
- if mode == "cooling":
496
- return {"temperature": summer_temp, "rh": summer_rh}
497
- elif mode == "heating":
498
- return {"temperature": winter_temp, "rh": winter_rh}
499
- else:
500
- return {"temperature": (winter_temp + summer_temp) / 2, "rh": (winter_rh + summer_rh) / 2}
501
- elif indoor_conditions.get("type") == "Time-varying":
502
- schedule = indoor_conditions.get("schedule", [])
503
- if schedule:
504
- hour_idx = hour % 24
505
- for entry in schedule:
506
- if entry["hour"] == hour_idx:
507
- return {"temperature": entry["temperature"], "rh": entry["rh"]}
508
- return {"temperature": (winter_temp + summer_temp) / 2, "rh": (winter_rh + summer_rh) / 2}
509
- else: # Adaptive
510
- return {"temperature": TFMCalculations.get_adaptive_comfort_temp(outdoor_temp), "rh": (winter_rh + summer_rh) / 2}
511
-
512
- @staticmethod
513
- def calculate_tfm_loads(components: Dict, hourly_data: List[Dict], indoor_conditions: Dict, internal_loads: Dict, building_info: Dict, sim_period: Dict, hvac_settings: Dict) -> List[Dict]:
514
- """Calculate TFM loads for heating and cooling with user-defined filters and temperature threshold."""
515
- filtered_data = TFMCalculations.filter_hourly_data(hourly_data, sim_period, building_info)
516
- temp_loads = []
517
- building_orientation = building_info.get("orientation_angle", 0.0)
518
- operation_hours = building_info.get("operation_hours", 8)
519
- operating_periods = [{"start": 8, "end": 8 + operation_hours}] # Convert float to dict
520
- area = building_info.get("floor_area", 100.0)
521
-
522
- if "material_library" not in st.session_state.project_data:
523
- st.session_state.project_data["material_library"] = MaterialLibrary()
524
- logger.info("Initialized MaterialLibrary in session_state for solar calculations")
525
-
526
- for comp_list in components.values():
527
- for comp in comp_list:
528
- comp.ctf = CTFCalculator.calculate_ctf_coefficients(comp)
529
-
530
- for hour_data in filtered_data:
531
- hour = hour_data["hour"]
532
- outdoor_temp = hour_data["dry_bulb"]
533
- indoor_cond = TFMCalculations.get_indoor_conditions(indoor_conditions, hour, outdoor_temp, building_info)
534
- indoor_temp = indoor_cond["temperature"]
535
- conduction_cooling = conduction_heating = solar = internal = ventilation_cooling = ventilation_heating = infiltration_cooling = infiltration_heating = 0
536
- is_operating = False
537
- for period in operating_periods:
538
- start_hour = period.get("start", 8)
539
- end_hour = period.get("end", 18)
540
- if start_hour <= hour % 24 <= end_hour:
541
- is_operating = True
542
- break
543
- mode = "none" if abs(outdoor_temp - building_info.get("winter_indoor_design_temp", 20.0)) < 0.01 else \
544
- "cooling" if outdoor_temp > building_info.get("summer_indoor_design_temp", 24.0) else "heating"
545
- if is_operating and mode == "cooling":
546
- for comp_list in components.values():
547
- for comp in comp_list:
548
- cool_load, _ = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="cooling")
549
- conduction_cooling += cool_load
550
- solar += TFMCalculations.calculate_solar_load(comp, hour_data, hour, building_orientation, mode="cooling")
551
- logger.info(f"Component {comp.name} solar load: {solar:.3f} kW")
552
- internal = TFMCalculations.calculate_internal_load(internal_loads, hour, operation_hours, area)
553
- ventilation_cooling, _ = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="cooling")
554
- infiltration_cooling, _ = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="cooling")
555
- elif is_operating and mode == "heating":
556
- for comp_list in components.values():
557
- for comp in comp_list:
558
- _, heat_load = TFMCalculations.calculate_conduction_load(comp, outdoor_temp, indoor_temp, hour, mode="heating")
559
- conduction_heating += heat_load
560
- internal = TFMCalculations.calculate_internal_load(internal_loads, hour, operation_hours, area)
561
- _, ventilation_heating = TFMCalculations.calculate_ventilation_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="heating")
562
- _, infiltration_heating = TFMCalculations.calculate_infiltration_load(internal_loads, outdoor_temp, indoor_temp, area, building_info, mode="heating")
563
- else:
564
- internal = 0
565
-
566
- logger.info(f"Hour {hour} total loads - conduction: {conduction_cooling:.3f} kW, solar: {solar:.3f} kW, internal: {internal:.3f} kW")
567
-
568
- total_cooling = conduction_cooling + solar + internal + ventilation_cooling + infiltration_cooling
569
- total_heating = max(conduction_heating + ventilation_heating + infiltration_heating - internal, 0)
570
- if mode == "cooling":
571
- total_heating = 0
572
- elif mode == "heating":
573
- total_cooling = 0
574
- temp_loads.append({
575
- "hour": hour,
576
- "month": hour_data["month"],
577
- "day": hour_data["day"],
578
- "conduction_cooling": conduction_cooling,
579
- "conduction_heating": conduction_heating,
580
- "solar": solar,
581
- "internal": internal,
582
- "ventilation_cooling": ventilation_cooling,
583
- "ventilation_heating": ventilation_heating,
584
- "infiltration_cooling": infiltration_cooling,
585
- "infiltration_heating": infiltration_heating,
586
- "total_cooling": total_cooling,
587
- "total_heating": total_heating
588
- })
589
-
590
- loads_by_day = defaultdict(list)
591
- for load in temp_loads:
592
- day_key = (load["month"], load["day"])
593
- loads_by_day[day_key].append(load)
594
- final_loads = []
595
- for day_key, day_loads in loads_by_day.items():
596
- cooling_hours = sum(1 for load in day_loads if load["total_cooling"] > 0)
597
- heating_hours = sum(1 for load in day_loads if load["total_heating"] > 0)
598
- for load in day_loads:
599
- if cooling_hours > heating_hours:
600
- load["total_heating"] = 0
601
- elif heating_hours > cooling_hours:
602
- load["total_cooling"] = 0
603
- else:
604
- load["total_cooling"] = 0
605
- load["total_heating"] = 0
606
- final_loads.append(load)
607
- return final_loads
608
 
609
- def display_hvac_loads_page():
610
- """Display the HVAC Loads page and perform calculations."""
611
- st.header("HVAC Loads")
612
-
613
- # Access project data
614
- project_data = st.session_state.project_data
615
- components = project_data.get("components", {})
616
- climate_data = project_data.get("climate_data", {})
617
- hourly_data = climate_data.get("hourly_data", [])
618
- internal_loads = project_data.get("internal_loads", {})
619
- building_info = project_data.get("building_info", {})
620
-
621
- # Simulation period (assumed to be in climate_data)
622
- sim_period = climate_data.get("typical_extreme_periods", {"type": "Full Year"})
623
-
624
- # Indoor conditions (modify as needed based on UI inputs)
625
- indoor_conditions = {
626
- "type": "Fixed", # Could be "Time-varying" or "Adaptive" based on UI
627
- "cooling_setpoint": {"temperature": building_info.get("summer_indoor_design_temp", 24.0), "rh": building_info.get("summer_indoor_design_rh", 50.0)},
628
- "heating_setpoint": {"temperature": building_info.get("winter_indoor_design_temp", 20.0), "rh": building_info.get("winter_indoor_design_rh", 50.0)}
629
- }
630
-
631
- # HVAC settings
632
- hvac_settings = {
633
- "operating_hours": [{"start": 8, "end": 8 + building_info.get("operation_hours", 8)}]
634
- }
635
-
636
- if st.button("Calculate HVAC Loads"):
637
- try:
638
- loads = TFMCalculations.calculate_tfm_loads(
639
- components=components,
640
- hourly_data=hourly_data,
641
- indoor_conditions=indoor_conditions,
642
- internal_loads=internal_loads,
643
- building_info=building_info,
644
- sim_period=sim_period,
645
- hvac_settings=hvac_settings
646
- )
647
-
648
- # Update project_data
649
- project_data["hvac_loads"]["cooling"]["hourly"] = [load for load in loads if load["total_cooling"] > 0]
650
- project_data["hvac_loads"]["heating"]["hourly"] = [load for load in loads if load["total_heating"] > 0]
651
- project_data["hvac_loads"]["cooling"]["peak"] = max((load["total_cooling"] for load in loads), default=0)
652
- project_data["hvac_loads"]["heating"]["peak"] = max((load["total_heating"] for load in loads), default=0)
653
-
654
- # Display results
655
- st.subheader("Cooling Loads (kW)")
656
- cooling_df = pd.DataFrame(project_data["hvac_loads"]["cooling"]["hourly"])
657
- if not cooling_df.empty:
658
- st.dataframe(cooling_df[["month", "day", "hour", "total_cooling", "conduction_cooling", "solar", "internal", "ventilation_cooling", "infiltration_cooling"]])
659
-
660
- st.subheader("Heating Loads (kW)")
661
- heating_df = pd.DataFrame(project_data["hvac_loads"]["heating"]["hourly"])
662
- if not heating_df.empty:
663
- st.dataframe(heating_df[["month", "day", "hour", "total_heating", "conduction_heating", "internal", "ventilation_heating", "infiltration_heating"]])
664
-
665
- st.write(f"Peak Cooling Load: {project_data['hvac_loads']['cooling']['peak']:.2f} kW")
666
- st.write(f"Peak Heating vanguard: {project_data['hvac_loads']['heating']['peak']:.2f} kW")
667
-
668
- except Exception as e:
669
- st.error(f"Error calculating HVAC loads: {str(e)}")
670
- logger.exception("HVAC load calculation error")
671
 
672
  if __name__ == "__main__":
673
- display_hvac_loads_page()
 
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()