Integrations

Optional adapters for coupling TMHP to building-energy simulators and co-simulation masters. These modules are intentionally kept out of the top-level package import path so the core package remains usable without the adapter-specific runtimes.

For usage-first guides, model boundaries, and simulator wiring, see EnergyPlus Python Plugin and FMI FMU export.

Install the FMI dependencies with the integrations extra:

uv sync --extra integrations --locked

The EnergyPlus adapter is different: pyenergyplus is supplied by an EnergyPlus installation and is not a PyPI dependency.

Interoperability contracts

FMI co-simulation is exposed through separate FMI 2.0 and FMI 3.0 adapters. FMI 2.0 uses PythonFMU; FMI 3.0 uses PythonFMU3. The generated FMUs are tool-coupling artifacts: Python, TMHP, CoolProp, NumPy, and SciPy must be available in the importing environment. The FMU model descriptions are validated with FMPy in tests. The XML declares units for power, temperature, DHW draw, and COP so importers can perform basic unit checks. The slaves reject invalid importer inputs (non-finite time/input values, non-positive communication step sizes, or negative DHW draw) before advancing the internal step() state and expose failure_reason="invalid_input". The FMI 3.0 slave reports that case as a discarded step with early return.

EnergyPlus coupling uses the Python Plugin DataExchange API. The adapter first resolves and validates all handles, then reads only finite boundary values before calling analyze_steady(). Invalid boundary data is reported through issue_severe(), the plant actuator is driven to a safe off state, and the plugin returns a non-zero status instead of silently computing with bad data. Compressor electricity is exposed with explicit plugin-global units: tmhp_E_cmp_J receives timestep energy in joules by multiplying cycle power in watts by EnergyPlus’s fractional-hour system_time_step(), while optional tmhp_E_cmp_W receives the instantaneous cycle power. Older IDFs that declare only tmhp_E_cmp are still accepted; that legacy global is treated as a joule sink.

Adapter package

