Skip to content

Commit

Permalink
Merge branch 'master' into feature/GSYE-636-hp-param-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
spyrostz authored Oct 16, 2023
2 parents 64f3d45 + 595b11f commit f1e9f00
Show file tree
Hide file tree
Showing 89 changed files with 1,128 additions and 922 deletions.
5 changes: 4 additions & 1 deletion src/gsy_e/gsy_e_core/area_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
Leaf, scm_leaf_mapping, CoefficientLeaf, forward_leaf_mapping,
forecast_scm_leaf_mapping) # NOQA
from gsy_e.models.leaves import * # NOQA # pylint: disable=wildcard-import
from gsy_e.models.strategy.trading_strategy_base import TradingStrategyBase

logger = getLogger(__name__)

Expand All @@ -55,8 +56,10 @@ def default(self, o):
return self._encode_leaf(o)
if isinstance(o, BaseStrategy):
return self._encode_subobject(o)
if isinstance(o, TradingStrategyBase):
return self._encode_subobject(o)
if isinstance(o, Duration):
return None
return o.seconds
return super().default(o)

@staticmethod
Expand Down
15 changes: 12 additions & 3 deletions src/gsy_e/gsy_e_core/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from pendulum import DateTime

import gsy_e.constants
from gsy_e.gsy_e_core.area_serializer import area_to_string
from gsy_e.gsy_e_core.enums import PAST_MARKET_TYPE_FILE_SUFFIX_MAPPING
from gsy_e.gsy_e_core.matching_engine_singleton import bid_offer_matcher
from gsy_e.gsy_e_core.sim_results.file_export_endpoints import file_export_endpoints_factory
Expand Down Expand Up @@ -105,9 +106,9 @@ def __init__(self, root_area: Area, path: str, subdir: str,

self.plot_dir = os.path.join(self.directory, "plot")

def export_json_data(self, directory: dir) -> None:
def _export_json_data(self) -> None:
"""Write aggregated results into JSON files."""
json_dir = os.path.join(directory, "aggregated_results")
json_dir = os.path.join(self.directory, "aggregated_results")
mkdir_from_str(json_dir)
settings_file = os.path.join(json_dir, "const_settings.json")
with open(settings_file, "w", encoding="utf-8") as outfile:
Expand All @@ -118,6 +119,13 @@ def export_json_data(self, directory: dir) -> None:
with open(json_file, "w", encoding="utf-8") as outfile:
json.dump(value, outfile, indent=2)

def _export_setup_json(self) -> None:

setup_json_file = os.path.join(self.directory, "setup_file.json")
setup_json = json.loads(area_to_string(self.area))
with open(setup_json_file, "w", encoding="utf-8") as outfile:
json.dump(setup_json, outfile, indent=2)

@staticmethod
def _file_path(directory: dir, area_slug: str) -> dir:
"""Return directory for the provided area_slug."""
Expand All @@ -132,7 +140,8 @@ def export(self, power_flow=None) -> None:
if not os.path.exists(self.plot_dir):
os.makedirs(self.plot_dir)

self.export_json_data(self.directory)
self._export_json_data()
self._export_setup_json()

PlotEnergyProfile(self.endpoint_buffer, self.plot_dir).plot(self.area)
PlotUnmatchedLoads(self.area, self.file_stats_endpoint, self.plot_dir).plot()
Expand Down
6 changes: 6 additions & 0 deletions src/gsy_e/gsy_e_core/sim_results/endpoint_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def __init__(self, is_scm: bool):
"simulation_raw_data")
self.simulation_configuration_tree_validator = get_schema_validator(
"results_configuration_tree")
self.simulation_state_validator = get_schema_validator("simulation_state")

def validate_simulation_raw_data(self, data: Dict):
"""Validate flattened_area_core_stats_dict."""
Expand All @@ -68,6 +69,10 @@ def validate_configuration_tree(self, data: Dict):
"""Validate configuration_tree dict."""
self.simulation_configuration_tree_validator.validate(data=data, raise_exception=True)

def validate_simulation_state(self, data: Dict):
"""Validate simulation state."""
self.simulation_state_validator.validate(data, raise_exception=True)


