Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a7c3229
GSYE-891: Fix some small things in the heat pump classes and adapt Mi…
hannesdiedrich May 13, 2026
ca1f6f0
GSYE-891: Add first version of SorTES tank heat pump
hannesdiedrich May 13, 2026
7dabf55
GSYE-891: Fix some issues in the SorTesTankEnergyParameters and adapt…
hannesdiedrich May 13, 2026
c58b317
GSYE-891: Add unit tests from AI
hannesdiedrich May 13, 2026
0d73c49
GSYE-891: Slight changes to the unit tests
hannesdiedrich May 13, 2026
7790033
GSYE-891: Fix corner case heat-demand < energy-extracted from sortes
hannesdiedrich May 18, 2026
864b29c
GSYE-891: Remove HeatPumpChargingState.MAINTAIN_SOC from the MINUTES_…
hannesdiedrich May 18, 2026
7a02b20
GSYE-891: Add time horizont for SorTesTankMinimiseSwitchStrategy._is_…
hannesdiedrich May 18, 2026
b0f89ef
GSYE-891: Exchange comparison with MMR to preferred bying rate
hannesdiedrich May 19, 2026
de7d499
Rename column in Sortes export file in order to align with Fractles e…
hannesdiedrich May 19, 2026
095993a
GSYE-891: Fix bug in energy assertions ans also correct the condenser…
hannesdiedrich May 21, 2026
0f2b27a
GSYE-891: Adapt corner case logging in _calc_energy_to_buy_minimum in…
hannesdiedrich May 22, 2026
720eacf
GSYE-891: Add serializor for sortes heat pump
hannesdiedrich May 22, 2026
5e53114
GSYE-891: Add _handle_state_at_soc_limits in order to seperate functi…
hannesdiedrich May 27, 2026
e940291
GSYE-891: Improve export CSV files for Sortes HP
hannesdiedrich May 27, 2026
efe2125
GSYE-891: Only log corner case when energy_to_be_bought_for_heat is l…
hannesdiedrich May 27, 2026
991d9d9
GSYE-891: Change COp value for dry cooler
hannesdiedrich May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/gsy_e/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,20 @@ class HeatPumpSettingsDefaultParameters:

# Set the precision of the decimal numbers used in the simulation.
getcontext().prec = 12


class SorTesConfiguration:
"""Collection of SorTes heat tank configuration parameters."""

MINUTES_BEFORE_SWITCH_ALLOWED = 1 * 60
MINUTES_TIME_HORIZONT_LOW_RATES = 1 * 60
PREFERRED_BUYING_RATE = 20
MIN_SOC_TOLERANCE = 10
MAX_SOC_TOLERANCE = 90
CAPACITY_KWH = 25
COP_HEAT_SOURCE = 1
COP_CONDENSER = 35 # nominal value for SHSL-D1-005-1x350
COP_EVAPORATOR = 1
CONVERSION_CHARGE_CONDENSER_POWER = 1 / 1.2
CONVERSION_DISCHARGE_EVAPORATOR_POWER = 1 / 1.2
AMBIENT_TEMPERATURE_CORRECTION = 5
56 changes: 55 additions & 1 deletion src/gsy_e/gsy_e_core/sim_results/file_export_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""

# pylint: disable=too-many-return-statements, broad-exception-raised
# pylint: disable=too-many-return-statements, broad-exception-raised, too-many-locals
from abc import ABC, abstractmethod
from typing import Dict, List

Expand All @@ -33,6 +33,7 @@
MultipleTankHeatPumpStrategy,
HeatPumpStrategyWithoutTanks,
)
from gsy_e.models.strategy.heatpump_with_sortes_tank import HeatPumpWithSorTesTankStrategy
from gsy_e.models.strategy.load_hours import LoadHoursStrategy
from gsy_e.models.strategy.pv import PVStrategy
from gsy_e.models.strategy.storage import StorageStrategy
Expand All @@ -50,6 +51,14 @@ def is_heatpump_strategy_without_tanks(area: Area):
)


def is_heatpump_strategy_with_sortes_tank(area: Area):
"""Return if area has a heat pump strategy with Sortes tanks connected."""
return isinstance(
area.strategy,
HeatPumpWithSorTesTankStrategy,
)


