Ground-source heat pump boiler (GSHPB)

The GSHPB family pairs the shared refrigerant cycle with a ground-loop source side (vertical borehole field) and the same DHW tank sink as ASHPB.

Overview

GSHPB solves the closed refrigerant cycle against a borehole heat exchanger characterised by a precomputed g-function. The class is tmhp.GroundSourceHeatPumpBoiler. Three composed variants extend it the same way ASHPB’s do:

  • tmhp.GSHPB_STC_preheat

  • tmhp.GSHPB_STC_tank

  • tmhp.GSHPB_PV_ESS

Base usage

from tmhp import GroundSourceHeatPumpBoiler

gshpb = GroundSourceHeatPumpBoiler(
    ref="R410A",
    N_1=1, N_2=1,      # single borehole
    H_b=150.0,         # depth [m]
)

result = gshpb.analyze_steady(
    T_tank_w=55.0,
    T_source=10.0,     # ground-loop fluid inlet [°C]
    Q_ref_cond=8_000,
)

Source-side mechanics

For ground-source models, the source-side dynamics are encoded in a g-function — the dimensionless thermal response of a borehole field to a unit heat-extraction step. tmhp precomputes the g-function once via pygfunction and interpolates it during the simulation, so the per-step cost stays constant whether the field is one borehole or a hundred.

g-function vs ln(t/t_s) for three rectangular borehole field geometries: 1×1, 2×2, and 4×4.

Dimensionless g-function for three rectangular borehole-field geometries. The 1 × 1 field is the single-borehole baseline; 2 × 2 and 4 × 4 diverge as borehole-to-borehole thermal interference accumulates over the multi-year horizon. Generated by scripts/visualization/g_function_curve.py.

Sink-side mechanics

Same as ASHPB — single-node DHW tank, implicit per-step solve.

Composed variants

Usage patterns for the three variants are identical to ASHPB’s — see Air-source heat pump boiler (ASHPB) for full STC preheat and PV + ESS examples. Only the base class swaps; the schedules and routing are unchanged.

The standalone GSHPB: borehole-field g-function source, R32 cycle, DHW tank charge.

from tmhp import GroundSourceHeatPumpBoiler

gshpb = GroundSourceHeatPumpBoiler(
    ref="R410A",
    N_1=1, N_2=1,
    H_b=150.0,
)
result = gshpb.analyze_steady(
    T_tank_w=55.0,
    T_source=10.0,
    Q_ref_cond=8_000,
)

Adds a flat-plate STC that preheats mains water entering the tank. Reduces the tank-charge duty the heat pump has to deliver.

from tmhp import GSHPB_STC_preheat
from tmhp.subsystems import SolarThermalCollector

stc = SolarThermalCollector(A_stc=4.0, stc_tilt=35.0, stc_azimuth=180.0)
model = GSHPB_STC_preheat(stc=stc, ref="R410A", N_1=1, N_2=1, H_b=150.0)

STC charges a separate top node of a stratified tank; the heat pump charges the bottom. Top-of-tank water is drawn first.

from tmhp import GSHPB_STC_tank
from tmhp.subsystems import SolarThermalCollector

stc = SolarThermalCollector(A_stc=4.0, stc_tilt=35.0, stc_azimuth=180.0)
model = GSHPB_STC_tank(stc=stc, ref="R410A", N_1=1, N_2=1, H_b=150.0)

Photovoltaic generation + ESS preferentially feeds the compressor and auxiliaries.

from tmhp import GSHPB_PV_ESS
from tmhp.subsystems import EnergyStorageSystem, PhotovoltaicSystem

model = GSHPB_PV_ESS(
    pv=PhotovoltaicSystem(),
    ess=EnergyStorageSystem(),
    ref="R410A",
    N_1=1, N_2=1, H_b=150.0,
)

STC preheat

class tmhp.GSHPB_STC_preheat(*, stc, **kwargs)[source]

Bases: GroundSourceHeatPumpBoiler

GSHPB + SolarThermalCollector in mains_preheat placement.

Physical configuration

The STC preheats mains cold water before it enters the storage tank. The raised inlet temperature (T_tank_w_in_override_K) reduces the thermal load on the heat pump compressor.

Orchestration responsibility

This class owns all simulation logic for the STC:

  • _run_subsystems: activation probe + calc_performance() → sets T_tank_w_in_override_K when active

  • _augment_results: result column assembly (uses pump outlet temperature directly; no re-evaluation at solved tank temp)

  • _postprocess: STC exergy calculation. Tank boundary corrections are not applied because the preheated inlet temperature is already reflected in the core X_tank_w_in [W] column (X_in_tank_add = 0).

