288 water to water heat pump bypass mode#344
Conversation
… in heattransfer solver asset.
There was a problem hiding this comment.
Pull request overview
This PR introduces a bypass mode for the water-to-water heat pump / heat transfer asset and propagates related sign/setpoint and controller-path/factor changes through the controller layer, solver asset equations, and unit/integration tests.
Changes:
- Add
bypass_modesupport to the solverHeatTransferAsset, with separate “normal” vs “bypass” equation generation. - Extend heat pump/controller setpoints to support bypass and primary-side mass-flow/pressure control, and adjust network factor/path handling.
- Update a broad set of unit/integration tests to reflect new sign conventions, setpoint keys, and solver behavior.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| unit_test/solver/network/assets/test_heat_transfer_asset.py | Updates expectations for equation selection/call counts and mass-flow setup in solver-level tests. |
| unit_test/integration/test_heat_transfer_asset.py | Adjusts integration scenarios for updated mass-flow initialization and new prescribe flags. |
| unit_test/entities/test_production_cluster.py | Adapts convergence test to new sign convention behavior. |
| unit_test/entities/test_heat_pump.py | Updates heat pump tests for new sign conventions and additional solver state; currently misaligned with new required setpoint keys. |
| unit_test/entities/test_heat_exchanger.py | Updates mass-flow setup and expected output sign. |
| unit_test/entities/test_ates_cluster.py | Adjusts expected well temperatures due to changed ATES behavior. |
| unit_test/entities/controller/test_controller_new_class.py | Updates expectations for factor_to_first_network now being a list of factors. |
| unit_test/entities/controller/test_controller_network.py | Updates factor representation and storage setpoint expectations; adapts pressure-setting behavior. |
| src/omotes_simulator_core/solver/network/assets/heat_transfer_asset.py | Implements bypass mode and refactors equation generation; changes flow-direction handling and electric power computation. |
| src/omotes_simulator_core/infrastructure/app.py | Changes default simulation window/timestep and hardcodes a bypass test run + CSV export in __main__. |
| src/omotes_simulator_core/entities/network_controller.py | Refactors factor/path application, storage charge/discharge logic, heat-transfer setpoints, and pressure-setting key selection. |
| src/omotes_simulator_core/entities/assets/utils.py | Changes heat-demand↔mass-flow conversion to use internal energy difference instead of heat capacity approximation. |
| src/omotes_simulator_core/entities/assets/production_cluster.py | Adjusts mass-flow calculation sign and convergence criterion. |
| src/omotes_simulator_core/entities/assets/heat_pump.py | Adds bypass setpoint support, primary-side control flag, and updates setpoint parsing/sign conventions. |
| src/omotes_simulator_core/entities/assets/heat_exchanger.py | Adjusts heat-loss sign convention in outputs. |
| src/omotes_simulator_core/entities/assets/demand_cluster.py | Updates convergence check sign usage (now sensitive to negative allocations). |
| src/omotes_simulator_core/entities/assets/controller/controller_network.py | Changes factor tracking to list-of-factors and uses a product; extends storage setpoints; updates pressure-setting API. |
| src/omotes_simulator_core/entities/assets/controller/controller_heat_transfer.py | Adds bypass option and updates how primary/secondary heat demand is set (factor vs bypass behavior). |
| src/omotes_simulator_core/entities/assets/ates_cluster.py | Updates temperature assignment and convergence logic; tweaks ROSIM initialization/run cadence; adds heat supplied computation. |
| src/omotes_simulator_core/entities/assets/asset_defaults.py | Adds PROPERTY_BYPASS constant. |
| src/omotes_simulator_core/entities/assets/asset_abstract.py | Removes solver_asset type annotation from the base abstract asset. |
| src/omotes_simulator_core/adapter/transforms/controller_mapper.py | Changes “root network” selection for path computation and updates how paths are generated. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| self.pre_scribe_mass_flow_secondary = pre_scribe_mass_flow_secondary | ||
| # Define the flag that indicates whether the mass flow rate or the pressure is prescribed | ||
| # at the hot side of the heat pump | ||
| self.pre_scribe_mass_flow_primary = pre_scribe_mass_flow_secondary | ||
| # Define the mass flow rate set point for the asset on the secondary side |
There was a problem hiding this comment.
pre_scribe_mass_flow_primary is initialized from pre_scribe_mass_flow_secondary, so the primary-side control flag will always mirror the secondary-side flag. This prevents independently controlling primary mass-flow vs pressure (and is likely a copy/paste bug). Add a dedicated constructor parameter (or default) for pre_scribe_mass_flow_primary and assign it here.
| # Assign setpoints to the HeatPump asset | ||
| self.temperature_in_primary = setpoints_primary[PRIMARY + PROPERTY_TEMPERATURE_IN] | ||
| if self.first_time_step or self.solver_asset.prev_sol[0] == 0.0: | ||
| self.temperature_in_primary = setpoints_primary[SECONDARY + PROPERTY_TEMPERATURE_IN] |
There was a problem hiding this comment.
_set_setpoints_primary() reads setpoints_primary[SECONDARY + PROPERTY_TEMPERATURE_IN], but the required keys are defined with the PRIMARY prefix. This will raise KeyError (or use the wrong temperature) whenever primary and secondary temperatures differ. Use PRIMARY + PROPERTY_TEMPERATURE_IN here.
| self.temperature_in_primary = setpoints_primary[SECONDARY + PROPERTY_TEMPERATURE_IN] | |
| self.temperature_in_primary = setpoints_primary[PRIMARY + PROPERTY_TEMPERATURE_IN] |
| setpoints = { | ||
| PRIMARY + PROPERTY_TEMPERATURE_IN: 273.15 + 10.0, | ||
| PRIMARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 20.0, | ||
| PRIMARY + PROPERTY_HEAT_DEMAND: 300, | ||
| PROPERTY_SET_PRESSURE: False, |
There was a problem hiding this comment.
This test passes PROPERTY_SET_PRESSURE, but _set_setpoints_primary() now requires PRIMARY + PROPERTY_SET_PRESSURE. Align the test setpoints dict with the required key to avoid a missing-key ValueError.
| return abs(self.get_heat_supplied() - (-self.thermal_power_allocation)) < ( | ||
| (-self.thermal_power_allocation) * 0.001 | ||
| return abs(self.get_heat_supplied() - self.thermal_power_allocation) < ( | ||
| self.thermal_power_allocation * 0.001 |
There was a problem hiding this comment.
The convergence tolerance should be non-negative. Since consumer profiles can yield negative demands, self.thermal_power_allocation * 0.001 can become negative and make this check always fail. Use abs(self.thermal_power_allocation) * 0.001 for the tolerance.
| self.thermal_power_allocation * 0.001 | |
| abs(self.thermal_power_allocation) * 0.001 |
There was a problem hiding this comment.
Zou hier inderdaad niet nog een extra abs ergens omheen moeten?
| else: | ||
| if self.iteration_flow_direction_secondary == FlowDirection.ZERO: | ||
| pset_out = self.pressure_set_point_secondary | ||
| pset_in = self.pressure_set_point_secondary | ||
| else: | ||
| if self.iteration_flow_direction_secondary == FlowDirection.POSITIVE: | ||
| pset_out = self.pressure_set_point_secondary / 2 | ||
| pset_in = self.pressure_set_point_secondary | ||
| else: | ||
| pset_out = self.pressure_set_point_secondary | ||
| pset_in = self.pressure_set_point_secondary / 2 | ||
| equations.append( |
There was a problem hiding this comment.
In bypass mode, pressure splitting depends on iteration_flow_direction_secondary, but that value is never recomputed inside get_equations_bypass(). This can become stale across timesteps/setpoint changes and lead to incorrect pset_in/pset_out selection. Recompute the iteration flow direction(s) at the start of bypass equation generation, similar to get_equations_normal().
| SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 15.0, | ||
| SECONDARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 25.0, | ||
| SECONDARY + PROPERTY_HEAT_DEMAND: 310, | ||
| SECONDARY + PROPERTY_HEAT_DEMAND: -310, | ||
| PROPERTY_SET_PRESSURE: True, # Boolean value | ||
| } |
There was a problem hiding this comment.
These secondary-setpoint tests no longer match the HeatPump API: _set_setpoints_secondary() now requires SECONDARY + PROPERTY_SET_PRESSURE and PROPERTY_BYPASS, but the test still passes PROPERTY_SET_PRESSURE and omits PROPERTY_BYPASS. Update the test inputs/expectations accordingly (including the sign convention changes in the implementation).
| result = run(r".\testdata\heat_pump_bypass.esdl") | ||
| t2 = datetime.now() | ||
|
|
||
| logger.info(f"Results dataframe shape=({result.shape})") | ||
| result.to_csv(r".\testdata\heat_pump_bypass_results.csv", index=False) |
There was a problem hiding this comment.
The __main__ block is hardcoded to run heat_pump_bypass.esdl and write a CSV into ./testdata, which is a surprising side effect and can break when run from other working directories. Consider removing these hardcoded paths or gating them behind explicit CLI args/flags.
| import datetime | ||
|
|
||
| from numpy.ma.core import product | ||
|
|
||
| from omotes_simulator_core.entities.assets.asset_defaults import ( |
There was a problem hiding this comment.
Importing product from numpy.ma.core relies on NumPy’s internal/private module structure. Prefer numpy.prod(...) or math.prod(...) (and import from the public namespace) to avoid breakage across NumPy versions.
There was a problem hiding this comment.
Just import numpy or import math.prod.
| \dot{m}_{0} + \dot{m}_{1} = 0 | ||
|
|
||
| If the mass flow at the inflow node of the primary and secondary side is not zero, we | ||
| prescribe the follwoing energy balance equation for the heat transfer asset: |
There was a problem hiding this comment.
Typo in docstring: "follwoing" → "following".
| prescribe the follwoing energy balance equation for the heat transfer asset: | |
| prescribe the following energy balance equation for the heat transfer asset: |
vanmeerkerk
left a comment
There was a problem hiding this comment.
Er is erg veel gewijzgd waarvan sommige onderdelen naar mijn mening een apart issue hadden moeten worden. Over het algemeen ziet het er oké uit nog wat docstring opmerkingen.
Ik vermoed wel dat er nu een fout wordt gemaakt in het heat_transfer asset met de huidige implementatie.
| SECONDARY + PROPERTY_TEMPERATURE_OUT: 273.15 + 80, | ||
| SECONDARY + PROPERTY_TEMPERATURE_IN: 273.15 + 50, | ||
| PROPERTY_SET_PRESSURE: False, | ||
| if bypass: |
There was a problem hiding this comment.
Ik heb hier wel een vraag over. Zetten we nu standaard temperaturen in dit asset met set_asset of is dit alleen een initialisatie stap?
There was a problem hiding this comment.
Klopt de volgorder van de temperaturen; ik zou verwachtten dat PRIM_OUT gleijk moet zijn aan SEC_IN. Dit lijkt nu niet het geval?
| import datetime | ||
|
|
||
| from numpy.ma.core import product | ||
|
|
||
| from omotes_simulator_core.entities.assets.asset_defaults import ( |
There was a problem hiding this comment.
Just import numpy or import math.prod.
| for storage in self.storages: | ||
| storage_settings[storage.id] = { | ||
| PROPERTY_HEAT_DEMAND: +1 * storage.effective_max_charge_power * factor, | ||
| PROPERTY_TEMPERATURE_OUT: storage.temperature_out, |
There was a problem hiding this comment.
Waarom heb je de temperatuur nodig voor een storage? Deze wordt toch niet geset maar gebasseerd op de internal state?
| @@ -45,8 +45,6 @@ class AssetAbstract(ABC): | |||
|
|
|||
| connected_ports: list[str] | |||
| """List of ids of the connected ports.""" | |||
There was a problem hiding this comment.
Je verwijdert solver_asset: Waarom? Alles refereert intern nog steeds naar solver_asset...
| @@ -171,23 +169,30 @@ def set_setpoints(self, setpoints: dict) -> None: | |||
| setpoints_set = set(setpoints.keys()) | |||
| # Check if all setpoints are in the setpoints | |||
| if necessary_setpoints.issubset(setpoints_set): | |||
There was a problem hiding this comment.
Persoonlijk vind ik de -1 * explicieter, waardoor het direct zichtbaar is dat er nog iets met de waarde gebeurt. De - is te missen.
| self.flow_direction_secondary = self.flow_direction( | ||
| self.mass_flow_rate_rate_set_point_secondary | ||
| ) | ||
| self.flow_direction_primary = self.flow_direction(self.prev_sol[0]) |
There was a problem hiding this comment.
Hiermee bypass je wel je initiele setpoints; is dat handig?
| @@ -270,7 +281,7 @@ def get_equations(self) -> list[EquationObject]: | |||
| ) = self.get_ordered_connection_point_list() | |||
|
|
|||
| if np.all(np.abs(self.prev_sol[0:-1:3]) < MASSFLOW_ZERO_LIMIT): | |||
There was a problem hiding this comment.
Klopt het nu dat we alleen de richting zetten op basis van de vorige oplossing als de stroming 0 was (< MASSFLOW_ZERO_LIMIT); ik vraag mij af of dit goed gaat.
| if iteration_flow_direction_secondary == FlowDirection.ZERO: | ||
| mset = 0.0 | ||
| if self.iteration_flow_direction_secondary == FlowDirection.ZERO: | ||
| mset = self.mass_flow_rate_rate_set_point_secondary |
There was a problem hiding this comment.
Dus als 0 forceren we niet langer 0 maar gaan we terug naar het setpoint; dit kan niet juist zijn?
There was a problem hiding this comment.
Als je dit doet kan je net zo goed deze if loop weghalen en altijd het setpoint zetten. Ik denk dat je hier een fout maakt om het te laten werken. Ik herinner mij dat het zetten van mset=0.0 resulteerde in een matrix fout maar dat zit eerder in hoe je je setpoints zet op basis van een vorige oplossing.
| if (self.iteration_flow_direction_primary != FlowDirection.ZERO) or ( | ||
| self.iteration_flow_direction_secondary != FlowDirection.ZERO | ||
| ): | ||
| equations.append( |
There was a problem hiding this comment.
Hier gaat echt iets fout met het zetten vvan m=0.0
| ) | ||
| return equations | ||
|
|
||
| def set_internal_energy_equations_bypass(self, equations): |
Unit test are not yet working but code seems to be working