Air-source heat pump (ASHP — space conditioning)¶
ASHP conditions a building zone (heating + cooling) rather than charging a DHW tank. The refrigerant cycle and outdoor-coil source side are shared with the air-source boiler family; what differs is the demand side — a zone energy balance instead of a tank.
Overview¶
The class is tmhp.AirSourceHeatPump. Use it when the heat
pump’s job is space conditioning rather than DHW production.
Base usage¶
from tmhp import AirSourceHeatPump
ashp = AirSourceHeatPump(ref="R32")
# See API reference below for the full constructor and
# analyze_steady / analyze_dynamic signatures.
Source-side mechanics¶
Outdoor coil with variable-speed fan and an ε-NTU air-side heat exchanger — the shared air-source environmental-side model.
Sink-side mechanics¶
A zone temperature / load proxy stands in for the building. The
caller supplies indoor-unit load as Q_r_iu: positive values
select cooling, negative values select heating, and zero values
represent off operation. There is no tank energy balance.
API reference¶
Air source heat pump — physics-based cycle model with indoor unit.
Resolves a vapour-compression refrigerant cycle coupled to
an outdoor-air heat exchanger and an indoor-air heat exchanger.
Supports both cooling (Q_r_iu > 0) and heating (Q_r_iu < 0)
modes. The indoor load Q_r_iu is imposed externally each timestep.
At each time step the model finds the minimum-power operating point (compressor + indoor fan + outdoor fan) via bounded 2-D optimisation over the evaporator and condenser approach temperature differences.
Architecture mirrors AirSourceHeatPumpBoiler — uses the same
shared utility functions (calc_ref_state, calc_HX_perf_for_target_heat,
calc_fan_power_from_dV_fan) and the same postprocess_exergy()
pattern, but replaces the tank energy balance with direct air-side
heat exchange at the indoor unit.
- tmhp.air_source_heat_pump.OBJ_CAPACITY_LIMITED = 1000000000.0¶
Objective floor for a capacity-limited operating point (valid operation).
- tmhp.air_source_heat_pump.OBJ_INFEASIBLE = 1000000000000.0¶
Objective value for an operating point the cycle cannot reach (invalid).
- class tmhp.air_source_heat_pump.AirSourceHeatPump(ref='R32', V_cmp_ref=None, eta_cmp_isen=None, eta_cmp_vol=None, eta_cmp=None, dT_superheat=3.0, dT_subcool=3.0, UA_ou_rated=None, UA_iu_rated=None, n_ou=0.65, n_iu=0.65, dV_ou_fan_a_rated=None, dP_ou_fan_rated=None, A_cross_ou=None, eta_ou_fan_rated=None, dV_iu_fan_a_rated=None, dP_iu_fan_rated=None, A_cross_iu=None, eta_iu_fan_rated=None, hp_capacity=4000.0, T_a_room=27.0, dT_hx_min=0.5, PR_cycle_min=1.5, PR_cycle_max=5.0, rps_min=15.0, rps_max=150.0, dT_approach_bounds=(1.0, 20.0), vsd_coeffs_ou=None, vsd_coeffs_iu=None, V_disp_cmp=None, eta_cmp_mech=None, UA_cond_rated=None, UA_evap_rated=None, n_cond=None, n_evap=None, UA_cond_design=None, UA_evap_design=None, dV_ou_fan_a_design=None, dP_ou_fan_design=None, eta_ou_fan_design=None, dV_iu_fan_a_design=None, dP_iu_fan_design=None, eta_iu_fan_design=None, *, rps_rated=None, m_dot_ref_rated=None, rated_condition=None, dV_ou_fan_a_ref=None, dV_ou_fan_a_min=None, dV_ou_fan_a_max=None, dV_iu_fan_a_ref=None, dV_iu_fan_a_min=None, dV_iu_fan_a_max=None)[source]¶
Bases:
ReferenceStateMixinAir source heat pump with indoor-unit air heat exchange.
The refrigerant cycle is resolved via CoolProp with user-specified superheat / subcool margins. A bounded 2-D optimiser minimises total electrical input (
E_cmp + E_iu_fan + E_ou_fan) over the evaporator and condenser approach temperatures.When the requested duty exceeds what the compressor can deliver at
rps_max, the search switches to maximising delivered capacity — see_optimize_operation(). Such an hour is reported withcapacity_clamped == "max"and aQ_ref_iu [W]below the request; the difference is unmet load, not a solver failure.- Parameters:
ref (
str)V_cmp_ref (
float|None)eta_cmp_isen (
float|Callable|None)eta_cmp_vol (
float|Callable|None)eta_cmp (
float|Callable|None)dT_superheat (
float)dT_subcool (
float)UA_ou_rated (
float|None)UA_iu_rated (
float|None)n_ou (
float)n_iu (
float)dV_ou_fan_a_rated (
float|None)dP_ou_fan_rated (
float|None)A_cross_ou (
float|None)eta_ou_fan_rated (
float|None)dV_iu_fan_a_rated (
float|None)dP_iu_fan_rated (
float|None)A_cross_iu (
float|None)eta_iu_fan_rated (
float|None)hp_capacity (
float)T_a_room (
float)dT_hx_min (
float)PR_cycle_min (
float)PR_cycle_max (
float)rps_min (
float)rps_max (
float)dT_approach_bounds (
tuple[float,float])vsd_coeffs_ou (
dict|None)vsd_coeffs_iu (
dict|None)V_disp_cmp (
float|None)eta_cmp_mech (
float|Callable|None)UA_cond_rated (
float|None)UA_evap_rated (
float|None)n_cond (
float|None)n_evap (
float|None)UA_cond_design (
float|None)UA_evap_design (
float|None)dV_ou_fan_a_design (
float|None)dP_ou_fan_design (
float|None)eta_ou_fan_design (
float|None)dV_iu_fan_a_design (
float|None)dP_iu_fan_design (
float|None)eta_iu_fan_design (
float|None)rps_rated (
float|None)m_dot_ref_rated (
float|None)rated_condition (
Mapping[str,Any] |RatingCondition|None)dV_ou_fan_a_ref (
float|None)dV_ou_fan_a_min (
float|None)dV_ou_fan_a_max (
float|None)dV_iu_fan_a_ref (
float|None)dV_iu_fan_a_min (
float|None)dV_iu_fan_a_max (
float|None)
- __init__(ref='R32', V_cmp_ref=None, eta_cmp_isen=None, eta_cmp_vol=None, eta_cmp=None, dT_superheat=3.0, dT_subcool=3.0, UA_ou_rated=None, UA_iu_rated=None, n_ou=0.65, n_iu=0.65, dV_ou_fan_a_rated=None, dP_ou_fan_rated=None, A_cross_ou=None, eta_ou_fan_rated=None, dV_iu_fan_a_rated=None, dP_iu_fan_rated=None, A_cross_iu=None, eta_iu_fan_rated=None, hp_capacity=4000.0, T_a_room=27.0, dT_hx_min=0.5, PR_cycle_min=1.5, PR_cycle_max=5.0, rps_min=15.0, rps_max=150.0, dT_approach_bounds=(1.0, 20.0), vsd_coeffs_ou=None, vsd_coeffs_iu=None, V_disp_cmp=None, eta_cmp_mech=None, UA_cond_rated=None, UA_evap_rated=None, n_cond=None, n_evap=None, UA_cond_design=None, UA_evap_design=None, dV_ou_fan_a_design=None, dP_ou_fan_design=None, eta_ou_fan_design=None, dV_iu_fan_a_design=None, dP_iu_fan_design=None, eta_iu_fan_design=None, *, rps_rated=None, m_dot_ref_rated=None, rated_condition=None, dV_ou_fan_a_ref=None, dV_ou_fan_a_min=None, dV_ou_fan_a_max=None, dV_iu_fan_a_ref=None, dV_iu_fan_a_min=None, dV_iu_fan_a_max=None)[source]¶
- Parameters:
ref (
str)V_cmp_ref (
float|None)eta_cmp_isen (
float|Callable|None)eta_cmp_vol (
float|Callable|None)eta_cmp (
float|Callable|None)dT_superheat (
float)dT_subcool (
float)UA_ou_rated (
float|None)UA_iu_rated (
float|None)n_ou (
float)n_iu (
float)dV_ou_fan_a_rated (
float|None)dP_ou_fan_rated (
float|None)A_cross_ou (
float|None)eta_ou_fan_rated (
float|None)dV_iu_fan_a_rated (
float|None)dP_iu_fan_rated (
float|None)A_cross_iu (
float|None)eta_iu_fan_rated (
float|None)hp_capacity (
float)T_a_room (
float)dT_hx_min (
float)PR_cycle_min (
float)PR_cycle_max (
float)rps_min (
float)rps_max (
float)dT_approach_bounds (
tuple[float,float])vsd_coeffs_ou (
dict|None)vsd_coeffs_iu (
dict|None)V_disp_cmp (
float|None)eta_cmp_mech (
float|Callable|None)UA_cond_rated (
float|None)UA_evap_rated (
float|None)n_cond (
float|None)n_evap (
float|None)UA_cond_design (
float|None)UA_evap_design (
float|None)dV_ou_fan_a_design (
float|None)dP_ou_fan_design (
float|None)eta_ou_fan_design (
float|None)dV_iu_fan_a_design (
float|None)dP_iu_fan_design (
float|None)eta_iu_fan_design (
float|None)rps_rated (
float|None)m_dot_ref_rated (
float|None)rated_condition (
Mapping[str,Any] |RatingCondition|None)dV_ou_fan_a_ref (
float|None)dV_ou_fan_a_min (
float|None)dV_ou_fan_a_max (
float|None)dV_iu_fan_a_ref (
float|None)dV_iu_fan_a_min (
float|None)dV_iu_fan_a_max (
float|None)
- ref: str¶
- V_cmp_ref: float¶
- dT_superheat: float¶
- dT_subcool: float¶
- dT_hx_min: float¶
- PR_cycle_min: float¶
- PR_cycle_max: float¶
- rps_min: float¶
- rps_max: float¶
- dT_approach_bounds: tuple[float, float]¶
- hp_capacity: float¶
- n_ou: float¶
- n_iu: float¶
- dP_ou_fan_rated: float¶
- eta_ou_fan_rated: float¶
- E_ou_fan_rated: float¶
- vsd_coeffs_ou: dict¶
- fan_params_ou: dict¶
- dP_iu_fan_rated: float¶
- eta_iu_fan_rated: float¶
- E_iu_fan_rated: float¶
- vsd_coeffs_iu: dict¶
- fan_params_iu: dict¶
- T_a_room: float¶
- eta_cmp_isen: float | Callable¶
- eta_cmp_vol: float | Callable¶
- eta_cmp: float | Callable¶
- max_capacity(T0, T_a_room=None, *, mode='heating', grid_points=13, rel_tol=0.005)[source]¶
Largest duty the unit can actually deliver at this condition.
Nameplate capacity is a label attached to one rating point, not a ceiling. A real unit delivers more than nameplate when the outdoor air is mild and less when it is severe, because the limit is set by whichever of the compressor speed range, the pressure-ratio envelope and the air-side heat exchangers binds first. Clipping a load schedule at nameplate therefore errs in both directions at once. This returns the ceiling the model itself implies, so a schedule can be clipped against the machine rather than against its label and the shortfall reported as a result.
A ladder of trial duties brackets the ceiling and a bisection closes on it, each duty tested against the whole approach-temperature grid so the answer does not depend on a search path.
Warning
The answer is only as trustworthy as the compressor correlations at the speeds it reaches. Those correlations are fits, and a validation carried out at part load does not cover the top of the speed range. Check that the returned
cop_sys [-]is credible before using the ceiling, and consider loweringrps_maxto the highest speed the validation actually reached.- Parameters:
T0 (
float) – Outdoor air temperature [°C].T_a_room (
float|None) – Room air temperature [°C]. Uses the constructor default if None.mode (
str) –"heating"or"cooling".grid_points (
int) – Resolution of the approach-temperature sweep on each axis.rel_tol (
float) – Bisection stops once the bracket is this narrow relative to the ceiling.
- Returns:
Q_max [W](0.0 when the unit cannot run at all here), the approach temperatures there, the electrical input and system COP at that point, andbindingnaming what stopped it:"compressor"when the speed range ran out,"cycle"when the pressure-ratio envelope or the air side did, and"unbounded"when the ladder never ran out, which makes the figure a lower bound.- Return type:
dict
- analyze_steady(Q_r_iu, T0, T_a_room=None, *, return_dict=True, postprocess=True, verbose=True)[source]¶
Run a steady-state performance snapshot.
- Parameters:
Q_r_iu (
float) – Indoor thermal load [W]. >0 cooling, <0 heating, 0 off.T0 (
float) – Dead-state / outdoor-air temperature [°C].T_a_room (
float|None) – Room air temperature [°C]. Uses constructor default if None.return_dict (
bool) – If True return dict; else single-row DataFrame.postprocess (
bool) – If True, apply postprocess_exergy to the output.verbose (
bool) – If True, print warnings upon convergence failure.
- Returns:
Cycle state plus diagnostic flags.
Two keys are useful for branching:
"converged"(bool) — True only when the inner HX optimisation and the SciPy optimiser both succeeded."failure_reason"(str) — one of"none","cycle_invalid"(the refrigerant cycle itself was infeasible),"hx_not_converged"(cycle OK but the HX residual exceeded tolerance), or"optimizer_failed"(SciPy reportedsuccess=False).
ASHP triggers an off-mode fallback for any of the non-
"none"reasons —E_cmp [W]will be 0 and the COP keys will be NaN in that case. Treatfailure_reason != "none"as “do not trust the numbers”.- Return type:
dict|DataFrame
- analyze_dynamic(simulation_period_sec, dt_s, Q_r_iu_schedule, T0_schedule, T_a_room_schedule=None, result_save_csv_path=None)[source]¶
Run a time-stepping dynamic simulation.
- Parameters:
simulation_period_sec (
int) – Total simulation duration [s].dt_s (
int) – Time step size [s].Q_r_iu_schedule (array-like) – Indoor thermal load per step [W].
T0_schedule (array-like) – Outdoor temperature per step [°C].
T_a_room_schedule (array-like | None) – Room air temperature per step [°C]. If None, uses constructor default.
result_save_csv_path (
str|None) – Optional CSV output path.
- Returns:
Per-timestep result DataFrame.
- Return type:
DataFrame
- postprocess_exergy(df)[source]¶
Compute ASHP-specific exergy variables.
Mirrors
AirSourceHeatPumpBoiler.postprocess_exergy()with adaptations for indoor-unit air exchange.Pipeline:
Refrigerant state-point exergy (CoolProp)
Electricity = exergy (compressor, IU fan, OU fan)
Air exergy (outdoor unit + indoor unit)
HX Carnot exergy (condenser, evaporator)
Component-level exergy destruction
Exergetic efficiency metrics
- Parameters:
df (
DataFrame)- Return type:
DataFrame