type stc:

SolarThermalCollector

param stc:

Pure physics engine. No mode constraint required.

type stc:

SolarThermalCollector

type **kwargs:

param **kwargs:

Forwarded to GroundSourceHeatPumpBoiler.

raises TypeError:

If stc is not a SolarThermalCollector instance.

__init__(*, stc, **kwargs)[source]
Parameters:

stc (SolarThermalCollector)

STC with stratified tank

class tmhp.GSHPB_STC_tank(*, stc, **kwargs)[source]

Bases: GroundSourceHeatPumpBoiler

GSHPB + SolarThermalCollector in tank_circuit placement.

Physical configuration

The STC collector loop is connected directly to the storage tank. The STC draws water from the tank, heats it via solar energy, and returns it through the pump. The STC is activated only when the collector outlet temperature exceeds the current tank temperature.

Orchestration responsibility

This class owns all simulation logic for the STC:

  • _run_subsystems: activation probe + calc_performance()

  • _build_residual_fn: fully implicit coupling to tank solver

  • _augment_results: result column assembly (re-evaluates at solved tank temperature for accuracy)

  • _postprocess: STC exergy calculation and tank boundary correction (X_tot, Xc_tank)

type stc:

SolarThermalCollector

param stc:

Pure physics engine. No mode constraint required.

type stc:

SolarThermalCollector

type **kwargs:

param **kwargs:

Forwarded to GroundSourceHeatPumpBoiler.

raises TypeError:

If stc is not a SolarThermalCollector instance.

__init__(*, stc, **kwargs)[source]
Parameters:

stc (SolarThermalCollector)

PV + ESS

class tmhp.GSHPB_PV_ESS(*, pv, ess=None, eta_inv=0.95, T_inv_K=313.15, **kwargs)[source]

Bases: GroundSourceHeatPumpBoiler

GSHPB scenario where the heat pump is supplied by PV + ESS + Grid.

The PV/ESS routing is resolved synchronously inside _augment_results after the HP electrical load is known. No 1-step lag: the PV energy is allocated to the exact HP load produced in the same timestep.

Parameters:
  • pv (PhotovoltaicSystem) – Pure-physics PV + charge-controller model.

  • ess (EnergyStorageSystem) – Pure-physics battery model with charge() / discharge().

  • eta_inv (float) – Inverter DC→AC efficiency [–].

  • T_inv_K (float) – Inverter temperature for entropy calculation [K].

  • **kwargs – Forwarded to GroundSourceHeatPumpBoiler.

__init__(*, pv, ess=None, eta_inv=0.95, T_inv_K=313.15, **kwargs)[source]
Parameters:

API reference

Integrated System Model: Ground Source Heat Pump Boiler (GSHPB).

This system class orchestrates the dynamic interaction between distinct thermodynamic sub-components to simulate the overall heating performance. While implemented as an integrated model, its physical calculations represent the behavior of:

  1. Refrigerant Cycle (Vapor-Compression): Evaluates thermodynamic states using CoolProp, enforcing superheat/subcool margins.

  2. Heat Pump Compressor: Models the compression process using isentropic and volumetric efficiencies to compute the actual discharge enthalpy and mass flow rate. The compressor power is determined from the enthalpy difference and the mass flow rate.

  3. Expansion Valve: Modeled as an isenthalpic expansion device (constant enthalpy) that throttles the refrigerant from the condensing pressure down to the evaporating pressure.

  4. Heat Exchangers (Condenser & Evaporator):

    • Condenser: Placed inside the hot-water tank (hydronic), utilizing a static overall heat transfer coefficient (UA_cond_design).

    • Evaporator: Coupled to a borehole heat exchanger (BHE) fluid loop, acting as a secondary heat exchanger to absorb heat from the circulating ground fluid.

  5. Thermal Storage Tank: Modeled with lumped-capacitance and DHW mixing logic.

  6. Ground Source Heat Exchanger (Borefield): A dynamic multi-borehole simulation using pygfunction-based g-functions. It tracks the transient thermal response of the ground, enabling robust modeling of long-term ground temperature drift due to continuous heat extraction.

At each time step the model finds the minimum-power operating point via 1D Brent optimization over the evaporator approach temperature difference, while the condenser temperature is solved analytically.

Note

See the project paper for the underlying refrigerant-cycle theory and the pygfunction-based borehole heat-exchanger model used here.

