mabuseif commited on
Commit
549061e
·
verified ·
1 Parent(s): 8bef111

Update utils/heating_load.py

Browse files
Files changed (1) hide show
  1. utils/heating_load.py +289 -421
utils/heating_load.py CHANGED
@@ -1,48 +1,157 @@
1
  """
2
  Heating load calculation module for HVAC Load Calculator.
3
- This module implements enhanced steady-state methods with thermal mass effects,
4
- pressure-driven infiltration, and schedule-based internal gains.
5
  Updated 2025-04-28: Added F-factor for floor losses, ground temperature validation, and negative load prevention.
 
6
  """
7
 
8
- from typing import Dict, List, Any, Optional, Tuple
9
  import math
10
  import numpy as np
11
- import pandas as pd
12
- import os
13
- from datetime import datetime, timedelta
14
  from enum import Enum
15
 
16
- # Import data models and utilities
17
- from data.building_components import Wall, Roof, Floor, Window, Door, Orientation, ComponentType
18
- from utils.psychrometrics import Psychrometrics
19
- from utils.heat_transfer import HeatTransferCalculations
 
 
 
 
 
 
 
 
 
20
 
21
- # Define paths
22
- DATA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 
 
 
 
 
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  class HeatingLoadCalculator:
25
- """Class for calculating heating loads using enhanced steady-state methods."""
26
 
27
  def __init__(self):
28
- """Initialize heating load calculator with heat transfer and psychrometrics utilities."""
29
  self.heat_transfer = HeatTransferCalculations()
30
  self.psychrometrics = Psychrometrics()
31
-
32
  def validate_inputs(self, temp: float, rh: float, area: float, u_value: float, ground_temp: Optional[float] = None) -> None:
33
- """
34
- Validate input parameters for calculations.
35
-
36
- Args:
37
- temp: Temperature in °C
38
- rh: Relative humidity in %
39
- area: Area in m²
40
- u_value: U-value in W/(m²·K)
41
- ground_temp: Ground temperature in °C (optional)
42
-
43
- Raises:
44
- ValueError: If inputs are out of acceptable ranges
45
- """
46
  if not -50 <= temp <= 60:
47
  raise ValueError(f"Temperature {temp}°C is outside valid range (-50 to 60°C)")
48
  if not 0 <= rh <= 100:
@@ -53,309 +162,121 @@ class HeatingLoadCalculator:
53
  raise ValueError(f"U-value {u_value} W/(m²·K) cannot be negative")
54
  if ground_temp is not None and not -10 <= ground_temp <= 40:
55
  raise ValueError(f"Ground temperature {ground_temp}°C is outside valid range (-10 to 40°C)")
56
-
57
  def calculate_wall_heating_load(self, wall: Wall, outdoor_temp: float, indoor_temp: float) -> float:
58
- """
59
- Calculate heating load through a wall with thermal mass effects.
60
-
61
- Args:
62
- wall: Wall object
63
- outdoor_temp: Outdoor temperature in °C
64
- indoor_temp: Indoor temperature in °C
65
-
66
- Returns:
67
- Heating load in W
68
- """
69
  self.validate_inputs(outdoor_temp, 80.0, wall.area, wall.u_value)
70
-
71
- u_value = wall.u_value
72
- area = wall.area
73
  delta_t = indoor_temp - outdoor_temp
74
-
75
- # Apply thermal mass effect
76
- thermal_mass = getattr(wall, "thermal_mass", 100000) # J/K, default
77
- time_constant = getattr(wall, "time_constant", 2.0) # hours, default
78
- lag_factor = self.heat_transfer.thermal_lag_factor(thermal_mass, time_constant, 1.0)
79
-
80
- heating_load = u_value * area * delta_t * lag_factor
81
-
82
  return max(0, heating_load)
83
-
84
  def calculate_roof_heating_load(self, roof: Roof, outdoor_temp: float, indoor_temp: float) -> float:
85
- """
86
- Calculate heating load through a roof with thermal mass effects.
87
-
88
- Args:
89
- roof: Roof object
90
- outdoor_temp: Outdoor temperature in °C
91
- indoor_temp: Indoor temperature in °C
92
-
93
- Returns:
94
- Heating load in W
95
- """
96
  self.validate_inputs(outdoor_temp, 80.0, roof.area, roof.u_value)