# pylint: disable=too-many-instance-attributes
# pylint: disable=logging-too-many-args
Expand Down Expand Up @@ -158,6 +163,7 @@ def validate_results(self):
"""Validate updated stats and raise exceptions if they are not valid."""
self.results_validator.validate_simulation_raw_data(self.flattened_area_core_stats_dict)
self.results_validator.validate_configuration_tree(self.area_result_dict)
self.results_validator.validate_simulation_state(self.simulation_state)

def _create_results_validator(self):
self.results_validator = SimulationResultValidator(is_scm=False)
Expand Down
24 changes: 12 additions & 12 deletions src/gsy_e/models/strategy/energy_parameters/heat_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class HeatPumpEnergyParametersException(Exception):

class HeatPumpEnergyParametersBase(ABC):
"""
Base class for common functionality across all heatpump strategies / models. Does not depend
on a specific heatpump model, and cannot be instantiated on its own.
Base class for common functionality across all heatpump strategies / models that include heat
storage. Does not depend on a specific heatpump model, and cannot be instantiated on its own.
"""

# pylint: disable=too-many-arguments
Expand All @@ -47,6 +47,7 @@ def __init__(
self._tank_volume_l = tank_volume_l
self._Q_specific = SPECIFIC_HEAT_CONST_WATER * tank_volume_l * WATER_DENSITY # [kWh / K]
self._slot_length = GlobalConfig.slot_length
self._maximum_power_rating_kW = maximum_power_rating_kW
self._max_energy_consumption_kWh = (
maximum_power_rating_kW * self._slot_length.total_hours())
self.state = HeatPumpState(initial_temp_C, self._slot_length)
Expand Down Expand Up @@ -74,6 +75,7 @@ def serialize(self):
"max_temp_C": self._max_temp_C,
"min_temp_C": self._min_temp_C,
"max_energy_consumption_kWh": self._max_energy_consumption_kWh,
"maximum_power_rating_kW": self._maximum_power_rating_kW,
"tank_volume_l": self._tank_volume_l
}

Expand All @@ -82,7 +84,6 @@ def _rotate_profiles(self, current_time_slot: Optional[DateTime] = None):
self.state.delete_past_state_values(current_time_slot)

def _populate_state(self, time_slot: DateTime):
# order matters here
self.state.update_storage_temp(time_slot)