class tmhp.ground_source_heat_pump_boiler.GroundSourceHeatPumpBoiler(ref='R410A', V_disp_cmp=0.0005, eta_cmp_isen=None, eta_cmp_vol=None, eta_cmp_mech=0.855, UA_cond_design=500, UA_evap_design=500, T0=0.0, Ts=16.0, T_tank_w_upper_bound=65.0, T_tank_w_lower_bound=55.0, T_mix_w_out=40.0, T_tank_w_in=15.0, hp_capacity=8000.0, dV_mix_w_out_max=0.0001, r0=0.2, H=0.8, x_shell=0.01, x_ins=0.05, k_shell=25, k_ins=0.03, h_o=15, N_1=1, N_2=1, B=6.0, D_b=0, H_b=200, r_b=0.08, R_b=None, k_g=1.5, k_p=0.4, r_out=0.016, r_in=0.013, D_s=0.025, dV_b_f_lpm=24, k_s=2.0, c_s=800, rho_s=2000, E_pmp=200, dT_superheat=3.0, dT_subcool=3.0, tank_always_full=True, prevent_simultaneous_flow=False, tank_level_lower_bound=0.5, tank_level_upper_bound=1.0, dV_tank_w_in_refill=0.001, hp_on_schedule=None, stc=None, pv=None, uv=None, t_max_s=31536000, dt_s=3600, boundary_condition='uniform_temperature', *, refrigerant=None)[source]

Bases: object

Ground source heat pump boiler with BHE and lumped-tank model.

The refrigerant cycle is resolved via CoolProp with user-specified superheat / subcool margins. An optimizer minimises total cycle electrical input subject to NTU-based evaporator constraints and analytical condenser temperature relations.

Parameters:
  • ref (str)

  • V_disp_cmp (float)

  • eta_cmp_isen (Union[float, Callable, None])

  • eta_cmp_vol (Union[float, Callable, None])

  • eta_cmp_mech (float | Callable)

  • UA_cond_design (float)

  • UA_evap_design (float)

  • T0 (float)

  • Ts (float)

  • T_tank_w_upper_bound (float)

  • T_tank_w_lower_bound (float)

  • T_mix_w_out (float)

  • T_tank_w_in (float)

  • hp_capacity (float)

  • dV_mix_w_out_max (float)

  • r0 (float)

  • H (float)

  • x_shell (float)

  • x_ins (float)

  • k_shell (float)

  • k_ins (float)

  • h_o (float)

  • N_1 (int)

  • N_2 (int)

  • B (float)

  • D_b (float)

  • H_b (float)

  • r_b (float)

  • R_b (Optional[float])

  • k_g (float)

  • k_p (float)

  • r_out (float)

  • r_in (float)

  • D_s (float)

  • dV_b_f_lpm (float)

  • k_s (float)

  • c_s (float)

  • rho_s (float)

  • E_pmp (float)

  • dT_superheat (float)

  • dT_subcool (float)

  • tank_always_full (bool)

  • prevent_simultaneous_flow (bool)

  • tank_level_lower_bound (float)

  • tank_level_upper_bound (float)

  • dV_tank_w_in_refill (float)

  • hp_on_schedule (Optional[list[tuple[float, float]]])

  • stc (Optional[SolarThermalCollector])

  • t_max_s (float)

  • dt_s (float)

  • boundary_condition (str)

  • refrigerant (Optional[str])

