DHW Systems

All system classes share the base DHWSystem interface. Subclasses add recirc-loop or return-to-primary logic as needed.

Base class

class ecoengine.objects.dhwsystems.DHWSystem.DHWSystem(water_heaters: list[WaterHeater], storage_tank: StorageTank | None, supply_temp_f: float, storage_temp_f: float, max_daily_run_hr: float = 16.0, defrost_factor: float = 1.0)[source]

Bases: object

Base class for all domestic hot water system configurations.

Holds one or more WaterHeater objects, a StorageTank, and system-wide temperature setpoints. Subclasses implement configuration-specific sizing and simulation step logic.

Construction

Do not call __init__ directly. Use one of the two factory class methods:

  • DHWSystem.from_size(building, controls, …)

    Runs the sizing algorithm to find minimum required capacity and storage volume, then builds one WaterHeater holding all that capacity. The WaterHeater receives the provided Controls object; its on-sensor parameters drive the stratification factor used during sizing.

  • DHWSystem.from_components(storage_volume_storageT_gal, water_heaters, …)

    Builds the system from explicitly provided components.

__init__(water_heaters: list[WaterHeater], storage_tank: StorageTank | None, supply_temp_f: float, storage_temp_f: float, max_daily_run_hr: float = 16.0, defrost_factor: float = 1.0) None[source]
Parameters:
  • water_heaters (list[WaterHeater]) – One or more heater units in this system.

  • storage_tank (StorageTank | None) – Primary storage tank. None only during intermediate construction.

  • supply_temp_f (float) – DHW delivery temperature to building occupants [°F].

  • storage_temp_f (float) – Hot water storage setpoint temperature [°F].

  • max_daily_run_hr (float) – Maximum hours the heating system may run per day (1-24). Used to calculate the required generation rate during sizing.

  • defrost_factor (float) – Fraction of rated capacity available after accounting for defrost cycles (0-1). Typically 1.0 for non-frosting conditions.

classmethod from_size(building: Building, supply_temp_f: float, storage_temp_f: float, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0, control_schedule: list[str] | None = None, control_map: dict[str, Controls] | None = None, strat_slope: float = 2.8, load_shift_fract_total_vol: float = 1.0) DHWSystem[source]

Size the system for the given building, then build it.

Runs the sizing algorithm to determine the minimum required heating capacity and storage volume. Creates one WaterHeater backed by a NominalPerformanceMap (constant-output placeholder) holding all of the required capacity, and a StorageTank sized to the minimum required volume.

The control_map is used during sizing: each Controls in the map contributes a stratification factor, and the minimum (worst-case) value drives storage volume. The same schedule and map are assigned to the created WaterHeater for use at simulation time.

Parameters:
  • building (Building) – The building whose DHW load drives the sizing calculation. Must have a ClimateZone (real or design-condition) so that design inlet water temperature is available.

  • supply_temp_f (float) – DHW delivery temperature [°F].

  • storage_temp_f (float) – Hot water storage setpoint [°F].

  • max_daily_run_hr (float) – Maximum hours the system may run per day. Lower values drive higher capacity requirements and smaller storage requirements.

  • defrost_factor (float) – Fraction of rated capacity available after defrost (0-1).

  • control_schedule (list[str] | None) – 24-element list mapping each hour of the day to a key in control_map. Standard keys are "normal", "loadUp", and "shed". Assigned to the created WaterHeater. None when no load-shifting or multi-mode control is required.

  • control_map (dict[str, Controls] | None) – Maps control_schedule integers to Controls objects. All Controls in the map are considered during sizing (worst-case strat factor). None when no control logic is configured.

  • strat_slope (float) – Temperature gradient through the tank’s transition zone [°F per percentage-point of tank height]. Stored on the StorageTank and used during stratification factor calculations. Subclasses that model different tank geometries should override this via their own from_size() implementation.

Return type:

DHWSystem

classmethod from_components(storage_volume_storageT_gal: float, water_heaters: list[WaterHeater], supply_temp_f: float, storage_temp_f: float, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0, strat_slope: float = 2.8) DHWSystem[source]

Build the system from explicitly provided storage volume and heaters.

Use this path when storage volume and heating capacity are already known (e.g. from a previous sizing run, from equipment specs, or from user input) rather than being calculated from a building load. Each WaterHeater in the list should already carry its own Controls object.

Parameters:
  • storage_volume_storageT_gal (float) – Physical tank volume at storage temperature [gallons].

  • water_heaters (list[WaterHeater]) – Pre-built list of WaterHeater objects for this system. There is no restriction on list length — one heater is the common case, but staged systems may have more.

  • supply_temp_f (float) – DHW delivery temperature [°F].

  • storage_temp_f (float) – Hot water storage setpoint [°F].

  • max_daily_run_hr (float) – Maximum hours the system may run per day.

  • defrost_factor (float) – Fraction of rated capacity available after defrost (0-1).

  • strat_slope (float) – Temperature gradient through the tank’s transition zone [°F per percentage-point of tank height]. Stored on the StorageTank for use during simulation.

Return type:

DHWSystem

size(building: Building, control_schedule: list[str] | None = None, control_map: dict[str, Controls] | None = None, strat_slope: float = 2.8, load_shift_fract_total_vol: float = 1.0) None[source]

Compute the minimum heating capacity and storage volume for this system in the given building. Stores results internally; retrieve them with get_minimum_capacity_kbtuh() and get_minimum_storage_storageT_gal().

Sizing is always performed on the 24-hour daily load shape. If the building is currently set to the annual shape, this method temporarily switches it back, sizes, then restores the annual shape.

If the control_map contains a "shed" key, load-shift sizing is run in addition to normal sizing. The final capacity and storage volume are the maximum of the two paths.

Parameters:
  • building (Building)

  • control_schedule (list[str] | None) – 24-element schedule of control keys ("normal", "loadUp", "shed"). Required for load-shift sizing; ignored otherwise.

  • control_map (dict[str, Controls] | None) – All Controls objects that may be active during operation. Each Controls contributes a stratification factor; the minimum value (worst case) drives storage volume sizing. Each Controls is also checked for short-cycling risk. None uses sizing defaults.

  • strat_slope (float) – Temperature gradient through the tank’s transition zone [°F per percentage-point of tank height]. Should match the value that will be set on the StorageTank.

  • load_shift_fract_total_vol (float) – Demand scaling factor for load-shift sizing (0–1). Derived from load_shift_percent via _load_shift_fract_total_vol(). Default 1.0 (no scaling — size for 100% of days). Only applied to the LS sizing path; normal sizing is always against the full daily demand.

Raises:

ValueError – If the building has no design inlet water temperature available (no ClimateZone was provided at construction).

get_minimum_capacity_kbtuh() float[source]

Return the minimum required heating capacity [kBTU/hr] from sizing.

Raises:

RuntimeError – If size() has not been called yet.

get_minimum_storage_storageT_gal() float[source]

Return the minimum required storage volume at storage temperature [gallons] from sizing.

Raises:

RuntimeError – If size() has not been called yet.

get_sizing_curve(building: Building, strat_slope: float = 2.8, step: float = 0.25) dict[source]

Compute the primary sizing curve — capacity vs. storage for varying run hours.

Sweeps max_daily_run_hr from 24 down to the physical minimum (where the hourly generation rate equals peak hourly demand), sizing without load-shift at each point. The resulting curve shows the full capacity-vs-storage tradeoff available to the designer.

Because this calls the same internal sizing methods as size(), subclass overrides (e.g. RTPSystem adding recirc capacity) are reflected automatically.

The sweep is split into two segments:

  • [24, max_daily_run_hr) — the “over-designed” region to the left of the recommended point.

  • [max_daily_run_hr, min_run_hr) — the recommended point and everything to the right.

recommended_index is the length of the first segment, i.e. the index in the returned lists that corresponds to max_daily_run_hr.

Parameters:
  • building (Building) – The building whose DHW load drives the sizing.

  • strat_slope (float) – Temperature gradient [°F per %-height] for the stratification factor calculation. Should match the value used for sizing. Default 2.8.

  • step (float) – Run-hour step size for the sweep. Default 0.25 hr.

Returns:

"heat_hours" : list[float] — run hrs at each point "capacity_kbtuh" : list[float] — capacity [kBTU/hr] "storage_storageT_gal" : list[float] — storage [gal at storageT] "recommended_index" : int — index of max_daily_run_hr

Return type:

dict

get_ls_sizing_curve(building: Building, control_schedule: list[str], control_map: dict[str, Controls], strat_slope: float = 2.8, load_shift_percent: float = 1.0) dict[source]

Compute the load-shift sizing curve — capacity and storage as a function of demand-coverage percentile.

Sweeps load_shift_percent from 0.25 to 1.00 in 0.01 steps (76 points). At each step the demand fraction fract_total_vol is derived from the normal-distribution model, and both normal and LS sizing are run; the maximum of each is stored. This matches exactly what size() does at a given load_shift_fract_total_vol.

The x-axis of the resulting curve is the coverage percentile: at 1.00 the system is sized to handle the most demanding day (largest storage / capacity); at 0.25 it accepts that 75 % of days will breach the shed window (smallest system).

Parameters:
  • building (Building)

  • control_schedule (list[str]) – 24-element schedule with "normal", "loadUp", "shed" keys.

  • control_map (dict[str, Controls]) – Must include at least "normal" and "shed" entries.

  • strat_slope (float) – Temperature gradient [°F per %-height] for stratification factor. Default 2.8.

  • load_shift_percent (float) – The system’s configured coverage percentile (0.25–1.0). Used only to locate recommended_index in the returned arrays. Default 1.0.

Returns:

"load_shift_percent" : list[float] — 0.25 → 1.00 "capacity_kbtuh" : list[float] "storage_storageT_gal" : list[float] "recommended_index" : int

Return type:

dict

Raises:

ValueError – If control_map has no "shed" key (load-shift not configured).

plot_sizing_curve(building: Building, control_schedule: list[str] | None = None, control_map: dict[str, Controls] | None = None, load_shift_percent: float = 1.0, strat_slope: float = 2.8, title: str = 'Primary Sizing Curve', filepath: str | None = None) plotly.graph_objects.Figure[source]

Return a Plotly figure of the primary sizing curve with an interactive slider that moves a diamond marker along the curve.

Normal sizing (no load-shift):

  • X-axis: primary tank volume [gal at storage temperature]

  • Y-axis: heating capacity [kBTU/hr]

  • Slider label: storage volume and run hours per day

Load-shift sizing (control_map contains a "shed" key):

  • X-axis: load-shift coverage percentile [%]

  • Y-axis: primary tank volume [gal at storage temperature]

  • Slider label: % load-shift days covered and storage volume

In both cases the recommended point (at max_daily_run_hr or the configured load_shift_percent) is highlighted with a blue diamond at page load.

Parameters:
  • building (Building)

  • control_schedule (list[str] | None) – Required for load-shift plots; ignored otherwise.

  • control_map (dict[str, Controls] | None) – If it contains a "shed" key the load-shift curve is produced; otherwise the normal sizing curve is used.

  • load_shift_percent (float) – Configured load-shift coverage percentile (0.25–1.0). Only used when producing the load-shift curve to position the recommended marker. Default 1.0.

  • strat_slope (float) – Temperature gradient [°F per %-height]. Default 2.8.

  • title (str) – Figure title. Default "Primary Sizing Curve".

  • filepath (str | None) – If provided, write the figure to this path as a self-contained HTML file. The figure is also returned regardless.

Return type:

plotly.graph_objects.Figure

Raises:

ImportError – If plotly is not installed.

simulate_step(building: Building, timestep_interval: int, interval_min: int = 1, mode: str = 'normal') dict[source]

Execute one simulation timestep: query controls, apply heating, draw DHW from tank. Returns per-step metrics.

Order of operations

  1. Query Building for demand, OAT, and inlet water temperature.

  2. Determine outlet_temp_f for the current hour from active Controls.

  3. Update each WaterHeater’s on/off state via its Controls.

  4. Sum heating output from all active heaters; apply to storage tank.

  5. Draw DHW from tank to meet demand.

  6. Return per-step metrics dict.

param building:

type building:

Building

param timestep_interval:

Current simulation interval index from the start of the simulation.

type timestep_interval:

int

param interval_min:

Length of each interval in minutes.

type interval_min:

int

param mode:

Ignored — operating mode is determined automatically by each WaterHeater’s control_schedule for the current hour.

type mode:

str

returns:

Keys: ‘demand_supplyT_gal’, ‘usable_volume_supplyT_gal’, ‘heater_output_kbtuh’, ‘heater_power_in_kw’, ‘oat_f’, ‘inlet_water_temp_f’, ‘tank_temps_f’.

rtype:

dict

check_for_outage(demand_supplyT_gal: float) bool[source]

Return True if the storage tank cannot meet the given demand.

Checks whether usable volume at supply temperature has reached zero.

Parameters:

demand_supplyT_gal (float) – Hot water demand at supply temperature [gallons].

Return type:

bool

No-recirc systems