self.state.set_temp_decrease_K(
Expand All @@ -92,19 +93,19 @@ def _populate_state(self, time_slot: DateTime):

@abstractmethod
def _calc_energy_to_buy_maximum(self, time_slot: DateTime) -> float:
raise NotImplementedError
pass

@abstractmethod
def _calc_energy_to_buy_minimum(self, time_slot: DateTime) -> float:
raise NotImplementedError
pass

@abstractmethod
def _calc_temp_decrease_K(self, time_slot: DateTime) -> float:
raise NotImplementedError
pass

@abstractmethod
def _calc_temp_increase_K(self, time_slot: DateTime, energy_kWh: float) -> float:
raise NotImplementedError
def _calc_temp_increase_K(self, time_slot: DateTime, traded_energy_kWh: float) -> float:
pass

def _calc_energy_demand(self, time_slot: DateTime):
self.state.set_min_energy_demand_kWh(
Expand Down Expand Up @@ -164,7 +165,7 @@ def serialize(self):
"consumption_profile_uuid": self._consumption_kWh.input_profile_uuid,
"external_temp_C": self._ext_temp_C.input_profile,
"external_temp_profile_uuid": self._ext_temp_C.input_profile_uuid,
"source_type": self._source_type
"source_type": self._source_type,
}

def _temp_diff_to_Q_kWh(self, diff_temp_K: float) -> float:
Expand Down Expand Up @@ -204,11 +205,10 @@ def _calc_temp_decrease_K(self, time_slot: DateTime) -> float:

return temp_decrease_K

def _calc_temp_increase_K(self, time_slot: DateTime, energy_kWh: float) -> float:
return self._Q_kWh_to_temp_diff(self._calc_Q_from_energy_kWh(time_slot, energy_kWh))
def _calc_temp_increase_K(self, time_slot: DateTime, traded_energy_kWh: float) -> float:
return self._Q_kWh_to_temp_diff(self._calc_Q_from_energy_kWh(time_slot, traded_energy_kWh))

def _populate_state(self, time_slot: DateTime):
# order matters here
super()._populate_state(time_slot)
self.state.set_energy_consumption_kWh(time_slot, self._consumption_kWh.profile[time_slot])

Expand Down
73 changes: 47 additions & 26 deletions src/gsy_e/models/strategy/energy_parameters/virtual_heat_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
logger = logging.getLogger(__name__)

CALIBRATION_COEFFICIENT = 0.85
WATER_SPECIFIC_TEMPERATURE = 4182
WATER_SPECIFIC_HEAT_CAPACITY = 4182 # [J/kg°C]
GROUND_WATER_TEMPERATURE_C = 12


Expand Down Expand Up @@ -70,58 +70,65 @@ def _rotate_profiles(self, current_time_slot: Optional[DateTime] = None):
self.state.delete_past_state_values(current_time_slot)

def _calc_energy_to_buy_maximum(self, time_slot: DateTime) -> float:
max_energy_consumption = self._storage_temp_to_energy(self._max_temp_C, time_slot)
max_energy_consumption = self._target_storage_temp_to_energy(self._max_temp_C, time_slot)
assert max_energy_consumption > -FLOATING_POINT_TOLERANCE
return min(self._max_energy_consumption_kWh, max_energy_consumption)

def _calc_energy_to_buy_minimum(self, time_slot: DateTime) -> float:
max_energy_consumption = self._storage_temp_to_energy(
min_energy_consumption = self._target_storage_temp_to_energy(
self.state.get_storage_temp_C(time_slot), time_slot)
assert max_energy_consumption > -FLOATING_POINT_TOLERANCE
return min(self._max_energy_consumption_kWh, max_energy_consumption)
assert min_energy_consumption > -FLOATING_POINT_TOLERANCE
return min(self._max_energy_consumption_kWh, min_energy_consumption)

def _calc_temp_decrease_K(self, time_slot: DateTime) -> float:
dh_supply_temp = self._water_supply_temp_C.profile[time_slot]
dh_return_temp = self._water_return_temp_C.profile[time_slot]
m_m3 = self._dh_water_flow_m3.profile[time_slot]
m_kg_per_sec = m_m3 * 1000 / 3600
q_out = m_kg_per_sec * WATER_SPECIFIC_TEMPERATURE * (dh_supply_temp - dh_return_temp)
q_out = m_kg_per_sec * WATER_SPECIFIC_HEAT_CAPACITY * (dh_supply_temp - dh_return_temp)
temp_differential_per_sec = -q_out / (
WATER_DENSITY * WATER_SPECIFIC_TEMPERATURE * self._tank_volume_l)
WATER_DENSITY * WATER_SPECIFIC_HEAT_CAPACITY * self._tank_volume_l)
temp_decrease = temp_differential_per_sec * GlobalConfig.slot_length.total_seconds()
assert temp_decrease <= 0.0
return abs(temp_decrease)

def _calc_temp_increase_K(self, time_slot: DateTime, energy_kWh: float) -> float:
storage_temp = self._energy_to_storage_temp(energy_kWh, time_slot)
def _calc_temp_increase_K(self, time_slot: DateTime, traded_energy_kWh: float) -> float:
storage_temp = self._energy_to_storage_temp(traded_energy_kWh, time_slot)
current_storage_temp = self.state.get_storage_temp_C(time_slot)
if storage_temp <= current_storage_temp:
return 0
return storage_temp - current_storage_temp

def _storage_temp_to_energy(self, storage_temp: float, time_slot: DateTime) -> float:
if not self._min_temp_C < storage_temp < self._max_temp_C:
def _target_storage_temp_to_energy(
self, target_storage_temp_C: float, time_slot: DateTime) -> float:
"""
Return the energy needed to be consumed by the heatpump in order to generate enough heat
to warm the water tank to storage_temp degrees C.
"""
if not self._min_temp_C < target_storage_temp_C < self._max_temp_C:
logger.info(
"Storage temp %s cannot exceed min (%s) / max (%s) tank temperatures.",
storage_temp, self._min_temp_C, self._max_temp_C)
storage_temp = max(min(storage_temp, self._max_temp_C), self._min_temp_C)
target_storage_temp_C, self._min_temp_C, self._max_temp_C)
target_storage_temp_C = max(
min(target_storage_temp_C, self._max_temp_C), self._min_temp_C)