97
-
98
- u_value = roof.u_value
99
- area = roof.area
100
  delta_t = indoor_temp - outdoor_temp
101
-
102
- # Apply thermal mass effect
103
- thermal_mass = getattr(roof, "thermal_mass", 200000) # J/K, default
104
- time_constant = getattr(roof, "time_constant", 3.0) # hours, default
105
- lag_factor = self.heat_transfer.thermal_lag_factor(thermal_mass, time_constant, 1.0)
106
-
107
- heating_load = u_value * area * delta_t * lag_factor
108
-
109
  return max(0, heating_load)
110
-
111
  def calculate_floor_heating_load(self, floor: Floor, ground_temp: float, indoor_temp: float) -> float:
112
- """
113
- Calculate heating load through a floor with thermal mass effects and perimeter losses.
114
-
115
- Args:
116
- floor: Floor object
117
- ground_temp: Ground or adjacent space temperature in °C
118
- indoor_temp: Indoor temperature in °C
119
-
120
- Returns:
121
- Heating load in W
122
- """
123
- self.validate_inputs(ground_temp, 80.0, floor.area, floor.u_value, ground_temp=ground_temp)
124
-
125
- heating_load = 0.0
126
  delta_t = indoor_temp - ground_temp
127
-
128
- # Area-based conduction
129
- u_value = floor.u_value
130
- area = floor.area
131
- thermal_mass = getattr(floor, "thermal_mass", 150000) # J/K, default
132
- time_constant = getattr(floor, "time_constant", 2.5) # hours, default
133
- lag_factor = self.heat_transfer.thermal_lag_factor(thermal_mass, time_constant, 1.0)
134
- conduction_load = u_value * area * delta_t * lag_factor
135
-
136
- heating_load += conduction_load
137
-
138
- # NEW: Perimeter losses (F-factor method, ASHRAE Fundamentals Chapter 18)
139
- if getattr(floor, 'ground_contact', False):
140
- perimeter = getattr(floor, 'perimeter', 0.0)
141
- if perimeter < 0:
142
- raise ValueError(f"Perimeter for floor {floor.name} cannot be negative")
143
- if perimeter > 0:
144
- f_factor = 0.73 # W/(m·K) for uninsulated slab-on-grade (Table 18.3)
145
- perimeter_load = f_factor * perimeter * delta_t
146
- heating_load += perimeter_load
147
-
148
  return max(0, heating_load)
149
-
150
  def calculate_window_heating_load(self, window: Window, outdoor_temp: float, indoor_temp: float) -> float:
151
- """
152
- Calculate heating load through a window using steady-state conduction.
153
-
154
- Args:
155
- window: Window object
156
- outdoor_temp: Outdoor temperature in °C
157
- indoor_temp: Indoor temperature in °C
158
-
159
- Returns:
160
- Heating load in W
161
- """
162
  self.validate_inputs(outdoor_temp, 80.0, window.area, window.u_value)
163
-
164
- u_value = window.u_value
165
- area = window.area
166
  delta_t = indoor_temp - outdoor_temp
167
- heating_load = u_value * area * delta_t
168
-
169
- return max(0, heating_load)
170
-
171
  def calculate_door_heating_load(self, door: Door, outdoor_temp: float, indoor_temp: float) -> float:
172
- """
173
- Calculate heating load through a door using steady-state conduction.
174
-
175
- Args:
176
- door: Door object
177
- outdoor_temp: Outdoor temperature in °C
178
- indoor_temp: Indoor temperature in °C
179
-
180
- Returns:
181
- Heating load in W
182
- """
183
  self.validate_inputs(outdoor_temp, 80.0, door.area, door.u_value)
184
-
185
- u_value = door.u_value
186
- area = door.area
187
  delta_t = indoor_temp - outdoor_temp
