Subsystems

Composable subsystems that the *_stc_* and *_pv_ess model variants plug onto the core cycle, tank backends, plus the UV-treatment subsystem.

Solar thermal collector

class tmhp.subsystems.SolarThermalCollector(A_stc=2.0, stc_tilt=35.0, stc_azimuth=180.0, A_stc_pipe=2.0, alpha_stc=0.95, h_o_stc=15.0, h_r_stc=2.0, k_ins_stc=0.03, x_air_stc=0.01, x_ins_stc=0.05, preheat_start_hour=6.0, preheat_end_hour=18.0, dV_stc_w=0.001, E_stc_pump=50.0)[source]

Bases: object

Solar Thermal Collector (STC) — pure physics engine.

Bundles collector geometry, optical and thermal properties, and pump parameters. This class is a stateless physics calculator: given physical inputs it returns a performance dict. All simulation orchestration (activation logic, result assembly, exergy calculation) is the responsibility of the scenario class that uses this engine.

The public API consists of:

Parameters:
  • A_stc (float) – Collector gross area [m²].

  • stc_tilt (float) – Tilt from horizontal [°].

  • stc_azimuth (float) – Azimuth angle (180 = south) [°].

  • A_stc_pipe (float) – Pipe surface area [m²].

  • alpha_stc (float) – Absorptivity [–].

  • h_o_stc (float) – External convective coefficient [W/(m²·K)].

  • h_r_stc (float) – Radiative coefficient [W/(m²·K)].

  • k_ins_stc (float) – Insulation conductivity [W/(m·K)].

  • x_air_stc (float) – Air gap thickness [m].

  • x_ins_stc (float) – Insulation thickness [m].

  • preheat_start_hour (float) – Preheat window start hour.

  • preheat_end_hour (float) – Preheat window end hour.

  • dV_stc_w (float) – Default STC loop flow rate [m³/s].

  • E_stc_pump (float) – STC pump rated power [W].

A_stc: float = 2.0
stc_tilt: float = 35.0
stc_azimuth: float = 180.0
A_stc_pipe: float = 2.0
alpha_stc: float = 0.95
h_o_stc: float = 15.0
h_r_stc: float = 2.0
k_ins_stc: float = 0.03
x_air_stc: float = 0.01
x_ins_stc: float = 0.05
preheat_start_hour: float = 6.0
preheat_end_hour: float = 18.0
dV_stc_w: float = 0.001
E_stc_pump: float = 50.0
calc_overall_heat_transfer_coeff()[source]

Compute overall U-value from parallel resistances.

The collector has two heat-loss paths in parallel:

  • Path 1 (top): radiation gap ‖ air gap → external conv

  • Path 2 (bottom): insulation → external conv

Returns:

Overall heat-loss coefficient [W/(m²·K)].

Return type:

float

calc_performance(I_DN_stc, I_dH_stc, T_stc_w_in_K, T0_K, dV_stc=None, is_active=True)[source]

Compute STC thermal performance for one timestep.

Parameters:
  • I_DN_stc (float) – Direct-normal irradiance [W/m²].

  • I_dH_stc (float) – Diffuse-horizontal irradiance [W/m²].

  • T_stc_w_in_K (float) – Inlet water temperature [K].

  • T0_K (float) – Dead-state temperature [K].

  • dV_stc (float | None) – Override flow rate [m³/s]; defaults to self.dV_stc_w.

  • is_active (bool) – If False, return NaN-filled dict.

Returns:

Performance results including: I_sol_stc, Q_sol_stc, Q_stc_w_in, Q_stc_w_out, Q_stc_pump_w_out, ksi_stc, T_stc_w_out_K, T_stc_pump_w_out_K, T_stc_w_in_K, T_stc_K, Q_l_stc.

Return type:

dict

is_preheat_on(hour_of_day)[source]

Check whether hour_of_day falls in the window.

Parameters:

hour_of_day (float) – Hour within the day (0–24).

Return type:

bool