class ecoengine.objects.dhwsystems.InstantWHSystem.InstantWHSystem(supply_temp_f: float, storage_temp_f: float, defrost_factor: float = 1.0)[source]

Bases: DHWSystem

Instantaneous (tankless) water heater system.

No storage tank — the heater meets demand in real time each timestep. Sizing sets the minimum capacity needed to serve peak instantaneous demand; storage volume is always zero. Load shifting is not supported because there is no tank to pre-charge.

__init__(supply_temp_f: float, storage_temp_f: float, defrost_factor: float = 1.0)[source]
Parameters:
  • water_heaters (list[WaterHeater]) – One or more heater units in this system.

  • storage_tank (StorageTank | None) – Primary storage tank. None only during intermediate construction.

  • supply_temp_f (float) – DHW delivery temperature to building occupants [°F].

  • storage_temp_f (float) – Hot water storage setpoint temperature [°F].

  • max_daily_run_hr (float) – Maximum hours the heating system may run per day (1-24). Used to calculate the required generation rate during sizing.

  • defrost_factor (float) – Fraction of rated capacity available after accounting for defrost cycles (0-1). Typically 1.0 for non-frosting conditions.

classmethod from_size(building, supply_temp_f: float, storage_temp_f: float, defrost_factor: float = 1.0) InstantWHSystem[source]

Size the system for the given building, then return it.

Parameters:
size(building, **kwargs) None[source]

Set minimum capacity to serve peak instantaneous demand.

Capacity is the kBTU/hr required to heat the peak one-minute demand volume from design inlet temperature to supply temperature. Storage volume is always zero.

Parameters:
  • building (Building) – Must have a ClimateZone so that design inlet temperature is available.

  • **kwargs – Accepted but ignored (load-shift params, strat_slope, etc.).

simulate_step(building, timestep_interval: int, interval_min: int = 1, mode: str = 'normal') dict[source]

Serve demand instantly each timestep — no tank draw or charge cycle.

Capacity is computed from this timestep’s actual demand and inlet temperature, so it tracks demand exactly. Usable volume is always zero (no storage).

class ecoengine.objects.dhwsystems.MPNoRecircSystem.MPNoRecircSystem(water_heaters, storage_tank, supply_temp_f, storage_temp_f)[source]

Bases: DHWSystem

Multi-pass system with no recirculation loop. Water passes through the heat pump multiple times to reach storage temperature.

__init__(water_heaters, storage_tank, supply_temp_f, storage_temp_f)[source]
Parameters:
size(building)[source]

Size a multi-pass no-recirc system.

Parameters:

building (Building)

simulate_step(building, timestep_min, mode='normal')[source]

Run one timestep for a multi-pass no-recirc system.

Recirculation systems

class ecoengine.objects.dhwsystems.recirc_systems.RecircSystem.RecircSystem(water_heaters, storage_tank, supply_temp_f, storage_temp_f, return_temp_f, return_flow_gpm, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0)[source]

Bases: DHWSystem

Base class for DHW systems that include a recirculation loop.

Not intended for direct use — exists to share recirc-loop attributes and helpers between SwingSystem and ParallelLoopSystem. Systems where recirc return water enters the primary tank directly are handled separately in the rtp_systems section.

__init__(water_heaters, storage_tank, supply_temp_f, storage_temp_f, return_temp_f, return_flow_gpm, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0)[source]
Parameters:
  • water_heaters (list[WaterHeater])

  • storage_tank (StorageTank)

  • supply_temp_f (float) – DHW delivery temperature [°F].

  • storage_temp_f (float) – Storage setpoint [°F].

  • return_temp_f (float) – Temperature of water returning from the recirculation loop [°F].

  • return_flow_gpm (float) – Recirculation loop flow rate [GPM] (assumed constant).

  • max_daily_run_hr (float) – Maximum hours the primary heating system may run per day.

  • defrost_factor (float) – Fraction of rated capacity available after defrost cycles (0–1).

get_recirc_loss_kbtuh() float[source]

Return the steady-state recirculation heat loss rate [kBTU/hr].

Computed from loop flow rate and the temperature difference between supply and return:

loss = flow_gpm × (supply_T − return_T) × ρCp × 60 min/hr / 1000

Return type:

float

get_daily_recirc_loss_kbtu() float[source]

Return total daily recirculation heat loss [kBTU].

Return type:

float

simulate_step(building, timestep_interval, interval_min=1, mode='normal')[source]

Delegate to DHWSystem.simulate_step() via super().

class ecoengine.objects.dhwsystems.recirc_systems.SwingSystem.SwingSystem(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, tm_safety_factor: float = 1.2, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0)[source]

Bases: RecircSystem

Swing tank system: a single fully-mixed tank in SERIES with the primary storage acts as both the recirculation temperature-maintenance (TM) volume and a draw-through buffer.

All DHW demand passes through the swing tank before delivery, so the primary storage only needs to supply the deficit not met by the swing tank’s thermal mass. This reduces effective primary demand (captured by _eff_mix_fraction) but requires the TM element to maintain the swing tank at or above supply temperature.

Sizing order (overrides DHWSystem)

  1. Size TM (table lookup from recirc loss rate).

  2. Compute running volume via swing-tank simulation — yields _eff_mix_fraction as a side-effect.

  3. Compute primary capacity using _eff_mix_fraction and storage temp.

Construction

Use the factory classmethod:

system = SwingSystem.from_size(
    building        = building,
    supply_temp_f   = 120.0,
    storage_temp_f  = 150.0,
    return_temp_f   = 110.0,
    return_flow_gpm = 3.0,
    tm_safety_factor = 1.2,
)
__init__(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, tm_safety_factor: float = 1.2, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0)[source]
Parameters:
  • water_heaters (list[WaterHeater])

  • storage_tank (StorageTank)

  • supply_temp_f (float) – DHW delivery temperature [°F].

  • storage_temp_f (float) – Storage setpoint [°F].

  • return_temp_f (float) – Temperature of water returning from the recirculation loop [°F].

  • return_flow_gpm (float) – Recirculation loop flow rate [GPM] (assumed constant).

  • max_daily_run_hr (float) – Maximum hours the primary heating system may run per day.

  • defrost_factor (float) – Fraction of rated capacity available after defrost cycles (0–1).

classmethod from_size(building: Building, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, tm_safety_factor: float = 1.2, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0, control_schedule: list[str] | None = None, control_map: dict[str, Controls] | None = None, strat_slope: float = 2.8, load_shift_fract_total_vol: float = 1.0) SwingSystem[source]

Size the system for the given building, then build it.

