mabuseif commited on
Commit
2b08ecf
·
verified ·
1 Parent(s): ce95872

Update utils/heating_load.py

Browse files
Files changed (1) hide show
  1. utils/heating_load.py +182 -215
utils/heating_load.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
  Heating load calculation module for HVAC Load Calculator.
3
- This module implements steady-state methods for calculating heating loads.
 
4
  """
5
 
6
  from typing import Dict, List, Any, Optional, Tuple
@@ -21,16 +22,38 @@ DATA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21
 
22
 
23
  class HeatingLoadCalculator:
24
- """Class for calculating heating loads using steady-state methods."""
25
 
26
  def __init__(self):
27
- """Initialize heating load calculator."""
28
  self.heat_transfer = HeatTransferCalculations()
29
  self.psychrometrics = Psychrometrics()
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  def calculate_wall_heating_load(self, wall: Wall, outdoor_temp: float, indoor_temp: float) -> float:
32
  """
33
- Calculate heating load through a wall using steady-state conduction.
34
 
35
  Args:
36
  wall: Wall object
@@ -40,19 +63,24 @@ class HeatingLoadCalculator:
40
  Returns:
41
  Heating load in W
42
  """
43
- # Get wall properties
 
44
  u_value = wall.u_value
45
  area = wall.area
46
-
47
- # Calculate heating load
48
  delta_t = indoor_temp - outdoor_temp
49
- heating_load = u_value * area * delta_t
50
 
51
- return heating_load
 
 
 
 
 
 
 
52
 
53
  def calculate_roof_heating_load(self, roof: Roof, outdoor_temp: float, indoor_temp: float) -> float:
54
  """
55
- Calculate heating load through a roof using steady-state conduction.
56
 
57
  Args:
58
  roof: Roof object
@@ -62,19 +90,24 @@ class HeatingLoadCalculator:
62
  Returns:
63
  Heating load in W
64
  """
65
- # Get roof properties
 
66
  u_value = roof.u_value
67
  area = roof.area
68
-
69
- # Calculate heating load
70
  delta_t = indoor_temp - outdoor_temp
71
- heating_load = u_value * area * delta_t
72
 
73
- return heating_load
 
 
 
 
 
 
 
74
 
75
  def calculate_floor_heating_load(self, floor: Floor, ground_temp: float, indoor_temp: float) -> float:
76
  """
77
- Calculate heating load through a floor.
78
 
79
  Args:
80
  floor: Floor object
@@ -84,15 +117,20 @@ class HeatingLoadCalculator:
84
  Returns:
85
  Heating load in W
86
  """
87
- # Get floor properties
 
88
  u_value = floor.u_value
89
  area = floor.area
90
-
91
- # Calculate heating load
92
  delta_t = indoor_temp - ground_temp
93
- heating_load = u_value * area * delta_t
94
 
95
- return heating_load
 
 
 
 
 
 
 
96
 
97
  def calculate_window_heating_load(self, window: Window, outdoor_temp: float, indoor_temp: float) -> float:
98
  """
@@ -106,15 +144,14 @@ class HeatingLoadCalculator:
106
  Returns:
107
  Heating load in W
108
  """
109
- # Get window properties
 
110
  u_value = window.u_value
111
  area = window.area
112
-
113
- # Calculate heating load
114
  delta_t = indoor_temp - outdoor_temp
115
  heating_load = u_value * area * delta_t
116
 
117
- return heating_load
118
 
119
  def calculate_door_heating_load(self, door: Door, outdoor_temp: float, indoor_temp: float) -> float:
120
  """
@@ -128,31 +165,51 @@ class HeatingLoadCalculator:
128
  Returns:
129
  Heating load in W
130
  """
131
- # Get door properties
 
132
  u_value = door.u_value
133
  area = door.area
134
-
135
- # Calculate heating load
136
  delta_t = indoor_temp - outdoor_temp
137
  heating_load = u_value * area * delta_t
138
 
139
- return heating_load
140
 
141
- def calculate_infiltration_heating_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float,
142
- outdoor_rh: float, indoor_rh: float) -> Dict[str, float]:
 
 
143
  """
144
- Calculate sensible and latent heating loads due to infiltration.
145
 
146
  Args:
147
- flow_rate: Infiltration flow rate in m³/s
148
  outdoor_temp: Outdoor temperature in °C
149
  indoor_temp: Indoor temperature in °C
150
  outdoor_rh: Outdoor relative humidity in %
151
  indoor_rh: Indoor relative humidity in %
 
 
 
152
 
153
  Returns:
154
  Dictionary with sensible, latent, and total heating loads in W
