mabuseif commited on
Commit
5b3968a
·
verified ·
1 Parent(s): 549061e

Update utils/heating_load.py

Browse files
Files changed (1) hide show
  1. utils/heating_load.py +472 -379
utils/heating_load.py CHANGED
@@ -1,443 +1,536 @@
1
  """
2
  Heating load calculation module for HVAC Load Calculator.
3
- Implements enhanced steady-state methods with thermal mass effects, pressure-driven infiltration, and schedule-based internal gains.
4
- Updated 2025-04-28: Added F-factor for floor losses, ground temperature validation, and negative load prevention.
5
- Updated 2025-04-30: Added dynamic F-factor, climate skip logic, debug prints, and restored original features (summary, annual energy).
6
  """
7
 
8
- from typing import Dict, List, Any, Optional
9
  import math
10
  import numpy as np
11
- from datetime import datetime, time
12
  from enum import Enum
 
13
 
14
- # --- Enums ---
15
- class Orientation(Enum):
16
- """Represents building component orientations."""
17
- NORTH = "North"
18
- NORTHEAST = "Northeast"
19
- EAST = "East"
20
- SOUTHEAST = "Southeast"
21
- SOUTH = "South"
22
- SOUTHWEST = "Southwest"
23
- WEST = "West"
24
- NORTHWEST = "Northwest"
25
- HORIZONTAL = "Horizontal"
26
- NOT_APPLICABLE = "N/A"
27
 
28
- class ComponentType(Enum):
29
- """Represents types of building components."""
30
- WALL = "Wall"
31
- ROOF = "Roof"
32
- FLOOR = "Floor"
33
- WINDOW = "Window"
34
- DOOR = "Door"
35
 
36
- # --- Data Models ---
37
- @dataclass
38
- class BuildingComponent:
39
- """Base class for building components."""
40
- id: str
41
- name: str
42
- component_type: ComponentType
43
- u_value: float # W/(m²·K)
44
- area: float # m²
45
- orientation: Orientation
46
-
47
- @dataclass
48
- class Wall(BuildingComponent):
49
- """Wall component with thermal and solar properties."""
50
- wall_type: str
51
- wall_group: str
52
- absorptivity: float
53
- shading_coefficient: float
54
- infiltration_rate_cfm: float
55
- thermal_mass: float = 100000.0 # J/K
56
- time_constant: float = 2.0 # hours
57
-
58
- @dataclass
59
- class Roof(BuildingComponent):
60
- """Roof component with ventilation properties."""
61
- roof_type: str
62
- roof_group: str
63
- slope: str
64
- absorptivity: float
65
- thermal_mass: float = 200000.0 # J/K
66
- time_constant: float = 3.0 # hours
67
-
68
- @dataclass
69
- class Floor(BuildingComponent):
70
- """Floor component with ground contact and insulation properties."""
71
- floor_type: str
72
- ground_contact: bool
73
- ground_temperature_c: float
74
- perimeter: float
75
- insulated: bool = False
76
- thermal_mass: float = 150000.0 # J/K
77
- time_constant: float = 2.5 # hours
78
-
79
- @dataclass
80
- class Window(BuildingComponent):
81
- """Window component with solar and frame properties."""
82
- shgc: float
83
- shading_device: str
84
- shading_coefficient: float
85
- frame_type: str
86
- frame_percentage: float
87
- infiltration_rate_cfm: float
88
-
89
- @dataclass
90
- class Door(BuildingComponent):
91
- """Door component with infiltration properties."""
92
- door_type: str
93
- infiltration_rate_cfm: float
94
-
95
- # --- Constants ---
96
- AIR_DENSITY = 1.2 # kg/m³
97
- SPECIFIC_HEAT = 1000 # J/(kg·K)
98
- LATENT_HEAT_VAPORIZATION = 2260e3 # J/kg
99
- STANDARD_PRESSURE = 101325 # Pa
100
- GRAVITY = 9.81 # m/s²
101
-
102
- # --- Utility Classes (Embedded to Replace External Dependencies) ---
103
- class Psychrometrics:
104
- """Simplified psychrometric calculations."""
105
- def humidity_ratio(self, temp_c: float, rh: float) -> float:
106
- """Calculate humidity ratio (kg/kg dry air)."""
107
- p_sat = 610.78 * np.exp(17.2694 * temp_c / (temp_c + 237.3))
108
- p_v = rh / 100 * p_sat
109
- return 0.62198 * p_v / (STANDARD_PRESSURE - p_v)
110
-
111
- class HeatTransferCalculations:
112
- """Simplified heat transfer calculations."""
113
- def thermal_lag_factor(self, thermal_mass: float, time_constant: float, time_step: float) -> float:
114
- """Calculate thermal lag factor for transient effects."""
115
- return 1.0 - np.exp(-time_step / time_constant) if time_constant > 0 else 1.0
116
-
117
- def wind_pressure_difference(self, wind_speed: float, wind_coefficient: float = 0.4) -> float:
118
- """Calculate wind-induced pressure difference (Pa)."""
119
- return 0.5 * AIR_DENSITY * wind_coefficient * wind_speed ** 2
120
-
121
- def stack_pressure_difference(self, height: float, indoor_temp_k: float, outdoor_temp_k: float) -> float:
122
- """Calculate stack effect pressure difference (Pa)."""
123
- delta_t = abs(indoor_temp_k - outdoor_temp_k)
124
- return 0.034 * AIR_DENSITY * height * delta_t / min(indoor_temp_k, outdoor_temp_k)
125
-
126
- def combined_pressure_difference(self, wind_pd: float, stack_pd: float) -> float:
127
- """Combine wind and stack pressure differences (Pa)."""
128
- return np.sqrt(wind_pd ** 2 + stack_pd ** 2)
129
-
130
- def crack_method_infiltration(self, crack_length: float, coefficient: float, pressure_difference: float) -> float:
131
- """Calculate infiltration flow rate (m³/s)."""
132
- flow_rate = coefficient * crack_length * np.sqrt(pressure_difference)
133
- print(f"Infiltration: Crack Length: {crack_length} m, Pressure: {pressure_difference:.2f} Pa, Flow Rate: {flow_rate:.6f} m³/s")
134
- return flow_rate
135
-
136
- def infiltration_heat_transfer(self, flow_rate: float, delta_t: float) -> float:
137
- """Calculate sensible heat transfer due to infiltration (W)."""
138
- return AIR_DENSITY * SPECIFIC_HEAT * flow_rate * delta_t
139
-
140
- def infiltration_latent_heat_transfer(self, flow_rate: float, delta_w: float) -> float:
141
- """Calculate latent heat transfer due to infiltration (W)."""
142
- return AIR_DENSITY * LATENT_HEAT_VAPORIZATION * flow_rate * delta_w
143
-
144
- # --- Heating Load Calculator ---
145
  class HeatingLoadCalculator:
146
- """Class for calculating heating loads with enhanced steady-state methods."""
147
 
148
  def __init__(self):
149
- """Initialize with embedded utilities."""
150
- self.heat_transfer = HeatTransferCalculations()
151
  self.psychrometrics = Psychrometrics()
152
-
153
- def validate_inputs(self, temp: float, rh: float, area: float, u_value: float, ground_temp: Optional[float] = None) -> None:
154
- """Validate input parameters."""
155
- if not -50 <= temp <= 60:
156
- raise ValueError(f"Temperature {temp}°C is outside valid range (-50 to 60°C)")
157
- if not 0 <= rh <= 100:
158
- raise ValueError(f"Relative humidity {rh}% is outside valid range (0 to 100%)")
159
- if area < 0:
160
- raise ValueError(f"Area {area}m² cannot be negative")
161
- if u_value < 0:
162
- raise ValueError(f"U-value {u_value} W/(m²·K) cannot be negative")
163
- if ground_temp is not None and not -10 <= ground_temp <= 40:
164
- raise ValueError(f"Ground temperature {ground_temp}°C is outside valid range (-10 to 40°C)")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
  def calculate_wall_heating_load(self, wall: Wall, outdoor_temp: float, indoor_temp: float) -> float:
167
- """Calculate wall heating load with thermal mass effects."""
168
- self.validate_inputs(outdoor_temp, 80.0, wall.area, wall.u_value)
 
 
 
 
 
 
 
 
 
169
  delta_t = indoor_temp - outdoor_temp
170
- lag_factor = self.heat_transfer.thermal_lag_factor(wall.thermal_mass, wall.time_constant, 1.0)
171
- heating_load = wall.u_value * wall.area * delta_t * lag_factor
172
- return max(0, heating_load)
 
 
 
 
 
 
 
173
 
174
  def calculate_roof_heating_load(self, roof: Roof, outdoor_temp: float, indoor_temp: float) -> float:
175
- """Calculate roof heating load with thermal mass effects."""
176
- self.validate_inputs(outdoor_temp, 80.0, roof.area, roof.u_value)
 
 
 
 
 
 
 
 
 
177
  delta_t = indoor_temp - outdoor_temp
178
- lag_factor = self.heat_transfer.thermal_lag_factor(roof.thermal_mass, roof.time_constant, 1.0)
179
- heating_load = roof.u_value * roof.area * delta_t * lag_factor
180
- return max(0, heating_load)
 
 
 
 
 
 
181
 
182
  def calculate_floor_heating_load(self, floor: Floor, ground_temp: float, indoor_temp: float) -> float:
183
- """Calculate floor heating load with perimeter losses and thermal mass."""
184
- self.validate_inputs(ground_temp, 80.0, floor.area, floor.u_value, ground_temp)
 
 
 
 
 
 
 
 
 
185
  delta_t = indoor_temp - ground_temp
186
- lag_factor = self.heat_transfer.thermal_lag_factor(floor.thermal_mass, floor.time_constant, 1.0)
187
- conduction_load = floor.u_value * floor.area * delta_t * lag_factor
188
- f_factor = 0.3 if floor.insulated else 0.73 # Dynamic F-factor
189
- perimeter_load = f_factor * floor.perimeter * delta_t if floor.ground_contact else 0
190
- heating_load = conduction_load + perimeter_load
191
- print(f"Floor {floor.name}: Conduction: {conduction_load:.2f} W, Perimeter: {perimeter_load:.2f} W, Total: {heating_load:.2f} W")
192
- return max(0, heating_load)
 
 
 
 
 
 
 
193
 
194
  def calculate_window_heating_load(self, window: Window, outdoor_temp: float, indoor_temp: float) -> float:
195
- """Calculate window heating load."""
196
- self.validate_inputs(outdoor_temp, 80.0, window.area, window.u_value)
 
 
 
 
 
 
 
 
 
197
  delta_t = indoor_temp - outdoor_temp
198
- return max(0, window.u_value * window.area * delta_t)
 
 
 
 
199
 
200
  def calculate_door_heating_load(self, door: Door, outdoor_temp: float, indoor_temp: float) -> float:
201
- """Calculate door heating load."""
202
- self.validate_inputs(outdoor_temp, 80.0, door.area, door.u_value)
 
 
 
 
 
 
 
 
 
203
  delta_t = indoor_temp - outdoor_temp
204
- return max(0, door.u_value * door.area * delta_t)
205
-
206
- def calculate_infiltration_heating_load(self, building_volume: float, outdoor_temp: float,
207
- indoor_temp: float, outdoor_rh: float, indoor_rh: float,
208
- wind_speed: float = 4.0, height: float = 3.0,
209
- crack_length: float = 20.0) -> Dict[str, float]:
210
- """Calculate infiltration heating loads."""
211
- self.validate_inputs(outdoor_temp, outdoor_rh, building_volume, 0.0)
212
- wind_pd = self.heat_transfer.wind_pressure_difference(wind_speed, wind_coefficient=0.4)
213
- stack_pd = self.heat_transfer.stack_pressure_difference(height, indoor_temp + 273.15, outdoor_temp + 273.15)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  total_pd = self.heat_transfer.combined_pressure_difference(wind_pd, stack_pd)
215
- flow_rate = self.heat_transfer.crack_method_infiltration(crack_length, coefficient=0.0002, total_pd) # Adjusted coefficient
216
- sensible_load = self.heat_transfer.infiltration_heat_transfer(flow_rate, indoor_temp - outdoor_temp)
217
- w_outdoor = self.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
218
- w_indoor = self.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
 
 
 
 
 
 
 
 
 
 
219
  delta_w = max(0, w_indoor - w_outdoor)
 
 
 
220
  latent_load = self.heat_transfer.infiltration_latent_heat_transfer(flow_rate, delta_w)