__init__(A_stc=2.0, stc_tilt=35.0, stc_azimuth=180.0, A_stc_pipe=2.0, alpha_stc=0.95, h_o_stc=15.0, h_r_stc=2.0, k_ins_stc=0.03, x_air_stc=0.01, x_ins_stc=0.05, preheat_start_hour=6.0, preheat_end_hour=18.0, dV_stc_w=0.001, E_stc_pump=50.0)
Parameters:
  • A_stc (float)

  • stc_tilt (float)

  • stc_azimuth (float)

  • A_stc_pipe (float)

  • alpha_stc (float)

  • h_o_stc (float)

  • h_r_stc (float)

  • k_ins_stc (float)

  • x_air_stc (float)

  • x_ins_stc (float)

  • preheat_start_hour (float)

  • preheat_end_hour (float)

  • dV_stc_w (float)

  • E_stc_pump (float)

Photovoltaic system

class tmhp.subsystems.PhotovoltaicSystem(A_pv=5.0, alp_pv=0.9, pv_tilt=35.0, pv_azimuth=180.0, h_o=15.0, eta_pv=0.15, beta_pv=0.0045, T_ref_pv_K=298.15, eta_ctrl=0.95, T_ctrl_K=308.15)[source]

Bases: object

Photovoltaic System (PV + Charge Controller) — pure physics engine.

Computes PV energy generation from irradiance inputs. This class is a stateless physics calculator: given physical inputs it returns a performance dict. All routing logic (ESS charge/discharge, Grid import, dump) is the responsibility of the scenario class.

The public API:

Parameters:
  • A_pv (float)

  • alp_pv (float)

  • pv_tilt (float)

  • pv_azimuth (float)

  • h_o (float)

  • eta_pv (float)

  • beta_pv (float)

  • T_ref_pv_K (float)

  • eta_ctrl (float)

  • T_ctrl_K (float)

A_pv: float = 5.0
alp_pv: float = 0.9
pv_tilt: float = 35.0
pv_azimuth: float = 180.0
h_o: float = 15.0
eta_pv: float = 0.15
beta_pv: float = 0.0045
T_ref_pv_K: float = 298.15
eta_ctrl: float = 0.95
T_ctrl_K: float = 308.15
calc_performance(I_DN, I_dH, T0_K)[source]

Compute PV generation for one timestep.

Parameters:
  • I_DN (float) – Direct-normal irradiance [W/m²].

  • I_dH (float) – Diffuse-horizontal irradiance [W/m²].

  • T0_K (float) – Dead-state (ambient) temperature [K].

Returns:

Keys: I_sol_pv, T_pv_K, eta_pv_actual, E_pv_out, E_ctrl_out, Q_l_pv, Q_l_ctrl, X_sol, X_pv_out, X_ctrl_out, X_c_pv, X_c_ctrl, X_l_pv, X_l_ctrl.

Return type:

dict

__init__(A_pv=5.0, alp_pv=0.9, pv_tilt=35.0, pv_azimuth=180.0, h_o=15.0, eta_pv=0.15, beta_pv=0.0045, T_ref_pv_K=298.15, eta_ctrl=0.95, T_ctrl_K=308.15)
Parameters:
  • A_pv (float)

  • alp_pv (float)

  • pv_tilt (float)

  • pv_azimuth (float)

  • h_o (float)

  • eta_pv (float)

  • beta_pv (float)

  • T_ref_pv_K (float)

  • eta_ctrl (float)

  • T_ctrl_K (float)

Energy storage system

class tmhp.subsystems.EnergyStorageSystem(C_ess_max=3600000.0, SOC_init=0.0, SOC_min=0.1, SOC_max=1.0, eta_ess_chg=0.9, eta_ess_dis=0.9, T_ess_K=313.15)[source]

Bases: object

Energy Storage System (Battery) — pure physics engine.

Accepts charging / discharging requests (DC power, [W]) and enforces capacity and SOC limits. Internal losses and exergy destruction are computed from round-trip efficiencies.

State (SOC_ess) is updated in-place at each call to charge() or discharge(). Routing decisions (which request to fulfil and in what order) are entirely the responsibility of the scenario class.

Parameters:
  • C_ess_max (float) – Rated energy capacity [J]. Default 3.6 MJ (= 1 kWh).

  • SOC_init (float) – Initial state of charge [–].

  • SOC_min (float) – Minimum allowable SOC [–] (depth-of-discharge guard).

  • SOC_max (float) – Maximum allowable SOC [–].

  • eta_ess_chg (float) – Charging efficiency (electricity-in to stored) [–].

  • eta_ess_dis (float) – Discharging efficiency (stored to electricity-out) [–].

  • T_ess_K (float) – Representative battery temperature used for entropy calc [K].