Runs the sizing algorithm to determine the minimum required heating capacity and storage volume. Creates one WaterHeater backed by a NominalPerformanceMap (constant-output placeholder) holding all of the required capacity, and a StorageTank sized to the minimum required volume.

The control_map is used during sizing: each Controls in the map contributes a stratification factor, and the minimum (worst-case) value drives storage volume. The same schedule and map are assigned to the created WaterHeater for use at simulation time.

Parameters:
  • building (Building) – The building whose DHW load drives the sizing calculation. Must have a ClimateZone (real or design-condition) so that design inlet water temperature is available.

  • supply_temp_f (float) – DHW delivery temperature [°F].

  • storage_temp_f (float) – Hot water storage setpoint [°F].

  • max_daily_run_hr (float) – Maximum hours the system may run per day. Lower values drive higher capacity requirements and smaller storage requirements.

  • defrost_factor (float) – Fraction of rated capacity available after defrost (0-1).

  • control_schedule (list[str] | None) – 24-element list mapping each hour of the day to a key in control_map. Standard keys are "normal", "loadUp", and "shed". Assigned to the created WaterHeater. None when no load-shifting or multi-mode control is required.

  • control_map (dict[str, Controls] | None) – Maps control_schedule integers to Controls objects. All Controls in the map are considered during sizing (worst-case strat factor). None when no control logic is configured.

  • strat_slope (float) – Temperature gradient through the tank’s transition zone [°F per percentage-point of tank height]. Stored on the StorageTank and used during stratification factor calculations. Subclasses that model different tank geometries should override this via their own from_size() implementation.

Return type:

DHWSystem

size(building: Building, control_schedule: list[str] | None = None, control_map: dict[str, Controls] | None = None, strat_slope: float = 2.8, load_shift_fract_total_vol: float = 1.0) None[source]

Size the swing tank system.

Overrides DHWSystem.size() to enforce the correct order: volume first (which yields _eff_mix_fraction), then capacity. The SwingTank capacity formula uses storage temperature, not supply temperature, matching the original codebase’s _primaryHeatHrs2kBTUHR.

get_sizing_curve(building: Building, strat_slope: float = 2.8, step: float = 0.25) dict[source]

Override to enforce volume-before-capacity order required by SwingSystem.

_calc_required_capacity() uses _eff_mix_fraction, which is set as a side-effect of _calc_running_volume_supplyT_gal(). The base class loop calls capacity first, which leaves _eff_mix_fraction at its previous value. This override swaps the order.

property tm_off_temp_f: float

Temperature at which the TM element shuts off (the swing tank’s fully-charged setpoint).

Derived from tm_water_heater’s Controls rather than stored separately. Falls back to supply_temp_f + _ELEMENT_DEADBAND_F before from_size() has been called (e.g. during sizing).

The Simulator reads this attribute to initialize the swing tank at the correct starting temperature.

get_minimum_tm_volume_gal() float[source]
get_minimum_tm_ca_volume_gal() float[source]

Return the minimum CA-approved swing tank volume [gal].

Snaps the required TM volume to the nearest (next-largest) entry in the CA commercially-available tank size table [80, 96, 168, 288, 480]. Capped at 480 gal when the standard sizing table drives a larger result.

get_minimum_tm_capacity_kbtuh() float[source]
simulate_step(building: Building, timestep_interval: int, interval_min: int = 1, mode: str = 'normal') dict[source]

Execute one simulation timestep for the swing tank system.

The swing tank sits in series between the primary storage and the building. All DHW demand passes through the swing tank; the primary storage only needs to supply what the swing tank cannot absorb from its own thermal mass.

Order of operations

  1. Query building for demand, OAT, and inlet water temperature.

  2. Update primary WaterHeater states; apply heat to primary StratifiedTank.

  3. Compute physical gallons that must flow from primary to swing tank this timestep based on current swing tank temperature and demand.

  4. Obtain the average temperature of that draw block from the primary tank’s stratification profile (handles partial hot-zone depletion).

  5. Mix primary inflow into swing tank (tm_storage_tank.mix_primary_inflow).

  6. Apply recirculation heat loss to swing tank (add_recirc_return).

  7. Update TM WaterHeater state; apply TM heat to swing tank.

  8. Draw the computed physical volume from the primary storage tank.

  9. Determine usable volume: 0 if swing tank is below supply temperature, otherwise derived from the primary tank’s stratification profile.

  10. Merge primary and TM energy outputs and return per-step metrics.

param building:

type building:

Building

param timestep_interval:

type timestep_interval:

int

param interval_min:

type interval_min:

int

param mode:

Ignored — operating mode is determined by each heater’s control schedule.

type mode:

str

returns:

Same keys as DHWSystem.simulate_step(). heater_output_kbtuh and heater_power_in_kw include both primary and TM heater contributions.

rtype:

dict

class ecoengine.objects.dhwsystems.recirc_systems.SwingERTrdOffSystem.SwingERTrdOffSystem(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, tm_safety_factor: float = 1.2, er_safety_factor: float = 1.0, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0)[source]

Bases: SwingSystem

Swing tank system with an electric resistance (ER) element sized to compensate when the primary HPWH cannot maintain the swing tank at supply temperature during peak demand events.

This class is identical to SwingSystem in all simulation behaviour — simulate_step() is fully inherited without override. The only runtime difference is that the tm_water_heater carries a higher nominal_capacity_kbtuh: the base temperature-maintenance capacity (sized to handle recirc losses) plus the ER addition (sized to close the worst-case single-minute temperature deficit found by the sizing simulation).

Sizing flow

  1. SwingSystem.size() — TM volume, running volume, primary capacity.

  2. _size_er_element() — 2-day sizing simulation (primary empty + full start); finds max swing-tank temperature deficit; computes ER capacity.

  3. _minimum_tm_capacity_kbtuh is bumped by the ER result before from_size() builds the tm_water_heater.

Construction

Two factory classmethods are provided:

from_size — full sizing from scratch:

system = SwingERTrdOffSystem.from_size(
    building        = building,
    supply_temp_f   = 120.0,
    storage_temp_f  = 150.0,
    return_temp_f   = 110.0,
    return_flow_gpm = 3.0,
    er_safety_factor = 1.0,
)

from_components — sizes only the ER element given pre-built (possibly undersized) primary components:

system = SwingERTrdOffSystem.from_components(
    water_heaters           = swing.water_heaters,
    storage_tank            = swing.storage_tank,
    tm_storage_tank         = swing.tm_storage_tank,
    initial_tm_capacity_kbtuh = swing.tm_water_heater.performance_map.nominal_capacity_kbtuh,
    building                = building,
    supply_temp_f           = 120.0,
    storage_temp_f          = 150.0,
    return_temp_f           = 110.0,
    return_flow_gpm         = 3.0,
)
__init__(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, tm_safety_factor: float = 1.2, er_safety_factor: float = 1.0, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0)[source]
Parameters:
  • water_heaters (list[WaterHeater])

  • storage_tank (StorageTank)

  • supply_temp_f (float) – DHW delivery temperature [°F].

  • storage_temp_f (float) – Storage setpoint [°F].

  • return_temp_f (float) – Temperature of water returning from the recirculation loop [°F].

  • return_flow_gpm (float) – Recirculation loop flow rate [GPM] (assumed constant).

  • max_daily_run_hr (float) – Maximum hours the primary heating system may run per day.

  • defrost_factor (float) – Fraction of rated capacity available after defrost cycles (0–1).

classmethod from_components(water_heaters: list[WaterHeater], storage_tank, tm_storage_tank: MixedStorageTank, initial_tm_capacity_kbtuh: float, building: Building, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, er_safety_factor: float = 1.0, control_schedule: list[str] | None = None, control_map: dict[str, Controls] | None = None) SwingERTrdOffSystem[source]

Build a SwingERTrdOffSystem from pre-sized components by sizing only the ER element of the swing tank.

Use this when you already have a SwingSystem whose primary water heaters and/or primary storage tank have been intentionally undersized, and you want to compensate by adding an ER element to the swing tank. The primary components are accepted as-is; only the swing tank TM element capacity is recomputed.

Parameters:
  • water_heaters (list[WaterHeater]) – Pre-built primary water heaters (already at their final, possibly reduced, capacity).

  • storage_tank (StorageTank) – Pre-built primary storage tank.

  • tm_storage_tank (MixedStorageTank) – Pre-built swing tank (its total_volume_gal drives ER sizing).

  • initial_tm_capacity_kbtuh (float) – Starting TM element capacity [kBTU/hr] before any ER addition. Typically taken from the original SwingSystem.tm_water_heater.

  • building (Building) – Building model used for the ER sizing simulation.

  • supply_temp_f (float) – Hot-water supply temperature [°F].

  • storage_temp_f (float) – Primary storage temperature [°F].

  • return_temp_f (float) – Recirculation loop return temperature [°F].

  • return_flow_gpm (float) – Recirculation loop flow rate [GPM].

  • er_safety_factor (float) – Multiplier applied to the raw ER capacity result (default 1.0).

  • control_schedule (list[str] | None) – 24-element hour-of-day schedule for the primary heaters’ controls.

  • control_map (dict[str, Controls] | None) – Controls map for the primary heaters.

Returns:

Fully constructed system with tm_water_heater sized to include the ER addition. Primary components are the objects passed in.

Return type:

SwingERTrdOffSystem

classmethod from_size(building: Building, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, tm_safety_factor: float = 1.2, er_safety_factor: float = 1.0, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0, control_schedule: list[str] | None = None, control_map: dict[str, Controls] | None = None, strat_slope: float = 2.8, load_shift_fract_total_vol: float = 1.0) SwingERTrdOffSystem[source]

Size the system for the given building, then build it.

Runs the sizing algorithm to determine the minimum required heating capacity and storage volume. Creates one WaterHeater backed by a NominalPerformanceMap (constant-output placeholder) holding all of the required capacity, and a StorageTank sized to the minimum required volume.

The control_map is used during sizing: each Controls in the map contributes a stratification factor, and the minimum (worst-case) value drives storage volume. The same schedule and map are assigned to the created WaterHeater for use at simulation time.

Parameters:
  • building (Building) – The building whose DHW load drives the sizing calculation. Must have a ClimateZone (real or design-condition) so that design inlet water temperature is available.

  • supply_temp_f (float) – DHW delivery temperature [°F].

  • storage_temp_f (float) – Hot water storage setpoint [°F].

  • max_daily_run_hr (float) – Maximum hours the system may run per day. Lower values drive higher capacity requirements and smaller storage requirements.

  • defrost_factor (float) – Fraction of rated capacity available after defrost (0-1).

  • control_schedule (list[str] | None) – 24-element list mapping each hour of the day to a key in control_map. Standard keys are "normal", "loadUp", and "shed". Assigned to the created WaterHeater. None when no load-shifting or multi-mode control is required.

  • control_map (dict[str, Controls] | None) – Maps control_schedule integers to Controls objects. All Controls in the map are considered during sizing (worst-case strat factor). None when no control logic is configured.

  • strat_slope (float) – Temperature gradient through the tank’s transition zone [°F per percentage-point of tank height]. Stored on the StorageTank and used during stratification factor calculations. Subclasses that model different tank geometries should override this via their own from_size() implementation.

Return type:

DHWSystem

size(building: Building, control_schedule: list[str] | None = None, control_map: dict[str, Controls] | None = None, strat_slope: float = 2.8, load_shift_fract_total_vol: float = 1.0) None[source]

Size the system: base SwingSystem sizing followed by ER element sizing.

get_er_capacity_kbtuh() float[source]

Return the additional ER capacity added to the TM element [kBTU/hr].

get_er_capacity_kw() float[source]

Return the additional ER capacity added to the TM element [kW].

get_er_sized_points(building: Building, additional_er_safety: float = 1.0) tuple[list[float], list[float], int][source]

Compute ER element size vs. percent-of-peak-load-covered trade-off points.

Iterates building magnitude from 120% down to the fraction where no ER is needed, sizing the ER element at each step. At 100% the already- computed sizing result is used directly.

Parameters:
  • building (Building) – Building used during sizing. magnitude is temporarily mutated and restored before the method returns.

  • additional_er_safety (float) – Safety factor applied to ER sizing for all fractions except 100% (which uses the value from the original size() call).

Returns:

  • er_cap_kw (list[float]) – Total TM element capacity (base + ER) [kW] at each fraction, ordered from lowest to highest coverage.

  • fract_covered (list[float]) – Percent-of-building values (e.g. 60.0, 70.0, … 120.0), same order.

  • startind (int) – Index in the returned lists corresponding to 100% coverage (the actual sized system).

get_er_sizing_curve(building: Building, additional_er_safety: float = 1.0) go.Figure[source]

Return a Plotly figure showing the ER element size vs. percent coverage trade-off curve, with a slider to select the operating point.

The curve traces total TM element capacity (base + ER) [kW] against the fraction of peak building load covered by the primary HPWH (%). A diamond marker and slider indicate the 100%-coverage (actual sized) point.

Parameters:
  • building (Building)

  • additional_er_safety (float) – Safety factor for the re-sizing iterations (default 1.0).

Return type:

plotly.graph_objects.Figure

class ecoengine.objects.dhwsystems.recirc_systems.ParallelLoopSystem.ParallelLoopSystem(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, tm_on_temp_f: float, tm_off_temp_f: float, tm_off_time_hr: float = 0.5, tm_safety_factor: float = 1.2, tm_storage_tank=None, tm_water_heater=None, num_tm_heaters: int = 1, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0)[source]

Bases: RecircSystem

Parallel loop system: a separate temperature-maintenance (TM) tank sits in parallel with the primary storage tank.

  • Primary tank — stratified StorageTank, sized for DHW demand only.

  • TM tank — MixedStorageTank, sized to absorb recirc loop losses.

The two tanks operate completely independently each timestep.

Construction

Use the factory classmethod rather than calling __init__ directly:

system = ParallelLoopSystem.from_size(
    building        = building,
    supply_temp_f   = 120.0,
    storage_temp_f  = 150.0,
    return_temp_f   = 110.0,
    return_flow_gpm = 3.0,
    tm_on_temp_f    = 115.0,
    tm_off_temp_f   = 120.0,
)
__init__(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, tm_on_temp_f: float, tm_off_temp_f: float, tm_off_time_hr: float = 0.5, tm_safety_factor: float = 1.2, tm_storage_tank=None, tm_water_heater=None, num_tm_heaters: int = 1, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0)[source]
Parameters:
  • water_heaters (list[WaterHeater]) – Primary system heaters.

  • storage_tank (StorageTank | None) – Primary storage tank (None during intermediate construction).

  • supply_temp_f (float) – DHW delivery temperature [°F].

  • storage_temp_f (float) – Primary hot water storage setpoint [°F].

  • return_temp_f (float) – Temperature of water returning from the recirculation loop [°F].

  • return_flow_gpm (float) – Recirculation loop flow rate [GPM].

  • tm_on_temp_f (float) – TM tank turn-on temperature — element fires when tank drops to this [°F].

  • tm_off_temp_f (float) – TM tank turn-off temperature — element shuts off when tank reaches this [°F].

  • tm_off_time_hr (float) – Maximum allowed off-cycle duration for the TM heater [hr]. The TM tank volume is sized so the tank cools from tm_off_temp_f to tm_on_temp_f in exactly this much time under recirc loss alone. Must be > 0 and <= 1.0. Default 0.5.

  • tm_safety_factor (float) – Multiplier applied to the recirc loss rate when sizing TM capacity. Must be > 1.0. Default 1.2.

  • tm_storage_tank (MixedStorageTank | None) – TM storage tank (populated by from_size() or size()).

  • tm_water_heater (WaterHeater | None) – TM heater representing a single unit (populated by from_size() or size()).

  • num_tm_heaters (int) – Number of identical TM heater units. Output from tm_water_heater is multiplied by this before being applied to tank heating and recorded. Default 1.

  • max_daily_run_hr (float) – Maximum hours the primary heating system may run per day.

  • defrost_factor (float) – Fraction of rated capacity available after defrost (0–1).

classmethod from_size(building, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, tm_on_temp_f: float, tm_off_temp_f: float, tm_off_time_hr: float = 0.5, tm_safety_factor: float = 1.2, num_tm_heaters: int = 1, max_daily_run_hr: float = 24.0, defrost_factor: float = 1.0, control_schedule=None, control_map=None, strat_slope: float = 2.8, load_shift_fract_total_vol: float = 1.0) ParallelLoopSystem[source]

Size the system for the given building, then build it.

Parameters:
  • building (Building)

  • supply_temp_f (float)

  • storage_temp_f (float)

  • return_temp_f (float) – Temperature of the recirculation loop return water [°F].

  • return_flow_gpm (float) – Recirculation loop flow rate [GPM].

  • tm_on_temp_f (float) – TM element turn-on temperature [°F].

  • tm_off_temp_f (float) – TM element turn-off temperature [°F].

  • tm_off_time_hr (float) – Max TM heater off-cycle duration [hr]. Default 0.5.

  • tm_safety_factor (float) – TM capacity safety multiplier (must be > 1.0). Default 1.2.

  • num_tm_heaters (int) – Number of identical TM heater units. The total sized TM capacity is divided by this to get per-unit capacity; simulate_step scales back by num_tm_heaters. Default 1.

  • max_daily_run_hr (float) – Max primary heater run time per day. Default 24.0.

  • defrost_factor (float) – Primary heater defrost derating (0–1). Default 1.0.

  • control_schedule (list[str] | None)

  • control_map (dict[str, Controls] | None)

  • strat_slope (float)

Return type:

ParallelLoopSystem

size(building, control_schedule=None, control_map=None, strat_slope: float = 2.8, load_shift_fract_total_vol: float = 1.0) None[source]

Size both the primary DHW system and the TM system.

Primary sizing uses the standard DHWSystem max-deficit algorithm. TM sizing uses recirc loss rate, off-time, and safety factor.

size_tm_system() None[source]

Size the temperature-maintenance tank and heater from recirc loss parameters.

Formulas

TM volume:

The tank must hold enough thermal mass that during a full off-cycle (tm_off_time_hr) it cools from tm_off_temp_f to tm_on_temp_f while losing heat at the steady recirc loss rate:

TMVol_gal = (recirc_loss_btuhr / rhoCp)
  • (tm_off_time_hr / (tm_off_temp_f − tm_on_temp_f))

TM capacity:

Must outpace the recirc loss by the safety factor:

TMCap_kbtuh = safety_factor × recirc_loss_kbtuh

get_minimum_tm_volume_gal() float[source]
get_minimum_tm_capacity_kbtuh() float[source]
simulate_step(building: Building, timestep_interval: int, interval_min: int = 1, mode: str = 'normal') dict[source]

Execute one simulation timestep for the parallel loop system.

Delegates the entire primary system step to DHWSystem.simulate_step() via super(), then layers on the TM system (recirc loss → heater response) and merges the energy outputs into the returned dict.

Primary system (via super())

  1. Query building for demand, OAT, and inlet water temperature.

  2. Update primary WaterHeater states; apply heat to StratifiedTank.

  3. Draw DHW demand; measure usable volume and tank temperature profile.

TM system (parallel, independent)

  1. Apply recirc loop heat loss to MixedStorageTank via add_recirc_return().

  2. Update TM WaterHeater state; heat TM tank if active.

The recirc loop connects only to the TM tank — the primary StratifiedTank receives no recirc return flow.

param building:

type building:

Building

param timestep_interval:

type timestep_interval:

int

param interval_min:

type interval_min:

int

param mode:

Ignored — operating mode determined by each heater’s control schedule.

type mode:

str

returns:

Same keys as DHWSystem.simulate_step(). heater_output_kbtuh and heater_power_in_kw include both primary and TM heater contributions.

rtype:

dict

Return-to-Primary (RTP) systems

class ecoengine.objects.dhwsystems.rtp_systems.RTPSystem.RTPSystem(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, max_daily_run_hr: float = 16.0, defrost_factor: float = 1.0, tm_safety_factor: float = 1.0)[source]

Bases: DHWSystem

Base class for Return-to-Primary (RTP) systems.

RTP systems route recirculation loop return flow back through the primary heat pump rather than into a separate TM system. Sizing adds daily BTUs needed to offset recirculation losses on top of the DHW-use BTUs.

__init__(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, max_daily_run_hr: float = 16.0, defrost_factor: float = 1.0, tm_safety_factor: float = 1.0)[source]
Parameters:
  • water_heaters (list[WaterHeater])

  • storage_tank (StorageTank)

  • supply_temp_f (float)

  • storage_temp_f (float)

  • return_temp_f (float) – Temperature of recirculation return water [°F].

  • return_flow_gpm (float) – Recirculation loop flow rate [GPM].

  • max_daily_run_hr (float) – Maximum hours the heating system may run per day. Default 16.

  • defrost_factor (float) – Fraction of rated capacity available after defrost (0-1). Default 1.0.

  • tm_safety_factor (float) – Multiplier applied to recirculation loss during sizing only (not simulation). Increases sized capacity and volume to provide a buffer above the bare steady-state recirc loss. Default 1.0.

get_recirc_loss_kbtuh() float[source]

Return the steady-state recirculation heat loss rate [kBTU/hr].

Formula

recirc_loss = return_flow_gpm × 60 × RHO_CP × (supply_temp - return_temp) / 1000

rtype:

float

class ecoengine.objects.dhwsystems.rtp_systems.SinglePassRTPSystem.SinglePassRTPSystem(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, max_daily_run_hr: float = 16.0, defrost_factor: float = 1.0, tm_safety_factor: float = 1.0)[source]

Bases: RTPSystem

Single-pass Return-to-Primary system.

Cold water passes through the heat pump once per heating cycle and is delivered directly at supply temperature. Recirc loop return flow feeds back into the primary heat pump (not a separate TM tank), so the required heating capacity includes the steady-state recirc loss scaled by the run time ratio (24 / max_daily_run_hr).

Storage volume sizing adds the daily recirc volume equivalent to the building magnitude before running the maximum-deficit algorithm. This ensures that continuous recirc heat loss — which drains storage even when the heater is off — is accounted for without re-normalising the load shape. The same boost is applied for load-shift sizing to cover shed and load-up windows.

Construction

Use the factory classmethod rather than calling __init__ directly:

system = SinglePassRTPSystem.from_size(
    building        = building,
    supply_temp_f   = 120.0,
    storage_temp_f  = 150.0,
    return_temp_f   = 110.0,
    return_flow_gpm = 3.0,
)
classmethod from_size(building, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, max_daily_run_hr: float = 16.0, defrost_factor: float = 1.0, tm_safety_factor: float = 1.0, control_schedule: list[str] | None = None, control_map: dict[str, Controls] | None = None, strat_slope: float = 1.7, load_shift_fract_total_vol: float = 1.0) SinglePassRTPSystem[source]

Size the system for the given building, then build it.

Parameters:
  • building (Building)

  • supply_temp_f (float) – DHW delivery temperature [°F].

  • storage_temp_f (float) – Hot water storage setpoint [°F].

  • return_temp_f (float) – Recirculation loop return temperature [°F].

  • return_flow_gpm (float) – Recirculation loop flow rate [GPM].

  • max_daily_run_hr (float) – Maximum hours the heater may run per day. Default 16.

  • defrost_factor (float) – Fraction of rated capacity available after defrost (0-1). Default 1.0.

  • control_schedule (list[str] | None) – 24-element list of control keys. None for no load-shifting.

  • control_map (dict[str, Controls] | None) – Controls objects keyed by schedule label.

  • strat_slope (float) – Temperature gradient [°F per %-height] for stratification factor. Default 1.7 (mirrors original SPRTP.setStratificationPercentageSlope).

  • load_shift_fract_total_vol (float) – Demand scaling factor for load-shift sizing (0-1). Default 1.0.

Return type:

SinglePassRTPSystem

size(building, control_schedule: list[str] | None = None, control_map: dict[str, Controls] | None = None, strat_slope: float = 1.7, load_shift_fract_total_vol: float = 1.0) None[source]

Size the single-pass RTP system.

Runs the standard DHWSystem sizing pipeline (which dispatches to the RTPSystem capacity override and the LS volume override below), then stores the recirc capacity contribution as a separate result for reporting.

Parameters:
get_recirc_capacity_kbtuh() float[source]

Return the recirc-loss contribution to total heating capacity [kBTU/hr].

This is the portion of the sized capacity dedicated to continuously offsetting recirculation loop losses.

Raises:

RuntimeError – If size() has not been called yet.

simulate_step(building, timestep_interval: int, interval_min: int = 1, mode: str = 'normal') dict[source]

Run one timestep for a single-pass RTP system.

Delegates the DHW draw and heater logic to the base class, then applies recirculation losses to the storage tank. The recirc return flow cools the bottom of the tank every minute, reducing usable volume. The returned dict is updated to reflect post-recirc tank state.

class ecoengine.objects.dhwsystems.rtp_systems.MultiPassRTPSystem.MultiPassRTPSystem(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, max_daily_run_hr: float = 16.0, defrost_factor: float = 1.0, tm_safety_factor: float = 1.0)[source]

Bases: RTPSystem

Multi-pass Return-to-Primary system.

Heating capacity is sized identically to SinglePassRTPSystem (DHW demand plus steady-state recirc loss scaled by the run-time ratio), but the default maximum daily run hours is 14 rather than 16.

Running volume (tank size) is determined by a 2-day, 1-minute-timestep simulation called the “growing-slug” method:

  • The heater is assumed to run continuously at the sized capacity.

  • At each minute the mixing valve behavior determines how much water is drawn from primary storage and at what inlet temperature.

  • That drawn volume is added to a fully-mixed virtual “slug” representing the accumulated cold water that needs to be reheated.

  • The heater heats the slug each minute.

  • When the slug temperature reaches supply_temp_f it is considered “served” and the slug volume resets to zero.

  • The peak slug volume across the 2-day simulation is the minimum required physical tank volume.

The resulting tank is a SlugOverlayTank with strat_slope = 0.8.

Simulation