188
- heating_load = u_value * area * delta_t
189
-
190
- return max(0, heating_load)
191
-
192
  def calculate_infiltration_heating_load(self, building_volume: float, outdoor_temp: float,
193
  indoor_temp: float, outdoor_rh: float, indoor_rh: float,
194
  wind_speed: float = 4.0, height: float = 3.0,
195
- crack_length: float = 10.0) -> Dict[str, float]:
196
- """
197
- Calculate sensible and latent heating loads due to pressure-driven infiltration.
198
-
199
- Args:
200
- building_volume: Building volume in m³
201
- outdoor_temp: Outdoor temperature in °C
202
- indoor_temp: Indoor temperature in °C
203
- outdoor_rh: Outdoor relative humidity in %
204
- indoor_rh: Indoor relative humidity in %
205
- wind_speed: Wind speed in m/s (default: 4.0 m/s)
206
- height: Building height in m (default: 3.0 m)
207
- crack_length: Total crack length in m (default: 10.0 m)
208
-
209
- Returns:
210
- Dictionary with sensible, latent, and total heating loads in W
211
- """
212
  self.validate_inputs(outdoor_temp, outdoor_rh, building_volume, 0.0)
213
-
214
- # Calculate infiltration flow rate
215
  wind_pd = self.heat_transfer.wind_pressure_difference(wind_speed, wind_coefficient=0.4)
216
- stack_pd = self.heat_transfer.stack_pressure_difference(
217
- height=height,
218
- indoor_temp=indoor_temp + 273.15,
219
- outdoor_temp=outdoor_temp + 273.15
220
- )
221
  total_pd = self.heat_transfer.combined_pressure_difference(wind_pd, stack_pd)
222
- flow_rate = self.heat_transfer.crack_method_infiltration(
223
- crack_length=crack_length,
224
- coefficient=0.0001,
225
- pressure_difference=total_pd
226
- )
227
-
228
- # Calculate sensible heating load
229
- sensible_load = self.heat_transfer.infiltration_heat_transfer(
230
- flow_rate=flow_rate,
231
- delta_t=indoor_temp - outdoor_temp
232
- )
233
-
234
- # Calculate humidity ratios
235
  w_outdoor = self.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
236
  w_indoor = self.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
237
-
238
- # Calculate latent heating load
239
- delta_w = w_indoor - w_outdoor
240
- latent_load = self.heat_transfer.infiltration_latent_heat_transfer(
241
- flow_rate=flow_rate,
242
- delta_w=delta_w
243
- ) if delta_w > 0 else 0
244
-
245
- total_load = sensible_load + latent_load
246
-
247
  return {
248
  "sensible": max(0, sensible_load),
249
  "latent": max(0, latent_load),
250
- "total": max(0, total_load)
251
  }
252
-
253
  def calculate_ventilation_heating_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float,
254
- outdoor_rh: float, indoor_rh: float) -> Dict[str, float]:
255
- """
256
- Calculate sensible and latent heating loads due to ventilation.
257
-
258
- Args:
259
- flow_rate: Ventilation flow rate in m³/s
260
- outdoor_temp: Outdoor temperature in °C
261
- indoor_temp: Indoor temperature in °C
262
- outdoor_rh: Outdoor relative humidity in %
263
- indoor_rh: Indoor relative humidity in %
264
-
265
- Returns:
266
- Dictionary with sensible, latent, and total heating loads in W
267
- """
268
  self.validate_inputs(outdoor_temp, outdoor_rh, 0.0, 0.0)
269
-
270
- sensible_load = self.heat_transfer.infiltration_heat_transfer(
271
- flow_rate=flow_rate,
272
- delta_t=indoor_temp - outdoor_temp
273
- )
274
  w_outdoor = self.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
275
  w_indoor = self.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
276
- delta_w = w_indoor - w_outdoor
277
- latent_load = self.heat_transfer.infiltration_latent_heat_transfer(
278
- flow_rate=flow_rate,
279
- delta_w=delta_w
280
- ) if delta_w > 0 else 0
281
- total_load = sensible_load + latent_load
282
-
283
  return {
284
  "sensible": max(0, sensible_load),
285
  "latent": max(0, latent_load),
286
- "total": max(0, total_load)
287
  }