__init__(ref='R410A', V_disp_cmp=0.0005, eta_cmp_isen=None, eta_cmp_vol=None, eta_cmp_mech=0.855, UA_cond_design=500, UA_evap_design=500, T0=0.0, Ts=16.0, T_tank_w_upper_bound=65.0, T_tank_w_lower_bound=55.0, T_mix_w_out=40.0, T_tank_w_in=15.0, hp_capacity=8000.0, dV_mix_w_out_max=0.0001, r0=0.2, H=0.8, x_shell=0.01, x_ins=0.05, k_shell=25, k_ins=0.03, h_o=15, N_1=1, N_2=1, B=6.0, D_b=0, H_b=200, r_b=0.08, R_b=None, k_g=1.5, k_p=0.4, r_out=0.016, r_in=0.013, D_s=0.025, dV_b_f_lpm=24, k_s=2.0, c_s=800, rho_s=2000, E_pmp=200, dT_superheat=3.0, dT_subcool=3.0, tank_always_full=True, prevent_simultaneous_flow=False, tank_level_lower_bound=0.5, tank_level_upper_bound=1.0, dV_tank_w_in_refill=0.001, hp_on_schedule=None, stc=None, pv=None, uv=None, t_max_s=31536000, dt_s=3600, boundary_condition='uniform_temperature', *, refrigerant=None)[source]
Parameters:
  • ref (str)

  • V_disp_cmp (float)

  • eta_cmp_isen (Union[float, Callable, None])

  • eta_cmp_vol (Union[float, Callable, None])

  • eta_cmp_mech (float | Callable)

  • UA_cond_design (float)

  • UA_evap_design (float)

  • T0 (float)

  • Ts (float)

  • T_tank_w_upper_bound (float)

  • T_tank_w_lower_bound (float)

  • T_mix_w_out (float)

  • T_tank_w_in (float)

  • hp_capacity (float)

  • dV_mix_w_out_max (float)

  • r0 (float)

  • H (float)

  • x_shell (float)

  • x_ins (float)

  • k_shell (float)

  • k_ins (float)

  • h_o (float)

  • N_1 (int)

  • N_2 (int)

  • B (float)

  • D_b (float)

  • H_b (float)

  • r_b (float)

  • R_b (Optional[float])

  • k_g (float)

  • k_p (float)

  • r_out (float)

  • r_in (float)

  • D_s (float)

  • dV_b_f_lpm (float)

  • k_s (float)

  • c_s (float)

  • rho_s (float)

  • E_pmp (float)

  • dT_superheat (float)

  • dT_subcool (float)

  • tank_always_full (bool)

  • prevent_simultaneous_flow (bool)

  • tank_level_lower_bound (float)

  • tank_level_upper_bound (float)

  • dV_tank_w_in_refill (float)

  • hp_on_schedule (Optional[list[tuple[float, float]]])

  • stc (Optional[SolarThermalCollector])

  • t_max_s (float)

  • dt_s (float)

  • boundary_condition (str)

  • refrigerant (Optional[str])

analyze_dynamic(simulation_period_sec, dt_s, T_tank_w_init_C, dhw_usage_schedule, T0_schedule, I_DN_schedule=None, I_dH_schedule=None, T_sup_w_schedule=None, tank_level_init=1.0, result_save_csv_path=None)[source]
Parameters:
  • simulation_period_sec (float)

  • dt_s (float)

  • T_tank_w_init_C (float)

  • tank_level_init (float)

Return type:

DataFrame

analyze_steady(T_tank_w, T_source, Q_ref_cond, T0=0.0, *, return_dict=True)[source]

Run a steady-state performance snapshot.

Evaluates the refrigerant cycle at a given operating point (T_tank_w, T_source, Q_ref_cond) without solving the tank energy balance or tracking dynamic flows.

Parameters:
  • T_tank_w (float) – Tank water temperature [°C] — treated as a given input.

  • T_source (float) – Source fluid temperature entering the heat pump [°C].

  • Q_ref_cond (float) – Target condenser heat rate [W].

  • T0 (float) – Dead-state / outdoor-air temperature [°C] (for exergy calculations).

  • return_dict (bool) – If True return dict; else single-row DataFrame.

Returns:

Cycle state plus diagnostic flags. Notable keys:

  • "converged" (bool) — True only when the HX optimisation and the SciPy optimiser both succeeded.

  • "failure_reason" (str) — one of "none", "cycle_invalid", "hx_not_converged", or "optimizer_failed".

Important: GSHPB frequently reports failure_reason="hx_not_converged" on realistic operating points because its inner NTU/HX residual tolerance is strict. The returned E_cmp [W] / Q_ref_cond [W] / cop_sys [-] are still usable in that case — only "cycle_invalid" forces an off-mode fallback (E_cmp=0, COP=NaN). Branch on E_cmp [W] > 0 rather than failure_reason == "none" if you only want to discard truly broken results.

Return type:

dict | pd.DataFrame

postprocess_exergy(df)[source]

Compute GSHPB-specific exergy variables.

Parameters:

df (DataFrame)

Return type:

DataFrame

GSHPB with SolarThermalCollector — mains_preheat placement.

Phase 3 restructuring: all simulation orchestration logic (activation probe, result assembly, exergy calculation) is implemented directly in this class. SolarThermalCollector is used purely as a physics engine (calc_performance()), with no dependency on step(), assemble_results(), or calc_exergy().

Usage

from enex_analysis import SolarThermalCollector
from enex_analysis.gshpb_stc_preheat import GSHPB_STC_preheat

stc = SolarThermalCollector(A_stc=4.0)
model = GSHPB_STC_preheat(
    stc=stc,
    ref="R134a",
    hp_capacity=15_000.0,
    T_tank_w_lower_bound=55.0,
    T_tank_w_upper_bound=65.0,
    T_mix_w_out=42.0,
)
df = model.analyze_dynamic(...)
class tmhp.gshpb_stc_preheat.GSHPB_STC_preheat(*, stc, **kwargs)[source]