Building-energy-simulation co-simulation adapters (#165).

Optional subpackage — each adapter needs an extra dependency that the core tmhp package does not require, so this package is not imported by tmhp/__init__.py; import tmhp stays usable without those deps and only from tmhp.integrations... import ... pulls them in.

Adapters depend only on the public seams of the heat-pump models (AirSourceHeatPumpBoiler.step() / .analyze_steady()), never on private helpers:

  • tmhp.integrations.fmu (#165 P1) — FMI 2.0 FMU wrapping step() (the dynamic kernel; the FMU owns the storage-tank state). Needs pythonfmu.

  • tmhp.integrations.fmu3 — FMI 3.0 Co-Simulation FMU wrapping the same step() seam. Needs pythonfmu3.

  • tmhp.integrations.energyplus_plugin (#165 P2) — EnergyPlus Python Plugin surrogating the ASHPB as a PlantComponent:UserDefined through analyze_steady() (the steady-state seam; EnergyPlus owns the WaterHeater:Mixed tank state). Runs inside EnergyPlus’s embedded CPython, so it needs the bundled pyenergyplus (not pip-installable).

class tmhp.integrations.TmhpAshpbSlave(**kwargs)[source]

Bases: Fmi2Slave

ASHPB single-timestep co-simulation kernel (FMI 2.0).

Parameters:

kwargs (Any)

author = 'BET Lab'
description = 'tmhp ASHPB one-dt co-simulation kernel (FMI 2.0)'
__init__(**kwargs)[source]
Parameters:

kwargs (Any)

to_xml(model_options=None)[source]

Build a static FMI 2.0 model description for PythonFMU.

Parameters:

model_options (Optional[dict[str, str]])

Return type:

Element

exit_initialization_mode()[source]
Return type:

None

do_step(current_time, step_size)[source]
Parameters:
  • current_time (float)

  • step_size (float)

Return type:

bool

class tmhp.integrations.TmhpAshpbFmi3Slave(**kwargs)[source]

Bases: Fmi3Slave

ASHPB single-timestep co-simulation kernel (FMI 3.0).

Parameters:

kwargs (Any)

author = 'BET Lab'
description = 'TMHP ASHPB one-dt co-simulation kernel (FMI 3.0)'
__init__(**kwargs)[source]
Parameters:

kwargs (Any)

to_xml(model_options=None)[source]

Build a static FMI 3.0 model description for PythonFMU3.

Parameters:

model_options (Optional[dict[str, str]])

Return type:

Element

exit_initialization_mode()[source]

Finalize parameters and initialize the carried ASHPB state.

Return type:

None

do_step(current_time, step_size)[source]

Advance the FMU by one FMI 3.0 communication step.

Parameters:
  • current_time (float)

  • step_size (float)

Return type:

Fmi3StepResult

class tmhp.integrations.TmhpPlantInit[source]

Bases: EnergyPlusPlugin

One-shot sizing actuators so the plant loop dispatches load to the ASHPB.

Bind to the PlantComponent:UserDefined initialization program-calling manager (E+ runs it once before plant sizing).

__init__()[source]
on_user_defined_component_model(state)[source]
Parameters:

state (Any)

Return type:

int

class tmhp.integrations.TmhpPlantSurrogate[source]

Bases: EnergyPlusPlugin

Per-call ASHPB driver: read loop inlet + outdoor, solve, actuate.

Bind to the PlantComponent:UserDefined simulation program-calling manager. Exposes compressor electricity through the plugin global tmhp_E_cmp_J (declare it in PythonPlugin:Variables and meter it as summed joules). tmhp_E_cmp_W can be declared for averaged power output.

__init__()[source]
on_user_defined_component_model(state)[source]
Parameters:

state (Any)

Return type:

int

FMI 2.0 co-simulation

FMI 2.0 co-simulation FMU wrapping the tmhp ASHPB step() kernel (#165 P1).

The slave advances AirSourceHeatPumpBoiler one communication step at a time through the public step() seam, so any FMI master (fmpy, OMSimulator, Dymola, …) can drive the refrigerant-cycle-resolved heat-pump model as a co-simulation component.

Build (PythonFMU CLI or API):

pythonfmu build -f src/tmhp/integrations/fmu.py <project_folder>
# or: FmuBuilder.build_FMU(".../fmu.py", dest="out/")

Run:

import fmpy
fmpy.simulate_fmu("TmhpAshpbSlave.fmu", ...)

Lead track is FMI 2.0 (co-simulation only): tmhp exposes no continuous state derivatives, and 2.0 co-sim is the most broadly importable flavour. Boundary outputs are sanitized to avoid non-finite numeric values, while converged and failure_reason preserve step-level diagnostics for the importing master.

Note

Native-wheel caveat (CoolProp/numpy/scipy): the FMU is a tool-coupling artifact, not a hermetic binary — the importing environment must provide tmhp and its native dependencies for the chosen (OS, arch, Python-ABI). No save-state/rollback at this scope (single-pass co-sim only).

class tmhp.integrations.fmu.TmhpAshpbSlave(**kwargs)[source]

Bases: Fmi2Slave

ASHPB single-timestep co-simulation kernel (FMI 2.0).

Parameters:

kwargs (Any)

author = 'BET Lab'
description = 'tmhp ASHPB one-dt co-simulation kernel (FMI 2.0)'
__init__(**kwargs)[source]
Parameters:

kwargs (Any)

to_xml(model_options=None)[source]

Build a static FMI 2.0 model description for PythonFMU.

Parameters:

model_options (Optional[dict[str, str]])

Return type:

Element

exit_initialization_mode()[source]
Return type:

None

do_step(current_time, step_size)[source]
Parameters:
  • current_time (float)

  • step_size (float)

Return type:

bool

FMI 3.0 co-simulation

FMI 3.0 co-simulation FMU wrapping the TMHP ASHPB step() kernel.

This adapter mirrors tmhp.integrations.fmu at the TMHP boundary but uses pythonfmu3 and the FMI 3.0 Fmi3StepResult return contract. It targets FMI 3.0 Co-Simulation only: the FMU owns the ASHPB dynamic state and advances it once for each importer communication step.

Build:

pythonfmu3 build -f src/tmhp/integrations/fmu3.py .

The adapter intentionally does not expose FMI 3.0 clocks, Scheduled Execution, or array variables. Those features are useful for embedded controls and multi-rate models; TMHP’s current ASHPB FMU boundary is scalar Co-Simulation.

class tmhp.integrations.fmu3.TmhpAshpbFmi3Slave(**kwargs)[source]

Bases: Fmi3Slave

ASHPB single-timestep co-simulation kernel (FMI 3.0).

Parameters:

kwargs (Any)

author = 'BET Lab'
description = 'TMHP ASHPB one-dt co-simulation kernel (FMI 3.0)'
__init__(**kwargs)[source]
Parameters:

kwargs (Any)

to_xml(model_options=None)[source]

Build a static FMI 3.0 model description for PythonFMU3.

Parameters:

model_options (Optional[dict[str, str]])

Return type:

Element

exit_initialization_mode()[source]

Finalize parameters and initialize the carried ASHPB state.

Return type:

None

do_step(current_time, step_size)[source]

Advance the FMU by one FMI 3.0 communication step.

Parameters:
  • current_time (float)

  • step_size (float)

Return type:

Fmi3StepResult

EnergyPlus Python Plugin

EnergyPlus Python Plugin adapter for the tmhp ASHPB (#165 P2).

Surrogates AirSourceHeatPumpBoiler inside EnergyPlus as a PlantComponent:UserDefined device. Each plant-solver call hands the plugin the loop inlet temperature, mass flow, specific heat, and load request (plus the site outdoor drybulb); the plugin solves the refrigerant cycle with the public analyze_steady() seam (#165 P0) and writes back the outlet-temperature and mass-flow actuators, routing compressor electricity to a metered output.

Why analyze_steady (not step()): EnergyPlus owns the storage-tank state through WaterHeater:Mixed and asks the component for a steady answer each timestep (given the inlet/outdoor conditions and the loop load, what is the leaving temperature and the electric input). The dynamic step() kernel — and its tank model — is instead what the FMU adapter (#165 P1, tmhp.integrations.fmu) wraps. The two BES adapters therefore ride on different seams of the same model.

Runtime: EnergyPlus runs Python plugins in its embedded CPython, so pyenergyplus is provided by the EnergyPlus install (not pip-installable) and tmhp + native deps (CoolProp/numpy/scipy) must be built for that interpreter’s ABI and pointed to via PythonPlugin:SearchPaths. Verify with an import-only smoke plugin first — a wrong-ABI CoolProp wheel fails silently.

Configuration (environment variables, all optional):

TMHP_ASHPB_REF

refrigerant (default R32; e.g. R290, R410A)

TMHP_ASHPB_CAPACITY

nominal HP capacity in W (default 15000)

TMHP_ASHPB_PRESET

named parameter preset (default empty = model defaults)

TMHP_ASHPB_VDISP_CC

compressor displacement in cm³ (required with preset)

TMHP_ASHPB_FAN_M3S

rated outdoor fan flow in m³/s (required with preset)

TMHP_UD_NAME

PlantComponent:UserDefined object name (default ASHPB_UserDefined)

TMHP_LOOP_DESIGN_VDOT

design loop volume flow in m³/s (default 0.003)

TMHP_EPLUS_ECMP_ENERGY_GLOBAL

plugin global receiving timestep energy [J] (default tmhp_E_cmp_J)

TMHP_EPLUS_ECMP_POWER_GLOBAL

optional plugin global receiving power [W] (default tmhp_E_cmp_W)

TMHP_PLUGIN_LOG

if set, append a per-call/tally log to this path

IDF wiring (see the issue #180 / the validated demo for a full example): one PlantComponent:UserDefined named ASHPB_UserDefined with an init manager bound to TmhpPlantInit and a simulation manager bound to TmhpPlantSurrogate, a PythonPlugin:Variables global tmhp_E_cmp_J for summed energy metering, and optionally tmhp_E_cmp_W for averaged power reporting. The legacy global tmhp_E_cmp is still accepted as an energy sink for older IDFs.

API strings (actuator component type "Plant Connection 1"; internal variables "... for Plant Connection 1") are verified against EnergyPlus 24.2.0; reconfirm them for other releases.

class tmhp.integrations.energyplus_plugin.TmhpPlantInit[source]

Bases: EnergyPlusPlugin

One-shot sizing actuators so the plant loop dispatches load to the ASHPB.

Bind to the PlantComponent:UserDefined initialization program-calling manager (E+ runs it once before plant sizing).

__init__()[source]
on_user_defined_component_model(state)[source]
Parameters:

state (Any)

Return type:

int

class tmhp.integrations.energyplus_plugin.TmhpPlantSurrogate[source]

Bases: EnergyPlusPlugin

Per-call ASHPB driver: read loop inlet + outdoor, solve, actuate.

Bind to the PlantComponent:UserDefined simulation program-calling manager. Exposes compressor electricity through the plugin global tmhp_E_cmp_J (declare it in PythonPlugin:Variables and meter it as summed joules). tmhp_E_cmp_W can be declared for averaged power output.

__init__()[source]
on_user_defined_component_model(state)[source]
Parameters:

state (Any)

Return type:

int