mabuseif commited on
Commit
b081a29
·
verified ·
1 Parent(s): be61f5a

Update utils/heating_load.py

Browse files
Files changed (1) hide show
  1. utils/heating_load.py +131 -206
utils/heating_load.py CHANGED
@@ -2,6 +2,7 @@
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
@@ -20,7 +21,6 @@ from utils.heat_transfer import HeatTransferCalculations
20
  # Define paths
21
  DATA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
 
23
-
24
  class HeatingLoadCalculator:
25
  """Class for calculating heating loads using enhanced steady-state methods."""
26
 
@@ -29,7 +29,7 @@ class HeatingLoadCalculator:
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
 
@@ -38,6 +38,7 @@ class HeatingLoadCalculator:
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
@@ -50,6 +51,8 @@ class HeatingLoadCalculator:
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
  """
@@ -107,7 +110,7 @@ class HeatingLoadCalculator:
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,18 +120,30 @@ class HeatingLoadCalculator:
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
 
@@ -363,6 +378,7 @@ class HeatingLoadCalculator:
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,
@@ -377,10 +393,10 @@ class HeatingLoadCalculator:
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(
383
- flow_rate=flow_rate,
384
  outdoor_temp=outdoor_temp,
385
  indoor_temp=indoor_temp,
386
  outdoor_rh=outdoor_rh,
@@ -389,262 +405,171 @@ class HeatingLoadCalculator:
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"] +
407
- loads["infiltration_sensible"] + loads["infiltration_latent"] +
408
- loads["ventilation_sensible"] + loads["ventilation_latent"] -
409
- loads["internal_gains_offset"]
 
 
 
 
410
  )
411
- loads["total"] = loads["subtotal"] * safety_factor
 
 
 
 
 
412
 
413
  return loads
414
 
415
  def calculate_heating_load_summary(self, design_loads: Dict[str, float]) -> Dict[str, float]:
416
  """
417
- Calculate heating load summary.
418
 
419
  Args:
420
  design_loads: Dictionary with design heating loads
421
 
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,
436
- "internal_gains_offset": design_loads["internal_gains_offset"],
437
- "subtotal": design_loads["subtotal"],
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],
444
- design_temp: float, indoor_temp: float) -> Dict[str, float]:
 
445
  """
446
- Calculate monthly heating loads based on design load and monthly temperatures.
447
 
448
  Args:
449
- design_loads: Dictionary with design heating loads
450
- monthly_temps: Dictionary with monthly average temperatures
451
- design_temp: Design outdoor temperature in °C
452
- indoor_temp: Indoor temperature in °C
 
 
453
 
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
469
 
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
477
- base_temp: Base temperature for degree days in °C (default: 18°C)
478
 
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
501
- heating_system_efficiency: Heating system efficiency (0-1)
502
 
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
-
523
- # Create a singleton instance
524
- heating_load_calculator = HeatingLoadCalculator()
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",
533
- component_type=ComponentType.WALL,
534
- u_value=0.5,
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",
545
- component_type=ComponentType.ROOF,
546
- u_value=0.3,
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",
557
- component_type=ComponentType.WINDOW,
558
- u_value=2.8,
559
- area=5.0,
560
- orientation=Orientation.NORTH,
561
- shgc=0.7,
562
- vt=0.8,
563
- window_type="Double Glazed",
564
- glazing_layers=2,
565
- gas_fill="Air",
566
- low_e_coating=False
567
- )
568
-
569
- building_components = {
570
- "walls": [wall],
571
- "roofs": [roof],
572
- "windows": [window],
573
- "doors": [],
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,
592
- "sensible_gain": 70
593
- },
594
- "lights": {
595
- "power": 500.0,
596
- "use_factor": 0.9
597
- },
598
- "equipment": {
599
- "power": 1000.0,
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():
643
- print(f"{month}: {load:.2f} W")
644
-
645
- print("\nHeating Degree Days:")
646
- for month, hdd in hdds.items():
647
- print(f"{month}: {hdd:.2f} HDD")
648
-
649
- print("\nAnnual Heating Energy:")
650
- print(f"Total: {energy['annual']:.2f} kWh")
 
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
 
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
 
 
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
 
 
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
 
51
  raise ValueError(f"Area {area}m² cannot be negative")
52
  if u_value < 0:
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
  """
 
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
 
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
 
 
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,
 
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,
 
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),
464
+ "windows": design_loads.get("windows", 0),
465
+ "doors": design_loads.get("doors", 0),
466
+ "infiltration": design_loads.get("infiltration_sensible", 0) + design_loads.get("infiltration_latent", 0),
467
+ "ventilation": design_loads.get("ventilation_sensible", 0) + design_loads.get("ventilation_latent", 0),
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