C_ess_max: float = 3600000.0
SOC_init: float = 0.0
SOC_min: float = 0.1
SOC_max: float = 1.0
eta_ess_chg: float = 0.9
eta_ess_dis: float = 0.9
T_ess_K: float = 313.15
SOC_ess: float
charge(E_req_chg, dt, T0_K)[source]

Request to charge the ESS with E_req_chg [W] for dt [s].

Returns a dict with keys E_ess_chg, E_ess_dis, SOC_ess plus exergy keys.

Parameters:
  • E_req_chg (float)

  • dt (float)

  • T0_K (float)

Return type:

dict

discharge(E_req_dis, dt, T0_K)[source]

Request to discharge E_req_dis [W] from the ESS for dt [s].

Returns a dict with keys E_ess_dis (actual), E_ess_chg, SOC_ess plus exergy keys.

Parameters:
  • E_req_dis (float)

  • dt (float)

  • T0_K (float)

Return type:

dict

__init__(C_ess_max=3600000.0, SOC_init=0.0, SOC_min=0.1, SOC_max=1.0, eta_ess_chg=0.9, eta_ess_dis=0.9, T_ess_K=313.15)
Parameters:
  • C_ess_max (float)

  • SOC_init (float)

  • SOC_min (float)

  • SOC_max (float)

  • eta_ess_chg (float)

  • eta_ess_dis (float)

  • T_ess_K (float)

UV lamp

class tmhp.subsystems.UVLamp(lamp_watts=0.0, exposure_sec=0.0, num_switching=1, period_sec=10800)[source]

Bases: object

UV disinfection lamp subsystem.

The lamp switches on periodically (num_switching times per period_sec, each for exposure_sec). All electrical input is converted to heat inside the tank (Q_contribution = E_uv).

Parameters:
  • lamp_watts (float) – Rated lamp power [W].

  • exposure_sec (float) – Duration of each on-cycle [s].

  • num_switching (int) – Number of on-cycles per period.

  • period_sec (float) – Switching period [s] (default 3 h = 10 800 s).

lamp_watts: float = 0.0
exposure_sec: float = 0.0
num_switching: int = 1
period_sec: float = 10800
step(ctx, ctrl, dt, T_tank_w_in_K)[source]

Compute UV lamp state for one timestep.

Parameters:
  • ctx (StepContext)

  • ctrl (ControlState)

  • dt (float)

  • T_tank_w_in_K (float)

Return type:

dict

assemble_results(ctx, ctrl, step_state, T_solved_K)[source]

Report UV power for DataFrame output.

Parameters:
  • ctx (StepContext)

  • ctrl (ControlState)

  • step_state (dict)

  • T_solved_K (float)

Return type:

dict

calc_exergy(df, T0_K)[source]

UV exergy = electricity (handled by E→X conversion).

No additional post-processing needed. Returns None.

Parameters:
  • df (DataFrame)

  • T0_K (Series)

Return type:

SubsystemExergy | None

__init__(lamp_watts=0.0, exposure_sec=0.0, num_switching=1, period_sec=10800)
Parameters:
  • lamp_watts (float)

  • exposure_sec (float)

  • num_switching (int)

  • period_sec (float)

UV treatment

UV water treatment utility functions.

Functions for UV lamp power scheduling, turbidity-based parameter lookup, and required exposure time calculation (Radial Model).

tmhp.uv_treatment.calc_uv_lamp_power(current_time_s, period_sec, num_switching, exposure_sec, lamp_watts)[source]

Calculate UV lamp power at a given time instant.

The lamp switches on num_switching times per period_sec, each activation lasting exposure_sec.

Parameters:
  • current_time_s (float) – Current simulation time [s].

  • period_sec (float) – Switching period (e.g. 3 h → 10800 s).

  • num_switching (int) – Number of on-cycles per period.

  • exposure_sec (float) – Duration of each on-cycle [s].

  • lamp_watts (float) – Rated lamp power [W].

Returns:

Instantaneous lamp power [W] (0 or lamp_watts).

Return type:

float

tmhp.uv_treatment.get_uv_params_from_turbidity(turbidity_ntu)[source]

Return UV parameters from a turbidity lookup table.

Table data based on Table 1. Effect of Turbidity on UVT, UV Absorbance, UV Intensity, and Exposure Time.

Parameters:

turbidity_ntu (float) – Turbidity value [NTU].

Returns:

Keys: uv_absorbance, uv_transmittance_percent, reference_intensity_mw_cm2, reference_exposure_time_sec.

Return type:

dict

tmhp.uv_treatment.calc_uv_exposure_time(radius_cm, uvc_output_W, lamp_arc_length_cm, target_dose_mj_cm2=186, turbidity_ntu=0.25)[source]

Calculate required UV lamp exposure time via Radial Model.

Reference: ADA453967.pdf — Radial Model for UV disinfection.

Parameters:
  • radius_cm (float) – Tank radius (lamp-to-wall distance) [cm].

  • uvc_output_W (float) – UV-C output power of the lamp [W].

  • lamp_arc_length_cm (float) – Arc length of the lamp [cm].

  • target_dose_mj_cm2 (float) – Target germicidal dose [mJ/cm²]. Default 186 (EPA 4-log virus).

  • turbidity_ntu (float) – Water turbidity [NTU].

Returns:

Required single-exposure time [min].

Return type:

float

Stratified tank

Multi-node (1-D finite-volume) stratified thermal-storage tank.

Implements the multi-node model of Cadau et al. (Development and Analysis of a Multi-Node Dynamic Model for the Simulation of Stratified Thermal Energy Storage, Energies 2019, 12:4275): the tank is divided into N vertically stacked nodes (node 0 = top/hottest, node N-1 = bottom/coldest), each with a uniform temperature T_i. The per-node energy balance combines

  • advection — port inflow/outflow and inter-node vertical flow, upwinded by the flow direction (Cadau Eq. 4-6);

  • pseudo-conductionk·(T_{i-1}-T_i) - k·(T_i-T_{i+1}) between neighbouring nodes, lumping conductive + convective exchange; and

  • ambient lossUA_i·(T_i - T_amb) through the side wall.

It is integrated implicitly (backward Euler) as a tridiagonal solve, so the update is unconditionally stable and smooth in the state — a property the legacy single-node lumped tank lacks and that an MPC-internal model needs. The lumped fully-mixed tank is exactly the N=1 limit of this model.

This is a standalone, testable component; wiring it into GroundSourceHeatPumpBoiler as a swappable tank backend is a separate step.

Conventions

  • Node index increases downward: T[0] top (hot), T[N-1] bottom (cold).

  • charge_flow [m³/s] ≥ 0 (HP charging) enters the top node at T_charge and exits the bottom node (hot return to top, cold draw to HP) — a downward internal flow.

  • draw_flow [m³/s] ≥ 0 (load) draws hot water from the top node and admits cold makeup T_makeup at the bottom node — an upward internal flow.

  • The net inter-node flow is charge_flow - draw_flow (downward positive); advection is upwinded by its direction (Cadau Eq. 4-6).

  • Temperatures in °C; energy balances are temperature-difference based so the reference cancels.

class tmhp.stratified_tank.StratifiedTank(n_nodes, volume, height, *, k_eff=0.606, ua=0.0, rho=1000, cp=4186)[source]

Bases: object

Multi-node stratified hot-water tank (implicit tridiagonal stepper).

Parameters:
  • n_nodes (int) – Number of vertical nodes N (N=1 recovers the lumped tank).

  • volume (float) – Total tank volume [m³].

  • height (float) – Tank height [m] (sets node thickness dz = height/N and the cross-sectional area volume/height for conduction).

  • k_eff (float, optional) – Effective inter-node conductivity [W/m/K] (water conduction plus any turbulent-mixing enhancement). Defaults to water (k_w).

  • ua (float, optional) – Total tank-to-ambient loss coefficient UA [W/K], split uniformly across nodes. Defaults to 0 (adiabatic).

  • rho (float, optional) – Water density [kg/m³] and specific heat [J/kg/K]; default to the project constants. Assumed constant (Cadau: density assumed constant per node).

  • cp (float, optional) – Water density [kg/m³] and specific heat [J/kg/K]; default to the project constants. Assumed constant (Cadau: density assumed constant per node).