def is_heatpump_strategy_with_tanks(area: Area):
"""Return if area has a heat pump strategy."""
return isinstance(
Expand Down Expand Up @@ -407,6 +416,49 @@ def _row(self, slot, market):
return rows


class HeatPumpWithSortesTankDataExporter(BaseDataExporter):
"""Data exporter dedicated to heat pump areas"""

def __init__(self, area, past_markets):
assert is_heatpump_strategy_with_sortes_tank(area)
self.area = area
self.past_markets = past_markets

@property
def labels(self) -> List:
return [
"slot",
"energy traded [kWh]",
"COP",
"heat demand [kJ]",
"SOC",
"auxiliary_energy_kWh",
"energy_used_for_dis_charging_kWh",
]

@property
def rows(self) -> List:
return [self._row(market.time_slot, market) for market in self.past_markets]

def _traded(self, market):
return (
market.traded_energy[self.area.name] if self.area.name in market.traded_energy else 0
)

def _row(self, slot, market):
rows = [slot]
hp_stats = self.area.strategy.state.get_results_dict(slot)
rows += [
round(self._traded(market), ROUND_TOLERANCE_EXPORT),
round(hp_stats["cop"], ROUND_TOLERANCE_EXPORT),
round(hp_stats["heat_demand_kJ"], ROUND_TOLERANCE_EXPORT),
round(hp_stats["soc"], ROUND_TOLERANCE_EXPORT),
round(hp_stats["auxiliary_energy_kWh"], 4),
round(hp_stats["energy_used_for_dis_charging_kWh"], 4),
]
return rows


class FileExportEndpoints:
"""Handle data preparation for csv-file and plot export."""

Expand Down Expand Up @@ -439,6 +491,8 @@ def export_data_factory(
return HeatPumpDataExporter(area, area.parent.past_markets)
if is_heatpump_strategy_without_tanks(area):
return HeatPumpWithoutTanksDataExporter(area, area.parent.past_markets)
if is_heatpump_strategy_with_sortes_tank(area):
return HeatPumpWithSortesTankDataExporter(area, area.parent.past_markets)
return LeafDataExporter(area, area.parent.past_markets)
if past_market_type == AvailableMarketTypes.BALANCING:
return BalancingDataExporter(area.past_balancing_markets)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,8 @@ def combined_state(self) -> CombinedHeatpumpTanksState:
"""Combined heatpump and tanks state."""
return self._state

def last_time_slot(self, current_market_slot: DateTime) -> DateTime:
@staticmethod
def last_time_slot(current_market_slot: DateTime) -> DateTime:
"""Calculate the previous time slot from the current one."""
return current_market_slot - GlobalConfig.slot_length

Expand Down
9 changes: 3 additions & 6 deletions src/gsy_e/models/strategy/heat_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from typing import Dict, TYPE_CHECKING, Optional, Union, List
from decimal import Decimal

from gsy_framework.constants_limits import ConstSettings
from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE
from gsy_framework.constants_limits import ConstSettings, FLOATING_POINT_TOLERANCE
from gsy_framework.data_classes import Trade, TraderDetails
from gsy_framework.enums import AvailableMarketTypes
from gsy_framework.exceptions import GSyException
Expand Down Expand Up @@ -272,8 +271,7 @@ def __init__(
)

def _init_price_params(self, order_updater_parameters, preferred_buying_rate):
self.use_default_updater_params: bool = not order_updater_parameters
if self.use_default_updater_params:
if not order_updater_parameters:
order_updater_parameters = {
AvailableMarketTypes.SPOT: HeatPumpOrderUpdaterParameters()
}
Expand Down Expand Up @@ -423,8 +421,7 @@ def serialize(self):
}

def _init_price_params(self, order_updater_parameters):
self.use_default_updater_params: bool = not order_updater_parameters
if self.use_default_updater_params:
if not order_updater_parameters:
order_updater_parameters = {
AvailableMarketTypes.SPOT: HeatPumpOrderUpdaterParameters()
}
Expand Down
44 changes: 33 additions & 11 deletions src/gsy_e/models/strategy/heat_pump_soc_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def event_activate(self):
else:
assert False, "GlobalConfig.market_maker_rate was not initiated yet."

def calculate(self, time_slot: DateTime, _buy_rate: float = 0.0):
def calculate(self, time_slot: DateTime, buy_rate: float = 0.0):
"""
Calculate the bid energy depending on the current state of the heat pump, the current SOC
and whether the market maker rate is cheap or expensive compared to the average market
Expand All @@ -83,13 +83,10 @@ def calculate(self, time_slot: DateTime, _buy_rate: float = 0.0):
if not self._is_time_for_state_change(time_slot):
# If state change is not possible, but at the same time the soc is below the min or
# above the max SOC value of the tank, then maintain the SOC.
if self._charger.get_average_soc(time_slot) >= self.MAX_SOC_TOLERANCE:
target_state = HeatPumpChargingState.MAINTAIN_SOC
if self._charger.get_average_soc(time_slot) <= self.MIN_SOC_TOLERANCE:
target_state = HeatPumpChargingState.MAINTAIN_SOC
target_state = self._handle_state_at_soc_limits(time_slot, target_state)
else:
# If the state change is possible, check the market maker rate to set the new state
target_state = self._should_charge_or_discharge(time_slot)
target_state = self._should_charge_or_discharge(time_slot, buy_rate)
if self._current_state != target_state:
# If the target state is the same as the current state, do nothing. Otherwise,
# update the current state and the last switch timestamp.
Expand All @@ -98,13 +95,32 @@ def calculate(self, time_slot: DateTime, _buy_rate: float = 0.0):

return self._get_energy_from_target_state(target_state, time_slot)

def _handle_state_at_soc_limits(
self, time_slot: DateTime, current_state: HeatPumpChargingState
) -> HeatPumpChargingState:
# If state change is not possible, but at the same time the soc is below the min or
# above the max SOC value of the tank, then maintain the SOC.
target_state = current_state
if self._get_tank_soc(time_slot) >= self.MAX_SOC_TOLERANCE:
target_state = HeatPumpChargingState.MAINTAIN_SOC
if self._get_tank_soc(time_slot) <= self.MIN_SOC_TOLERANCE:
target_state = HeatPumpChargingState.MAINTAIN_SOC
return target_state

def _get_tank_soc(self, time_slot: DateTime) -> float:
return self._charger.get_average_soc(time_slot)

def _is_time_for_state_change(self, time_slot: DateTime) -> bool:
if not self._last_switch:
# If the last_switch has not been set yet, the simulation is starting and no switch has
# occurred yet. Change state if needed.
self._last_switch = time_slot
return True
if time_slot - self._last_switch < duration(minutes=self.MINUTES_BEFORE_SWITCH_ALLOWED):
if (
self._current_state != HeatPumpChargingState.MAINTAIN_SOC
and time_slot - self._last_switch
< duration(minutes=self.MINUTES_BEFORE_SWITCH_ALLOWED)
):
# If not enough time has passed since the last switch, do not allow state change.
return False
# Otherwise, allow the state change.
Expand All @@ -119,20 +135,26 @@ def _get_energy_from_target_state(
return self._energy_params.get_min_energy_demand_kWh(time_slot)
return self._energy_params.get_energy_demand_kWh(time_slot)

def _should_charge_or_discharge(self, time_slot: DateTime) -> HeatPumpChargingState:
if GlobalConfig.market_maker_rate[time_slot] <= self._average_rate:
def _should_charge_or_discharge(
self, time_slot: DateTime, buy_rate: float
) -> HeatPumpChargingState:

if self._is_energy_affordable(time_slot, buy_rate):
# If the market maker rate is lower than the average rate, charge except if the SOC is
# too high.
if self._charger.get_average_soc(time_slot) >= self.MAX_SOC_TOLERANCE:
if self._get_tank_soc(time_slot) >= self.MAX_SOC_TOLERANCE:
return HeatPumpChargingState.MAINTAIN_SOC
return HeatPumpChargingState.CHARGE

# If the market maker rate is higher than the average rate, discharge except if the SOC
# is too low.
if self._charger.get_average_soc(time_slot) > self.MIN_SOC_TOLERANCE:
if self._get_tank_soc(time_slot) > self.MIN_SOC_TOLERANCE:
return HeatPumpChargingState.DISCHARGE
return HeatPumpChargingState.MAINTAIN_SOC

def _is_energy_affordable(self, time_slot: DateTime, _buy_rate: float) -> bool:
return GlobalConfig.market_maker_rate[time_slot] <= self._average_rate


def heat_pump_soc_management_factory(
energy_params: "HeatPumpEnergyParameters", preferred_buying_rate: float
Expand Down
Loading
Loading