Spaces:
Sleeping
Sleeping
Update utils/heating_load.py
Browse files- 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 |
-
|
| 123 |
-
area = floor.area
|
| 124 |
delta_t = indoor_temp - ground_temp
|
| 125 |
|
| 126 |
-
#
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 393 |
-
|
| 394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 401 |
-
schedule=
|
| 402 |
)
|
| 403 |
|
|
|
|
| 404 |
loads["subtotal"] = (
|
| 405 |
-
loads["walls"] +
|
| 406 |
-
loads["
|
| 407 |
-
loads["
|
| 408 |
-
loads["
|
| 409 |
-
loads["
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
)
|
| 411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
|
| 413 |
return loads
|
| 414 |
|
| 415 |
def calculate_heating_load_summary(self, design_loads: Dict[str, float]) -> Dict[str, float]:
|
| 416 |
"""
|
| 417 |
-
|
| 418 |
|
| 419 |
Args:
|
| 420 |
design_loads: Dictionary with design heating loads
|
| 421 |
|
| 422 |
Returns:
|
| 423 |
-
|
| 424 |
"""
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
"
|
| 434 |
-
"
|
| 435 |
-
"
|
| 436 |
-
"
|
| 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,
|
| 443 |
-
monthly_temps: Dict[str, float],
|
| 444 |
-
|
|
|
|
| 445 |
"""
|
| 446 |
-
Calculate monthly heating loads
|
| 447 |
|
| 448 |
Args:
|
| 449 |
-
|
| 450 |
-
monthly_temps: Dictionary with monthly
|
| 451 |
-
|
| 452 |
-
|
|
|
|
|
|
|
| 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,
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
continue
|
| 465 |
-
|
| 466 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 474 |
|
| 475 |
Args:
|
| 476 |
-
monthly_temps: Dictionary with monthly
|
| 477 |
-
base_temp: Base temperature for degree days
|
| 478 |
|
| 479 |
Returns:
|
| 480 |
-
|
| 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 |
-
|
| 490 |
-
|
| 491 |
|
| 492 |
-
return
|
| 493 |
|
| 494 |
def calculate_annual_heating_energy(self, monthly_loads: Dict[str, float],
|
| 495 |
-
|
| 496 |
"""
|
| 497 |
-
Calculate annual heating energy consumption
|
| 498 |
|
| 499 |
Args:
|
| 500 |
monthly_loads: Dictionary with monthly heating loads in W
|
| 501 |
-
|
| 502 |
|
| 503 |
Returns:
|
| 504 |
-
|
| 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 |
-
|
| 515 |
-
energy = load *
|
| 516 |
-
|
| 517 |
-
annual_energy += energy
|
| 518 |
|
| 519 |
-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|