While not heating the system draws hot water via mixing_valve_behavior and removes the physical gallons from the SlugOverlayTank. When the heater turns on, the tank’s slug overlay is activated from the sub-supply-temp zone of the usable volume. All demand draws and heater output are redirected to the slug until the slug temperature reaches supply_temp_f, at which point the heater turns off and the slug’s BTUs are merged back into the tank.

Load-shift sizing is not supported.

classmethod from_size(building, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, max_daily_run_hr: float = 14.0, defrost_factor: float = 1.0, tm_safety_factor: float = 1.0, control_schedule: list[str] | None = None, control_map: dict[str, Controls] | None = None, strat_slope: float = 0.8, percent_useable: float = 1.0, capacity_boost_trial_days: int = 3, capacity_boost_iterations: int = 3, default_to_min_volume: bool = True) MultiPassRTPSystem[source]

Size the system for the given building, then build it.

Parameters:
  • building (Building)

  • supply_temp_f (float)

  • storage_temp_f (float)

  • return_temp_f (float)

  • return_flow_gpm (float)

  • max_daily_run_hr (float) – Maximum hours the heater may run per day. Default 14.

  • defrost_factor (float)

  • tm_safety_factor (float) – Multiplier applied to recirculation loss during sizing only. Default 1.0.

  • control_schedule (list[str] | None) – Passed to WaterHeater; load-shift sizing is not supported and will raise if a "shed" key appears in control_map.

  • control_map (dict[str, Controls] | None)

  • strat_slope (float) – SlugOverlayTank stratification slope [°F / %-height]. Default 0.8.

  • percent_useable (float) – Fraction of total tank volume above the cold-water inlet pipe (0–1). Control on-sensors must sit above (1 - percent_useable) height.

Raises:

ValueError – If any on_sensor_fract in control_map falls inside the unusable zone.

size(building, control_schedule: list[str] | None = None, control_map: dict[str, Controls] | None = None, strat_slope: float = 0.8, load_shift_fract_total_vol: float = 1.0, default_to_min_volume: bool = False) None[source]

Size the multi-pass RTP system.

Capacity is sized the same way as SinglePassRTPSystem (DHW load plus recirc contribution via RTPSystem._calc_required_capacity). Storage volume comes from the growing-slug simulation in _calc_running_volume_supplyT_gal, which returns physical gallons directly — no stratification-factor conversion is applied.

Parameters:
  • building (Building)

  • control_schedule (list[str] | None) – Must not contain a load-shift schedule; raises ValueError if so.

  • control_map (dict[str, Controls] | None)

  • strat_slope (float)

  • load_shift_fract_total_vol (float) – Unused; retained for interface compatibility.

Raises:

ValueError – If control_map contains a "shed" key (load-shift not supported).

get_sizing_curve(building, strat_slope: float = 0.8, step: float = 0.5) dict[source]

Compute the MPRTP sizing curve — capacity vs. storage for decreasing run hours.

Unlike other DHW systems, MPRTP does not model an “over-designed” region above the default max_daily_run_hr. The curve only sweeps downward from the system’s max_daily_run_hr to the physical minimum.

Each point is produced by a full from_size() call so that the capacity-boost simulation loop (which may increase capacity beyond the analytic estimate) is applied at every run-hour value, not just the recommended point.

recommended_index is always 0 — the first point corresponds to the configured max_daily_run_hr and is the recommended design.

Parameters:
  • building (Building)

  • strat_slope (float) – Stratification slope [°F per %-height]. Defaults to 0.8.

  • step (float) – Run-hour step size for the sweep. Default 0.5 hr.

Returns:

"heat_hours" : list[float] — run hrs at each point "capacity_kbtuh" : list[float] — capacity [kBTU/hr] "storage_storageT_gal" : list[float] — storage [gal at storageT] "recommended_index" : int — always 0 for MPRTP

Return type:

dict

simulate_step(building, timestep_interval: int, interval_min: int = 1, mode: str = 'normal') dict[source]

Run one simulation timestep for a multi-pass RTP system.

Order of operations:

  1. Query Building for demand, OAT, and inlet water temperature.

  2. Keep the tank’s cold-temp baseline current from the building inlet.

  3. Determine on/off heater state (NOT heating: standard Controls logic; HEATING: turn off when slug temperature reaches supply_temp_f).

  4. Handle slug lifecycle transitions (activate on turn-on, deactivate on turn-off).

  5. Apply heating and demand via the mixing valve (heating: redirect draw and heater output to the slug; not heating: draw physical gallons from the tank via mixing_valve_behavior).

  6. Return per-step metrics.

Parameters:
  • building (Building)

  • timestep_interval (int)

  • interval_min (int)

  • mode (str) – Ignored; operating mode is set by the control schedule.

Return type:

dict

class ecoengine.objects.dhwsystems.rtp_systems.SP_RTPInParallelSystem.SP_RTPInParallelSystem(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, max_daily_run_hr: float = 16.0, defrost_factor: float = 1.0, tm_safety_factor: float = 1.0)[source]

Bases: SinglePassRTPSystem

Single-pass RTP system where the recirculation return is fed in parallel (mixed into the tank rather than routed through the heat pump inlet).

size(building)[source]

Size an SP RTP in-parallel system.

Parameters:

building (Building)

simulate_step(building, timestep_min, mode='normal')[source]

Run one timestep for an SP RTP in-parallel system.

class ecoengine.objects.dhwsystems.rtp_systems.SP_RTPInSeriesSystem.SP_RTPInSeriesSystem(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, max_daily_run_hr: float = 16.0, defrost_factor: float = 1.0, tm_safety_factor: float = 1.0)[source]

Bases: SinglePassRTPSystem

Single-pass RTP system where the recirculation return is fed in series (back into the inlet of the primary heat pump).

size(building)[source]

Size an SP RTP in-series system.

Parameters:

building (Building)

simulate_step(building, timestep_min, mode='normal')[source]

Run one timestep for an SP RTP in-series system.

class ecoengine.objects.dhwsystems.rtp_systems.MP_RTPInSeriesSystem.MP_RTPInSeriesSystem(water_heaters, storage_tank, supply_temp_f: float, storage_temp_f: float, return_temp_f: float, return_flow_gpm: float, max_daily_run_hr: float = 16.0, defrost_factor: float = 1.0, tm_safety_factor: float = 1.0)[source]

Bases: MultiPassRTPSystem

Multi-pass RTP system where the recirculation return is fed in series (back into the inlet of the primary heat pump).

size(building)[source]

Size an MP RTP in-series system.

Parameters:

building (Building)

simulate_step(building, timestep_min, mode='normal')[source]

Run one timestep for an MP RTP in-series system.