Bases: GroundSourceHeatPumpBoiler

GSHPB + SolarThermalCollector in mains_preheat placement.

Physical configuration

The STC preheats mains cold water before it enters the storage tank. The raised inlet temperature (T_tank_w_in_override_K) reduces the thermal load on the heat pump compressor.

Orchestration responsibility

This class owns all simulation logic for the STC:

  • _run_subsystems: activation probe + calc_performance() → sets T_tank_w_in_override_K when active

  • _augment_results: result column assembly (uses pump outlet temperature directly; no re-evaluation at solved tank temp)

  • _postprocess: STC exergy calculation. Tank boundary corrections are not applied because the preheated inlet temperature is already reflected in the core X_tank_w_in [W] column (X_in_tank_add = 0).

type stc:

SolarThermalCollector

param stc:

Pure physics engine. No mode constraint required.

type stc:

SolarThermalCollector

type **kwargs:

param **kwargs:

Forwarded to GroundSourceHeatPumpBoiler.

raises TypeError:

If stc is not a SolarThermalCollector instance.

__init__(*, stc, **kwargs)[source]
Parameters:

stc (SolarThermalCollector)

GSHPB with SolarThermalCollector — tank_circuit placement.

Phase 3 restructuring: all simulation orchestration logic (activation probe, result assembly, exergy calculation) is implemented directly in this class. SolarThermalCollector is used purely as a physics engine (calc_performance()), with no dependency on step(), assemble_results(), or calc_exergy().

Usage

from enex_analysis import SolarThermalCollector
from enex_analysis.gshpb_stc_tank import GSHPB_STC_tank

stc = SolarThermalCollector(A_stc=4.0)
model = GSHPB_STC_tank(
    stc=stc,
    ref="R134a",
    hp_capacity=15_000.0,
    T_tank_w_lower_bound=55.0,
    T_tank_w_upper_bound=65.0,
    T_mix_w_out=42.0,
)
df = model.analyze_dynamic(...)
class tmhp.gshpb_stc_tank.GSHPB_STC_tank(*, stc, **kwargs)[source]

Bases: GroundSourceHeatPumpBoiler

GSHPB + SolarThermalCollector in tank_circuit placement.

Physical configuration

The STC collector loop is connected directly to the storage tank. The STC draws water from the tank, heats it via solar energy, and returns it through the pump. The STC is activated only when the collector outlet temperature exceeds the current tank temperature.

Orchestration responsibility

This class owns all simulation logic for the STC:

  • _run_subsystems: activation probe + calc_performance()

  • _build_residual_fn: fully implicit coupling to tank solver

  • _augment_results: result column assembly (re-evaluates at solved tank temperature for accuracy)

  • _postprocess: STC exergy calculation and tank boundary correction (X_tot, Xc_tank)

type stc:

SolarThermalCollector

param stc:

Pure physics engine. No mode constraint required.

type stc:

SolarThermalCollector

type **kwargs:

param **kwargs:

Forwarded to GroundSourceHeatPumpBoiler.

raises TypeError:

If stc is not a SolarThermalCollector instance.

__init__(*, stc, **kwargs)[source]
Parameters:

stc (SolarThermalCollector)

GSHPB Scenario: Heat Pump driven by PV + ESS with Grid/Dump integration.

Energy routing (all logic lives here, subsystems are pure physics):

  1. PV generation → pv.calc_performance()

  2. DC routing: - PV surplus → ess.charge(); leftover → dump - PV deficit → ess.discharge(); leftover → grid import

  3. Inverter conversion loss applied to DC supply

  4. Grid import covers any remaining AC shortfall

class tmhp.gshpb_pv_ess.GSHPB_PV_ESS(*, pv, ess=None, eta_inv=0.95, T_inv_K=313.15, **kwargs)[source]

Bases: GroundSourceHeatPumpBoiler

GSHPB scenario where the heat pump is supplied by PV + ESS + Grid.

The PV/ESS routing is resolved synchronously inside _augment_results after the HP electrical load is known. No 1-step lag: the PV energy is allocated to the exact HP load produced in the same timestep.

Parameters:
  • pv (PhotovoltaicSystem) – Pure-physics PV + charge-controller model.

  • ess (EnergyStorageSystem) – Pure-physics battery model with charge() / discharge().

  • eta_inv (float) – Inverter DC→AC efficiency [–].

  • T_inv_K (float) – Inverter temperature for entropy calculation [K].

  • **kwargs – Forwarded to GroundSourceHeatPumpBoiler.

__init__(*, pv, ess=None, eta_inv=0.95, T_inv_K=313.15, **kwargs)[source]
Parameters: