From c1630381ea1cd79c53872821a5958329c953c16d Mon Sep 17 00:00:00 2001 From: hannesdiedrich Date: Wed, 3 Jun 2026 15:28:42 +0200 Subject: [PATCH] Small adaptions to SorTES heat pump strategy --- src/gsy_e/constants.py | 2 +- .../strategy/heatpump_with_sortes_tank.py | 35 ++++++++++++++++--- tests/strategies/test_sortes_heat_pump.py | 27 ++++++++++++-- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/gsy_e/constants.py b/src/gsy_e/constants.py index c1d16681b..57f7ff6a9 100644 --- a/src/gsy_e/constants.py +++ b/src/gsy_e/constants.py @@ -114,7 +114,7 @@ class SorTesConfiguration: MINUTES_BEFORE_SWITCH_ALLOWED = 1 * 60 MINUTES_TIME_HORIZONT_LOW_RATES = 1 * 60 - PREFERRED_BUYING_RATE = 20 + DEFAULT_PREFERRED_BUYING_RATE = 20 MIN_SOC_TOLERANCE = 10 MAX_SOC_TOLERANCE = 90 CAPACITY_KWH = 25 diff --git a/src/gsy_e/models/strategy/heatpump_with_sortes_tank.py b/src/gsy_e/models/strategy/heatpump_with_sortes_tank.py index c731079bf..b688852db 100644 --- a/src/gsy_e/models/strategy/heatpump_with_sortes_tank.py +++ b/src/gsy_e/models/strategy/heatpump_with_sortes_tank.py @@ -103,6 +103,7 @@ def __init__( self, energy_params: "SorTesTankEnergyParameters", average_trade_rate: Union[str, float, dict], + preferred_buying_rate: Optional[float] = None, ): self._last_switch: Optional[DateTime] = None self._energy_params = energy_params @@ -110,6 +111,11 @@ def __init__( self._average_trade_rate = profile_factory( average_trade_rate, None, profile_type=InputProfileTypes.IDENTITY ) + self._preferred_buying_rate = ( + SorTesConfiguration.DEFAULT_PREFERRED_BUYING_RATE + if preferred_buying_rate is None + else preferred_buying_rate + ) @property def current_state(self) -> HeatPumpChargingState: @@ -127,9 +133,7 @@ def _is_energy_affordable(self, time_slot: DateTime, _buy_rate: float) -> bool: <= ts < time_slot.add(minutes=SorTesConfiguration.MINUTES_TIME_HORIZONT_LOW_RATES) ] - return all( - value < SorTesConfiguration.PREFERRED_BUYING_RATE for value in rates_in_time_horizont - ) + return all(value < self._preferred_buying_rate for value in rates_in_time_horizont) def event_activate(self): """Perform commands on event activate.""" @@ -279,8 +283,10 @@ def __init__( self, heat_demand_Q_profile: Union[str, float, dict], ambient_temp_C_profile: Union[str, float, dict], + source_temp_C_profile: Union[str, float, dict], target_temp_C_profile: Union[str, float, dict], average_trade_rate: Union[str, float, dict], + preferred_buying_rate: Optional[float] = None, source_type: HeatPumpSourceType = ConstSettings.HeatPumpSettings.SOURCE_TYPE, ): # pylint: disable=too-many-arguments, too-many-positional-arguments @@ -292,6 +298,9 @@ def __init__( self._ambient_temp_C: StrategyProfileBase = profile_factory( ambient_temp_C_profile, None, profile_type=InputProfileTypes.IDENTITY ) + self._source_temp_C: StrategyProfileBase = profile_factory( + source_temp_C_profile, None, profile_type=InputProfileTypes.IDENTITY + ) self._target_temp_C: StrategyProfileBase = profile_factory( target_temp_C_profile, None, profile_type=InputProfileTypes.IDENTITY ) @@ -299,7 +308,15 @@ def __init__( self._cop_model = cop_model_factory(COPModelType.UNIVERSAL, source_type) self._bought_energy_kWh = 0.0 - self._soc_management = SorTesTankMinimiseSwitchStrategy(self, average_trade_rate) + self._soc_management = SorTesTankMinimiseSwitchStrategy( + energy_params=self, + average_trade_rate=average_trade_rate, + preferred_buying_rate=preferred_buying_rate, + ) + + # for serializer + self._average_trade_rate = average_trade_rate + self._preferred_buying_rate = preferred_buying_rate @property def state(self) -> SorTesTankState: @@ -311,8 +328,11 @@ def serialize(self): return { "heat_demand_Q_J": self._heat_demand_Q_J.input_profile, "ambient_temp_C": self._ambient_temp_C.input_profile, + "source_temp_C": self._source_temp_C.input_profile, "target_temp_C": self._target_temp_C.input_profile, "source_type": self._source_type, + "average_trade_rate": self._average_trade_rate, + "preferred_buying_rate": self._preferred_buying_rate, } @property @@ -365,7 +385,7 @@ def _populate_state(self, time_slot: DateTime): def _calc_and_set_cop(self, time_slot: DateTime): cop = self._cop_model.calc_cop( - source_temp_C=self._ambient_temp_C.get_value(time_slot), + source_temp_C=self._source_temp_C.get_value(time_slot), condenser_temp_C=self._target_temp_C.get_value(time_slot), ) self._state.set_cop(time_slot, cop) @@ -489,6 +509,7 @@ def _rotate_profiles(self, time_slot: Optional[DateTime] = None): self._state.delete_past_state_values(time_slot) self._heat_demand_Q_J.read_or_rotate_profiles() self._ambient_temp_C.read_or_rotate_profiles() + self._source_temp_C.read_or_rotate_profiles() self._target_temp_C.read_or_rotate_profiles() def _charge_or_discharge_tank(self, time_slot: DateTime): @@ -570,8 +591,10 @@ def __init__( self, heat_demand_Q_profile: Union[str, float, dict], ambient_temp_C_profile: Union[str, float, dict], + source_temp_C_profile: Union[str, float, dict], target_temp_C_profile: Union[str, float, dict], average_trade_rate: Union[str, float, dict], + preferred_buying_rate: Optional[float] = None, source_type: HeatPumpSourceType = ConstSettings.HeatPumpSettings.SOURCE_TYPE, order_updater_parameters: dict[ AvailableMarketTypes, HeatPumpOrderUpdaterParameters @@ -584,8 +607,10 @@ def __init__( self._energy_params = SorTesTankEnergyParameters( heat_demand_Q_profile=heat_demand_Q_profile, ambient_temp_C_profile=ambient_temp_C_profile, + source_temp_C_profile=source_temp_C_profile, target_temp_C_profile=target_temp_C_profile, average_trade_rate=average_trade_rate, + preferred_buying_rate=preferred_buying_rate, source_type=source_type, ) diff --git a/tests/strategies/test_sortes_heat_pump.py b/tests/strategies/test_sortes_heat_pump.py index aadfcd75b..ac90a8aa6 100644 --- a/tests/strategies/test_sortes_heat_pump.py +++ b/tests/strategies/test_sortes_heat_pump.py @@ -1,4 +1,5 @@ # pylint: disable=protected-access, attribute-defined-outside-init, too-many-public-methods +# pylint: disable=too-many-arguments, too-many-positional-arguments import math from unittest.mock import MagicMock, patch @@ -378,8 +379,10 @@ def test_delete_past_state_values_clears_all_time_series_attributes(self): AMBIENT_TEMP_C = 15.0 +SOURCE_TEMP_C = 15.0 TARGET_TEMP_C = 50.0 # delta = 35 → COP = 6.08 - 0.09*35 + 0.0005*35^2 = 6.08 - 3.15 + 0.6125 = 3.5425 +# delta uses SOURCE_TEMP_C (not ambient): TARGET_TEMP_C - SOURCE_TEMP_C = 50 - 15 = 35 EXPECTED_COP = 6.08 - 0.09 * 35 + 0.0005 * 35**2 # SorTesPerformanceMaps.get_power_charging(AMBIENT_TEMP_C + 5) = get_power_charging(20) = 3.0 kW @@ -417,6 +420,7 @@ def _create_energy_params( self, heat_demand_Q_profile=None, ambient_temp_C_profile=None, + source_temp_C_profile=None, target_temp_C_profile=None, average_trade_rate=25.0, ): @@ -424,11 +428,14 @@ def _create_energy_params( heat_demand_Q_profile = {START_TIME_SLOT: 3600.0, NEXT_SLOT: 3600.0} if ambient_temp_C_profile is None: ambient_temp_C_profile = {START_TIME_SLOT: AMBIENT_TEMP_C, NEXT_SLOT: AMBIENT_TEMP_C} + if source_temp_C_profile is None: + source_temp_C_profile = {START_TIME_SLOT: SOURCE_TEMP_C, NEXT_SLOT: SOURCE_TEMP_C} if target_temp_C_profile is None: target_temp_C_profile = {START_TIME_SLOT: TARGET_TEMP_C, NEXT_SLOT: TARGET_TEMP_C} return SorTesTankEnergyParameters( heat_demand_Q_profile=heat_demand_Q_profile, ambient_temp_C_profile=ambient_temp_C_profile, + source_temp_C_profile=source_temp_C_profile, target_temp_C_profile=target_temp_C_profile, average_trade_rate=average_trade_rate, ) @@ -526,7 +533,13 @@ def test_event_market_cycle_sets_heat_demand_in_state(self): def test_serialize_contains_required_keys(self): ep = self._create_energy_params() result = ep.serialize() - for key in ("heat_demand_Q_J", "ambient_temp_C", "target_temp_C", "source_type"): + for key in ( + "heat_demand_Q_J", + "ambient_temp_C", + "source_temp_C", + "target_temp_C", + "source_type", + ): assert key in result def test_serialize_source_type_matches_default(self): @@ -537,15 +550,18 @@ def test_serialize_source_type_matches_default(self): def test_serialize_input_profiles_match_constructor_arguments(self): heat_profile = {START_TIME_SLOT: 1000.0, NEXT_SLOT: 2000.0} ambient_profile = {START_TIME_SLOT: 10.0, NEXT_SLOT: 12.0} + source_profile = {START_TIME_SLOT: 8.0, NEXT_SLOT: 9.0} target_profile = {START_TIME_SLOT: 45.0, NEXT_SLOT: 50.0} ep = self._create_energy_params( heat_demand_Q_profile=heat_profile, ambient_temp_C_profile=ambient_profile, + source_temp_C_profile=source_profile, target_temp_C_profile=target_profile, ) result = ep.serialize() assert result["heat_demand_Q_J"] == heat_profile assert result["ambient_temp_C"] == ambient_profile + assert result["source_temp_C"] == source_profile assert result["target_temp_C"] == target_profile def test_event_market_cycle_min_demand_not_greater_than_max_demand(self): @@ -636,6 +652,7 @@ def _create_strategy(self, **kwargs): defaults = { "heat_demand_Q_profile": {START_TIME_SLOT: 3600.0, NEXT_SLOT: 3600.0}, "ambient_temp_C_profile": {START_TIME_SLOT: AMBIENT_TEMP_C, NEXT_SLOT: AMBIENT_TEMP_C}, + "source_temp_C_profile": {START_TIME_SLOT: SOURCE_TEMP_C, NEXT_SLOT: SOURCE_TEMP_C}, "target_temp_C_profile": {START_TIME_SLOT: TARGET_TEMP_C, NEXT_SLOT: TARGET_TEMP_C}, "average_trade_rate": 19.0, } @@ -723,7 +740,13 @@ def test_state_is_same_object_as_energy_params_state(self): def test_serialize_contains_all_energy_param_keys(self): strategy = self._create_strategy() result = strategy.serialize() - for key in ("heat_demand_Q_J", "ambient_temp_C", "target_temp_C", "source_type"): + for key in ( + "heat_demand_Q_J", + "ambient_temp_C", + "source_temp_C", + "target_temp_C", + "source_type", + ): assert key in result def test_serialize_contains_all_order_updater_keys(self):