155
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  # Calculate sensible heating load
157
  sensible_load = self.heat_transfer.infiltration_heat_transfer(
158
  flow_rate=flow_rate,
@@ -163,23 +220,19 @@ class HeatingLoadCalculator:
163
  w_outdoor = self.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
164
  w_indoor = self.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
165
 
166
- # Calculate latent heating load (only if indoor humidity is higher than outdoor)
167
  delta_w = w_indoor - w_outdoor
168
- if delta_w > 0:
169
- latent_load = self.heat_transfer.infiltration_latent_heat_transfer(
170
- flow_rate=flow_rate,
171
- delta_w=delta_w
172
- )
173
- else:
174
- latent_load = 0
175
 
176
- # Calculate total heating load
177
  total_load = sensible_load + latent_load
178
 
179
  return {
180
- "sensible": sensible_load,
181
- "latent": latent_load,
182
- "total": total_load
183
  }
184
 
185
  def calculate_ventilation_heating_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float,
@@ -197,79 +250,91 @@ class HeatingLoadCalculator:
197
  Returns:
198
  Dictionary with sensible, latent, and total heating loads in W
199
  """
200
- # Ventilation load calculation is the same as infiltration
201
- return self.calculate_infiltration_heating_load(
 
202
  flow_rate=flow_rate,
203
- outdoor_temp=outdoor_temp,
204
- indoor_temp=indoor_temp,
205
- outdoor_rh=outdoor_rh,
206
- indoor_rh=indoor_rh
207
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
  def calculate_internal_gains_offset(self, people_load: float, lights_load: float,
210
- equipment_load: float, usage_factor: float = 0.7) -> float:
 
211
  """
212
- Calculate internal gains offset for heating load.
213
 
214
  Args:
215
  people_load: Heat gain from people in W
216
  lights_load: Heat gain from lights in W
217
  equipment_load: Heat gain from equipment in W
218
  usage_factor: Usage factor for internal gains (0-1)
 
 
219
 
220
  Returns:
221
  Internal gains offset in W
222
  """
223
- # Calculate total internal gains
224
  total_gains = people_load + lights_load + equipment_load
225
-
226
- # Apply usage factor
227
- offset = total_gains * usage_factor
228
-
229
- return offset
230
 
231
  def calculate_design_heating_load(self, building_components: Dict[str, List[Any]],
232
  outdoor_conditions: Dict[str, Any],
233
  indoor_conditions: Dict[str, Any],
234
  internal_loads: Dict[str, Any],
 
235
  safety_factor: float = 1.15) -> Dict[str, float]:
236
  """
237
- Calculate design heating load for a building.
238
 
239
  Args:
240
  building_components: Dictionary with lists of building components
241
  outdoor_conditions: Dictionary with outdoor conditions
242
  indoor_conditions: Dictionary with indoor conditions
243
  internal_loads: Dictionary with internal loads
 
244
  safety_factor: Safety factor for heating load (default: 1.15)
245
 
246
  Returns:
247
  Dictionary with design heating loads
248
  """
249
- # Extract building components
250
  walls = building_components.get("walls", [])
251
  roofs = building_components.get("roofs", [])
252
  floors = building_components.get("floors", [])
253
  windows = building_components.get("windows", [])
254
  doors = building_components.get("doors", [])
255
 
256
- # Extract outdoor conditions
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
 
261
- # Extract indoor conditions
262
  indoor_temp = indoor_conditions.get("temperature", 21.0)
263
  indoor_rh = indoor_conditions.get("relative_humidity", 40.0)
264
 
265
- # Extract internal loads
266
  people = internal_loads.get("people", {})
267
  lights = internal_loads.get("lights", {})
268
  equipment = internal_loads.get("equipment", {})
269
  infiltration = internal_loads.get("infiltration", {})
270
  ventilation = internal_loads.get("ventilation", {})
271
 
272
- # Initialize loads
273
  loads = {
274
  "walls": 0,
275
  "roofs": 0,
@@ -286,65 +351,32 @@ class HeatingLoadCalculator:
286
  "total": 0
287
  }
288
 
289
- # Calculate wall loads
290
  for wall in walls:
291
- wall_load = self.calculate_wall_heating_load(
292
- wall=wall,
293
- outdoor_temp=outdoor_temp,
294
- indoor_temp=indoor_temp
295
- )
296
- loads["walls"] += wall_load
297
-
298
- # Calculate roof loads
299
  for roof in roofs:
300
- roof_load = self.calculate_roof_heating_load(
301
- roof=roof,
302
- outdoor_temp=outdoor_temp,
303
- indoor_temp=indoor_temp
304
- )
305
- loads["roofs"] += roof_load
306
-
307
- # Calculate floor loads
308
  for floor in floors:
309
- floor_load = self.calculate_floor_heating_load(
310
- floor=floor,
311
- ground_temp=ground_temp,
312
- indoor_temp=indoor_temp
313
- )
314
- loads["floors"] += floor_load
315
-
316
- # Calculate window loads
317
  for window in windows:
318
- window_load = self.calculate_window_heating_load(
319
- window=window,
320
- outdoor_temp=outdoor_temp,
321
- indoor_temp=indoor_temp
322
- )
323
- loads["windows"] += window_load
324
-
325
- # Calculate door loads
326
  for door in doors:
327
- door_load = self.calculate_door_heating_load(
328
- door=door,
329
- outdoor_temp=outdoor_temp,
330
- indoor_temp=indoor_temp
331
- )
332
- loads["doors"] += door_load
333
 
334
- # Calculate infiltration loads
335
  if infiltration:
336
- flow_rate = infiltration.get("flow_rate", 0.0)
337
  infiltration_loads = self.calculate_infiltration_heating_load(
338
- flow_rate=flow_rate,
339
  outdoor_temp=outdoor_temp,
340
  indoor_temp=indoor_temp,
341
  outdoor_rh=outdoor_rh,
342
- indoor_rh=indoor_rh
 
 
 
343
  )
344
  loads["infiltration_sensible"] = infiltration_loads["sensible"]
345
  loads["infiltration_latent"] = infiltration_loads["latent"]
346
 
347
- # Calculate ventilation loads
348
  if ventilation:
349
  flow_rate = ventilation.get("flow_rate", 0.0)
350
  ventilation_loads = self.calculate_ventilation_heating_load(
@@ -357,19 +389,18 @@ class HeatingLoadCalculator:
357
  loads["ventilation_sensible"] = ventilation_loads["sensible"]
358
  loads["ventilation_latent"] = ventilation_loads["latent"]
359
 
360
- # Calculate internal gains offset
361
  people_load = people.get("number", 0) * people.get("sensible_gain", 70)
362
  lights_load = lights.get("power", 0) * lights.get("use_factor", 1.0)
363
  equipment_load = equipment.get("power", 0) * equipment.get("use_factor", 1.0)
364
-
365
  loads["internal_gains_offset"] = self.calculate_internal_gains_offset(
366
  people_load=people_load,
367
  lights_load=lights_load,
368
  equipment_load=equipment_load,
369
- usage_factor=internal_loads.get("usage_factor", 0.7)
 
 
370
  )
371
 
372
- # Calculate subtotal
373
  loads["subtotal"] = (
374
  loads["walls"] + loads["roofs"] + loads["floors"] +
375
  loads["windows"] + loads["doors"] +
@@ -377,8 +408,6 @@ class HeatingLoadCalculator:
377
  loads["ventilation_sensible"] + loads["ventilation_latent"] -
378
  loads["internal_gains_offset"]
379
  )
380
-
381
- # Apply safety factor
382
  loads["total"] = loads["subtotal"] * safety_factor
383
 
384
  return loads
@@ -393,18 +422,14 @@ class HeatingLoadCalculator:
393
  Returns:
394
  Dictionary with heating load summary
395
  """
396
- # Calculate envelope loads
397
- envelope_loads = (
398
- design_loads["walls"] + design_loads["roofs"] + design_loads["floors"] +
399
- design_loads["windows"] + design_loads["doors"]
400
- )
401
-
402
- # Calculate ventilation and infiltration loads
403
  ventilation_loads = design_loads["ventilation_sensible"] + design_loads["ventilation_latent"]
404
  infiltration_loads = design_loads["infiltration_sensible"] + design_loads["infiltration_latent"]
405
 
406
- # Create summary
407
- summary = {
408
  "envelope_loads": envelope_loads,
409
  "ventilation_loads": ventilation_loads,
410
  "infiltration_loads": infiltration_loads,
@@ -413,8 +438,6 @@ class HeatingLoadCalculator:
413
  "safety_factor": design_loads["safety_factor"],
414
  "total": design_loads["total"]
415
  }
416
-
417
- return summary
418
 
419
  def calculate_monthly_heating_loads(self, design_loads: Dict[str, float],
420
  monthly_temps: Dict[str, float],
@@ -431,25 +454,15 @@ class HeatingLoadCalculator:
431
  Returns:
432
  Dictionary with monthly heating loads
433
  """
434
- # Calculate design temperature difference
435
  design_delta_t = indoor_temp - design_temp
436
-
437
- # Calculate monthly loads
438
  monthly_loads = {}
439
 
440
  for month, temp in monthly_temps.items():
441
- # Calculate temperature difference for this month
442
  delta_t = indoor_temp - temp
443
-
444
- # Skip months where outdoor temperature is higher than indoor
445
  if delta_t <= 0:
446
  monthly_loads[month] = 0
447
  continue
448
-
449
- # Calculate load ratio based on temperature difference
450
  load_ratio = delta_t / design_delta_t
451
-
452
- # Calculate monthly load
453
  monthly_loads[month] = design_loads["total"] * load_ratio
454
 
455
  return monthly_loads
@@ -457,7 +470,7 @@ class HeatingLoadCalculator:
457
  def calculate_heating_degree_days(self, monthly_temps: Dict[str, float],
458
  base_temp: float = 18.0) -> Dict[str, float]:
459
  """
460
- Calculate heating degree days for each month.
461
 
462
  Args:
463
  monthly_temps: Dictionary with monthly average temperatures
@@ -466,31 +479,22 @@ class HeatingLoadCalculator:
466
  Returns:
467
  Dictionary with monthly heating degree days
468
  """
469
- # Calculate monthly heating degree days
 
 
 
470
  monthly_hdds = {}
471
 
472
  for month, temp in monthly_temps.items():
473
- # Calculate degree days
474
- days_in_month = 30 # Approximate
475
- if month in ["Apr", "Jun", "Sep", "Nov"]:
476
- days_in_month = 30
477
- elif month == "Feb":
478
- days_in_month = 28 # Ignore leap years for simplicity
479
- else:
480
- days_in_month = 31
481
-
482
- # Calculate daily degree days
483
  daily_hdd = max(0, base_temp - temp)
484
-
485
- # Calculate monthly degree days
486
- monthly_hdds[month] = daily_hdd * days_in_month
487
 
488
  return monthly_hdds
489
 
490
  def calculate_annual_heating_energy(self, monthly_loads: Dict[str, float],
491
  heating_system_efficiency: float = 0.8) -> Dict[str, float]:
492
  """
493
- Calculate annual heating energy consumption.
494
 
495
  Args:
496
  monthly_loads: Dictionary with monthly heating loads in W
@@ -499,32 +503,20 @@ class HeatingLoadCalculator:
499
  Returns:
500
  Dictionary with monthly and annual heating energy in kWh
501
  """
502
- # Calculate monthly energy consumption
 
 
 
503
  monthly_energy = {}
504
  annual_energy = 0
505
 
506
  for month, load in monthly_loads.items():
507
- # Calculate hours in month
508
- hours_in_month = 24 * 30 # Approximate
509
- if month in ["Apr", "Jun", "Sep", "Nov"]:
510
- hours_in_month = 24 * 30
511
- elif month == "Feb":
512
- hours_in_month = 24 * 28 # Ignore leap years for simplicity
513
- else:
514
- hours_in_month = 24 * 31
515
-
516
- # Calculate energy in kWh
517
  energy = load * hours_in_month / 1000 / heating_system_efficiency
518
-
519
- # Store monthly energy
520
  monthly_energy[month] = energy
521
-
522
- # Add to annual total
523
  annual_energy += energy
524
 
525
- # Add annual total to results
526
  monthly_energy["annual"] = annual_energy
527
-
528
  return monthly_energy
529
 
530
 
@@ -533,10 +525,8 @@ heating_load_calculator = HeatingLoadCalculator()
533
 
534
  # Example usage
535
  if __name__ == "__main__":
536
- # Create sample building components
537
  from data.building_components import Wall, Roof, Window, Door, Orientation, ComponentType
538
 
539
- # Create a sample wall
540
  wall = Wall(
541
  id="wall1",
542
  name="Exterior Wall",
@@ -545,10 +535,10 @@ if __name__ == "__main__":
545
  area=20.0,
546
  orientation=Orientation.NORTH,
547
  wall_type="Brick",
548
- wall_group="B"
 
 
549
  )
550
-
551
- # Create a sample roof
552
  roof = Roof(
553
  id="roof1",
554
  name="Flat Roof",
@@ -557,10 +547,10 @@ if __name__ == "__main__":
557
  area=50.0,
558
  orientation=Orientation.HORIZONTAL,
559
  roof_type="Concrete",
560
- roof_group="C"
 
 
561
  )
562
-
563
- # Create a sample window
564
  window = Window(
565
  id="window1",
566
  name="North Window",
@@ -576,7 +566,6 @@ if __name__ == "__main__":
576
  low_e_coating=False
577
  )
578
 
579
- # Define building components
580
  building_components = {
581
  "walls": [wall],
582
  "roofs": [roof],
@@ -585,19 +574,18 @@ if __name__ == "__main__":
585
  "floors": []
586
  }
587
 
588
- # Define conditions
589
  outdoor_conditions = {
590
  "design_temperature": -10.0,
591
  "design_relative_humidity": 80.0,
592
- "ground_temperature": 10.0
 
593
  }
594
-
595
  indoor_conditions = {
596
  "temperature": 21.0,
597
  "relative_humidity": 40.0
598
  }
599
 
600
- # Define internal loads
601
  internal_loads = {
602
  "people": {
603
  "number": 3,
@@ -612,64 +600,43 @@ if __name__ == "__main__":
612
  "use_factor": 0.7
613
  },
614
  "infiltration": {
615
- "flow_rate": 0.05
 
616
  },
617
  "ventilation": {
618
  "flow_rate": 0.1
619
  },
620
- "usage_factor": 0.7
 
621
  }
622
 
623
- # Calculate design heating load
624
  design_loads = heating_load_calculator.calculate_design_heating_load(
625
  building_components=building_components,
626
  outdoor_conditions=outdoor_conditions,
627
  indoor_conditions=indoor_conditions,
628
- internal_loads=internal_loads
 
629
  )
630
-
631
- # Calculate heating load summary
632
  summary = heating_load_calculator.calculate_heating_load_summary(design_loads)
633
 
634
- # Define monthly temperatures
635
  monthly_temps = {
636
- "Jan": -5.0,
637
- "Feb": -3.0,
638
- "Mar": 2.0,
639
- "Apr": 8.0,
640
- "May": 14.0,
641
- "Jun": 18.0,
642
- "Jul": 21.0,
643
- "Aug": 20.0,
644
- "Sep": 16.0,
645
- "Oct": 10.0,
646
- "Nov": 4.0,
647
- "Dec": -2.0
648
  }
649
 
650
- # Calculate monthly heating loads
651
  monthly_loads = heating_load_calculator.calculate_monthly_heating_loads(
652
  design_loads=design_loads,
653
  monthly_temps=monthly_temps,
654
  design_temp=outdoor_conditions["design_temperature"],
655
  indoor_temp=indoor_conditions["temperature"]
656
  )
657
-
658
- # Calculate heating degree days
659
  hdds = heating_load_calculator.calculate_heating_degree_days(monthly_temps)
660
-
661
- # Calculate annual heating energy
662
  energy = heating_load_calculator.calculate_annual_heating_energy(monthly_loads)
663
 
664
- # Print results
665
  print("Heating Load Summary:")
666
- print(f"Envelope Loads: {summary['envelope_loads']:.2f} W")
667
- print(f"Ventilation Loads: {summary['ventilation_loads']:.2f} W")
668
- print(f"Infiltration Loads: {summary['infiltration_loads']:.2f} W")
669
- print(f"Internal Gains Offset: {summary['internal_gains_offset']:.2f} W")
670
- print(f"Subtotal: {summary['subtotal']:.2f} W")
671
- print(f"Safety Factor: {summary['safety_factor']:.2f}")
672
- print(f"Total: {summary['total']:.2f} W")
673
 
674
  print("\nMonthly Heating Loads:")
675
  for month, load in monthly_loads.items():
@@ -680,4 +647,4 @@ if __name__ == "__main__":
680
  print(f"{month}: {hdd:.2f} HDD")
681
 
682
  print("\nAnnual Heating Energy:")
683
- print(f"Total: {energy['annual']:.2f} kWh")
 
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
  """
6
 
7
  from typing import Dict, List, Any, Optional, Tuple
 
22
 
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) -> 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
+
42
+ Raises:
43
+ ValueError: If inputs are out of acceptable ranges
44
+ """
45
+ if not -50 <= temp <= 60:
46
+ raise ValueError(f"Temperature {temp}°C is outside valid range (-50 to 60°C)")
47
+ if not 0 <= rh <= 100:
48
+ raise ValueError(f"Relative humidity {rh}% is outside valid range (0 to 100%)")
49
+ if area < 0:
50
+ raise ValueError(f"Area {area}m² cannot be negative")
51
+ if u_value < 0:
52
+ raise ValueError(f"U-value {u_value} W/(m²·K) cannot be negative")
53
+
54
  def calculate_wall_heating_load(self, wall: Wall, outdoor_temp: float, indoor_temp: float) -> float:
55
  """
56
+ Calculate heating load through a wall with thermal mass effects.
57
 
58
  Args:
59
  wall: Wall object
 
63
  Returns:
64
  Heating load in W
65
  """
66
+ self.validate_inputs(outdoor_temp, 80.0, wall.area, wall.u_value)
67
+
68
  u_value = wall.u_value
69
  area = wall.area
 
 
70
  delta_t = indoor_temp - outdoor_temp
 
71
 
72
+ # Apply thermal mass effect
73
+ thermal_mass = getattr(wall, "thermal_mass", 100000) # J/K, default
74
+ time_constant = getattr(wall, "time_constant", 2.0) # hours, default
75
+ lag_factor = self.heat_transfer.thermal_lag_factor(thermal_mass, time_constant, 1.0)
76
+
77
+ heating_load = u_value * area * delta_t * lag_factor
78
+
79
+ return max(0, heating_load)
80
 
81
  def calculate_roof_heating_load(self, roof: Roof, outdoor_temp: float, indoor_temp: float) -> float:
82
  """
83
+ Calculate heating load through a roof with thermal mass effects.
84
 
85
  Args:
86
  roof: Roof object
 
90
  Returns:
91
  Heating load in W
92
  """
93
+ self.validate_inputs(outdoor_temp, 80.0, roof.area, roof.u_value)
94
+
95
  u_value = roof.u_value
96
  area = roof.area
 
 
97
  delta_t = indoor_temp - outdoor_temp
 
98
 
99
+ # Apply thermal mass effect
100
+ thermal_mass = getattr(roof, "thermal_mass", 200000) # J/K, default
101
+ time_constant = getattr(roof, "time_constant", 3.0) # hours, default
102
+ lag_factor = self.heat_transfer.thermal_lag_factor(thermal_mass, time_constant, 1.0)
103
+
104
+ heating_load = u_value * area * delta_t * lag_factor
105
+
106
+ return max(0, heating_load)
107
 
108
  def calculate_floor_heating_load(self, floor: Floor, ground_temp: float, indoor_temp: float) -> float:
109
  """
110
+ Calculate heating load through a floor with thermal mass effects.
111
 
112
  Args:
113
  floor: Floor object
 
117
  Returns:
118
  Heating load in W
119
  """
120
+ self.validate_inputs(ground_temp, 80.0, floor.area, floor.u_value)
121
+
122
  u_value = floor.u_value
123
  area = floor.area
 
 
124
  delta_t = indoor_temp - ground_temp
 
125
 
126
+ # Apply thermal mass effect
127
+ thermal_mass = getattr(floor, "thermal_mass", 150000) # J/K, default
128
+ time_constant = getattr(floor, "time_constant", 2.5) # hours, default
129
+ lag_factor = self.heat_transfer.thermal_lag_factor(thermal_mass, time_constant, 1.0)
130
+
131
+ heating_load = u_value * area * delta_t * lag_factor
132
+
133
+ return max(0, heating_load)
134
 
135
  def calculate_window_heating_load(self, window: Window, outdoor_temp: float, indoor_temp: float) -> float:
136
  """
 
144
  Returns:
145
  Heating load in W
146
  """
147
+ self.validate_inputs(outdoor_temp, 80.0, window.area, window.u_value)
148
+
149
  u_value = window.u_value
150
  area = window.area
 
 
151
  delta_t = indoor_temp - outdoor_temp
152
  heating_load = u_value * area * delta_t
153
 
154
+ return max(0, heating_load)
155
 
156
  def calculate_door_heating_load(self, door: Door, outdoor_temp: float, indoor_temp: float) -> float:
157
  """
 
165
  Returns:
166
  Heating load in W
167
  """
168
+ self.validate_inputs(outdoor_temp, 80.0, door.area, door.u_value)
169
+
170
  u_value = door.u_value
171
  area = door.area
 
 
172
  delta_t = indoor_temp - outdoor_temp
173
  heating_load = u_value * area * delta_t
174
 
175
+ return max(0, heating_load)
176
 
177
+ def calculate_infiltration_heating_load(self, building_volume: float, outdoor_temp: float,
178
+ indoor_temp: float, outdoor_rh: float, indoor_rh: float,
179
+ wind_speed: float = 4.0, height: float = 3.0,
180
+ crack_length: float = 10.0) -> Dict[str, float]:
181
  """
182
+ Calculate sensible and latent heating loads due to pressure-driven infiltration.
183
 
184
  Args:
185
+ building_volume: Building volume in m³
186
  outdoor_temp: Outdoor temperature in °C
187
  indoor_temp: Indoor temperature in °C
188
  outdoor_rh: Outdoor relative humidity in %
189
  indoor_rh: Indoor relative humidity in %
190
+ wind_speed: Wind speed in m/s (default: 4.0 m/s)
191
+ height: Building height in m (default: 3.0 m)
192
+ crack_length: Total crack length in m (default: 10.0 m)
193
 
194
  Returns:
195
  Dictionary with sensible, latent, and total heating loads in W
196
  """
197
+ self.validate_inputs(outdoor_temp, outdoor_rh, building_volume, 0.0)
198
+
199
+ # Calculate infiltration flow rate
200
+ wind_pd = self.heat_transfer.wind_pressure_difference(wind_speed, wind_coefficient=0.4)
201
+ stack_pd = self.heat_transfer.stack_pressure_difference(
202
+ height=height,
203
+ indoor_temp=indoor_temp + 273.15,
204
+ outdoor_temp=outdoor_temp + 273.15
205
+ )
206
+ total_pd = self.heat_transfer.combined_pressure_difference(wind_pd, stack_pd)
207
+ flow_rate = self.heat_transfer.crack_method_infiltration(
208
+ crack_length=crack_length,
209
+ coefficient=0.0001,
210
+ pressure_difference=total_pd
211
+ )
212
+
213
  # Calculate sensible heating load
214
  sensible_load = self.heat_transfer.infiltration_heat_transfer(
215
  flow_rate=flow_rate,
 
220
  w_outdoor = self.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
221
  w_indoor = self.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
222
 
223
+ # Calculate latent heating load
224
  delta_w = w_indoor - w_outdoor
225
+ latent_load = self.heat_transfer.infiltration_latent_heat_transfer(
226
+ flow_rate=flow_rate,
227
+ delta_w=delta_w
228
+ ) if delta_w > 0 else 0
 
 
 
229
 
 
230
  total_load = sensible_load + latent_load
231
 
232
  return {
233
+ "sensible": max(0, sensible_load),
234
+ "latent": max(0, latent_load),
235
+ "total": max(0, total_load)
236
  }
237
 
238
  def calculate_ventilation_heating_load(self, flow_rate: float, outdoor_temp: float, indoor_temp: float,
 
250
  Returns:
251
  Dictionary with sensible, latent, and total heating loads in W
252
  """
253
+ self.validate_inputs(outdoor_temp, outdoor_rh, 0.0, 0.0)
254
+
255
+ sensible_load = self.heat_transfer.infiltration_heat_transfer(
256
  flow_rate=flow_rate,
257
+ delta_t=indoor_temp - outdoor_temp
 
 
 
258
  )
259
+ w_outdoor = self.psychrometrics.humidity_ratio(outdoor_temp, outdoor_rh)
260
+ w_indoor = self.psychrometrics.humidity_ratio(indoor_temp, indoor_rh)
261
+ delta_w = w_indoor - w_outdoor
262
+ latent_load = self.heat_transfer.infiltration_latent_heat_transfer(
263
+ flow_rate=flow_rate,
264
+ delta_w=delta_w
265
+ ) if delta_w > 0 else 0
266
+ total_load = sensible_load + latent_load
267
+
268
+ return {
269
+ "sensible": max(0, sensible_load),
270
+ "latent": max(0, latent_load),
271
+ "total": max(0, total_load)
272
+ }
273
 
274
  def calculate_internal_gains_offset(self, people_load: float, lights_load: float,
275
+ equipment_load: float, usage_factor: float = 0.7,
276
+ hour: int = 0, schedule: Optional[List[float]] = None) -> float:
277
  """
278
+ Calculate internal gains offset for heating load with schedule.
279
 
280
  Args:
281
  people_load: Heat gain from people in W
282
  lights_load: Heat gain from lights in W
283
  equipment_load: Heat gain from equipment in W
284
  usage_factor: Usage factor for internal gains (0-1)
285
+ hour: Hour of the day (0-23)
286
+ schedule: List of 24 hourly gain factors (0-1, default: None)
287
 
288
  Returns:
289
  Internal gains offset in W
290
  """
 
291
  total_gains = people_load + lights_load + equipment_load
292
+ schedule_factor = 1.0
293
+ if schedule and len(schedule) == 24:
294
+ schedule_factor = schedule[hour]
295
+ offset = total_gains * usage_factor * schedule_factor
296
+ return max(0, offset)
297
 
298
  def calculate_design_heating_load(self, building_components: Dict[str, List[Any]],
299
  outdoor_conditions: Dict[str, Any],
300
  indoor_conditions: Dict[str, Any],
301
  internal_loads: Dict[str, Any],
302
+ building_volume: float = 300.0,
303
  safety_factor: float = 1.15) -> Dict[str, float]:
304
  """
305
+ Calculate design heating load for a building with enhanced calculations.
306
 
307
  Args:
308
  building_components: Dictionary with lists of building components
309
  outdoor_conditions: Dictionary with outdoor conditions
310
  indoor_conditions: Dictionary with indoor conditions
311
  internal_loads: Dictionary with internal loads
312
+ building_volume: Building volume in m³ (default: 300 m³)
313
  safety_factor: Safety factor for heating load (default: 1.15)
314
 
315
  Returns:
316
  Dictionary with design heating loads
317
  """
 
318
  walls = building_components.get("walls", [])
319
  roofs = building_components.get("roofs", [])
320
  floors = building_components.get("floors", [])
321
  windows = building_components.get("windows", [])
322
  doors = building_components.get("doors", [])
323
 
 
324
  outdoor_temp = outdoor_conditions.get("design_temperature", -10.0)
325
  outdoor_rh = outdoor_conditions.get("design_relative_humidity", 80.0)
326
  ground_temp = outdoor_conditions.get("ground_temperature", 10.0)
327
+ wind_speed = outdoor_conditions.get("wind_speed", 4.0)
328
 
 
329
  indoor_temp = indoor_conditions.get("temperature", 21.0)
330
  indoor_rh = indoor_conditions.get("relative_humidity", 40.0)
331
 
 
332
  people = internal_loads.get("people", {})
333
  lights = internal_loads.get("lights", {})
334
  equipment = internal_loads.get("equipment", {})
335
  infiltration = internal_loads.get("infiltration", {})
336
  ventilation = internal_loads.get("ventilation", {})
337
 
 
338
  loads = {
339
  "walls": 0,
340
  "roofs": 0,
 
351
  "total": 0
352
  }
353
 
354
+ # Calculate loads
355
  for wall in walls:
356
+ loads["walls"] += self.calculate_wall_heating_load(wall, outdoor_temp, indoor_temp)
 
 
 
 
 
 
 
357
  for roof in roofs:
358
+ loads["roofs"] += self.calculate_roof_heating_load(roof, outdoor_temp, indoor_temp)
 
 
 
 
 
 
 
359
  for floor in floors:
360
+ loads["floors"] += self.calculate_floor_heating_load(floor, ground_temp, indoor_temp)
 
 
 
 
 
 
 
361
  for window in windows:
362
+ loads["windows"] += self.calculate_window_heating_load(window, outdoor_temp, indoor_temp)
 
 
 
 
 
 
 
363
  for door in doors:
364
+ loads["doors"] += self.calculate_door_heating_load(door, outdoor_temp, indoor_temp)
 
 
 
 
 
365
 
 
366
  if infiltration:
 
367
  infiltration_loads = self.calculate_infiltration_heating_load(
368
+ building_volume=building_volume,
369
  outdoor_temp=outdoor_temp,
370
  indoor_temp=indoor_temp,
371
  outdoor_rh=outdoor_rh,
372
+ indoor_rh=indoor_rh,
373
+ wind_speed=wind_speed,
374
+ height=infiltration.get("height", 3.0),
375
+ crack_length=infiltration.get("crack_length", 10.0)
376
  )
377
  loads["infiltration_sensible"] = infiltration_loads["sensible"]
378
  loads["infiltration_latent"] = infiltration_loads["latent"]
379
 
 
380
  if ventilation:
381
  flow_rate = ventilation.get("flow_rate", 0.0)
382
  ventilation_loads = self.calculate_ventilation_heating_load(
 
389
  loads["ventilation_sensible"] = ventilation_loads["sensible"]
390
  loads["ventilation_latent"] = ventilation_loads["latent"]
391
 
 
392
  people_load = people.get("number", 0) * people.get("sensible_gain", 70)
393
  lights_load = lights.get("power", 0) * lights.get("use_factor", 1.0)
394
  equipment_load = equipment.get("power", 0) * equipment.get("use_factor", 1.0)
 
395
  loads["internal_gains_offset"] = self.calculate_internal_gains_offset(
396
  people_load=people_load,
397
  lights_load=lights_load,
398
  equipment_load=equipment_load,
399
+ usage_factor=internal_loads.get("usage_factor", 0.7),
400
+ hour=0,
401
+ schedule=internal_loads.get("gains_schedule", None)
402
  )
403
 
 
404
  loads["subtotal"] = (
405
  loads["walls"] + loads["roofs"] + loads["floors"] +
406
  loads["windows"] + loads["doors"] +
 
408
  loads["ventilation_sensible"] + loads["ventilation_latent"] -
409
  loads["internal_gains_offset"]
410
  )
 
 
411
  loads["total"] = loads["subtotal"] * safety_factor
412
 
413
  return loads
 
422
  Returns:
423
  Dictionary with heating load summary
424
  """
425
+ envelope_loads = sum([
426
+ design_loads["walls"], design_loads["roofs"], design_loads["floors"],
427
+ design_loads["windows"], design_loads["doors"]
428
+ ])
 
 
 
429
  ventilation_loads = design_loads["ventilation_sensible"] + design_loads["ventilation_latent"]
430
  infiltration_loads = design_loads["infiltration_sensible"] + design_loads["infiltration_latent"]
431
 
432
+ return {
 
433
  "envelope_loads": envelope_loads,
434
  "ventilation_loads": ventilation_loads,
435
  "infiltration_loads": infiltration_loads,
 
438
  "safety_factor": design_loads["safety_factor"],
439
  "total": design_loads["total"]
440
  }
 
 
441
 
442
  def calculate_monthly_heating_loads(self, design_loads: Dict[str, float],
443
  monthly_temps: Dict[str, float],
 
454
  Returns:
455
  Dictionary with monthly heating loads
456
  """
 
457
  design_delta_t = indoor_temp - design_temp
 
 
458
  monthly_loads = {}
459
 
460
  for month, temp in monthly_temps.items():
 
461
  delta_t = indoor_temp - temp
 
 
462
  if delta_t <= 0:
463
  monthly_loads[month] = 0
464
  continue
 
 
465
  load_ratio = delta_t / design_delta_t
 
 
466
  monthly_loads[month] = design_loads["total"] * load_ratio
467
 
468
  return monthly_loads
 
470
  def calculate_heating_degree_days(self, monthly_temps: Dict[str, float],
471
  base_temp: float = 18.0) -> Dict[str, float]:
472
  """
473
+ Calculate heating degree days for each month with precise days.
474
 
475
  Args:
476
  monthly_temps: Dictionary with monthly average temperatures
 
479
  Returns:
480
  Dictionary with monthly heating degree days
481
  """
482
+ days_per_month = {
483
+ "Jan": 31, "Feb": 28, "Mar": 31, "Apr": 30, "May": 31, "Jun": 30,
484
+ "Jul": 31, "Aug": 31, "Sep": 30, "Oct": 31, "Nov": 30, "Dec": 31
485
+ }
486
  monthly_hdds = {}
487
 
488
  for month, temp in monthly_temps.items():
 
 
 
 
 
 
 
 
 
 
489
  daily_hdd = max(0, base_temp - temp)
490
+ monthly_hdds[month] = daily_hdd * days_per_month[month]
 
 
491
 
492
  return monthly_hdds
493
 
494
  def calculate_annual_heating_energy(self, monthly_loads: Dict[str, float],
495
  heating_system_efficiency: float = 0.8) -> Dict[str, float]:
496
  """
497
+ Calculate annual heating energy consumption with precise days.
498
 
499
  Args:
500
  monthly_loads: Dictionary with monthly heating loads in W
 
503
  Returns:
504
  Dictionary with monthly and annual heating energy in kWh
505
  """
506
+ days_per_month = {
507
+ "Jan": 31, "Feb": 28, "Mar": 31, "Apr": 30, "May": 31, "Jun": 30,
508
+ "Jul": 31, "Aug": 31, "Sep": 30, "Oct": 31, "Nov": 30, "Dec": 31
509
+ }
510
  monthly_energy = {}
511
  annual_energy = 0
512
 
513
  for month, load in monthly_loads.items():
514
+ hours_in_month = 24 * days_per_month[month]
 
 
 
 
 
 
 
 
 
515
  energy = load * hours_in_month / 1000 / heating_system_efficiency
 
 
516
  monthly_energy[month] = energy
 
 
517
  annual_energy += energy
518
 
 
519
  monthly_energy["annual"] = annual_energy
 
520
  return monthly_energy
521
 
522
 
 
525
 
526
  # Example usage
527
  if __name__ == "__main__":
 
528
  from data.building_components import Wall, Roof, Window, Door, Orientation, ComponentType
529
 
 
530
  wall = Wall(
531
  id="wall1",
532
  name="Exterior Wall",
 
535
  area=20.0,
536
  orientation=Orientation.NORTH,
537
  wall_type="Brick",
538
+ wall_group="B",
539
+ thermal_mass=100000,
540
+ time_constant=2.0
541
  )
 
 
542
  roof = Roof(
543
  id="roof1",
544
  name="Flat Roof",
 
547
  area=50.0,
548
  orientation=Orientation.HORIZONTAL,
549
  roof_type="Concrete",
550
+ roof_group="C",
551
+ thermal_mass=200000,
552
+ time_constant=3.0
553
  )
 
 
554
  window = Window(
555
  id="window1",
556
  name="North Window",
 
566
  low_e_coating=False
567
  )
568
 
 
569
  building_components = {
570
  "walls": [wall],
571
  "roofs": [roof],
 
574
  "floors": []
575
  }
576
 
 
577
  outdoor_conditions = {
578
  "design_temperature": -10.0,
579
  "design_relative_humidity": 80.0,
580
+ "ground_temperature": 10.0,
581
+ "wind_speed": 4.0
582
  }
 
583
  indoor_conditions = {
584
  "temperature": 21.0,
585
  "relative_humidity": 40.0
586
  }
587
 
588
+ gains_schedule = [0.2] * 8 + [0.8] * 8 + [0.4] * 8 # 8h low, 8h high, 8h medium
589
  internal_loads = {
590
  "people": {
591
  "number": 3,
 
600
  "use_factor": 0.7
601
  },
602
  "infiltration": {
603
+ "height": 3.0,
604
+ "crack_length": 10.0
605
  },
606
  "ventilation": {
607
  "flow_rate": 0.1
608
  },
609
+ "usage_factor": 0.7,
610
+ "gains_schedule": gains_schedule
611
  }
612
 
 
613
  design_loads = heating_load_calculator.calculate_design_heating_load(
614
  building_components=building_components,
615
  outdoor_conditions=outdoor_conditions,
616
  indoor_conditions=indoor_conditions,
617
+ internal_loads=internal_loads,
618
+ building_volume=300.0
619
  )
 
 
620
  summary = heating_load_calculator.calculate_heating_load_summary(design_loads)
621
 
 
622
  monthly_temps = {
623
+ "Jan": -5.0, "Feb": -3.0, "Mar": 2.0, "Apr": 8.0, "May": 14.0,
624
+ "Jun": 18.0, "Jul": 21.0, "Aug": 20.0, "Sep": 16.0, "Oct": 10.0,
625
+ "Nov": 4.0, "Dec": -2.0
 
 
 
 
 
 
 
 
 
626
  }
627
 
 
628
  monthly_loads = heating_load_calculator.calculate_monthly_heating_loads(
629
  design_loads=design_loads,
630
  monthly_temps=monthly_temps,
631
  design_temp=outdoor_conditions["design_temperature"],
632
  indoor_temp=indoor_conditions["temperature"]
633
  )
 
 
634
  hdds = heating_load_calculator.calculate_heating_degree_days(monthly_temps)
 
 
635
  energy = heating_load_calculator.calculate_annual_heating_energy(monthly_loads)
636
 
 
637
  print("Heating Load Summary:")
638
+ for key, value in summary.items():
639
+ print(f"{key.replace('_', ' ').title()}: {value:.2f} W")
 
 
 
 
 
640
 
641
  print("\nMonthly Heating Loads:")
642
  for month, load in monthly_loads.items():
 
647
  print(f"{month}: {hdd:.2f} HDD")
648
 
649
  print("\nAnnual Heating Energy:")
650
+ print(f"Total: {energy['annual']:.2f} kWh")