221
- return {
222
- "sensible": max(0, sensible_load),
223
- "latent": max(0, latent_load),
224
- "total": max(0, sensible_load + latent_load)
225
- }
226
-
227
- def calculate_ventilation_heating_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float,
228
- outdoor_rh: float, indoor_rh: float) -> Dict[str, float]:
229
- """Calculate ventilation heating loads."""
230
- self.validate_inputs(outdoor_temp, outdoor_rh, 0.0, 0.0)
231
- sensible_load = self.heat_transfer.infiltration_heat_transfer(flow_rate, indoor_temp - outdoor_temp)
232
- w_outdoor = self.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
233
- w_indoor = self.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  delta_w = max(0, w_indoor - w_outdoor)
 
 
235
  latent_load = self.heat_transfer.infiltration_latent_heat_transfer(flow_rate, delta_w)
236
- return {
237
- "sensible": max(0, sensible_load),
238
- "latent": max(0, latent_load),
239
- "total": max(0, sensible_load + latent_load)
240
- }
241
-
242
- def calculate_internal_gains_offset(self, people_load: float, lights_load: float,
243
- equipment_load: float, usage_factor: float = 0.7,
244
- hour: int = 12, schedule: Optional[List[float]] = None) -> float:
245
- """Calculate internal gains offset with schedule."""
246
- total_gains = people_load + lights_load + equipment_load
247
- schedule_factor = schedule[hour] if schedule and len(schedule) == 24 else 1.0
248
- return max(0, total_gains * usage_factor * schedule_factor)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
  def calculate_design_heating_load(self, building_components: Dict[str, List[Any]],
251
- outdoor_conditions: Dict[str, Any],
252
- indoor_conditions: Dict[str, Any],
253
- internal_loads: Dict[str, Any],
254
- building_volume: float = 300.0,
255
- safety_factor: float = 1.15) -> Dict[str, float]:
256
- """Calculate design heating load."""
257
- outdoor_temp = outdoor_conditions.get("design_temperature", -10.0)
258
- outdoor_rh = outdoor_conditions.get("design_relative_humidity", 80.0)
259
- ground_temp = outdoor_conditions.get("ground_temperature", 10.0)
260
- wind_speed = outdoor_conditions.get("wind_speed", 4.0)
261
- indoor_temp = indoor_conditions.get("temperature", 21.0)
262
- indoor_rh = indoor_conditions.get("relative_humidity", 40.0)
263
-
264
- # Climate skip logic
265
- if outdoor_temp >= indoor_temp - 1:
266
- return {
267
- "walls": 0, "roofs": 0, "floors": 0, "windows": 0, "doors": 0,
268
- "infiltration_sensible": 0, "infiltration_latent": 0,
269
- "ventilation_sensible": 0, "ventilation_latent": 0,
270
- "internal_gains_offset": 0, "subtotal": 0,
271
- "safety_factor": safety_factor, "total": 0
272
- }
273
-
274
  loads = {
275
- "walls": sum(self.calculate_wall_heating_load(wall, outdoor_temp, indoor_temp) for wall in building_components.get("walls", [])),
276
- "roofs": sum(self.calculate_roof_heating_load(roof, outdoor_temp, indoor_temp) for roof in building_components.get("roofs", [])),
277
- "floors": sum(self.calculate_floor_heating_load(floor, ground_temp, indoor_temp) for floor in building_components.get("floors", [])),
278
- "windows": sum(self.calculate_window_heating_load(window, outdoor_temp, indoor_temp) for window in building_components.get("windows", [])),
279
- "doors": sum(self.calculate_door_heating_load(door, outdoor_temp, indoor_temp) for door in building_components.get("doors", [])),
280
- "infiltration_sensible": 0,
281
- "infiltration_latent": 0,
282
- "ventilation_sensible": 0,
283
- "ventilation_latent": 0,
284
- "internal_gains_offset": 0,
285
- "subtotal": 0,
286
- "safety_factor": safety_factor,
287
- "total": 0
288
  }
289
-
290
- if internal_loads.get("infiltration"):
291
- infiltration_loads = self.calculate_infiltration_heating_load(
292
- building_volume, outdoor_temp, indoor_temp, outdoor_rh, indoor_rh,
293
- wind_speed, internal_loads["infiltration"].get("height", 3.0),
294
- internal_loads["infiltration"].get("crack_length", 20.0)
295
- )
296
- loads["infiltration_sensible"] = infiltration_loads["sensible"]
297
- loads["infiltration_latent"] = infiltration_loads["latent"]
298
-
299
- if internal_loads.get("ventilation"):
300
- ventilation_loads = self.calculate_ventilation_heating_load(
301
- internal_loads["ventilation"].get("flow_rate", 0.1),
302
- outdoor_temp, indoor_temp, outdoor_rh, indoor_rh
303
- )
304
- loads["ventilation_sensible"] = ventilation_loads["sensible"]
305
- loads["ventilation_latent"] = ventilation_loads["latent"]
306
-
307
- people = internal_loads.get("people", {})
308
- lights = internal_loads.get("lights", {})
309
- equipment = internal_loads.get("equipment", {})
310
- people_load = people.get("number", 0) * people.get("sensible_gain", 70.0)
311
- lights_load = lights.get("power", 0) * lights.get("use_factor", 0.8)
312
- equipment_load = equipment.get("power", 0) * equipment.get("use_factor", 0.7)
313
-
314
- schedule = None
315
- operating_hours = internal_loads.get("operating_hours", "8:00-18:00")
316
- if operating_hours:
317
- start_hour = int(operating_hours.split(":")[0])
318
- end_hour = int(operating_hours.split("-")[1].split(":")[0])
319
- schedule = [1.0 if start_hour <= h % 24 < end_hour else 0.5 for h in range(24)]
320
-
321
- loads["internal_gains_offset"] = self.calculate_internal_gains_offset(
322
- people_load, lights_load, equipment_load,
323
- internal_loads.get("usage_factor", 0.7), hour=12, schedule=schedule
324
  )
325
-
326
- loads["subtotal"] = sum(v for k, v in loads.items() if k not in ["internal_gains_offset", "subtotal", "safety_factor", "total"])
327
- loads["total"] = max(0, loads["subtotal"] - loads["internal_gains_offset"]) * safety_factor / 1000 # kW
328
-
 
 
 
 
 
 
 
 
329
  return loads
330
 
331
  def calculate_heating_load_summary(self, design_loads: Dict[str, float]) -> Dict[str, float]:
332
- """Summarize heating loads for reporting."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  return {
334
- "walls": design_loads.get("walls", 0),
335
- "roofs": design_loads.get("roofs", 0),
336
- "floors": design_loads.get("floors", 0),
337
- "windows": design_loads.get("windows", 0),
338
- "doors": design_loads.get("doors", 0),
339
- "infiltration": design_loads.get("infiltration_sensible", 0) + design_loads.get("infiltration_latent", 0),
340
- "ventilation": design_loads.get("ventilation_sensible", 0) + design_loads.get("ventilation_latent", 0),
341
- "internal_gains_offset": design_loads.get("internal_gains_offset", 0),
342
- "subtotal": design_loads.get("subtotal", 0),
343
- "safety_factor": design_loads.get("safety_factor", 1.15),
344
- "total": design_loads.get("total", 0) * 1000 # W
345
  }
346
 
347
- def calculate_monthly_heating_loads(self, building_components: Dict[str, List[Any]],
348
- monthly_temps: Dict[str, float], ground_temps: Dict[str, float],
349
- indoor_conditions: Dict[str, Any], internal_loads: Dict[str, Any],
350
- building_volume: float = 300.0) -> Dict[str, float]:
351
- """Calculate monthly heating loads."""
352
- monthly_loads = {}
353
- indoor_temp = indoor_conditions.get("temperature", 21.0)
354
- indoor_rh = indoor_conditions.get("relative_humidity", 40.0)
355
-
356
- for month, outdoor_temp in monthly_temps.items():
357
- ground_temp = ground_temps.get(month, outdoor_temp)
358
- outdoor_conditions = {
359
- "design_temperature": outdoor_temp,
360
- "design_relative_humidity": 80.0,
361
- "ground_temperature": ground_temp,
362
- "wind_speed": 4.0
363
- }
364
- if outdoor_temp >= indoor_temp:
365
- monthly_loads[month] = 0.0
366
- continue
367
- loads = self.calculate_design_heating_load(
368
- building_components, outdoor_conditions, indoor_conditions,
369
- internal_loads, building_volume, safety_factor=1.0
370
- )
371
- monthly_loads[month] = loads["total"] * 1000 # W
372
- return monthly_loads
373
-
374
- def calculate_heating_degree_days(self, monthly_temps: Dict[str, float], base_temp: float = 18.3) -> float:
375
- """Calculate annual heating degree days."""
376
  days_per_month = {
377
- "Jan": 31, "Feb": 28, "Mar": 31, "Apr": 30, "May": 31, "Jun": 30,
378
- "Jul": 31, "Aug": 31, "Sep": 30, "Oct": 31, "Nov": 30, "Dec": 31
379
  }
380
- hdd = 0.0
381
  for month, temp in monthly_temps.items():
382
  if temp < base_temp:
383
- hdd += (base_temp - temp) * days_per_month.get(month, 30)
 
384
  return hdd
385
 
386
- def calculate_annual_heating_energy(self, monthly_loads: Dict[str, float],
387
- operating_hours: str = "8:00-18:00") -> float:
388
- """Calculate annual heating energy in kWh."""
389
- hours_per_day = 10.0
390
- if operating_hours:
391
- start_hour = int(operating_hours.split(":")[0])
392
- end_hour = int(operating_hours.split("-")[1].split(":")[0])
393
- hours_per_day = end_hour - start_hour
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  days_per_month = {
395
- "Jan": 31, "Feb": 28, "Mar": 31, "Apr": 30, "May": 31, "Jun": 30,
396
- "Jul": 31, "Aug": 31, "Sep": 30, "Oct": 31, "Nov": 30, "Dec": 31
397
  }
398
- total_energy = 0.0
399
- for month, load in monthly_loads.items():
400
- hours = hours_per_day * days_per_month.get(month, 30)
401
- energy = load * hours / 1000 / 1000 # W to kWh
402
- total_energy += energy
403
- return total_energy
 
 
 
 
 
 
 
 
 
 
404
 
405
- # --- Example Execution ---
406
  if __name__ == "__main__":
407
- """Example with Geelong debug inputs."""
 
408
  calculator = HeatingLoadCalculator()
 
 
409
  components = {
410
- "walls": [Wall(id="w1", name="Wall 1", component_type=ComponentType.WALL, u_value=0.5, area=120, orientation=Orientation.NOT_APPLICABLE, wall_type="Brick", wall_group="A", absorptivity=0.6, shading_coefficient=1.0, infiltration_rate_cfm=0)],
411
- "roofs": [Roof(id="r1", name="Roof 1", component_type=ComponentType.ROOF, u_value=0.3, area=100, orientation=Orientation.HORIZONTAL, roof_type="Concrete", roof_group="A", slope="Flat", absorptivity=0.6)],
412
- "floors": [Floor(id="f1", name="Main Floor", component_type=ComponentType.FLOOR, u_value=0.4, area=100, orientation=Orientation.NOT_APPLICABLE, floor_type="Concrete", ground_contact=True, ground_temperature_c=16.0, perimeter=40.0, insulated=True)],
413
- "windows": [Window(id="win1", name="Window 1", component_type=ComponentType.WINDOW, u_value=2.0, area=1, orientation=Orientation.NOT_APPLICABLE, shgc=0.7, shading_device="None", shading_coefficient=1.0, frame_type="Aluminum", frame_percentage=20, infiltration_rate_cfm=0)],
414
- "doors": [Door(id="d1", name="Door 1", component_type=ComponentType.DOOR, u_value=2.0, area=1, orientation=Orientation.NOT_APPLICABLE, door_type="Solid Wood", infiltration_rate_cfm=0)]
415
  }
 
416
  outdoor_conditions = {
417
- "design_temperature": 17.5,
418
- "design_relative_humidity": 81.3,
419
- "ground_temperature": 16.0,
420
- "wind_speed": 4.0
421
  }
422
  indoor_conditions = {
423
- "temperature": 21.0,
424
- "relative_humidity": 40.0
425
  }
426
  internal_loads = {
427
- "people": {"number": 1, "sensible_gain": 70.0},
428
- "lights": {"power": 200.0, "use_factor": 0.8},
429
- "equipment": {"power": 100.0, "use_factor": 0.7},
430
- "infiltration": {"height": 3.0, "crack_length": 20.0},
431
- "ventilation": {"flow_rate": 0.115},
432
- "operating_hours": "8:00-18:00",
433
- "usage_factor": 0.7
434
  }
435
- result = calculator.calculate_design_heating_load(components, outdoor_conditions, indoor_conditions, internal_loads, building_volume=300.0)
436
- print(f"Total Heating Load: {result['total']:.2f} kW")
437
- print(f"Summary: {calculator.calculate_heating_load_summary(result)}")
438
- monthly_temps = {"Jul": 17.5}
439
- ground_temps = {"Jul": 16.0}
440
- monthly_result = calculator.calculate_monthly_heating_loads(components, monthly_temps, ground_temps, indoor_conditions, internal_loads)
441
- print(f"Monthly Loads: {monthly_result}")
442
- annual_energy = calculator.calculate_annual_heating_energy(monthly_result)
443
- print(f"Annual Energy: {annual_energy:.2f} kWh")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  Heating load calculation module for HVAC Load Calculator.
3
+ Implements ASHRAE steady-state methods with thermal mass effects and annual energy calculations.
 
 
4
  """
5
 
6
+ from typing import Dict, List, Any, Optional, Tuple
7
  import math
8
  import numpy as np
 
9
  from enum import Enum
10
+ from dataclasses import dataclass
11
 
12
+ # Import utility modules
13
+ from utils.psychrometrics import Psychrometrics
14
+ from utils.heat_transfer import HeatTransferCalculations
 
 
 
 
 
 
 
 
 
 
15
 
16
+ # Import data modules (assumed to match embedded classes; replace with actual imports if provided)
17
+ from data.building_components import Wall, Roof, Floor, Window, Door, Orientation, ComponentType
 
 
 
 
 
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  class HeatingLoadCalculator:
20
+ """Class for heating load calculations based on ASHRAE steady-state methods."""
21
 
22
  def __init__(self):
23
+ """Initialize heating load calculator with psychrometric and heat transfer calculations."""
 
24
  self.psychrometrics = Psychrometrics()
25
+ self.heat_transfer = HeatTransferCalculations()
26
+ self.safety_factor = 1.15 # 15% safety factor for design loads
27
+
28
+ def validate_inputs(self, components: Dict[str, List[Any]], outdoor_temp: float, indoor_temp: float) -> None:
29
+ """
30
+ Validate input parameters for heating load calculations.
31
+
32
+ Args:
33
+ components: Dictionary of building components
34
+ outdoor_temp: Outdoor design temperature in °C
35
+ indoor_temp: Indoor design temperature in °C
36
+
37
+ Raises:
38
+ ValueError: If inputs are invalid
39
+ """
40
+ if not components:
41
+ raise ValueError("Building components dictionary cannot be empty")
42
+ for component_type, comp_list in components.items():
43
+ if not isinstance(comp_list, list):
44
+ raise ValueError(f"Components for {component_type} must be a list")
45
+ for comp in comp_list:
46
+ if not hasattr(comp, 'area') or comp.area <= 0:
47
+ raise ValueError(f"Invalid area for {component_type}: {comp.name}")
48
+ if not hasattr(comp, 'u_value') or comp.u_value <= 0:
49
+ raise ValueError(f"Invalid U-value for {component_type}: {comp.name}")
50
+ if not -50 <= outdoor_temp <= 60 or not -50 <= indoor_temp <= 60:
51
+ raise ValueError("Temperatures must be between -50°C and 60°C")
52
+ if indoor_temp - outdoor_temp < 1:
53
+ raise ValueError("Indoor temperature must be at least 1°C above outdoor temperature for heating")
54
 
55
  def calculate_wall_heating_load(self, wall: Wall, outdoor_temp: float, indoor_temp: float) -> float:
56
+ """
57
+ Calculate heating load for a wall, including thermal lag effects.
58
+
59
+ Args:
60
+ wall: Wall component
61
+ outdoor_temp: Outdoor temperature in °C
62
+ indoor_temp: Indoor temperature in °C
63
+
64
+ Returns:
65
+ Heating load in W
66
+ """
67
  delta_t = indoor_temp - outdoor_temp
68
+ if delta_t <= 1:
69
+ return 0.0 # Skip calculation for small temperature differences
70
+
71
+ # Apply thermal lag factor
72
+ time_step = 1.0 # Hourly time step
73
+ lag_factor = self.heat_transfer.thermal_lag_factor(wall.thermal_mass, wall.time_constant, time_step)
74
+ adjusted_delta_t = delta_t * lag_factor
75
+
76
+ load = self.heat_transfer.conduction_heat_transfer(wall.u_value, wall.area, adjusted_delta_t)
77
+ return max(0, load)
78
 
79
  def calculate_roof_heating_load(self, roof: Roof, outdoor_temp: float, indoor_temp: float) -> float:
80
+ """
81
+ Calculate heating load for a roof, including thermal lag effects.
82
+
83
+ Args:
84
+ roof: Roof component
85
+ outdoor_temp: Outdoor temperature in °C
86
+ indoor_temp: Indoor temperature in °C
87
+
88
+ Returns:
89
+ Heating load in W
90
+ """
91
  delta_t = indoor_temp - outdoor_temp
92
+ if delta_t <= 1:
93
+ return 0.0
94
+
95
+ time_step = 1.0
96
+ lag_factor = self.heat_transfer.thermal_lag_factor(roof.thermal_mass, roof.time_constant, time_step)
97
+ adjusted_delta_t = delta_t * lag_factor
98
+
99
+ load = self.heat_transfer.conduction_heat_transfer(roof.u_value, roof.area, adjusted_delta_t)
100
+ return max(0, load)
101
 
102
  def calculate_floor_heating_load(self, floor: Floor, ground_temp: float, indoor_temp: float) -> float:
103
+ """
104
+ Calculate heating load for a floor, using dynamic F-factor for ground contact.
105
+
106
+ Args:
107
+ floor: Floor component
108
+ ground_temp: Ground temperature in °C
109
+ indoor_temp: Indoor temperature in °C
110
+
111
+ Returns:
112
+ Heating load in W
113
+ """
114
  delta_t = indoor_temp - ground_temp
115
+ if delta_t <= 1:
116
+ return 0.0
117
+
118
+ if floor.ground_contact:
119
+ # Dynamic F-factor based on insulation
120
+ f_factor = 0.3 if floor.insulated else 0.73 # W/m·K
121
+ load = f_factor * floor.perimeter * delta_t
122
+ else:
123
+ load = self.heat_transfer.conduction_heat_transfer(floor.u_value, floor.area, delta_t)
124
+
125
+ if hasattr(st.session_state, 'debug_mode') and st.session_state.debug_mode:
126
+ print(f"Debug: Floor {floor.name} load: {load:.2f} W, Delta T: {delta_t:.2f}°C")
127
+
128
+ return max(0, load)
129
 
130
  def calculate_window_heating_load(self, window: Window, outdoor_temp: float, indoor_temp: float) -> float:
131
+ """
132
+ Calculate heating load for a window.
133
+
134
+ Args:
135
+ window: Window component
136
+ outdoor_temp: Outdoor temperature in °C
137
+ indoor_temp: Indoor temperature in °C
138
+
139
+ Returns:
140
+ Heating load in W
141
+ """
142
  delta_t = indoor_temp - outdoor_temp
143
+ if delta_t <= 1:
144
+ return 0.0
145
+
146
+ load = self.heat_transfer.conduction_heat_transfer(window.u_value, window.area, delta_t)
147
+ return max(0, load)
148
 
149
  def calculate_door_heating_load(self, door: Door, outdoor_temp: float, indoor_temp: float) -> float:
150
+ """
151
+ Calculate heating load for a door.
152
+
153
+ Args:
154
+ door: Door component
155
+ outdoor_temp: Outdoor temperature in °C
156
+ indoor_temp: Indoor temperature in °C
157
+
158
+ Returns:
159
+ Heating load in W
160
+ """
161
  delta_t = indoor_temp - outdoor_temp
162
+ if delta_t <= 1:
163
+ return 0.0
164
+
165
+ load = self.heat_transfer.conduction_heat_transfer(door.u_value, door.area, delta_t)
166
+ return max(0, load)
167
+
168
+ def calculate_infiltration_heating_load(self, indoor_conditions: Dict[str, float],
169
+ outdoor_conditions: Dict[str, float],
170
+ infiltration: Dict[str, float],
171
+ building_height: float) -> Tuple[float, float]:
172
+ """
173
+ Calculate sensible and latent heating loads due to infiltration.
174
+
175
+ Args:
176
+ indoor_conditions: Indoor conditions (temperature, relative_humidity)
177
+ outdoor_conditions: Outdoor conditions (design_temperature, design_relative_humidity, wind_speed)
178
+ infiltration: Infiltration parameters (flow_rate, crack_length, height)
179
+ building_height: Building height in m
180
+
181
+ Returns:
182
+ Tuple of sensible and latent loads in W
183
+ """
184
+ delta_t = indoor_conditions['temperature'] - outdoor_conditions['design_temperature']
185
+ if delta_t <= 1:
186
+ return 0.0, 0.0
187
+
188
+ # Calculate pressure differences
189
+ wind_pd = self.heat_transfer.wind_pressure_difference(outdoor_conditions['wind_speed'])
190
+ stack_pd = self.heat_transfer.stack_pressure_difference(
191
+ building_height,
192
+ indoor_conditions['temperature'] + 273.15,
193
+ outdoor_conditions['design_temperature'] + 273.15
194
+ )
195
  total_pd = self.heat_transfer.combined_pressure_difference(wind_pd, stack_pd)
196
+
197
+ # Calculate infiltration flow rate
198
+ crack_length = infiltration.get('crack_length', 20.0)
199
+ flow_rate = self.heat_transfer.crack_method_infiltration(crack_length, 0.0002, total_pd)
200
+
201
+ # Calculate humidity ratio difference
202
+ w_indoor = self.psychrometrics.humidity_ratio(
203
+ indoor_conditions['temperature'],
204
+ indoor_conditions['relative_humidity']
205
+ )
206
+ w_outdoor = self.psychrometrics.humidity_ratio(
207
+ outdoor_conditions['design_temperature'],
208
+ outdoor_conditions['design_relative_humidity']
209
+ )
210
  delta_w = max(0, w_indoor - w_outdoor)
211
+
212
+ # Calculate sensible and latent loads
213
+ sensible_load = self.heat_transfer.infiltration_heat_transfer(flow_rate, delta_t)
214
  latent_load = self.heat_transfer.infiltration_latent_heat_transfer(flow_rate, delta_w)
215
+
216
+ if hasattr(st.session_state, 'debug_mode') and st.session_state.debug_mode:
217
+ print(f"Debug: Infiltration flow rate: {flow_rate:.6f} m³/s, Sensible load: {sensible_load:.2f} W, Latent load: {latent_load:.2f} W")
218
+
219
+ return max(0, sensible_load), max(0, latent_load)
220
+
221
+ def calculate_ventilation_heating_load(self, ventilation: Dict[str, float],
222
+ indoor_conditions: Dict[str, float],
223
+ outdoor_conditions: Dict[str, float]) -> Tuple[float, float]:
224
+ """
225
+ Calculate sensible and latent heating loads due to ventilation.
226
+
227
+ Args:
228
+ ventilation: Ventilation parameters (flow_rate)
229
+ indoor_conditions: Indoor conditions (temperature, relative_humidity)
230
+ outdoor_conditions: Outdoor conditions (design_temperature, design_relative_humidity)
231
+
232
+ Returns:
233
+ Tuple of sensible and latent loads in W
234
+ """
235
+ delta_t = indoor_conditions['temperature'] - outdoor_conditions['design_temperature']
236
+ if delta_t <= 1:
237
+ return 0.0, 0.0
238
+
239
+ flow_rate = ventilation['flow_rate']
240
+
241
+ w_indoor = self.psychrometrics.humidity_ratio(
242
+ indoor_conditions['temperature'],
243
+ indoor_conditions['relative_humidity']
244
+ )
245
+ w_outdoor = self.psychrometrics.humidity_ratio(
246
+ outdoor_conditions['design_temperature'],
247
+ outdoor_conditions['design_relative_humidity']
248
+ )
249
  delta_w = max(0, w_indoor - w_outdoor)
250
+
251
+ sensible_load = self.heat_transfer.infiltration_heat_transfer(flow_rate, delta_t)
252
  latent_load = self.heat_transfer.infiltration_latent_heat_transfer(flow_rate, delta_w)
253
+
254
+ return max(0, sensible_load), max(0, latent_load)
255
+
256
+ def calculate_internal_gains(self, internal_loads: Dict[str, Any]) -> float:
257
+ """
258
+ Calculate internal heat gains from people, lighting, and equipment.
259
+
260
+ Args:
261
+ internal_loads: Internal loads (people, lights, equipment)
262
+
263
+ Returns:
264
+ Total internal gains in W
265
+ """
266
+ total_gains = 0.0
267
+
268
+ # People gains
269
+ people = internal_loads.get('people', {})
270
+ if people.get('number', 0) > 0:
271
+ sensible_gain = people.get('sensible_gain', 70.0)
272
+ total_gains += people['number'] * sensible_gain
273
+
274
+ # Lighting gains
275
+ lights = internal_loads.get('lights', {})
276
+ if lights.get('power', 0) > 0:
277
+ total_gains += lights['power'] * lights.get('use_factor', 0.8)
278
+
279
+ # Equipment gains
280
+ equipment = internal_loads.get('equipment', {})
281
+ if equipment.get('power', 0) > 0:
282
+ total_gains += equipment['power'] * equipment.get('use_factor', 0.7)
283
+
284
+ return max(0, total_gains)
285
 
286
  def calculate_design_heating_load(self, building_components: Dict[str, List[Any]],
287
+ outdoor_conditions: Dict[str, float],
288
+ indoor_conditions: Dict[str, float],
289
+ internal_loads: Dict[str, Any]) -> Dict[str, float]:
290
+ """
291
+ Calculate design heating loads for all components.
292
+
293
+ Args:
294
+ building_components: Dictionary of building components
295
+ outdoor_conditions: Outdoor conditions (design_temperature, design_relative_humidity, ground_temperature, wind_speed)
296
+ indoor_conditions: Indoor conditions (temperature, relative_humidity)
297
+ internal_loads: Internal loads (people, lights, equipment, infiltration, ventilation)
298
+
299
+ Returns:
300
+ Dictionary of design loads in W
301
+ """
302
+ try:
303
+ self.validate_inputs(building_components, outdoor_conditions['design_temperature'], indoor_conditions['temperature'])
304
+ except ValueError as e:
305
+ raise ValueError(f"Input validation failed: {str(e)}")
306
+
 
 
 
307
  loads = {
308
+ 'walls': 0.0,
309
+ 'roofs': 0.0,
310
+ 'floors': 0.0,
311
+ 'windows': 0.0,
312
+ 'doors': 0.0,
313
+ 'infiltration_sensible': 0.0,
314
+ 'infiltration_latent': 0.0,
315
+ 'ventilation_sensible': 0.0,
316
+ 'ventilation_latent': 0.0,
317
+ 'internal_gains': 0.0
 
 
 
318
  }
319
+
320
+ # Calculate envelope loads
321
+ for wall in building_components.get('walls', []):
322
+ loads['walls'] += self.calculate_wall_heating_load(wall, outdoor_conditions['design_temperature'], indoor_conditions['temperature'])
323
+
324
+ for roof in building_components.get('roofs', []):
325
+ loads['roofs'] += self.calculate_roof_heating_load(roof, outdoor_conditions['design_temperature'], indoor_conditions['temperature'])
326
+
327
+ for floor in building_components.get('floors', []):
328
+ loads['floors'] += self.calculate_floor_heating_load(floor, outdoor_conditions['ground_temperature'], indoor_conditions['temperature'])
329
+
330
+ for window in building_components.get('windows', []):
331
+ loads['windows'] += self.calculate_window_heating_load(window, outdoor_conditions['design_temperature'], indoor_conditions['temperature'])
332
+
333
+ for door in building_components.get('doors', []):
334
+ loads['doors'] += self.calculate_door_heating_load(door, outdoor_conditions['design_temperature'], indoor_conditions['temperature'])
335
+
336
+ # Calculate infiltration and ventilation loads
337
+ building_height = internal_loads.get('infiltration', {}).get('height', 3.0)
338
+ infiltration_sensible, infiltration_latent = self.calculate_infiltration_heating_load(
339
+ indoor_conditions, outdoor_conditions, internal_loads.get('infiltration', {}), building_height
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  )
341
+ loads['infiltration_sensible'] = infiltration_sensible
342
+ loads['infiltration_latent'] = infiltration_latent
343
+
344
+ ventilation_sensible, ventilation_latent = self.calculate_ventilation_heating_load(
345
+ internal_loads.get('ventilation', {}), indoor_conditions, outdoor_conditions
346
+ )
347
+ loads['ventilation_sensible'] = ventilation_sensible
348
+ loads['ventilation_latent'] = ventilation_latent
349
+
350
+ # Calculate internal gains (negative for heating)
351
+ loads['internal_gains'] = -self.calculate_internal_gains(internal_loads)
352
+
353
  return loads
354
 
355
  def calculate_heating_load_summary(self, design_loads: Dict[str, float]) -> Dict[str, float]:
356
+ """
357
+ Summarize heating loads with safety factor.
358
+
359
+ Args:
360
+ design_loads: Dictionary of design loads in W
361
+
362
+ Returns:
363
+ Summary dictionary with total, subtotal, and safety factor
364
+ """
365
+ subtotal = sum(
366
+ load for key, load in design_loads.items()
367
+ if key not in ['internal_gains'] and load > 0
368
+ )
369
+ internal_gains = design_loads.get('internal_gains', 0)
370
+
371
+ total = max(0, subtotal + internal_gains) * self.safety_factor
372
+
373
  return {
374
+ 'subtotal': subtotal,
375
+ 'internal_gains': internal_gains,
376
+ 'total': total,
377
+ 'safety_factor': self.safety_factor
 
 
 
 
 
 
 
378
  }
379
 
380
+ def calculate_heating_degree_days(self, base_temp: float, monthly_temps: Dict[str, float]) -> float:
381
+ """
382
+ Calculate heating degree days for a year.
383
+
384
+ Args:
385
+ base_temp: Base temperature for HDD calculation in °C
386
+ monthly_temps: Dictionary of monthly average temperatures
387
+
388
+ Returns:
389
+ Total heating degree days
390
+ """
391
+ hdd = 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  days_per_month = {
393
+ 'Jan': 31, 'Feb': 28, 'Mar': 31, 'Apr': 30, 'May': 31, 'Jun': 30,
394
+ 'Jul': 31, 'Aug': 31, 'Sep': 30, 'Oct': 31, 'Nov': 30, 'Dec': 31
395
  }
396
+
397
  for month, temp in monthly_temps.items():
398
  if temp < base_temp:
399
+ hdd += (base_temp - temp) * days_per_month[month]
400
+
401
  return hdd
402
 