288
-
289
  def calculate_internal_gains_offset(self, people_load: float, lights_load: float,
290
- equipment_load: float, usage_factor: float = 0.7,
291
- hour: int = 0, schedule: Optional[List[float]] = None) -> float:
292
- """
293
- Calculate internal gains offset for heating load with schedule.
294
-
295
- Args:
296
- people_load: Heat gain from people in W
297
- lights_load: Heat gain from lights in W
298
- equipment_load: Heat gain from equipment in W
299
- usage_factor: Usage factor for internal gains (0-1)
300
- hour: Hour of the day (0-23)
301
- schedule: List of 24 hourly gain factors (0-1, default: None)
302
-
303
- Returns:
304
- Internal gains offset in W
305
- """
306
  total_gains = people_load + lights_load + equipment_load
307
- schedule_factor = 1.0
308
- if schedule and len(schedule) == 24:
309
- schedule_factor = schedule[hour]
310
- offset = total_gains * usage_factor * schedule_factor
311
- return max(0, offset)
312
-
313
  def calculate_design_heating_load(self, building_components: Dict[str, List[Any]],
314
- outdoor_conditions: Dict[str, Any],
315
- indoor_conditions: Dict[str, Any],
316
- internal_loads: Dict[str, Any],
317
- building_volume: float = 300.0,
318
- safety_factor: float = 1.15) -> Dict[str, float]:
319
- """
320
- Calculate design heating load for a building with enhanced calculations.
321
-
322
- Args:
323
- building_components: Dictionary with lists of building components
324
- outdoor_conditions: Dictionary with outdoor conditions
325
- indoor_conditions: Dictionary with indoor conditions
326
- internal_loads: Dictionary with internal loads
327
- building_volume: Building volume in m³ (default: 300 m³)
328
- safety_factor: Safety factor for heating load (default: 1.15)
329
-
330
- Returns:
331
- Dictionary with design heating loads
332
- """
333
- walls = building_components.get("walls", [])
334
- roofs = building_components.get("roofs", [])
335
- floors = building_components.get("floors", [])
336
- windows = building_components.get("windows", [])
337
- doors = building_components.get("doors", [])
338
-
339
  outdoor_temp = outdoor_conditions.get("design_temperature", -10.0)
340
  outdoor_rh = outdoor_conditions.get("design_relative_humidity", 80.0)
341
  ground_temp = outdoor_conditions.get("ground_temperature", 10.0)
342
  wind_speed = outdoor_conditions.get("wind_speed", 4.0)
343
-
344
  indoor_temp = indoor_conditions.get("temperature", 21.0)
345
  indoor_rh = indoor_conditions.get("relative_humidity", 40.0)
346
-
347
- people = internal_loads.get("people", {})
348
- lights = internal_loads.get("lights", {})
349
- equipment = internal_loads.get("equipment", {})
350
- infiltration = internal_loads.get("infiltration", {})
351
- ventilation = internal_loads.get("ventilation", {})
352
-
 
 
 
 
353
  loads = {
354
- "walls": 0,
355
- "roofs": 0,
356
- "floors": 0,
357
- "windows": 0,
358
- "doors": 0,
359
  "infiltration_sensible": 0,
360
  "infiltration_latent": 0,
361
  "ventilation_sensible": 0,
@@ -365,99 +286,51 @@ class HeatingLoadCalculator:
365
  "safety_factor": safety_factor,
366
  "total": 0
367
  }
368
-
369
- # Calculate loads
370
- for wall in walls:
371
- loads["walls"] += self.calculate_wall_heating_load(wall, outdoor_temp, indoor_temp)
372
- for roof in roofs:
373
- loads["roofs"] += self.calculate_roof_heating_load(roof, outdoor_temp, indoor_temp)
374
- for floor in floors:
375
- loads["floors"] += self.calculate_floor_heating_load(floor, ground_temp, indoor_temp)
376
- for window in windows:
377
- loads["windows"] += self.calculate_window_heating_load(window, outdoor_temp, indoor_temp)
378
- for door in doors:
379
- loads["doors"] += self.calculate_door_heating_load(door, outdoor_temp, indoor_temp)
380
-
381
- # Calculate infiltration loads
382
- if infiltration:
383
  infiltration_loads = self.calculate_infiltration_heating_load(
384
- building_volume=building_volume,
385
- outdoor_temp=outdoor_temp,
386
- indoor_temp=indoor_temp,
387
- outdoor_rh=outdoor_rh,
388
- indoor_rh=indoor_rh,
389
- wind_speed=wind_speed,
390
- height=infiltration.get("height", 3.0),
391
- crack_length=infiltration.get("crack_length", 10.0)
392
  )
