Source code for ecoengine.objects.components.heating.WaterHeater

from __future__ import annotations

from typing import TYPE_CHECKING, Literal

from ecoengine.objects.components.heating.PerformanceMap import NominalPerformanceMap, PerformanceMap

if TYPE_CHECKING:
    from ecoengine.objects.components.heating.Controls import Controls
    from ecoengine.objects.components.storage.StorageTank import StorageTank


[docs] class WaterHeater: """ Represents a water heater. And owns a PerformanceMap, a control schedule, and a control map. Tracks its own active/inactive state. The Performance map can be used to simulate heat pump, gas fired, or electric resitance. Control schedule and map ------------------------ control_schedule : list[str] 24-element list (one entry per hour of the day). Each value is a string key into control_map, selecting which Controls object is active during that hour. Standard keys are ``"normal"``, ``"loadUp"``, and ``"shed"``. control_map : dict[str, Controls] Maps schedule keys to Controls objects. For a simple single-mode system use ``{"normal": controls}`` with ``control_schedule = ["normal"] * 24``. When the map contains only ``"normal"``, the system is not load shifting. When it also contains ``"loadUp"`` and/or ``"shed"`` keys, load-shift sizing and simulation logic is activated. Construction ------------ Use one of the factory class methods rather than calling __init__ directly: * WaterHeater.from_nominal_capacity(nominal_capacity_kbtuh, control_schedule, control_map) Creates a NominalPerformanceMap (constant-output placeholder). Use this during preliminary sizing before a real equipment model is selected. * WaterHeater.from_model_name(model_name, control_schedule, control_map) Loads a PerformanceMap from the equipment model registry by name. Use this when the specific HPWH model is known. """
[docs] def __init__( self, performance_map: PerformanceMap | None, control_schedule: list[str] | None, control_map: dict[str, Controls] | None, fuel_type: Literal["electric", "gas"] = "electric", ) -> None: """ Parameters ---------- performance_map : PerformanceMap | None Performance map for this HPWH model. If None, all capacity/power queries return None. Prefer the factory class methods. control_schedule : list[str] | None 24-element list mapping each hour to a key in control_map. None when no control logic is configured. control_map : dict[str, Controls] | None Maps schedule strings to Controls objects. None when no control logic is configured. fuel_type : "electric" | "gas" Fuel source for this heater. Default is "electric". """ self.performance_map = performance_map self.control_schedule = control_schedule self.control_map = control_map self.fuel_type = fuel_type self._active = False
# ------------------------------------------------------------------ # Factory constructors # ------------------------------------------------------------------
[docs] @classmethod def from_nominal_capacity( cls, nominal_capacity_kbtuh: float, control_schedule: list[str] | None, control_map: dict[str, Controls] | None, fuel_type: Literal["electric", "gas"] = "electric", ) -> WaterHeater: """ Create a WaterHeater with a constant-capacity placeholder performance map. The heater will report nominal_capacity_kbtuh for every OAT and water temperature query. Use this during preliminary sizing before a specific HPWH model has been selected. Parameters ---------- nominal_capacity_kbtuh : float Fixed heating output capacity [kBTU/hr]. control_schedule : list[str] | None 24-element list mapping each hour to a key in control_map. control_map : dict[str, Controls] | None Maps schedule strings to Controls objects. fuel_type : "electric" | "gas" Fuel source for this heater. Default is "electric". Returns ------- WaterHeater """ return cls( performance_map=NominalPerformanceMap(nominal_capacity_kbtuh), control_schedule=control_schedule, control_map=control_map, fuel_type=fuel_type, )
[docs] @classmethod def from_model_name( cls, model_name: str, control_schedule: list[str] | None, control_map: dict[str, Controls] | None, num_units: int = 1, design_inlet_temp_f: float = 50.0, nominal_capacity_kbtuh: float | None = None, fuel_type: Literal["electric", "gas"] = "electric", ) -> WaterHeater: """ Create a WaterHeater by loading its PerformanceMap from the equipment model registry using the model name string. Parameters ---------- model_name : str Equipment model identifier as it appears in the model registry (e.g. ``'Rheem_PROPH80_T2_RH380-30'``). control_schedule : list[str] | None 24-element list mapping each hour to a key in control_map. control_map : dict[str, Controls] | None Maps schedule strings to Controls objects. num_units : int Number of identical units deployed; outputs scale linearly. Default 1. design_inlet_temp_f : float Cold-water inlet temperature used when not supplied per-call. Default 50 °F. nominal_capacity_kbtuh : float | None Total system capacity for ER fallback sizing. Default None. fuel_type : "electric" | "gas" Fuel source for this heater. Default is "electric". Returns ------- WaterHeater """ return cls( performance_map=PerformanceMap.from_model_name( model_name, num_units=num_units, design_inlet_temp_f=design_inlet_temp_f, nominal_capacity_kbtuh=nominal_capacity_kbtuh, ), control_schedule=control_schedule, control_map=control_map, fuel_type=fuel_type, )
# ------------------------------------------------------------------ # State # ------------------------------------------------------------------
[docs] def is_active(self) -> bool: """Return True if this heater is currently running.""" return self._active
[docs] def turn_on(self) -> None: """Activate this heater.""" self._active = True
[docs] def turn_off(self) -> None: """Deactivate this heater.""" self._active = False
[docs] def is_load_shifting(self) -> bool: """Return True if this heater has load-shift modes configured.""" if self.control_map is None: return False return "loadUp" in self.control_map or "shed" in self.control_map
[docs] def get_controls_for_hour(self, hour_of_day: int) -> Controls | None: """ Return the Controls object active during the given hour. Parameters ---------- hour_of_day : int Hour of the day (0-23). Returns ------- Controls | None None if no control schedule/map is configured. """ if self.control_schedule is None or self.control_map is None: return None key = self.control_schedule[hour_of_day] return self.control_map.get(key)
# ------------------------------------------------------------------ # Performance queries # ------------------------------------------------------------------
[docs] def get_capacity_kbtuh( self, oat_f: float, outlet_temp_f: float, inlet_temp_f: float | None = None, ) -> float | None: """ Return heating output capacity [kBTU/hr] at current conditions. Parameters ---------- oat_f : float outlet_temp_f : float inlet_temp_f : float | None Cold-water inlet temperature. Falls back to the performance map's design_inlet_temp_f when not provided. Returns ------- float | None """ if self.performance_map is not None: return self.performance_map.get_capacity_kbtuh(oat_f, outlet_temp_f, inlet_temp_f) return None
[docs] def get_power_in_kw( self, oat_f: float, outlet_temp_f: float, inlet_temp_f: float | None = None, ) -> float | None: """ Return electrical power input [kW] at current conditions. Parameters ---------- oat_f : float outlet_temp_f : float inlet_temp_f : float | None Cold-water inlet temperature. Falls back to the performance map's design_inlet_temp_f when not provided. Returns ------- float | None """ if self.performance_map is not None: return self.performance_map.get_power_in_kw(oat_f, outlet_temp_f, inlet_temp_f) return None
[docs] def get_output_kbtuh( self, oat_f: float, outlet_temp_f: float, inlet_temp_f: float | None = None, ) -> float: """ Return actual heating output this timestep: capacity if active, 0 if not. Parameters ---------- oat_f : float outlet_temp_f : float inlet_temp_f : float | None Cold-water inlet temperature forwarded to the performance map. Returns ------- float """ if not self._active: return 0.0 cap = self.get_capacity_kbtuh(oat_f, outlet_temp_f, inlet_temp_f) return cap if cap is not None else 0.0
[docs] def get_outlet_temp_f(self, hour_of_day: int) -> float: """ Look up the active Outlet Temperature for Controls for the given hour Parameters ---------- hour_of_day : int Hour of the day (0-23), used to select the active Controls from the control schedule. Raises ------ ValueError If no Controls are configured for the given hour. """ controls = self.get_controls_for_hour(hour_of_day) if controls is None: raise ValueError(f"No Controls configured for hour {hour_of_day}.") return controls.outlet_temp_f
[docs] def update_state(self, storage_tank: StorageTank, hour_of_day: int) -> None: """ Look up the active Controls for the given hour, then update active/inactive state based on current tank condition. If the heater is currently ON, check should_turn_off(). If the heater is currently OFF, check should_turn_on(). Parameters ---------- storage_tank : StorageTank hour_of_day : int Hour of the day (0-23), used to select the active Controls from the control schedule. Raises ------ ValueError If no Controls are configured for the given hour. """ controls = self.get_controls_for_hour(hour_of_day) if controls is None: raise ValueError(f"No Controls configured for hour {hour_of_day}.") if self._active: if controls.should_turn_off(storage_tank): self.turn_off() else: if controls.should_turn_on(storage_tank): self.turn_on()