__init__(n_nodes, volume, height, *, k_eff=0.606, ua=0.0, rho=1000, cp=4186)[source]
Parameters:
  • n_nodes (int)

  • volume (float)

  • height (float)

  • k_eff (float)

  • ua (float)

  • rho (float)

  • cp (float)

reset(T_init)[source]

Set node temperatures (scalar = uniform, or length-N array).

Return type:

ndarray

property stored_energy: float

Sensible energy relative to 0 °C [J] (Σ m_node·cp·T_i).

step(dt, *, charge_flow=0.0, T_charge=0.0, draw_flow=0.0, T_makeup=10.0, q_source=None, T_amb=20.0)[source]

Advance one timestep (backward Euler, charge + draw + heat source).

Parameters:
  • dt (float) – Timestep [s].

  • charge_flow (float, optional) – HP charge flow [m³/s] (hot T_charge into top, out at bottom).

  • T_charge (float, optional) – Charge inlet temperature [°C] (used when charge_flow > 0).

  • draw_flow (float, optional) – Load draw flow [m³/s] (hot from top, cold T_makeup into bottom).

  • T_makeup (float, optional) – Cold makeup temperature [°C] (used when draw_flow > 0).

  • q_source (float or array-like, optional) – Internal heat input [W] from an immersed heater/condenser. A scalar is applied to the top node; a length-N array is applied per node.

  • T_amb (float, optional) – Ambient temperature [°C] for the side-wall loss.

Returns:

T (new node temperatures), T_top (= hot draw outlet), T_outlet (bottom = cold HP return).

Return type:

dict

Hybrid stratified tank

1-D hybrid continuous–discrete multi-node stratified tank (Cruz-Loredo 2023).

Implements the hybrid thermocline model of De la Cruz-Loredo et al. (Experimental validation of a hybrid 1-D multi-node model of a hot water thermal energy storage tank, Applied Energy 2023, 332:120556). It augments the standard multi-node model (StratifiedTank) with a flat thermocline barrier at vertical position y_th that travels in plug flow at v_th = V̇/A_c.

The key device against numerical diffusion: while charging, the advective inflow into each node uses a discrete reference temperature of its upstream neighbour that is frozen until the thermocline front passes that neighbour’s mid-height (y_mid). The transition therefore propagates at the physical front speed instead of smearing across nodes. This is the charge-only thermocline form (Cruz-Loredo Eq. 7): discharge/idle destroys the barrier and the model reverts to the standard continuous multi-node behaviour.

Targets the plant / ground-truth role (high fidelity, non-smooth); the smooth StratifiedTank (Cadau) targets the MPC-internal role. Same geometry/units conventions as StratifiedTank (node 0 = top/hot).

class tmhp.hybrid_tank.HybridStratifiedTank(n_nodes, volume, height, *, k_eff=0.606, ua=0.0, rho=1000, cp=4186)[source]

Bases: object

Hybrid continuous–discrete multi-node tank with a flat thermocline.

Parameters:
  • n_nodes (int)

  • volume (float)

  • height (float)

  • k_eff (float)

  • ua (float)

  • rho (float)

  • cp (float)

__init__(n_nodes, volume, height, *, k_eff=0.606, ua=0.0, rho=1000, cp=4186)[source]
Parameters:
  • n_nodes (int)

  • volume (float)

  • height (float)

  • k_eff (float)

  • ua (float)

  • rho (float)

  • cp (float)

reset(T_init)[source]
Return type:

ndarray

property stored_energy: float
step(dt, *, charge_flow=0.0, T_charge=0.0, draw_flow=0.0, T_makeup=10.0, T_amb=20.0)[source]

Advance one timestep.

Pure charge (charge_flow > 0, draw_flow == 0) activates the hybrid frozen-reference thermocline; draw/idle/mixed flow uses the standard continuous multi-node update (the barrier is destroyed).

Parameters:
  • dt (float)

  • charge_flow (float)

  • T_charge (float)

  • draw_flow (float)

  • T_makeup (float)

  • T_amb (float)

Return type:

dict