403
+ def calculate_annual_heating_energy(self, design_loads: Dict[str, float],
404
+ monthly_temps: Dict[str, float],
405
+ indoor_temp: float,
406
+ operating_hours: str) -> float:
407
+ """
408
+ Calculate annual heating energy consumption.
409
+
410
+ Args:
411
+ design_loads: Dictionary of design loads in W
412
+ monthly_temps: Dictionary of monthly average temperatures
413
+ indoor_temp: Indoor design temperature in °C
414
+ operating_hours: Operating hours (e.g., '8:00-18:00')
415
+
416
+ Returns:
417
+ Annual heating energy in kWh
418
+ """
419
+ base_temp = indoor_temp
420
+ hdd = self.calculate_heating_degree_days(base_temp, monthly_temps)
421
+
422
+ # Parse operating hours
423
+ start_hour, end_hour = map(lambda x: int(x.split(':')[0]), operating_hours.split('-'))
424
+ daily_hours = end_hour - start_hour
425
+
426
+ # Calculate design condition degree days
427
+ design_temp = min(monthly_temps.values())
428
+ design_delta_t = indoor_temp - design_temp
429
+ if design_delta_t <= 1:
430
+ return 0.0
431
+
432
+ total_load = self.calculate_heating_load_summary(design_loads)['total']
433
+
434
+ # Scale load by HDD and operating hours
435
+ annual_energy = (total_load / design_delta_t) * hdd * (daily_hours / 24) / 1000 # kWh
436
+
437
+ return max(0, annual_energy)
438
+
439
+ def calculate_monthly_heating_loads(self, building_components: Dict[str, List[Any]],
440
+ outdoor_conditions: Dict[str, float],
441
+ indoor_conditions: Dict[str, float],
442
+ internal_loads: Dict[str, Any],
443
+ monthly_temps: Dict[str, float]) -> Dict[str, float]:
444
+ """
445
+ Calculate monthly heating loads.
446
+
447
+ Args:
448
+ building_components: Dictionary of building components
449
+ outdoor_conditions: Outdoor conditions
450
+ indoor_conditions: Indoor conditions
451
+ internal_loads: Internal loads
452
+ monthly_temps: Dictionary of monthly average temperatures
453
+
454
+ Returns:
455
+ Dictionary of monthly heating loads in kW
456
+ """
457
+ monthly_loads = {}
458
  days_per_month = {
459
+ 'Jan': 31, 'Feb': 28, 'Mar': 31, 'Apr': 30, 'May': 31, 'Jun': 30,
460
+ 'Jul': 31, 'Aug': 31, 'Sep': 30, 'Oct': 31, 'Nov': 30, 'Dec': 31
461
  }
462
+
463
+ for month, temp in monthly_temps.items():
464
+ modified_outdoor = outdoor_conditions.copy()
465
+ modified_outdoor['design_temperature'] = temp
466
+ modified_outdoor['ground_temperature'] = temp
467
+
468
+ try:
469
+ design_loads = self.calculate_design_heating_load(
470
+ building_components, modified_outdoor, indoor_conditions, internal_loads
471
+ )
472
+ summary = self.calculate_heating_load_summary(design_loads)
473
+ monthly_loads[month] = summary['total'] / 1000 # kW
474
+ except ValueError:
475
+ monthly_loads[month] = 0.0 # Skip invalid months
476
+
477
+ return monthly_loads
478
 
479
+ # Example usage
480
  if __name__ == "__main__":
481
+ import streamlit as st
482
+
483
  calculator = HeatingLoadCalculator()
484
+
485
+ # Example building components
486
  components = {
487
+ 'walls': [Wall(name="North Wall", area=20.0, u_value=0.5, orientation=Orientation.NORTH, thermal_mass=50000, time_constant=12.0)],
488
+ 'roofs': [Roof(name="Main Roof", area=100.0, u_value=0.3, orientation=Orientation.HORIZONTAL, thermal_mass=60000, time_constant=15.0)],
489
+ 'floors': [Floor(name="Ground Floor", area=100.0, u_value=0.4, perimeter=40.0, ground_contact=True, insulated=True, ground_temperature_c=10.0)],
490
+ 'windows': [Window(name="South Window", area=10.0, u_value=2.8, orientation=Orientation.SOUTH, shgc=0.7, shading_coefficient=0.8)],
491
+ 'doors': [Door(name="Main Door", area=2.0, u_value=2.0, orientation=Orientation.NORTH)]
492
  }
493
+
494
  outdoor_conditions = {
495
+ 'design_temperature': -5.0,
496
+ 'design_relative_humidity': 80.0,
497
+ 'ground_temperature': 10.0,
498
+ 'wind_speed': 4.0
499
  }
500
  indoor_conditions = {
501
+ 'temperature': 21.0,
502
+ 'relative_humidity': 40.0
503
  }
504
  internal_loads = {
505
+ 'people': {'number': 10, 'sensible_gain': 70.0, 'operating_hours': '8:00-18:00'},
506
+ 'lights': {'power': 1000.0, 'use_factor': 0.8, 'hours_operation': '8h'},
507
+ 'equipment': {'power': 500.0, 'use_factor': 0.7, 'hours_operation': '8h'},
508
+ 'infiltration': {'flow_rate': 0.05, 'height': 3.0, 'crack_length': 20.0},
509
+ 'ventilation': {'flow_rate': 0.1},
510
+ 'operating_hours': '8:00-18:00'
511
+ Libre
512
  }
513
+
514
+ st.session_state.debug_mode = True
515
+
516
+ design_loads = calculator.calculate_design_heating_load(components, outdoor_conditions, indoor_conditions, internal_loads)
517
+ summary = calculator.calculate_heating_load_summary(design_loads)
518
+
519
+ print(f"Total Heating Load: {summary['total']:.2f} W")
520
+ print(f"Wall Load: {design_loads['walls']:.2f} W")
521
+ print(f"Roof Load: {design_loads['roofs']:.2f} W")
522
+ print(f"Floor Load: {design_loads['floors']:.2f} W")
523
+ print(f"Window Load: {design_loads['windows']:.2f] W")
524
+ print(f"Door Load: {design_loads['doors']:.2f} W")
525
+ print(f"Infiltration Load: {design_loads['infiltration_sensible'] + design_loads['infiltration_latent']:.2f} W")
526
+ print(f"Ventilation Load: {design_loads['ventilation_sensible'] + design_loads['ventilation_latent']:.2f} W")
527
+
528
+ monthly_temps = {'Jan': -5.0, 'Feb': -3.0, 'Mar': 0.0, 'Apr': 5.0, 'May': 10.0, 'Jun': 15.0,
529
+ 'Jul': 18.0, 'Aug': 17.0, 'Sep': 12.0, 'Oct': 7.0, 'Nov': 2.0, 'Dec': -2.0}
530
+
531
+ annual_energy = calculator.calculate_annual_heating_energy(design_loads, monthly_temps, indoor_conditions['temperature'], internal_loads['operating_hours'])
532
+ print(f"Annual Heating Energy: {annual_energy:.2f} kWh")
533
+
534
+ monthly_loads = calculator.calculate_monthly_heating_loads(components, outdoor_conditions, indoor_conditions, internal_loads, monthly_temps)
535
+ for month, load in monthly_loads.items():
536
+ print(f"{month} Heating Load: {load:.2f} kW")