dh_supply_temp = self._water_supply_temp_C.profile[time_slot]
dh_return_temp = self._water_return_temp_C.profile[time_slot]
current_storage_temp = self.state.get_storage_temp_C(time_slot)
m_m3 = self._dh_water_flow_m3.profile[time_slot]
m_kg_per_sec = m_m3 * 1000 / 3600
q_out = m_kg_per_sec * WATER_SPECIFIC_TEMPERATURE * (dh_supply_temp - dh_return_temp)

if m_kg_per_sec <= FLOATING_POINT_TOLERANCE:
return 0.

dh_supply_temp = self._water_supply_temp_C.profile[time_slot]
dh_return_temp = self._water_return_temp_C.profile[time_slot]
current_storage_temp = self.state.get_storage_temp_C(time_slot)
q_out = m_kg_per_sec * WATER_SPECIFIC_HEAT_CAPACITY * (dh_supply_temp - dh_return_temp)

temp_differential_per_sec = (
(storage_temp - current_storage_temp) /
(target_storage_temp_C - current_storage_temp) /
GlobalConfig.slot_length.total_seconds())
q_in = (WATER_DENSITY * WATER_SPECIFIC_TEMPERATURE *
q_in = (WATER_DENSITY * WATER_SPECIFIC_HEAT_CAPACITY *
self._tank_volume_l * temp_differential_per_sec + q_out)
condenser_temp = (q_in / (m_kg_per_sec * WATER_SPECIFIC_TEMPERATURE)) + storage_temp
condenser_temp = (q_in /
(m_kg_per_sec * WATER_SPECIFIC_HEAT_CAPACITY)) + target_storage_temp_C
cop = CALIBRATION_COEFFICIENT * (condenser_temp /
(condenser_temp - GROUND_WATER_TEMPERATURE_C))
p_el = q_in / cop
Expand All @@ -137,19 +144,24 @@ def _storage_temp_to_energy(self, storage_temp: float, time_slot: DateTime) -> f
"Condenser Temperature: %s C. COP: %s.\n"
"Heatpump Power: %s W.\n"
"Heatpump Energy Consumption %s kWh.",
storage_temp, current_storage_temp, dh_supply_temp, dh_return_temp, m_m3,
temp_differential_per_sec, q_out, q_in, condenser_temp, cop, p_el, energy_kWh)
target_storage_temp_C, current_storage_temp, dh_supply_temp, dh_return_temp,
m_m3, temp_differential_per_sec, q_out, q_in, condenser_temp, cop, p_el,
energy_kWh)
return energy_kWh

def _energy_to_storage_temp(
self, energy_kWh: float, time_slot: DateTime) -> float:
"""
Return the water storage temperature after the heatpump has consumed energy_kWh energy and
produced heat with it.
"""
# pylint: disable=too-many-locals
dh_supply_temp = self._water_supply_temp_C.profile[time_slot]
dh_return_temp = self._water_return_temp_C.profile[time_slot]
current_storage_temp = self.state.get_storage_temp_C(time_slot)
m_m3 = self._dh_water_flow_m3.profile[time_slot]
m_kg_per_sec = m_m3 * 1000 / 3600
q_out = m_kg_per_sec * WATER_SPECIFIC_TEMPERATURE * (dh_supply_temp - dh_return_temp)
q_out = m_kg_per_sec * WATER_SPECIFIC_HEAT_CAPACITY * (dh_supply_temp - dh_return_temp)
p_el = convert_kWh_to_W(energy_kWh, GlobalConfig.slot_length)

(q_in_sym, cop_sym, storage_temp_sym,
Expand All @@ -160,10 +172,10 @@ def _energy_to_storage_temp(
sp.Eq((storage_temp_sym - current_storage_temp) /
GlobalConfig.slot_length.total_seconds(),
temp_differential_sym),
sp.Eq(WATER_DENSITY * WATER_SPECIFIC_TEMPERATURE *
sp.Eq(WATER_DENSITY * WATER_SPECIFIC_HEAT_CAPACITY *
self._tank_volume_l * temp_differential_sym + q_out,
q_in_sym),
sp.Eq((q_in_sym / (m_kg_per_sec * WATER_SPECIFIC_TEMPERATURE)) + storage_temp_sym,
sp.Eq((q_in_sym / (m_kg_per_sec * WATER_SPECIFIC_HEAT_CAPACITY)) + storage_temp_sym,
condenser_temp_sym),
sp.Eq(CALIBRATION_COEFFICIENT * (condenser_temp_sym /
(condenser_temp_sym - GROUND_WATER_TEMPERATURE_C)),
Expand Down Expand Up @@ -194,3 +206,12 @@ def event_traded_energy(self, time_slot: DateTime, energy_kWh: float):
"""React to an event_traded_energy."""
super().event_traded_energy(time_slot, energy_kWh)
self.state.update_energy_consumption_kWh(time_slot, energy_kWh)

def event_market_cycle(self, current_time_slot):
"""To be called at the start of the market slot. """
self._rotate_profiles(current_time_slot)
self._populate_state(current_time_slot)
supply_temp = self._water_supply_temp_C.profile[current_time_slot]
return_temp = self._water_return_temp_C.profile[current_time_slot]
assert supply_temp >= return_temp, f"Supply temperature {supply_temp} has to be greater " \
f"than {return_temp}, timeslot {current_time_slot}"
19 changes: 15 additions & 4 deletions src/gsy_e/models/strategy/heat_pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ def update(self, market: "MarketBase", use_default: bool = False):
if use_default or self.initial_rate is None:
self.initial_rate = get_feed_in_tariff_rate_from_config(market)

def serialize(self):
return {
"update_interval": self.update_interval,
"initial_buying_rate": self.initial_rate,
"final_buying_rate": self.final_rate
}


class HeatPumpStrategy(TradingStrategyBase):
"""Strategy for heat pumps with storages."""
Expand Down Expand Up @@ -112,6 +119,14 @@ def _init_price_params(self, order_updater_parameters, preferred_buying_rate):

self.preferred_buying_rate = preferred_buying_rate

def serialize(self):
"""Serialize strategy parameters."""
return {
"preferred_buying_rate": self.preferred_buying_rate,
**self._energy_params.serialize(),
**self._order_updater_params.get(AvailableMarketTypes.SPOT).serialize()
}

@staticmethod
def deserialize_args(constructor_args: Dict) -> Dict:
"""Deserialize the constructor arguments for the HeatPump strategy."""
Expand All @@ -133,10 +148,6 @@ def deserialize_args(constructor_args: Dict) -> Dict:
}
return constructor_args

def serialize(self):
"""Return serialised energy params."""
return {**self._energy_params.serialize()}

@property
def state(self) -> HeatPumpState:
return self._energy_params.state
Expand Down
8 changes: 8 additions & 0 deletions src/gsy_e/models/strategy/order_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ class OrderUpdaterParameters:
initial_rate: float
final_rate: float

def serialize(self):
"""Serialize parameters."""
return {
"update_interval": self.update_interval,
"initial_rate": self.initial_rate,
"final_rate": self.final_rate
}


class OrderUpdater:
"""
Expand Down
2 changes: 1 addition & 1 deletion src/gsy_e/models/strategy/state/heat_pump_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def set_energy_consumption_kWh(self, time_slot: DateTime, energy_kWh: float):
self._energy_consumption_kWh[time_slot] = energy_kWh

def update_energy_consumption_kWh(self, time_slot: DateTime, energy_kWh: float):
"""Set the energy consumption of the heatpump for a given time slot."""
"""Update the energy consumption of the heatpump for a given time slot."""
self._energy_consumption_kWh[time_slot] += energy_kWh

def update_temp_increase_K(self, time_slot: DateTime, temp_diff_K: float):
Expand Down
2 changes: 1 addition & 1 deletion src/gsy_e/models/strategy/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def serialize(self):
"battery_capacity_kWh": self.state.capacity,
"max_abs_battery_power_kW": self.state.max_abs_battery_power_kW,
"cap_price_strategy": self.cap_price_strategy,
"initial_energy_origin": self.state.initial_energy_origin,
"initial_energy_origin": self.state.initial_energy_origin.value,
"balancing_energy_ratio": self.balancing_energy_ratio,
**self.bid_update.serialize(),
**self.offer_update.serialize(),
Expand Down
Loading

0 comments on commit f1e9f00

Please sign in to comment.