393
  loads["infiltration_sensible"] = infiltration_loads["sensible"]
394
  loads["infiltration_latent"] = infiltration_loads["latent"]
395
-
396
- # Calculate ventilation loads
397
- if ventilation:
398
  ventilation_loads = self.calculate_ventilation_heating_load(
399
- flow_rate=ventilation.get("flow_rate", 0.1),
400
- outdoor_temp=outdoor_temp,
401
- indoor_temp=indoor_temp,
402
- outdoor_rh=outdoor_rh,
403
- indoor_rh=indoor_rh
404
  )
405
  loads["ventilation_sensible"] = ventilation_loads["sensible"]
406
  loads["ventilation_latent"] = ventilation_loads["latent"]
407
-
408
- # Calculate internal gains offset
 
 
409
  people_load = people.get("number", 0) * people.get("sensible_gain", 70.0)
410
  lights_load = lights.get("power", 0) * lights.get("use_factor", 0.8)
411
  equipment_load = equipment.get("power", 0) * equipment.get("use_factor", 0.7)
412
-
413
  schedule = None
414
  operating_hours = internal_loads.get("operating_hours", "8:00-18:00")
415
  if operating_hours:
416
  start_hour = int(operating_hours.split(":")[0])
417
  end_hour = int(operating_hours.split("-")[1].split(":")[0])
418
  schedule = [1.0 if start_hour <= h % 24 < end_hour else 0.5 for h in range(24)]
419
-
420
  loads["internal_gains_offset"] = self.calculate_internal_gains_offset(
421
- people_load=people_load,
422
- lights_load=lights_load,
423
- equipment_load=equipment_load,
424
- usage_factor=internal_loads.get("usage_factor", 0.7),
425
- hour=12, # Assume peak load at noon
426
- schedule=schedule
427
- )
428
-
429
- # Calculate subtotal
430
- loads["subtotal"] = (
431
- loads["walls"] +
432
- loads["roofs"] +
433
- loads["floors"] +
434
- loads["windows"] +
435
- loads["doors"] +
436
- loads["infiltration_sensible"] +
437
- loads["infiltration_latent"] +
438
- loads["ventilation_sensible"] +
439
- loads["ventilation_latent"]
440
  )
441
-
442
- # Apply internal gains offset and prevent negative loads
443
- loads["total"] = max(0, loads["subtotal"] - loads["internal_gains_offset"])
444
-
445
- # Apply safety factor
446
- loads["total"] *= safety_factor
447
-
448
  return loads
449
-
450
  def calculate_heating_load_summary(self, design_loads: Dict[str, float]) -> Dict[str, float]:
451
- """
452
- Summarize heating loads for reporting.
453
-
454
- Args:
455
- design_loads: Dictionary with design heating loads
456
-
457
- Returns:
458
- Summary dictionary with key load components
459
- """
460
- summary = {
461
  "walls": design_loads.get("walls", 0),
462
  "roofs": design_loads.get("roofs", 0),
463
  "floors": design_loads.get("floors", 0),
@@ -468,108 +341,103 @@ class HeatingLoadCalculator:
468
  "internal_gains_offset": design_loads.get("internal_gains_offset", 0),
469
  "subtotal": design_loads.get("subtotal", 0),
470
  "safety_factor": design_loads.get("safety_factor", 1.15),
471
- "total": design_loads.get("total", 0)
472
  }
473
- return summary
474
-
475
  def calculate_monthly_heating_loads(self, building_components: Dict[str, List[Any]],
476
- monthly_temps: Dict[str, float], ground_temps: Dict[str, float],
477
- indoor_conditions: Dict[str, Any], internal_loads: Dict[str, Any],
478
- building_volume: float = 300.0) -> Dict[str, float]:
479
- """
480
- Calculate monthly heating loads using monthly average temperatures.
481
-
482
- Args:
483
- building_components: Dictionary with lists of building components
484
- monthly_temps: Dictionary with monthly outdoor temperatures (°C)
485
- ground_temps: Dictionary with monthly ground temperatures (°C)
486
- indoor_conditions: Dictionary with indoor conditions
487
- internal_loads: Dictionary with internal loads
488
- building_volume: Building volume in m³ (default: 300 m³)
489
-
490
- Returns:
491
- Dictionary with monthly heating loads in W
492
- """
493
  monthly_loads = {}
494
  indoor_temp = indoor_conditions.get("temperature", 21.0)
495
  indoor_rh = indoor_conditions.get("relative_humidity", 40.0)
496
-
497
  for month, outdoor_temp in monthly_temps.items():
498
- ground_temp = ground_temps.get(month, outdoor_temp) # Fallback to outdoor temp if ground temp missing
499
  outdoor_conditions = {
500
  "design_temperature": outdoor_temp,
501
- "design_relative_humidity": 80.0, # Assume constant for simplicity
502
  "ground_temperature": ground_temp,
503
  "wind_speed": 4.0
504
  }
505
-
506
- # Skip if no heating is needed
507
  if outdoor_temp >= indoor_temp:
508
  monthly_loads[month] = 0.0
509
  continue
510
-
511
  loads = self.calculate_design_heating_load(
512
- building_components=building_components,
513
- outdoor_conditions=outdoor_conditions,
514
- indoor_conditions=indoor_conditions,
515
- internal_loads=internal_loads,
516
- building_volume=building_volume,
517
- safety_factor=1.0 # No safety factor for monthly loads
518
  )
519
- monthly_loads[month] = max(0, loads["total"])
520
-
521
  return monthly_loads
522
-
523
  def calculate_heating_degree_days(self, monthly_temps: Dict[str, float], base_temp: float = 18.3) -> float:
524
- """
525
- Calculate annual heating degree days.
526
-
527
- Args:
528
- monthly_temps: Dictionary with monthly outdoor temperatures (°C)
529
- base_temp: Base temperature for degree days (°C, default: 18.3°C)
530
-
531
- Returns:
532
- Total heating degree days
533
- """
534
- hdd = 0.0
535
  days_per_month = {
536
  "Jan": 31, "Feb": 28, "Mar": 31, "Apr": 30, "May": 31, "Jun": 30,
537
  "Jul": 31, "Aug": 31, "Sep": 30, "Oct": 31, "Nov": 30, "Dec": 31
538
  }
539
-
540
  for month, temp in monthly_temps.items():
541
  if temp < base_temp:
542
  hdd += (base_temp - temp) * days_per_month.get(month, 30)
543
-
544
  return hdd
545
-
546
  def calculate_annual_heating_energy(self, monthly_loads: Dict[str, float],
547
  operating_hours: str = "8:00-18:00") -> float:
548
- """
549
- Calculate annual heating energy consumption.
550
-
551
- Args:
552
- monthly_loads: Dictionary with monthly heating loads in W
553
- operating_hours: Operating hours (e.g., "8:00-18:00")
554
-
555
- Returns:
556
- Annual heating energy in kWh
557
- """
558
- hours_per_day = 10.0 # Default 10 hours
559
  if operating_hours:
560
  start_hour = int(operating_hours.split(":")[0])
561
  end_hour = int(operating_hours.split("-")[1].split(":")[0])
562
  hours_per_day = end_hour - start_hour
563
-
564
  days_per_month = {
565
  "Jan": 31, "Feb": 28, "Mar": 31, "Apr": 30, "May": 31, "Jun": 30,
566
  "Jul": 31, "Aug": 31, "Sep": 30, "Oct": 31, "Nov": 30, "Dec": 31
567
  }
568
-
569
  total_energy = 0.0
570
  for month, load in monthly_loads.items():
571
  hours = hours_per_day * days_per_month.get(month, 30)
572
- energy = load * hours / 1000 # Convert W to kW
573
  total_energy += energy
574
-
575
- return total_energy
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:
 
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,
 
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),
 
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")