diff --git a/src/gsy_e/constants.py b/src/gsy_e/constants.py index fefe513b9..6ac0152c7 100644 --- a/src/gsy_e/constants.py +++ b/src/gsy_e/constants.py @@ -21,13 +21,6 @@ # pylint: disable=unused-import import os -from gsy_framework.constants_limits import DATE_TIME_FORMAT, DATE_TIME_UI_FORMAT, TIME_ZONE # NOQA -from gsy_framework.constants_limits import TIME_FORMAT, DATE_FORMAT, GlobalConfig # NOQA - -# In order to cover conversion and reverse-conversion to 5 decimal points, the tolerance has to be -# 0.00002. That way off-by-one consecutive rounding errors would not be treated as errors, e.g. -# when recalculating the original energy rate in trade chains. -FLOATING_POINT_TOLERANCE = 0.00002 ROUND_TOLERANCE = 5 # Percentual standard deviation relative to the forecast energy, used to compute the (simulated) diff --git a/src/gsy_e/gsy_e_core/cli.py b/src/gsy_e/gsy_e_core/cli.py index 4ad2c7194..15d5a14d1 100644 --- a/src/gsy_e/gsy_e_core/cli.py +++ b/src/gsy_e/gsy_e_core/cli.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import logging import multiprocessing import platform @@ -23,25 +24,40 @@ from click.types import Choice from click_default_group import DefaultGroup from colorlog.colorlog import ColoredFormatter -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, DATE_FORMAT, TIME_FORMAT, TIME_ZONE from gsy_framework.exceptions import GSyException from gsy_framework.settings_validators import validate_global_settings from pendulum import today -import gsy_e.constants from gsy_e.gsy_e_core.simulation import run_simulation from gsy_e.gsy_e_core.util import ( - DateType, IntervalType, available_simulation_scenarios, convert_str_to_pause_after_interval, - read_settings_from_file, update_advanced_settings) + DateType, + IntervalType, + available_simulation_scenarios, + convert_str_to_pause_after_interval, + read_settings_from_file, + update_advanced_settings, +) from gsy_e.models.config import SimulationConfig log = logging.getLogger(__name__) -@click.group(name="gsy-e", cls=DefaultGroup, default="run", default_if_no_args=True, - context_settings={"max_content_width": 120}) -@click.option("-l", "--log-level", type=Choice(logging._nameToLevel.keys()), default="INFO", - show_default=True, help="Log level") +@click.group( + name="gsy-e", + cls=DefaultGroup, + default="run", + default_if_no_args=True, + context_settings={"max_content_width": 120}, +) +@click.option( + "-l", + "--log-level", + type=Choice(logging._nameToLevel.keys()), + default="INFO", + show_default=True, + help="Log level", +) def main(log_level): """Entrypoint for command-line interface interaction.""" handler = logging.StreamHandler() @@ -50,7 +66,7 @@ def main(log_level): ColoredFormatter( "%(log_color)s%(asctime)s.%(msecs)03d %(levelname)-8s (%(lineno)4d) %(name)-30s: " "%(message)s%(reset)s", - datefmt="%H:%M:%S" + datefmt="%H:%M:%S", ) ) root_logger = logging.getLogger() @@ -62,53 +78,124 @@ def main(log_level): @main.command() -@click.option("-d", "--duration", type=IntervalType("D:H"), default="1d", show_default=True, - help="Duration of simulation") -@click.option("-t", "--tick-length", type=IntervalType("M:S"), default="1s", show_default=True, - help="Length of a tick") -@click.option("-s", "--slot-length", type=IntervalType("M:S"), default="15m", show_default=True, - help="Length of a market slot") -@click.option("--slot-length-realtime", type=IntervalType("M:S"), default="0m", - show_default=True, help="Desired duration of slot in realtime") -@click.option("--setup", "setup_module_name", default="default_2a", - help=("Simulation setup module use. " - f"Available modules: [{', '.join(_setup_modules)}]")) -@click.option("-g", "--settings-file", default=None, - help="Settings file path") +@click.option( + "-d", + "--duration", + type=IntervalType("D:H"), + default="1d", + show_default=True, + help="Duration of simulation", +) +@click.option( + "-t", + "--tick-length", + type=IntervalType("M:S"), + default="1s", + show_default=True, + help="Length of a tick", +) +@click.option( + "-s", + "--slot-length", + type=IntervalType("M:S"), + default="15m", + show_default=True, + help="Length of a market slot", +) +@click.option( + "--slot-length-realtime", + type=IntervalType("M:S"), + default="0m", + show_default=True, + help="Desired duration of slot in realtime", +) +@click.option( + "--setup", + "setup_module_name", + default="default_2a", + help=("Simulation setup module use. " f"Available modules: [{', '.join(_setup_modules)}]"), +) +@click.option("-g", "--settings-file", default=None, help="Settings file path") @click.option("--seed", help="Manually specify random seed") -@click.option("--paused", is_flag=True, default=False, show_default=True, - help="Start simulation in paused state") -@click.option("--pause-at", type=str, default=None, - help="Automatically pause at a certain time. " - f"Accepted Input formats: ({gsy_e.constants.DATE_FORMAT}, " - f"{gsy_e.constants.TIME_FORMAT}) [default: disabled]") -@click.option("--incremental", is_flag=True, default=False, show_default=True, - help="Pause the simulation at the end of each time slot.") -@click.option("--repl/--no-repl", default=False, show_default=True, - help="Start REPL after simulation run.") +@click.option( + "--paused", + is_flag=True, + default=False, + show_default=True, + help="Start simulation in paused state", +) +@click.option( + "--pause-at", + type=str, + default=None, + help="Automatically pause at a certain time. " + f"Accepted Input formats: ({DATE_FORMAT}, " + f"{TIME_FORMAT}) [default: disabled]", +) +@click.option( + "--incremental", + is_flag=True, + default=False, + show_default=True, + help="Pause the simulation at the end of each time slot.", +) +@click.option( + "--repl/--no-repl", default=False, show_default=True, help="Start REPL after simulation run." +) @click.option("--no-export", is_flag=True, default=False, help="Skip export of simulation data") -@click.option("--export-path", type=str, default=None, show_default=False, - help="Specify a path for the csv export files (default: ~/gsy-e-simulation)") +@click.option( + "--export-path", + type=str, + default=None, + show_default=False, + help="Specify a path for the csv export files (default: ~/gsy-e-simulation)", +) @click.option("--enable-bc", is_flag=True, default=False, help="Run simulation on Blockchain") -@click.option("--enable-external-connection", is_flag=True, default=False, - help="External Agents interaction to simulation during runtime") -@click.option("--start-date", type=DateType(gsy_e.constants.DATE_FORMAT), - default=today(tz=gsy_e.constants.TIME_ZONE).format(gsy_e.constants.DATE_FORMAT), - show_default=True, - help=f"Start date of the Simulation ({gsy_e.constants.DATE_FORMAT})") -@click.option("--enable-dof/--disable-dof", - is_flag=True, default=True, - help=( - "Enable or disable Degrees of Freedom " - "(orders can't contain attributes/requirements).")) -@click.option("-m", "--market-type", type=int, - default=ConstSettings.MASettings.MARKET_TYPE, show_default=True, - help="Market type. 1 for one-sided market, 2 for two-sided market, " - "3 for coefficient-based trading.") -def run(setup_module_name, settings_file, duration, slot_length, tick_length, - enable_external_connection, start_date, - pause_at, incremental, slot_length_realtime, enable_dof: bool, - market_type: int, **kwargs): +@click.option( + "--enable-external-connection", + is_flag=True, + default=False, + help="External Agents interaction to simulation during runtime", +) +@click.option( + "--start-date", + type=DateType(DATE_FORMAT), + default=today(tz=TIME_ZONE).format(DATE_FORMAT), + show_default=True, + help=f"Start date of the Simulation ({DATE_FORMAT})", +) +@click.option( + "--enable-dof/--disable-dof", + is_flag=True, + default=True, + help=( + "Enable or disable Degrees of Freedom " "(orders can't contain attributes/requirements)." + ), +) +@click.option( + "-m", + "--market-type", + type=int, + default=ConstSettings.MASettings.MARKET_TYPE, + show_default=True, + help="Market type. 1 for one-sided market, 2 for two-sided market, " + "3 for coefficient-based trading.", +) +def run( + setup_module_name, + settings_file, + duration, + slot_length, + tick_length, + enable_external_connection, + start_date, + pause_at, + incremental, + slot_length_realtime, + enable_dof: bool, + market_type: int, + **kwargs, +): """Configure settings and run a simulation.""" # Force the multiprocessing start method to be 'fork' on macOS. if platform.system() == "Darwin": @@ -124,24 +211,31 @@ def run(setup_module_name, settings_file, duration, slot_length, tick_length, else: assert 1 <= market_type <= 3, "Market type should be an integer between 1 and 3." ConstSettings.MASettings.MARKET_TYPE = market_type - global_settings = {"sim_duration": duration, - "slot_length": slot_length, - "tick_length": tick_length, - "enable_degrees_of_freedom": enable_dof} + global_settings = { + "sim_duration": duration, + "slot_length": slot_length, + "tick_length": tick_length, + "enable_degrees_of_freedom": enable_dof, + } validate_global_settings(global_settings) simulation_config = SimulationConfig( - duration, slot_length, tick_length, start_date=start_date, + duration, + slot_length, + tick_length, + start_date=start_date, external_connection_enabled=enable_external_connection, - enable_degrees_of_freedom=enable_dof) + enable_degrees_of_freedom=enable_dof, + ) if incremental: kwargs["incremental"] = incremental if pause_at is not None: kwargs["pause_after"] = convert_str_to_pause_after_interval(start_date, pause_at) - run_simulation(setup_module_name, simulation_config, None, None, None, - slot_length_realtime, kwargs) + run_simulation( + setup_module_name, simulation_config, None, None, None, slot_length_realtime, kwargs + ) except GSyException as ex: log.exception(ex) diff --git a/src/gsy_e/gsy_e_core/rq_job_handler.py b/src/gsy_e/gsy_e_core/rq_job_handler.py index 5d749edf4..2d635c8f3 100644 --- a/src/gsy_e/gsy_e_core/rq_job_handler.py +++ b/src/gsy_e/gsy_e_core/rq_job_handler.py @@ -5,7 +5,7 @@ from datetime import datetime, date from typing import Dict, Optional -from gsy_framework.constants_limits import GlobalConfig, ConstSettings +from gsy_framework.constants_limits import GlobalConfig, ConstSettings, TIME_ZONE from gsy_framework.enums import ConfigurationType, SpotMarketTypeEnum, CoefficientAlgorithm from gsy_framework.settings_validators import validate_global_settings from pendulum import duration, instance, now @@ -94,7 +94,7 @@ def launch_simulation_from_rq_job( if settings.get("type") == ConfigurationType.CANARY_NETWORK.value: config.start_date = instance( - datetime.combine(date.today(), datetime.min.time()), tz=gsy_e.constants.TIME_ZONE + datetime.combine(date.today(), datetime.min.time()), tz=TIME_ZONE ) if ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.COEFFICIENTS.value: @@ -220,7 +220,7 @@ def _create_config_settings_object( "start_date": ( instance( datetime.combine(settings.get("start_date"), datetime.min.time()), - tz=gsy_e.constants.TIME_ZONE, + tz=TIME_ZONE, ) if "start_date" in settings else GlobalConfig.start_date @@ -292,9 +292,7 @@ def _handle_scm_past_slots_simulation_run( # Adding 4 hours of extra time to the SCM past slots simulation duration, in order to # compensate for the runtime of the SCM past slots simulation and to not have any results gaps # after this simulation run and the following Canary Network launch. - config.end_date = ( - now(tz=gsy_e.constants.TIME_ZONE).subtract(hours=config.hours_of_delay).add(hours=4) - ) + config.end_date = now(tz=TIME_ZONE).subtract(hours=config.hours_of_delay).add(hours=4) config.sim_duration = config.end_date - config.start_date GlobalConfig.sim_duration = config.sim_duration gsy_e.constants.RUN_IN_REALTIME = False diff --git a/src/gsy_e/gsy_e_core/sim_results/file_export_endpoints.py b/src/gsy_e/gsy_e_core/sim_results/file_export_endpoints.py index 28b6fd5a3..10bc56248 100644 --- a/src/gsy_e/gsy_e_core/sim_results/file_export_endpoints.py +++ b/src/gsy_e/gsy_e_core/sim_results/file_export_endpoints.py @@ -16,6 +16,7 @@ along with this program. If not, see . """ +# pylint: disable=too-many-return-statements, broad-exception-raised from abc import ABC, abstractmethod from statistics import mean from typing import TYPE_CHECKING, Dict, List @@ -179,8 +180,6 @@ def _specific_labels(self): return [ "unmatched demand [kWh]", "storage temperature C", - "temp decrease K", - "temp increase K", "COP", "heat demand J", ] @@ -189,8 +188,6 @@ def _specific_labels(self): return [ "unmatched demand [kWh]", "storage temperature C", - "temp decrease K", - "temp increase K", "COP", "heat demand J", "condenser temperature C", @@ -251,20 +248,24 @@ def _specific_row(self, slot, market): # pylint: disable=unidiomatic-typecheck if type(self.area.strategy) == HeatPumpStrategy: return [ - round(self.area.strategy.state.get_unmatched_demand_kWh(slot), ROUND_TOLERANCE), - round(self.area.strategy.state.get_storage_temp_C(slot), ROUND_TOLERANCE), - round(self.area.strategy.state.get_temp_decrease_K(slot), ROUND_TOLERANCE), - round(self.area.strategy.state.get_temp_increase_K(slot), ROUND_TOLERANCE), - round(self.area.strategy.state.get_cop(slot), ROUND_TOLERANCE), - round(self.area.strategy.state.get_heat_demand(slot), ROUND_TOLERANCE), + round( + self.area.strategy.state.tanks.get_unmatched_demand_kWh(slot), + ROUND_TOLERANCE, + ), + round( + self.area.strategy.state.tanks.get_average_tank_temperature(slot), + ROUND_TOLERANCE, + ), + round(self.area.strategy.state.heatpump.get_cop(slot), ROUND_TOLERANCE), + round(self.area.strategy.state.heatpump.get_heat_demand(slot), ROUND_TOLERANCE), ] # pylint: disable=unidiomatic-typecheck if type(self.area.strategy) == VirtualHeatpumpStrategy: return [ - round(self.area.strategy.state.get_unmatched_demand_kWh(slot), ROUND_TOLERANCE), + round( + self.area.strategy.state.tanks.get_unmatched_demand_kWh(slot), ROUND_TOLERANCE + ), round(self.area.strategy.state.get_storage_temp_C(slot), ROUND_TOLERANCE), - round(self.area.strategy.state.get_temp_decrease_K(slot), ROUND_TOLERANCE), - round(self.area.strategy.state.get_temp_increase_K(slot), ROUND_TOLERANCE), round(self.area.strategy.state.get_cop(slot), ROUND_TOLERANCE), round(self.area.strategy.state.get_heat_demand(slot), ROUND_TOLERANCE), round(self.area.strategy.state.get_condenser_temp(slot), ROUND_TOLERANCE), diff --git a/src/gsy_e/gsy_e_core/sim_results/plotly_graph.py b/src/gsy_e/gsy_e_core/sim_results/plotly_graph.py index 857d32f99..2dc2eb664 100644 --- a/src/gsy_e/gsy_e_core/sim_results/plotly_graph.py +++ b/src/gsy_e/gsy_e_core/sim_results/plotly_graph.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import os import pendulum @@ -22,7 +23,7 @@ import plotly.graph_objs as go from gsy_framework.utils import limit_float_precision -from gsy_e.constants import TIME_ZONE +from gsy_framework.constants_limits import TIME_ZONE from gsy_e.data_classes import PlotDescription from gsy_e.models.strategy.commercial_producer import CommercialStrategy from gsy_e.models.strategy.finite_power_plant import FinitePowerPlant @@ -41,23 +42,27 @@ purple = "rgba(156, 110, 177, alpha)" blue = "rgba(0,0,200,alpha)" -DEVICE_PLOT_COLORS = {"trade_energy_kWh": purple, - "sold_trade_energy_kWh": purple, - "bought_trade_energy_kWh": purple, - "trade_price_eur": blue} - -DEVICE_YAXIS = {"trade_energy_kWh": "Traded [kWh]", - "sold_trade_energy_kWh": "Supply/Traded [kWh]", - "bought_trade_energy_kWh": "Demand/Traded [kWh]", - "pv_production_kWh": "PV Production [kWh]", - "energy_consumption_kWh": "Energy Consumption [kWh]", - "storage_temp_C": "Heatpump Storage Temperature [C]", - "energy_buffer_kWh": "Energy Buffer [kWh]", - "production_kWh": "Power Production [kWh]", - "load_profile_kWh": "Load Profile [kWh]", - "smart_meter_profile_kWh": "Smart Meter Profile [kWh]", - "soc_history_%": "State of Charge [%]", - "trade_price_eur": "Energy Rate [EUR/kWh]"} +DEVICE_PLOT_COLORS = { + "trade_energy_kWh": purple, + "sold_trade_energy_kWh": purple, + "bought_trade_energy_kWh": purple, + "trade_price_eur": blue, +} + +DEVICE_YAXIS = { + "trade_energy_kWh": "Traded [kWh]", + "sold_trade_energy_kWh": "Supply/Traded [kWh]", + "bought_trade_energy_kWh": "Demand/Traded [kWh]", + "pv_production_kWh": "PV Production [kWh]", + "energy_consumption_kWh": "Energy Consumption [kWh]", + "storage_temp_C": "Heatpump Storage Temperature [C]", + "energy_buffer_kWh": "Energy Buffer [kWh]", + "production_kWh": "Power Production [kWh]", + "load_profile_kWh": "Load Profile [kWh]", + "smart_meter_profile_kWh": "Smart Meter Profile [kWh]", + "soc_history_%": "State of Charge [%]", + "trade_price_eur": "Energy Rate [EUR/kWh]", +} OPAQUE_ALPHA = 1 TRANSPARENT_ALPHA = 0.4 @@ -93,18 +98,11 @@ def _common_layout(data_desc: PlotDescription, xrange: list, showlegend=True, ho height=700, barmode=data_desc.barmode, title=data_desc.title, - yaxis=dict( - title=data_desc.ytitle - ), - xaxis=dict( - title=data_desc.xtitle, - range=xrange - ), - font=dict( - size=16 - ), + yaxis=dict(title=data_desc.ytitle), + xaxis=dict(title=data_desc.xtitle, range=xrange), + font=dict(size=16), showlegend=showlegend, - hovermode=hovermode + hovermode=hovermode, ) def graph_value(self, scale_value=1): @@ -123,7 +121,8 @@ def graph_value(self, scale_value=1): self.umHours[self.dataset["slot"][de]] = 0.0 else: self.umHours[self.dataset["slot"][de]] = ( - round(self.dataset[self.key][de], 5) * scale_value) + round(self.dataset[self.key][de], 5) * scale_value + ) @staticmethod def modify_time_axis(plot_desc: PlotDescription): @@ -144,12 +143,23 @@ def modify_time_axis(plot_desc: PlotDescription): raise ValueError(f"There is no time information in plot {plot_desc.title}") start_time = pendulum.datetime( - day_list[0].year, day_list[0].month, day_list[0].day, - day_list[0].hour, day_list[0].minute, day_list[0].second, tz=TIME_ZONE + day_list[0].year, + day_list[0].month, + day_list[0].day, + day_list[0].hour, + day_list[0].minute, + day_list[0].second, + tz=TIME_ZONE, ) end_time = pendulum.datetime( - day_list[-1].year, day_list[-1].month, day_list[-1].day, - day_list[-1].hour, day_list[-1].minute, day_list[-1].second, tz=TIME_ZONE) + day_list[-1].year, + day_list[-1].month, + day_list[-1].day, + day_list[-1].hour, + day_list[-1].minute, + day_list[-1].second, + tz=TIME_ZONE, + ) return [start_time, end_time], plot_desc.data @@ -161,34 +171,52 @@ def plot_slider_graph(cls, fig, stats_plot_dir, area_name, market_slot_data_mapp for i, _ in enumerate(market_slot_data_mapping): step = dict( method="update", - args=[{"visible": [False] * len(fig.data)}, - {"title": "Slider switched to slot: " + str(i)}], # layout attribute + args=[ + {"visible": [False] * len(fig.data)}, + {"title": "Slider switched to slot: " + str(i)}, + ], # layout attribute ) - for k in range(market_slot_data_mapping[i].start, - market_slot_data_mapping[i].end): + for k in range(market_slot_data_mapping[i].start, market_slot_data_mapping[i].end): step["args"][0]["visible"][k] = True # Toggle i'th trace to "visible" steps.append(step) - sliders = [dict( - active=0, - currentvalue={"prefix": "MarketSlot: "}, - pad={"t": len(market_slot_data_mapping)}, - steps=steps - )] + sliders = [ + dict( + active=0, + currentvalue={"prefix": "MarketSlot: "}, + pad={"t": len(market_slot_data_mapping)}, + steps=steps, + ) + ] output_file = os.path.join(stats_plot_dir, "offer_bid_trade_history.html") barmode = "group" title = f"OFFER BID TRADE AREA: {area_name}" xtitle = "Time" ytitle = "Rate [€ cents / kWh]" - fig.update_layout(autosize=True, barmode=barmode, width=1200, height=700, title=title, - yaxis=dict(title=ytitle), xaxis=dict(title=xtitle), - font=dict(size=16), showlegend=False, sliders=sliders) + fig.update_layout( + autosize=True, + barmode=barmode, + width=1200, + height=700, + title=title, + yaxis=dict(title=ytitle), + xaxis=dict(title=xtitle), + font=dict(size=16), + showlegend=False, + sliders=sliders, + ) py.offline.plot(fig, filename=output_file, auto_open=False) @classmethod - def plot_bar_graph(cls, plot_desc: PlotDescription, iname: str, - time_range=None, showlegend=True, hovermode="x"): + def plot_bar_graph( + cls, + plot_desc: PlotDescription, + iname: str, + time_range=None, + showlegend=True, + hovermode="x", + ): """Render bar graph, used by multiple plots.""" # pylint: disable=too-many-arguments if time_range is None: @@ -197,9 +225,7 @@ def plot_bar_graph(cls, plot_desc: PlotDescription, iname: str, except ValueError: return - layout = cls._common_layout( - plot_desc, time_range, showlegend, hovermode=hovermode - ) + layout = cls._common_layout(plot_desc, time_range, showlegend, hovermode=hovermode) fig = go.Figure(data=data, layout=layout) py.offline.plot(fig, filename=iname, auto_open=False) @@ -216,12 +242,12 @@ def _plot_line_time_series(cls, device_dict, var_name): # pylint: disable=too-many-locals color = _get_color(var_name, OPAQUE_ALPHA) fill_color = _get_color(var_name, TRANSPARENT_ALPHA) - time, var_data, longterm_min_var_data, longterm_max_var_data = ( - cls._prepare_input(device_dict, var_name)) + time, var_data, longterm_min_var_data, longterm_max_var_data = cls._prepare_input( + device_dict, var_name + ) yaxis = "y3" connectgaps = True - line = dict(color=color, - width=0.8) + line = dict(color=color, width=0.8) time_series = go.Scatter( x=time, y=var_data, @@ -234,7 +260,7 @@ def _plot_line_time_series(cls, device_dict, var_name): fill=None, xaxis="x", yaxis=yaxis, - connectgaps=connectgaps + connectgaps=connectgaps, ) longterm_max_hover = go.Scatter( x=time, @@ -247,7 +273,7 @@ def _plot_line_time_series(cls, device_dict, var_name): hoverinfo="y+name", xaxis="x", yaxis=yaxis, - connectgaps=connectgaps + connectgaps=connectgaps, ) longterm_min_hover = go.Scatter( x=time, @@ -260,7 +286,7 @@ def _plot_line_time_series(cls, device_dict, var_name): hoverinfo="y+name", xaxis="x", yaxis=yaxis, - connectgaps=connectgaps + connectgaps=connectgaps, ) shade = go.Scatter( x=time, @@ -273,7 +299,7 @@ def _plot_line_time_series(cls, device_dict, var_name): hoverinfo="none", xaxis="x", yaxis=yaxis, - connectgaps=connectgaps + connectgaps=connectgaps, ) hoverinfo_time = go.Scatter( x=time, @@ -282,20 +308,22 @@ def _plot_line_time_series(cls, device_dict, var_name): hoverinfo="x", xaxis="x", showlegend=False, - yaxis=yaxis + yaxis=yaxis, ) # it is not possible to use cls._hoverinfo here because the order matters here: return [longterm_min_hover, shade, time_series, longterm_max_hover, hoverinfo_time] @classmethod - def _plot_bar_time_series_traded(cls, device_dict, traded_varname, yaxis, - expected_varname=None, invert_y=False): + def _plot_bar_time_series_traded( + cls, device_dict, traded_varname, yaxis, expected_varname=None, invert_y=False + ): # pylint: disable=too-many-locals,too-many-arguments color_traded = _get_color(traded_varname, OPAQUE_ALPHA) fill_color_traded = _get_color(traded_varname, OPAQUE_ALPHA) - time_traded, energy_traded, min_energy_traded, max_energy_traded = ( - cls._prepare_input(device_dict, traded_varname, invert_y)) + time_traded, energy_traded, min_energy_traded, max_energy_traded = cls._prepare_input( + device_dict, traded_varname, invert_y + ) time_series_traded = go.Bar( x=time_traded, @@ -304,8 +332,8 @@ def _plot_bar_time_series_traded(cls, device_dict, traded_varname, yaxis, color=fill_color_traded, line=dict( color=color_traded, - width=1., - ) + width=1.0, + ), ), name=traded_varname, showlegend=True, @@ -318,7 +346,8 @@ def _plot_bar_time_series_traded(cls, device_dict, traded_varname, yaxis, color_expected = _get_color(expected_varname, OPAQUE_ALPHA) fill_color_expected = _get_color(expected_varname, TRANSPARENT_ALPHA) time_expected, energy_expected, min_energy_expected, max_energy_expected = ( - cls._prepare_input(device_dict, expected_varname)) + cls._prepare_input(device_dict, expected_varname) + ) time_series_expected = go.Bar( x=time_expected, y=energy_expected, @@ -326,8 +355,8 @@ def _plot_bar_time_series_traded(cls, device_dict, traded_varname, yaxis, color=fill_color_expected, line=dict( color=color_expected, - width=1., - ) + width=1.0, + ), ), name=expected_varname, showlegend=True, @@ -336,10 +365,11 @@ def _plot_bar_time_series_traded(cls, device_dict, traded_varname, yaxis, yaxis=yaxis, ) return [time_series_expected, time_series_traded] + cls._hoverinfo( - time_expected, min_energy_expected, max_energy_expected, yaxis, - only_time=True) + time_expected, min_energy_expected, max_energy_expected, yaxis, only_time=True + ) return [time_series_traded] + cls._hoverinfo( - time_traded, min_energy_traded, max_energy_traded, yaxis, only_time=True) + time_traded, min_energy_traded, max_energy_traded, yaxis, only_time=True + ) @classmethod def _hoverinfo(cls, time, longterm_min, longterm_max, yaxis, only_time=False): @@ -352,7 +382,7 @@ def _hoverinfo(cls, time, longterm_min, longterm_max, yaxis, only_time=False): hoverinfo="y+name", xaxis="x", showlegend=False, - yaxis=yaxis + yaxis=yaxis, ) hoverinfo_min = go.Scatter( x=time, @@ -362,7 +392,7 @@ def _hoverinfo(cls, time, longterm_min, longterm_max, yaxis, only_time=False): hoverinfo="y+name", xaxis="x", showlegend=False, - yaxis=yaxis + yaxis=yaxis, ) hoverinfo_time = go.Scatter( x=time, @@ -371,7 +401,7 @@ def _hoverinfo(cls, time, longterm_min, longterm_max, yaxis, only_time=False): hoverinfo="x", xaxis="x", showlegend=False, - yaxis=yaxis + yaxis=yaxis, ) if only_time: return [hoverinfo_time] @@ -381,7 +411,8 @@ def _hoverinfo(cls, time, longterm_min, longterm_max, yaxis, only_time=False): def _plot_candlestick_time_series_price(cls, device_dict, var_name, yaxis): # pylint: disable=too-many-locals time, trade_rate_list, longterm_min_trade_rate, longterm_max_trade_rate = ( - cls._prepare_input(device_dict, var_name)) + cls._prepare_input(device_dict, var_name) + ) plot_time = [] plot_local_min_trade_rate = [] plot_local_max_trade_rate = [] @@ -393,24 +424,27 @@ def _plot_candlestick_time_series_price(cls, device_dict, var_name, yaxis): plot_local_min_trade_rate.append(limit_float_precision(min(trade_rate_list[ii]))) plot_local_max_trade_rate.append(limit_float_precision(max(trade_rate_list[ii]))) plot_longterm_min_trade_rate.append( - limit_float_precision(longterm_min_trade_rate[ii])) + limit_float_precision(longterm_min_trade_rate[ii]) + ) plot_longterm_max_trade_rate.append( - limit_float_precision(longterm_max_trade_rate[ii])) + limit_float_precision(longterm_max_trade_rate[ii]) + ) color = _get_color(var_name, OPAQUE_ALPHA) - candle_stick = go.Candlestick(x=plot_time, - open=plot_local_min_trade_rate, - high=plot_longterm_max_trade_rate, - low=plot_longterm_min_trade_rate, - close=plot_local_max_trade_rate, - yaxis=yaxis, - xaxis="x", - hoverinfo="none", - name=var_name, - increasing=dict(line=dict(color=color)), - decreasing=dict(line=dict(color=color)), - ) + candle_stick = go.Candlestick( + x=plot_time, + open=plot_local_min_trade_rate, + high=plot_longterm_max_trade_rate, + low=plot_longterm_min_trade_rate, + close=plot_local_max_trade_rate, + yaxis=yaxis, + xaxis="x", + hoverinfo="none", + name=var_name, + increasing=dict(line=dict(color=color)), + decreasing=dict(line=dict(color=color)), + ) hoverinfo_local_max = go.Scatter( x=plot_time, y=plot_local_max_trade_rate, @@ -419,7 +453,7 @@ def _plot_candlestick_time_series_price(cls, device_dict, var_name, yaxis): hoverinfo="y+name", xaxis="x", showlegend=False, - yaxis=yaxis + yaxis=yaxis, ) hoverinfo_local_min = go.Scatter( x=plot_time, @@ -429,11 +463,12 @@ def _plot_candlestick_time_series_price(cls, device_dict, var_name, yaxis): hoverinfo="y+name", xaxis="x", showlegend=False, - yaxis=yaxis + yaxis=yaxis, ) return [candle_stick, hoverinfo_local_max, hoverinfo_local_min] + cls._hoverinfo( - plot_time, plot_longterm_min_trade_rate, plot_longterm_max_trade_rate, yaxis) + plot_time, plot_longterm_min_trade_rate, plot_longterm_max_trade_rate, yaxis + ) @classmethod def _prepare_input(cls, device_dict, var_name, invert_y=False): @@ -473,42 +508,56 @@ def plot_device_profile(cls, device_dict, device_name, output_file, device_strat y1axis_key = "trade_price_eur" y2axis_key = trade_energy_var_name y3axis_key = "soc_history_%" - yaxis_caption_list = [DEVICE_YAXIS[y1axis_key], DEVICE_YAXIS[y2axis_key], - DEVICE_YAXIS[y3axis_key]] + yaxis_caption_list = [ + DEVICE_YAXIS[y1axis_key], + DEVICE_YAXIS[y2axis_key], + DEVICE_YAXIS[y3axis_key], + ] data += cls._plot_candlestick_time_series_price(device_dict, y1axis_key, "y1") data += cls._plot_bar_time_series_traded(device_dict, y2axis_key, "y2") data += cls._plot_line_time_series(device_dict, y3axis_key) - layout = cls._device_plot_layout("overlay", f"{device_name}", - "Time", yaxis_caption_list) + layout = cls._device_plot_layout( + "overlay", f"{device_name}", "Time", yaxis_caption_list + ) - elif isinstance(device_strategy, (LoadHoursStrategy, SCMLoadHoursStrategy, - SCMLoadProfileStrategy)): + elif isinstance( + device_strategy, (LoadHoursStrategy, SCMLoadHoursStrategy, SCMLoadProfileStrategy) + ): y1axis_key = "trade_price_eur" y2axis_key = trade_energy_var_name y3axis_key = "load_profile_kWh" - yaxis_caption_list = [DEVICE_YAXIS[y1axis_key], DEVICE_YAXIS[y2axis_key], - DEVICE_YAXIS[y3axis_key]] + yaxis_caption_list = [ + DEVICE_YAXIS[y1axis_key], + DEVICE_YAXIS[y2axis_key], + DEVICE_YAXIS[y3axis_key], + ] data += cls._plot_candlestick_time_series_price(device_dict, y1axis_key, "y1") - data += cls._plot_bar_time_series_traded(device_dict, y2axis_key, "y2", - expected_varname=y3axis_key) + data += cls._plot_bar_time_series_traded( + device_dict, y2axis_key, "y2", expected_varname=y3axis_key + ) data += cls._plot_line_time_series(device_dict, y3axis_key) - layout = cls._device_plot_layout("overlay", f"{device_name}", - "Time", yaxis_caption_list) + layout = cls._device_plot_layout( + "overlay", f"{device_name}", "Time", yaxis_caption_list + ) elif isinstance(device_strategy, (SmartMeterStrategy, SCMSmartMeterStrategy)): y1axis_key = "trade_price_eur" y2axis_key = trade_energy_var_name y3axis_key = "smart_meter_profile_kWh" - yaxis_caption_list = [DEVICE_YAXIS[y1axis_key], DEVICE_YAXIS[y2axis_key], - DEVICE_YAXIS[y3axis_key]] + yaxis_caption_list = [ + DEVICE_YAXIS[y1axis_key], + DEVICE_YAXIS[y2axis_key], + DEVICE_YAXIS[y3axis_key], + ] data += cls._plot_candlestick_time_series_price(device_dict, y1axis_key, "y1") data += cls._plot_bar_time_series_traded( - device_dict, y2axis_key, "y2", expected_varname=y3axis_key) + device_dict, y2axis_key, "y2", expected_varname=y3axis_key + ) data += cls._plot_line_time_series(device_dict, y3axis_key) layout = cls._device_plot_layout("overlay", device_name, "Time", yaxis_caption_list) @@ -516,8 +565,11 @@ def plot_device_profile(cls, device_dict, device_name, output_file, device_strat y1axis_key = "trade_price_eur" y2axis_key = trade_energy_var_name y3axis_key = "pv_production_kWh" - yaxis_caption_list = [DEVICE_YAXIS[y1axis_key], DEVICE_YAXIS[y2axis_key], - DEVICE_YAXIS[y3axis_key]] + yaxis_caption_list = [ + DEVICE_YAXIS[y1axis_key], + DEVICE_YAXIS[y2axis_key], + DEVICE_YAXIS[y3axis_key], + ] data += cls._plot_candlestick_time_series_price(device_dict, y1axis_key, "y1") data += cls._plot_bar_time_series_traded( @@ -525,38 +577,49 @@ def plot_device_profile(cls, device_dict, device_name, output_file, device_strat ) data += cls._plot_line_time_series(device_dict, y3axis_key) - layout = cls._device_plot_layout("overlay", f"{device_name}", - "Time", yaxis_caption_list) + layout = cls._device_plot_layout( + "overlay", f"{device_name}", "Time", yaxis_caption_list + ) elif isinstance(device_strategy, HeatPumpStrategy): y1axis_key = "trade_price_eur" y2axis_key = trade_energy_var_name y3axis_key = "storage_temp_C" - yaxis_caption_list = [DEVICE_YAXIS[y1axis_key], DEVICE_YAXIS[y2axis_key], - DEVICE_YAXIS[y3axis_key]] + yaxis_caption_list = [ + DEVICE_YAXIS[y1axis_key], + DEVICE_YAXIS[y2axis_key], + DEVICE_YAXIS[y3axis_key], + ] data += cls._plot_candlestick_time_series_price(device_dict, y1axis_key, "y1") data += cls._plot_bar_time_series_traded( - device_dict, y2axis_key, "y2", expected_varname=y2axis_key) + device_dict, y2axis_key, "y2", expected_varname=y2axis_key + ) data += cls._plot_line_time_series(device_dict, y3axis_key) - layout = cls._device_plot_layout("overlay", f"{device_name}", - "Time", yaxis_caption_list) + layout = cls._device_plot_layout( + "overlay", f"{device_name}", "Time", yaxis_caption_list + ) elif type(device_strategy) == FinitePowerPlant: y1axis_key = "trade_price_eur" y2axis_key = trade_energy_var_name y3axis_key = "production_kWh" - yaxis_caption_list = [DEVICE_YAXIS[y1axis_key], DEVICE_YAXIS[y2axis_key], - DEVICE_YAXIS[y3axis_key]] + yaxis_caption_list = [ + DEVICE_YAXIS[y1axis_key], + DEVICE_YAXIS[y2axis_key], + DEVICE_YAXIS[y3axis_key], + ] data += cls._plot_candlestick_time_series_price(device_dict, y1axis_key, "y1") - data += cls._plot_bar_time_series_traded(device_dict, y2axis_key, "y2", - expected_varname=y3axis_key, invert_y=True) + data += cls._plot_bar_time_series_traded( + device_dict, y2axis_key, "y2", expected_varname=y3axis_key, invert_y=True + ) data += cls._plot_line_time_series(device_dict, y3axis_key) - layout = cls._device_plot_layout("overlay", f"{device_name}", - "Time", yaxis_caption_list) + layout = cls._device_plot_layout( + "overlay", f"{device_name}", "Time", yaxis_caption_list + ) elif type(device_strategy) in [CommercialStrategy, MarketMakerStrategy]: y1axis_key = "trade_price_eur" y2axis_key = sold_trade_energy_var_name @@ -565,22 +628,27 @@ def plot_device_profile(cls, device_dict, device_name, output_file, device_strat data += cls._plot_candlestick_time_series_price(device_dict, y1axis_key, "y1") data += cls._plot_bar_time_series_traded(device_dict, y2axis_key, "y2", invert_y=True) - layout = cls._device_plot_layout("overlay", f"{device_name}", - "Time", yaxis_caption_list) + layout = cls._device_plot_layout( + "overlay", f"{device_name}", "Time", yaxis_caption_list + ) elif type(device_strategy) == InfiniteBusStrategy: y1axis_key = "trade_price_eur" y2axis_key = sold_trade_energy_var_name y3axis_key = bought_trade_energy_var_name - yaxis_caption_list = [DEVICE_YAXIS[y1axis_key], DEVICE_YAXIS[y2axis_key], - DEVICE_YAXIS[y3axis_key]] + yaxis_caption_list = [ + DEVICE_YAXIS[y1axis_key], + DEVICE_YAXIS[y2axis_key], + DEVICE_YAXIS[y3axis_key], + ] data += cls._plot_candlestick_time_series_price(device_dict, y1axis_key, "y1") data += cls._plot_bar_time_series_traded(device_dict, y2axis_key, "y2") data += cls._plot_bar_time_series_traded(device_dict, y3axis_key, "y3") - layout = cls._device_plot_layout("overlay", f"{device_name}", - "Time", yaxis_caption_list) + layout = cls._device_plot_layout( + "overlay", f"{device_name}", "Time", yaxis_caption_list + ) else: return @@ -600,7 +668,7 @@ def _device_plot_layout(barmode, title, xaxis_caption, yaxis_caption_list): showgrid=True, domain=[pointer, pointer + d_domain_diff], rangemode="tozero", - autorange=True + autorange=True, ) return go.Layout( @@ -613,15 +681,10 @@ def _device_plot_layout(barmode, title, xaxis_caption, yaxis_caption_list): title=xaxis_caption, showgrid=True, anchor="y1", - rangeslider=dict(visible=True, - thickness=0.075, - bgcolor="rgba(100,100,100,0.3)" - ) - ), - font=dict( - size=16 + rangeslider=dict(visible=True, thickness=0.075, bgcolor="rgba(100,100,100,0.3)"), ), + font=dict(size=16), showlegend=True, legend=dict(x=1.1, y=1), - **yaxes + **yaxes, ) diff --git a/src/gsy_e/gsy_e_core/simulation/progress_info.py b/src/gsy_e/gsy_e_core/simulation/progress_info.py index e4db7cfdf..9a0c3fdfc 100644 --- a/src/gsy_e/gsy_e_core/simulation/progress_info.py +++ b/src/gsy_e/gsy_e_core/simulation/progress_info.py @@ -19,11 +19,12 @@ from logging import getLogger from typing import TYPE_CHECKING -from gsy_framework.utils import format_datetime from pendulum import DateTime, Duration, duration, now +from gsy_framework.utils import format_datetime +from gsy_framework.constants_limits import TIME_ZONE import gsy_e.constants -from gsy_e.constants import TIME_ZONE + if TYPE_CHECKING: from gsy_e.gsy_e_core.simulation.time_manager import SimulationTimeManager @@ -51,16 +52,18 @@ def _get_market_slot_time_str(cls, slot_number: int, config: "SimulationConfig") @staticmethod def _get_market_slot_time(slot_number: int, config: "SimulationConfig") -> DateTime: - return config.start_date.add( - minutes=config.slot_length.total_minutes() * slot_number - ) - - def update(self, slot_no: int, slot_count: int, time_params: "SimulationTimeManager", - config: "SimulationConfig") -> None: + return config.start_date.add(minutes=config.slot_length.total_minutes() * slot_number) + + def update( + self, + slot_no: int, + slot_count: int, + time_params: "SimulationTimeManager", + config: "SimulationConfig", + ) -> None: """Update progress info according to the simulation progress.""" run_duration = ( - now(tz=TIME_ZONE) - time_params.start_time - - duration(seconds=time_params.paused_time) + now(tz=TIME_ZONE) - time_params.start_time - duration(seconds=time_params.paused_time) ) if gsy_e.constants.RUN_IN_REALTIME: @@ -73,20 +76,25 @@ def update(self, slot_no: int, slot_count: int, time_params: "SimulationTimeMana self.elapsed_time = run_duration self.current_slot_str = self._get_market_slot_time_str(slot_no, config) self.current_slot_time = self._get_market_slot_time(slot_no, config) - self.next_slot_str = self._get_market_slot_time_str( - slot_no + 1, config) + self.next_slot_str = self._get_market_slot_time_str(slot_no + 1, config) self.current_slot_number = slot_no - log.warning("Slot %s of %s - (%.1f %%) %s elapsed, ETA: %s", slot_no+1, slot_count, - self.percentage_completed, self.elapsed_time, - self.eta) + log.warning( + "Slot %s of %s - (%.1f %%) %s elapsed, ETA: %s", + slot_no + 1, + slot_count, + self.percentage_completed, + self.elapsed_time, + self.eta, + ) - def log_simulation_finished(self, paused_duration: Duration, config: "SimulationConfig" - ) -> None: + def log_simulation_finished( + self, paused_duration: Duration, config: "SimulationConfig" + ) -> None: """Log that the simulation has finished.""" log.info( "Run finished in %s%s / %.2fx real time", self.elapsed_time, f" ({paused_duration} paused)" if paused_duration else "", - config.sim_duration / (self.elapsed_time - paused_duration) + config.sim_duration / (self.elapsed_time - paused_duration), ) diff --git a/src/gsy_e/gsy_e_core/simulation/results_manager.py b/src/gsy_e/gsy_e_core/simulation/results_manager.py index f9d730100..b8fcb54ad 100644 --- a/src/gsy_e/gsy_e_core/simulation/results_manager.py +++ b/src/gsy_e/gsy_e_core/simulation/results_manager.py @@ -19,13 +19,12 @@ from logging import getLogger from typing import TYPE_CHECKING, Optional -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, DATE_TIME_FORMAT, TIME_ZONE from gsy_framework.enums import SpotMarketTypeEnum from gsy_framework.kafka_communication.kafka_producer import kafka_connection_factory from pendulum import DateTime, now import gsy_e.constants -from gsy_e.constants import DATE_TIME_FORMAT, TIME_ZONE from gsy_e.gsy_e_core.export import CoefficientExportAndPlot, ExportAndPlot from gsy_e.gsy_e_core.sim_results.endpoint_buffer import ( CoefficientEndpointBuffer, diff --git a/src/gsy_e/gsy_e_core/simulation/time_manager.py b/src/gsy_e/gsy_e_core/simulation/time_manager.py index b696f97f6..3d71c3150 100644 --- a/src/gsy_e/gsy_e_core/simulation/time_manager.py +++ b/src/gsy_e/gsy_e_core/simulation/time_manager.py @@ -24,12 +24,11 @@ from typing import TYPE_CHECKING, Tuple import pendulum -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, TIME_ZONE from gsy_framework.enums import SpotMarketTypeEnum from pendulum import DateTime, duration, now import gsy_e.constants -from gsy_e.constants import TIME_ZONE if TYPE_CHECKING: from gsy_e.models.area import Area, AreaBase @@ -44,7 +43,8 @@ class TimeManagerBase: @staticmethod def _sleep_and_wake_up_if_stopped( - sleep_time_s: float, status: "SimulationStatusManager") -> None: + sleep_time_s: float, status: "SimulationStatusManager" + ) -> None: if sleep_time_s > 0: start_time = time() while time() - start_time < sleep_time_s and not status.stopped: @@ -82,8 +82,12 @@ def _set_area_current_tick(self, area: "Area", current_tick: int) -> None: self._set_area_current_tick(child, current_tick) def calculate_total_initial_ticks_slots( - self, config: "SimulationConfig", slot_resume: int, tick_resume: int, area: "AreaBase", - status: "SimulationStatusManager" + self, + config: "SimulationConfig", + slot_resume: int, + tick_resume: int, + area: "AreaBase", + status: "SimulationStatusManager", ) -> Tuple[int, int, int]: # pylint: disable = too-many-arguments """Calculate the initial slot and tick of the simulation, and the total slot count.""" @@ -109,13 +113,11 @@ def calculate_total_initial_ticks_slots( self._sleep_and_wake_up_if_stopped(seconds_until_next_tick, status) if self.slot_length_realtime: - self.tick_length_realtime_s = ( - self.slot_length_realtime.seconds / - config.ticks_per_slot) + self.tick_length_realtime_s = self.slot_length_realtime.seconds / config.ticks_per_slot return slot_count, slot_resume, tick_resume def handle_slowdown_and_realtime( - self, tick_no: int, config: "SimulationConfig", status: "SimulationStatusManager" + self, tick_no: int, config: "SimulationConfig", status: "SimulationStatusManager" ) -> None: """ Handle simulation slowdown and simulation realtime mode, and sleep the simulation @@ -133,8 +135,12 @@ def handle_slowdown_and_realtime( return if sleep_time_s > 0: - log.debug("Tick %s/%s: Sleep time of %s s was applied", - tick_no + 1, config.ticks_per_slot, sleep_time_s) + log.debug( + "Tick %s/%s: Sleep time of %s s was applied", + tick_no + 1, + config.ticks_per_slot, + sleep_time_s, + ) self.tick_time_counter = time() @@ -142,6 +148,7 @@ def handle_slowdown_and_realtime( @dataclass class SimulationTimeManagerScm(TimeManagerBase): """Handles simulation time management.""" + start_time: DateTime = None paused_time: int = 0 # Time spent in paused state, in seconds slot_length_realtime: duration = None @@ -166,14 +173,19 @@ def reset(self, not_restored_from_state: bool = True) -> None: self.paused_time = 0 def handle_slowdown_and_realtime_scm( - self, slot_no: int, slot_count: int, - config: "SimulationConfig", status: "SimulationStatusManager") -> None: + self, + slot_no: int, + slot_count: int, + config: "SimulationConfig", + status: "SimulationStatusManager", + ) -> None: """ Handle simulation slowdown and simulation realtime mode, and sleep the simulation accordingly for SCM simulations. """ slot_length_realtime_s = ( - self.slot_length_realtime.total_seconds() if self.slot_length_realtime else None) + self.slot_length_realtime.total_seconds() if self.slot_length_realtime else None + ) if gsy_e.constants.RUN_IN_REALTIME: slot_runtime_s = time() - self.slot_time_counter @@ -187,8 +199,9 @@ def handle_slowdown_and_realtime_scm( return if sleep_time_s > 0: - log.debug("Slot %s/%s: Sleep time of %s s was applied", - slot_no, slot_count, sleep_time_s) + log.debug( + "Slot %s/%s: Sleep time of %s s was applied", slot_no, slot_count, sleep_time_s + ) self.slot_time_counter = int(time()) @@ -199,13 +212,13 @@ def get_start_time_on_init(config: "SimulationConfig") -> DateTime: today = pendulum.today(tz=TIME_ZONE) seconds_since_midnight = time() - today.int_timestamp slot_no = int(seconds_since_midnight // config.slot_length.seconds) + 1 - start_time = config.start_date + duration(seconds=slot_no*config.slot_length.seconds) + start_time = config.start_date + duration(seconds=slot_no * config.slot_length.seconds) else: start_time = config.start_date return start_time def calc_resume_slot_and_count_realtime( - self, config: "SimulationConfig", slot_resume: int, status: "SimulationStatusManager" + self, config: "SimulationConfig", slot_resume: int, status: "SimulationStatusManager" ) -> Tuple[int, int]: """Calculate total slot count and the slot where to resume the realtime simulation.""" slot_count = int(config.sim_duration / config.slot_length) @@ -219,8 +232,12 @@ def calc_resume_slot_and_count_realtime( seconds_elapsed_in_slot = seconds_since_midnight % config.slot_length.seconds sleep_time_s = config.slot_length.total_seconds() - seconds_elapsed_in_slot self._sleep_and_wake_up_if_stopped(sleep_time_s, status) - log.debug("Resume Slot %s/%s: Sleep time of %s s was applied", - slot_resume, slot_count, sleep_time_s) + log.debug( + "Resume Slot %s/%s: Sleep time of %s s was applied", + slot_resume, + slot_count, + sleep_time_s, + ) return slot_count, slot_resume @@ -228,6 +245,7 @@ def calc_resume_slot_and_count_realtime( def simulation_time_manager_factory(slot_length_realtime: duration, hours_of_delay: int): """Factory for time manager objects.""" if ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.COEFFICIENTS.value: - return SimulationTimeManagerScm(slot_length_realtime=slot_length_realtime, - hours_of_delay=hours_of_delay) + return SimulationTimeManagerScm( + slot_length_realtime=slot_length_realtime, hours_of_delay=hours_of_delay + ) return SimulationTimeManager(slot_length_realtime=slot_length_realtime) diff --git a/src/gsy_e/gsy_e_core/util.py b/src/gsy_e/gsy_e_core/util.py index 3ef3c9066..768307036 100644 --- a/src/gsy_e/gsy_e_core/util.py +++ b/src/gsy_e/gsy_e_core/util.py @@ -29,7 +29,7 @@ from typing import TYPE_CHECKING, Optional from click.types import ParamType -from gsy_framework.constants_limits import ConstSettings, GlobalConfig, RangeLimit +from gsy_framework.constants_limits import ConstSettings, GlobalConfig, RangeLimit, DATE_FORMAT from gsy_framework.enums import BidOfferMatchAlgoEnum from gsy_framework.exceptions import GSyException from gsy_framework.utils import ( @@ -103,15 +103,15 @@ class DateType(ParamType): name = "date" - def __init__(self, date_type: gsy_e.constants.DATE_FORMAT): - if date_type == gsy_e.constants.DATE_FORMAT: - self.allowed_formats = gsy_e.constants.DATE_FORMAT + def __init__(self, date_type: DATE_FORMAT): + if date_type == DATE_FORMAT: + self.allowed_formats = DATE_FORMAT else: - raise ValueError(f"Invalid date_type. Choices: {gsy_e.constants.DATE_FORMAT} ") + raise ValueError(f"Invalid date_type. Choices: {DATE_FORMAT} ") def convert(self, value, param, ctx): try: - converted_format = from_format(value, gsy_e.constants.DATE_FORMAT) + converted_format = from_format(value, DATE_FORMAT) except ValueError: self.fail(f"'{value}' is not a valid date. Allowed formats: {self.allowed_formats}") return converted_format @@ -410,7 +410,7 @@ def export_default_settings_to_json_file(): "sim_duration": f"{GlobalConfig.DURATION_D * 24}h", "slot_length": f"{GlobalConfig.SLOT_LENGTH_M}m", "tick_length": f"{GlobalConfig.TICK_LENGTH_S}s", - "start_date": instance(GlobalConfig.start_date).format(gsy_e.constants.DATE_FORMAT), + "start_date": instance(GlobalConfig.start_date).format(DATE_FORMAT), } all_settings = {"basic_settings": base_settings, "advanced_settings": constsettings_to_dict()} settings_filename = os.path.join(gsye_root_path, "setup", "gsy_e_settings.json") diff --git a/src/gsy_e/models/area/scm_dataclasses.py b/src/gsy_e/models/area/scm_dataclasses.py index f009c4a42..a622425f0 100644 --- a/src/gsy_e/models/area/scm_dataclasses.py +++ b/src/gsy_e/models/area/scm_dataclasses.py @@ -4,12 +4,13 @@ from typing import Dict, List from uuid import uuid4 +from pendulum import DateTime + +from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE from gsy_framework.data_classes import Trade, TraderDetails from gsy_framework.sim_results.kpi_calculation_helper import KPICalculationHelper -from pendulum import DateTime import gsy_e.constants -from gsy_e.constants import FLOATING_POINT_TOLERANCE @dataclass diff --git a/src/gsy_e/models/area/scm_manager.py b/src/gsy_e/models/area/scm_manager.py index f14404cc3..c2eb19fe6 100644 --- a/src/gsy_e/models/area/scm_manager.py +++ b/src/gsy_e/models/area/scm_manager.py @@ -1,7 +1,7 @@ from math import isclose from typing import TYPE_CHECKING, Dict, Optional -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, FLOATING_POINT_TOLERANCE from gsy_framework.enums import SCMSelfConsumptionType from pendulum import DateTime @@ -9,7 +9,6 @@ from gsy_e.constants import ( DEFAULT_SCM_COMMUNITY_NAME, DEFAULT_SCM_GRID_NAME, - FLOATING_POINT_TOLERANCE, ) from gsy_e.gsy_e_core.util import get_slots_per_month from gsy_e.models.area.scm_dataclasses import ( diff --git a/src/gsy_e/models/config.py b/src/gsy_e/models/config.py index 2ce61b6a4..f5af82427 100644 --- a/src/gsy_e/models/config.py +++ b/src/gsy_e/models/config.py @@ -15,20 +15,21 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import json -from gsy_framework.constants_limits import ConstSettings, GlobalConfig +from gsy_framework.constants_limits import ConstSettings, GlobalConfig, TIME_ZONE from gsy_framework.exceptions import GSyException from gsy_framework.read_user_profile import InputProfileTypes, read_arbitrary_profile from pendulum import DateTime, Duration, duration, today -from gsy_e.constants import TIME_ZONE from gsy_e.gsy_e_core.redis_connections.area_market import external_redis_communicator_factory from gsy_e.gsy_e_core.util import change_global_config, format_interval class SimulationConfig: """Class defining parameters that describe the behavior of a simulation.""" + # pylint: disable=too-many-instance-attributes, too-many-arguments def __init__( self, @@ -73,11 +74,13 @@ def __init__( if self.ticks_per_slot != int(self.ticks_per_slot): raise GSyException( f"Non integer ticks per slot ({self.ticks_per_slot}) are not supported. " - "Adjust simulation parameters.") + "Adjust simulation parameters." + ) self.ticks_per_slot = int(self.ticks_per_slot) if self.ticks_per_slot < 10: raise GSyException( - f"Too few ticks per slot ({self.ticks_per_slot}). Adjust simulation parameters") + f"Too few ticks per slot ({self.ticks_per_slot}). Adjust simulation parameters" + ) self.total_ticks = self.sim_duration // self.slot_length * self.ticks_per_slot self.market_slot_list = [] @@ -88,7 +91,8 @@ def __init__( self.capacity_kW = capacity_kW or ConstSettings.PVSettings.DEFAULT_CAPACITY_KW self.external_connection_enabled = external_connection_enabled self.external_redis_communicator = external_redis_communicator_factory( - external_connection_enabled) + external_connection_enabled + ) if aggregator_device_mapping: self.external_redis_communicator.aggregator.set_aggregator_device_mapping( aggregator_device_mapping @@ -100,17 +104,25 @@ def __repr__(self): def as_dict(self): """Return config parameters as dict.""" - fields = {"sim_duration", "slot_length", "tick_length", "ticks_per_slot", - "total_ticks", "capacity_kW", "grid_fee_type", - "external_connection_enabled", "enable_degrees_of_freedom", "hours_of_delay"} + fields = { + "sim_duration", + "slot_length", + "tick_length", + "ticks_per_slot", + "total_ticks", + "capacity_kW", + "grid_fee_type", + "external_connection_enabled", + "enable_degrees_of_freedom", + "hours_of_delay", + } return { k: format_interval(v) if isinstance(v, Duration) else v for k, v in self.__dict__.items() if k in fields } - def update_config_parameters(self, *, - market_maker_rate=None, capacity_kW=None): + def update_config_parameters(self, *, market_maker_rate=None, capacity_kW=None): """Update provided config parameters.""" if market_maker_rate is not None: self.set_market_maker_rate(market_maker_rate) @@ -122,7 +134,8 @@ def set_market_maker_rate(self, market_maker_rate): Reads market_maker_rate from arbitrary input types """ self.market_maker_rate = read_arbitrary_profile( - InputProfileTypes.IDENTITY, market_maker_rate) + InputProfileTypes.IDENTITY, market_maker_rate + ) def create_simulation_config_from_global_config(): @@ -141,5 +154,5 @@ def create_simulation_config_from_global_config(): market_maker_rate=GlobalConfig.market_maker_rate, start_date=GlobalConfig.start_date, grid_fee_type=GlobalConfig.grid_fee_type, - enable_degrees_of_freedom=GlobalConfig.enable_degrees_of_freedom + enable_degrees_of_freedom=GlobalConfig.enable_degrees_of_freedom, ) diff --git a/src/gsy_e/models/market/__init__.py b/src/gsy_e/models/market/__init__.py index 5251b1ad4..395391e52 100644 --- a/src/gsy_e/models/market/__init__.py +++ b/src/gsy_e/models/market/__init__.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import uuid from collections import namedtuple from dataclasses import dataclass @@ -23,20 +24,26 @@ from threading import RLock from typing import Dict, List, Union, Optional, Callable, TYPE_CHECKING -from gsy_framework.constants_limits import ConstSettings, GlobalConfig +from gsy_framework.constants_limits import ( + ConstSettings, + GlobalConfig, + FLOATING_POINT_TOLERANCE, + DATE_TIME_FORMAT, +) from gsy_framework.data_classes import Offer, Trade, Bid from gsy_framework.enums import SpotMarketTypeEnum from numpy.random import random from pendulum import DateTime, duration -from gsy_e.constants import FLOATING_POINT_TOLERANCE, DATE_TIME_FORMAT from gsy_e.gsy_e_core.device_registry import DeviceRegistry from gsy_e.gsy_e_core.util import add_or_create_key, subtract_or_create_key from gsy_e.models.market.grid_fees.base_model import GridFees from gsy_e.models.market.grid_fees.constant_grid_fees import ConstantGridFees from gsy_e.models.market.market_redis_connection import ( - MarketRedisEventSubscriber, MarketRedisEventPublisher, - TwoSidedMarketRedisEventSubscriber) + MarketRedisEventSubscriber, + MarketRedisEventPublisher, + TwoSidedMarketRedisEventSubscriber, +) if TYPE_CHECKING: from gsy_e.models.config import SimulationConfig @@ -61,12 +68,14 @@ def wrapper(self, *args, **kwargs): with lock_object: return function(self, *args, **kwargs) + return wrapper @dataclass(frozen=True) class MarketSlotParams: """Parameters that describe a market slot.""" + opening_time: DateTime closing_time: DateTime delivery_start_time: DateTime @@ -87,10 +96,15 @@ class MarketBase: # pylint: disable=too-many-instance-attributes """ def __init__( # pylint: disable=too-many-arguments - self, time_slot: Optional[DateTime] = None, bc=None, - notification_listener: Optional[Callable] = None, readonly: bool = False, - grid_fee_type: int = ConstSettings.MASettings.GRID_FEE_TYPE, - grid_fees: Optional[GridFee] = None, name: Optional[str] = None): + self, + time_slot: Optional[DateTime] = None, + bc=None, + notification_listener: Optional[Callable] = None, + readonly: bool = False, + grid_fee_type: int = ConstSettings.MASettings.GRID_FEE_TYPE, + grid_fees: Optional[GridFee] = None, + name: Optional[str] = None, + ): self.name = name self.bc_interface = bc self.id = str(uuid.uuid4()) @@ -125,7 +139,8 @@ def __init__( # pylint: disable=too-many-arguments self.redis_api = ( MarketRedisEventSubscriber(self) if ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.ONE_SIDED.value - else TwoSidedMarketRedisEventSubscriber(self)) + else TwoSidedMarketRedisEventSubscriber(self) + ) setattr(self, RLOCK_MEMBER_NAME, RLock()) self._open_market_slot_parameters: Dict[DateTime, MarketSlotParams] = {} @@ -145,7 +160,8 @@ def info(self) -> Dict: "start_time": self.time_slot_str, "duration_min": GlobalConfig.slot_length.minutes, "time_slots": [self.time_slot_str], # Use a list for compatibility with future markets - "type_name": self.type_name} + "type_name": self.type_name, + } @property def type_name(self) -> str: @@ -173,8 +189,9 @@ def most_affordable_offers(self): """Return the offers with the least energy_rate value.""" cheapest_offer = self.sorted_offers[0] rate = cheapest_offer.energy_rate - return [o for o in self.sorted_offers if - abs(o.energy_rate - rate) < FLOATING_POINT_TOLERANCE] + return [ + o for o in self.sorted_offers if abs(o.energy_rate - rate) < FLOATING_POINT_TOLERANCE + ] def _create_fee_handler(self, grid_fee_type: int, grid_fees: GridFee) -> None: if not grid_fees: @@ -216,16 +233,15 @@ def _notify_listeners(self, event, **kwargs): for listener in sorted(self.notification_listeners, key=lambda l: random()): listener(event, market_id=self.id, **kwargs) - def _update_stats_after_trade( - self, trade: Trade, order: Union[Offer, Bid]) -> None: + def _update_stats_after_trade(self, trade: Trade, order: Union[Offer, Bid]) -> None: """Update the instance state in response to an occurring trade.""" self.trades.append(trade) self.market_fee += trade.fee_price self._update_accumulated_trade_price_energy(trade) - self.traded_energy = add_or_create_key( - self.traded_energy, trade.seller.name, order.energy) + self.traded_energy = add_or_create_key(self.traded_energy, trade.seller.name, order.energy) self.traded_energy = subtract_or_create_key( - self.traded_energy, trade.buyer.name, order.energy) + self.traded_energy, trade.buyer.name, order.energy + ) self._update_min_max_avg_trade_prices(order.energy_rate) def _update_accumulated_trade_price_energy(self, trade: Trade): @@ -233,10 +249,12 @@ def _update_accumulated_trade_price_energy(self, trade: Trade): self.accumulated_trade_energy += trade.traded_energy def _update_min_max_avg_trade_prices(self, price): - self.max_trade_price = round( - max(self.max_trade_price, price), 4) if self.max_trade_price else round(price, 4) - self.min_trade_price = round( - min(self.min_trade_price, price), 4) if self.min_trade_price else round(price, 4) + self.max_trade_price = ( + round(max(self.max_trade_price, price), 4) if self.max_trade_price else round(price, 4) + ) + self.min_trade_price = ( + round(min(self.min_trade_price, price), 4) if self.min_trade_price else round(price, 4) + ) self._avg_trade_price = None def __repr__(self): @@ -245,19 +263,16 @@ def __repr__(self): f" (E: {sum(o.energy for o in self.offers.values())} kWh" f" V: {sum(o.price for o in self.offers.values())})" f" trades: {len(self.trades)} (E: {self.accumulated_trade_energy} kWh," - f" V: {self.accumulated_trade_price})>") + f" V: {self.accumulated_trade_price})>" + ) @staticmethod def sorting(offers_bids: Dict, reverse_order=False) -> List[Union[Bid, Offer]]: """Sort a list of bids or offers by their energy_rate attribute.""" if reverse_order: # Sorted bids in descending order - return list(reversed(sorted( - offers_bids.values(), - key=lambda obj: obj.energy_rate))) - return sorted(offers_bids.values(), - key=lambda obj: obj.energy_rate, - reverse=reverse_order) + return list(reversed(sorted(offers_bids.values(), key=lambda obj: obj.energy_rate))) + return sorted(offers_bids.values(), key=lambda obj: obj.energy_rate, reverse=reverse_order) def update_clock(self, now: DateTime) -> None: """ @@ -302,8 +317,7 @@ def _get_market_slot_duration(config: Optional["SimulationConfig"]) -> duration: return config.slot_length return GlobalConfig.slot_length - def get_market_parameters_for_market_slot( - self, market_slot: DateTime) -> MarketSlotParams: + def get_market_parameters_for_market_slot(self, market_slot: DateTime) -> MarketSlotParams: """Retrieve the parameters for the selected market slot.""" return self._open_market_slot_parameters.get(market_slot) @@ -313,7 +327,8 @@ def open_market_slot_info(self) -> Dict[DateTime, MarketSlotParams]: return self._open_market_slot_parameters def set_open_market_slot_parameters( - self, current_market_slot: DateTime, created_market_slots: Optional[List[DateTime]]): + self, current_market_slot: DateTime, created_market_slots: Optional[List[DateTime]] + ): """Update the parameters of the newly opened market slots.""" for market_slot in created_market_slots: if market_slot in self._open_market_slot_parameters: @@ -321,8 +336,8 @@ def set_open_market_slot_parameters( self._open_market_slot_parameters[market_slot] = MarketSlotParams( delivery_start_time=self._calculate_closing_time(market_slot), - delivery_end_time=self._calculate_closing_time(market_slot) + - self._get_market_slot_duration(None), + delivery_end_time=self._calculate_closing_time(market_slot) + + self._get_market_slot_duration(None), opening_time=current_market_slot, - closing_time=self._calculate_closing_time(market_slot) + closing_time=self._calculate_closing_time(market_slot), ) diff --git a/src/gsy_e/models/market/balancing.py b/src/gsy_e/models/market/balancing.py index 7060f4fc8..d8634e450 100644 --- a/src/gsy_e/models/market/balancing.py +++ b/src/gsy_e/models/market/balancing.py @@ -15,21 +15,30 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import uuid from logging import getLogger from typing import Union, Dict, List, Optional # noqa -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, FLOATING_POINT_TOLERANCE from gsy_framework.data_classes import ( - BalancingOffer, BalancingTrade, Offer, TraderDetails, TradeBidOfferInfo) + BalancingOffer, + BalancingTrade, + Offer, + TraderDetails, + TradeBidOfferInfo, +) from pendulum import DateTime -from gsy_e.constants import FLOATING_POINT_TOLERANCE from gsy_e.events.event_structures import MarketEvent from gsy_e.gsy_e_core.device_registry import DeviceRegistry from gsy_e.gsy_e_core.exceptions import ( - InvalidOffer, MarketReadOnlyException, - OfferNotFoundException, InvalidBalancingTradeException, DeviceNotInRegistryError) + InvalidOffer, + MarketReadOnlyException, + OfferNotFoundException, + InvalidBalancingTradeException, + DeviceNotInRegistryError, +) from gsy_e.gsy_e_core.util import short_offer_bid_log_str from gsy_e.models.market.one_sided import OneSidedMarket @@ -40,9 +49,16 @@ class BalancingMarket(OneSidedMarket): """Market that regulates the supply and demand of energy.""" def __init__( # pylint: disable=too-many-arguments - self, time_slot=None, bc=None, notification_listener=None, readonly=False, - grid_fee_type=ConstSettings.MASettings.GRID_FEE_TYPE, - grid_fees=None, name=None, in_sim_duration=True): + self, + time_slot=None, + bc=None, + notification_listener=None, + readonly=False, + grid_fee_type=ConstSettings.MASettings.GRID_FEE_TYPE, + grid_fees=None, + name=None, + in_sim_duration=True, + ): self.unmatched_energy_upward = 0 self.unmatched_energy_downward = 0 self.accumulated_supply_balancing_trade_price = 0 @@ -50,28 +66,48 @@ def __init__( # pylint: disable=too-many-arguments self.accumulated_demand_balancing_trade_price = 0 self.accumulated_demand_balancing_trade_energy = 0 - super().__init__(time_slot, bc, notification_listener, readonly, grid_fee_type, - grid_fees, name, in_sim_duration=in_sim_duration) + super().__init__( + time_slot, + bc, + notification_listener, + readonly, + grid_fee_type, + grid_fees, + name, + in_sim_duration=in_sim_duration, + ) def offer( # pylint: disable=too-many-arguments - self, price: float, energy: float, seller: TraderDetails, - offer_id: Optional[str] = None, - original_price: Optional[float] = None, - dispatch_event: bool = True, - adapt_price_with_fees: bool = True, - add_to_history: bool = True, - time_slot: Optional[DateTime] = None): + self, + price: float, + energy: float, + seller: TraderDetails, + offer_id: Optional[str] = None, + original_price: Optional[float] = None, + dispatch_event: bool = True, + adapt_price_with_fees: bool = True, + add_to_history: bool = True, + time_slot: Optional[DateTime] = None, + ): assert False def balancing_offer( # pylint: disable=too-many-arguments - self, price: float, energy: float, seller: TraderDetails, - original_price=None, offer_id=None, from_agent: bool = False, - adapt_price_with_fees: bool = False, dispatch_event=True) -> BalancingOffer: + self, + price: float, + energy: float, + seller: TraderDetails, + original_price=None, + offer_id=None, + from_agent: bool = False, + adapt_price_with_fees: bool = False, + dispatch_event=True, + ) -> BalancingOffer: """Create a balancing offer.""" if seller.name not in DeviceRegistry.REGISTRY.keys() and not from_agent: - raise DeviceNotInRegistryError(f"Device {seller.name} " - f"not in registry ({DeviceRegistry.REGISTRY}).") + raise DeviceNotInRegistryError( + f"Device {seller.name} " f"not in registry ({DeviceRegistry.REGISTRY})." + ) if self.readonly: raise MarketReadOnlyException() if energy == 0: @@ -85,9 +121,7 @@ def balancing_offer( # pylint: disable=too-many-arguments if offer_id is None: offer_id = str(uuid.uuid4()) - offer = BalancingOffer( - offer_id, self.now, price, energy, seller, - time_slot=self.time_slot) + offer = BalancingOffer(offer_id, self.now, price, energy, seller, time_slot=self.time_slot) self.offers[offer.id] = offer self.offer_history.append(offer) @@ -101,33 +135,41 @@ def split_offer(self, original_offer, energy, orig_offer_price=None): self.offers.pop(original_offer.id, None) # same offer id is used for the new accepted_offer - accepted_offer = self.balancing_offer(offer_id=original_offer.id, - price=original_offer.price * - (energy / original_offer.energy), - energy=energy, - seller=original_offer.seller, - dispatch_event=False, - from_agent=True) + accepted_offer = self.balancing_offer( + offer_id=original_offer.id, + price=original_offer.price * (energy / original_offer.energy), + energy=energy, + seller=original_offer.seller, + dispatch_event=False, + from_agent=True, + ) residual_price = (1 - energy / original_offer.energy) * original_offer.price residual_energy = original_offer.energy - energy if orig_offer_price is None: orig_offer_price = original_offer.original_price or original_offer.price original_residual_price = ( - ((original_offer.energy - energy) / original_offer.energy) * orig_offer_price) - - residual_offer = self.balancing_offer(price=residual_price, - energy=residual_energy, - seller=original_offer.seller, - original_price=original_residual_price, - dispatch_event=False, - adapt_price_with_fees=False, - from_agent=True) + (original_offer.energy - energy) / original_offer.energy + ) * orig_offer_price + + residual_offer = self.balancing_offer( + price=residual_price, + energy=residual_energy, + seller=original_offer.seller, + original_price=original_residual_price, + dispatch_event=False, + adapt_price_with_fees=False, + from_agent=True, + ) log.debug( "[BALANCING_OFFER][SPLIT][%s, %s] (%s into %s and %s", - self.time_slot_str, self.name, short_offer_bid_log_str(original_offer), - short_offer_bid_log_str(accepted_offer), short_offer_bid_log_str(residual_offer)) + self.time_slot_str, + self.name, + short_offer_bid_log_str(original_offer), + short_offer_bid_log_str(accepted_offer), + short_offer_bid_log_str(residual_offer), + ) self.bc_interface.change_offer(accepted_offer, original_offer, residual_offer) @@ -135,20 +177,26 @@ def split_offer(self, original_offer, energy, orig_offer_price=None): MarketEvent.BALANCING_OFFER_SPLIT, original_offer=original_offer, accepted_offer=accepted_offer, - residual_offer=residual_offer) + residual_offer=residual_offer, + ) return accepted_offer, residual_offer def _determine_offer_price( - self, energy_portion, energy, trade_rate, - trade_bid_info, orig_offer_price): + self, energy_portion, energy, trade_rate, trade_bid_info, orig_offer_price + ): return self._update_offer_fee_and_calculate_final_price( - energy, trade_rate, energy_portion, orig_offer_price) + energy, trade_rate, energy_portion, orig_offer_price + ) def accept_offer( # pylint: disable=too-many-locals - self, offer_or_id: Union[str, BalancingOffer], buyer: TraderDetails, *, - energy: int = None, - trade_bid_info: Optional[TradeBidOfferInfo] = None) -> BalancingTrade: + self, + offer_or_id: Union[str, BalancingOffer], + buyer: TraderDetails, + *, + energy: int = None, + trade_bid_info: Optional[TradeBidOfferInfo] = None, + ) -> BalancingTrade: if self.readonly: raise MarketReadOnlyException() @@ -159,8 +207,7 @@ def accept_offer( # pylint: disable=too-many-locals raise OfferNotFoundException() if (offer.energy > 0 > energy) or (offer.energy < 0 < energy): - raise InvalidBalancingTradeException("BalancingOffer and energy " - "are not compatible") + raise InvalidBalancingTradeException("BalancingOffer and energy " "are not compatible") if energy is None: energy = offer.energy @@ -183,13 +230,15 @@ def accept_offer( # pylint: disable=too-many-locals accepted_offer, residual_offer = self.split_offer(offer, energy, orig_offer_price) fees, trade_price = self._determine_offer_price( - energy / offer.energy, energy, trade_rate, trade_bid_info, orig_offer_price) + energy / offer.energy, energy, trade_rate, trade_bid_info, orig_offer_price + ) offer = accepted_offer offer.update_price(trade_price) elif abs(energy - offer.energy) > FLOATING_POINT_TOLERANCE: raise InvalidBalancingTradeException( - f"Energy ({energy}) can't be greater than offered energy ({offer.energy}).") + f"Energy ({energy}) can't be greater than offered energy ({offer.energy})." + ) else: # Requested energy is equal to offer's energy - just proceed normally fees, trade_price = self._update_offer_fee_and_calculate_final_price( @@ -206,14 +255,20 @@ def accept_offer( # pylint: disable=too-many-locals self.offers.pop(offer.id, None) trade_id, residual_offer = self.bc_interface.handle_blockchain_trade_event( - offer, buyer, original_offer, residual_offer) - trade = BalancingTrade(trade_id, self.now, offer.seller, - buyer=buyer, - offer=offer, - traded_energy=energy, trade_price=trade_price, - residual=residual_offer, - fee_price=fees, - time_slot=offer.time_slot) + offer, buyer, original_offer, residual_offer + ) + trade = BalancingTrade( + trade_id, + self.now, + offer.seller, + buyer=buyer, + offer=offer, + traded_energy=energy, + trade_price=trade_price, + residual=residual_offer, + fee_price=fees, + time_slot=offer.time_slot, + ) self.bc_interface.track_trade_event(self.time_slot, trade) if offer.seller != buyer: diff --git a/src/gsy_e/models/market/one_sided.py b/src/gsy_e/models/market/one_sided.py index 743dccaeb..a107367b1 100644 --- a/src/gsy_e/models/market/one_sided.py +++ b/src/gsy_e/models/market/one_sided.py @@ -15,22 +15,26 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from copy import deepcopy from logging import getLogger from math import isclose from typing import Union, Dict, Optional, Callable, Tuple -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, FLOATING_POINT_TOLERANCE from gsy_framework.data_classes import Offer, Trade, TradeBidOfferInfo, TraderDetails, Bid from gsy_framework.enums import SpotMarketTypeEnum from gsy_framework.utils import limit_float_precision from pendulum import DateTime -from gsy_e.constants import FLOATING_POINT_TOLERANCE from gsy_e.events.event_structures import MarketEvent from gsy_e.gsy_e_core.exceptions import ( - MarketReadOnlyException, OfferNotFoundException, InvalidTrade, - NegativePriceOrdersException, NegativeEnergyOrderException) + MarketReadOnlyException, + OfferNotFoundException, + InvalidTrade, + NegativePriceOrdersException, + NegativeEnergyOrderException, +) from gsy_e.gsy_e_core.util import short_offer_bid_log_str from gsy_e.models.market import MarketBase, lock_market_action, GridFee @@ -43,15 +47,22 @@ class OneSidedMarket(MarketBase): The default market type that D3A simulation uses. Only devices that supply energy (producers) are able to place offers on the markets. """ + def __init__( # pylint: disable=too-many-arguments - self, time_slot: Optional[DateTime] = None, - bc=None, notification_listener: Optional[Callable] = None, - readonly: bool = False, grid_fee_type=ConstSettings.MASettings.GRID_FEE_TYPE, - grid_fees: Optional[GridFee] = None, name: Optional[str] = None, - in_sim_duration: bool = True): + self, + time_slot: Optional[DateTime] = None, + bc=None, + notification_listener: Optional[Callable] = None, + readonly: bool = False, + grid_fee_type=ConstSettings.MASettings.GRID_FEE_TYPE, + grid_fees: Optional[GridFee] = None, + name: Optional[str] = None, + in_sim_duration: bool = True, + ): assert ConstSettings.MASettings.MARKET_TYPE != SpotMarketTypeEnum.COEFFICIENTS.value - super().__init__(time_slot, bc, notification_listener, readonly, grid_fee_type, - grid_fees, name) + super().__init__( + time_slot, bc, notification_listener, readonly, grid_fee_type, grid_fees, name + ) # If True, the current market slot is included in the expected duration of the simulation self.in_sim_duration = in_sim_duration @@ -62,7 +73,8 @@ def __repr__(self): f" offers: {len(self.offers)} (E: {sum(o.energy for o in self.offers.values())} kWh" f" V: {sum(o.price for o in self.offers.values())})" f" trades: {len(self.trades)} (E: {self.accumulated_trade_energy} kWh," - f" V: {self.accumulated_trade_price})>") + f" V: {self.accumulated_trade_price})>" + ) @property def _class_name(self) -> str: @@ -81,9 +93,13 @@ def _update_new_offer_price_with_fee(self, price: float, original_price: float, :param energy: Energy of the offer :return: Updated price for the forwarded offer on this market, in cents """ - return self.fee_class.update_incoming_offer_with_fee( - limit_float_precision(price / energy), - limit_float_precision(original_price / energy)) * energy + return ( + self.fee_class.update_incoming_offer_with_fee( + limit_float_precision(price / energy), + limit_float_precision(original_price / energy), + ) + * energy + ) @lock_market_action def get_offers(self) -> Dict: @@ -99,13 +115,17 @@ def get_offers(self) -> Dict: @lock_market_action def offer( # pylint: disable=too-many-arguments, too-many-locals - self, price: float, energy: float, seller: TraderDetails, - offer_id: Optional[str] = None, - original_price: Optional[float] = None, - dispatch_event: bool = True, - adapt_price_with_fees: bool = True, - add_to_history: bool = True, - time_slot: Optional[DateTime] = None) -> Offer: + self, + price: float, + energy: float, + seller: TraderDetails, + offer_id: Optional[str] = None, + original_price: Optional[float] = None, + dispatch_event: bool = True, + adapt_price_with_fees: bool = True, + add_to_history: bool = True, + time_slot: Optional[DateTime] = None, + ) -> Offer: """Post offer inside the market.""" if self.readonly: @@ -123,21 +143,26 @@ def offer( # pylint: disable=too-many-arguments, too-many-locals if price < 0.0: raise NegativePriceOrdersException( - "Negative price after taxes, offer cannot be posted.") + "Negative price after taxes, offer cannot be posted." + ) if offer_id is None: offer_id = self.bc_interface.create_new_offer(energy, price, seller) - offer = Offer(offer_id, self.now, price, energy, - seller, original_price, - time_slot=time_slot) + offer = Offer( + offer_id, self.now, price, energy, seller, original_price, time_slot=time_slot + ) self.offers[offer.id] = offer if add_to_history is True: self.offer_history.append(offer) - log.debug("%s[OFFER][NEW][%s][%s] %s", - self._debug_log_market_type_identifier, self.name, - self.time_slot_str or offer.time_slot, offer) + log.debug( + "%s[OFFER][NEW][%s][%s] %s", + self._debug_log_market_type_identifier, + self.name, + self.time_slot_str or offer.time_slot, + offer, + ) if dispatch_event is True: self.dispatch_market_offer_event(offer) self.no_new_order = False @@ -161,57 +186,72 @@ def delete_offer(self, offer_or_id: Union[str, Offer]) -> None: raise OfferNotFoundException() self.bc_interface.cancel_offer(offer) - log.debug("%s[OFFER][DEL][%s][%s] %s", - self._debug_log_market_type_identifier, self.name, - self.time_slot_str or offer.time_slot, offer) + log.debug( + "%s[OFFER][DEL][%s][%s] %s", + self._debug_log_market_type_identifier, + self.name, + self.time_slot_str or offer.time_slot, + offer, + ) self._notify_listeners(MarketEvent.OFFER_DELETED, offer=offer) - def _update_offer_fee_and_calculate_final_price(self, energy, trade_rate, - energy_portion, original_price): + def _update_offer_fee_and_calculate_final_price( + self, energy, trade_rate, energy_portion, original_price + ): if self._is_constant_fees: fees = self.fee_class.grid_fee_rate * energy else: fees = self.fee_class.grid_fee_rate * original_price * energy_portion return fees, energy * trade_rate - def split_offer(self, original_offer: Offer, energy: float, - orig_offer_price: float) -> Tuple[Offer, Offer]: + def split_offer( + self, original_offer: Offer, energy: float, orig_offer_price: float + ) -> Tuple[Offer, Offer]: """Split offer into two, one with provided energy, the other with the residual.""" self.offers.pop(original_offer.id, None) # same offer id is used for the new accepted_offer original_accepted_price = energy / original_offer.energy * orig_offer_price - accepted_offer = self.offer(offer_id=original_offer.id, - price=original_offer.price * (energy / original_offer.energy), - energy=energy, - seller=original_offer.seller, - original_price=original_accepted_price, - dispatch_event=False, - adapt_price_with_fees=False, - add_to_history=False, - time_slot=original_offer.time_slot) + accepted_offer = self.offer( + offer_id=original_offer.id, + price=original_offer.price * (energy / original_offer.energy), + energy=energy, + seller=original_offer.seller, + original_price=original_accepted_price, + dispatch_event=False, + adapt_price_with_fees=False, + add_to_history=False, + time_slot=original_offer.time_slot, + ) residual_price = (1 - energy / original_offer.energy) * original_offer.price residual_energy = original_offer.energy - energy - original_residual_price = ((original_offer.energy - energy) / - original_offer.energy) * orig_offer_price - - residual_offer = self.offer(price=residual_price, - energy=residual_energy, - seller=original_offer.seller, - original_price=original_residual_price, - dispatch_event=False, - adapt_price_with_fees=False, - add_to_history=True, - time_slot=original_offer.time_slot) - - log.debug("%s[OFFER][SPLIT][%s, %s] (%s into %s and %s", - self._debug_log_market_type_identifier, - self.time_slot_str or residual_offer.time_slot, self.name, - short_offer_bid_log_str(original_offer), short_offer_bid_log_str(accepted_offer), - short_offer_bid_log_str(residual_offer)) + original_residual_price = ( + (original_offer.energy - energy) / original_offer.energy + ) * orig_offer_price + + residual_offer = self.offer( + price=residual_price, + energy=residual_energy, + seller=original_offer.seller, + original_price=original_residual_price, + dispatch_event=False, + adapt_price_with_fees=False, + add_to_history=True, + time_slot=original_offer.time_slot, + ) + + log.debug( + "%s[OFFER][SPLIT][%s, %s] (%s into %s and %s", + self._debug_log_market_type_identifier, + self.time_slot_str or residual_offer.time_slot, + self.name, + short_offer_bid_log_str(original_offer), + short_offer_bid_log_str(accepted_offer), + short_offer_bid_log_str(residual_offer), + ) self.bc_interface.change_offer(accepted_offer, original_offer, residual_offer) @@ -219,13 +259,14 @@ def split_offer(self, original_offer: Offer, energy: float, MarketEvent.OFFER_SPLIT, original_offer=original_offer, accepted_offer=accepted_offer, - residual_offer=residual_offer) + residual_offer=residual_offer, + ) return accepted_offer, residual_offer def _determine_offer_price( # pylint: disable=too-many-arguments - self, energy_portion, energy, trade_rate, - trade_bid_info, orig_offer_price): + self, energy_portion, energy, trade_rate, trade_bid_info, orig_offer_price + ): if ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.ONE_SIDED.value: return self._update_offer_fee_and_calculate_final_price( energy, trade_rate, energy_portion, orig_offer_price @@ -233,19 +274,24 @@ def _determine_offer_price( # pylint: disable=too-many-arguments if not trade_bid_info: # If trade bid info is not populated, return zero grid fees - return 0., energy * trade_rate + return 0.0, energy * trade_rate _, grid_fee_rate, trade_rate_incl_fees = self.fee_class.calculate_trade_price_and_fees( - trade_bid_info) + trade_bid_info + ) grid_fee_price = grid_fee_rate * energy return grid_fee_price, energy * trade_rate_incl_fees @lock_market_action def accept_offer( # pylint: disable=too-many-locals - self, offer_or_id: Union[str, Offer], buyer: TraderDetails, *, - energy: Optional[float] = None, - trade_bid_info: Optional[TradeBidOfferInfo] = None, - bid: Optional[Bid] = None) -> Trade: + self, + offer_or_id: Union[str, Offer], + buyer: TraderDetails, + *, + energy: Optional[float] = None, + trade_bid_info: Optional[TradeBidOfferInfo] = None, + bid: Optional[Bid] = None, + ) -> Trade: """Accept an offer and create a Trade.""" if self.readonly: @@ -279,20 +325,25 @@ def accept_offer( # pylint: disable=too-many-locals accepted_offer, residual_offer = self.split_offer(offer, energy, orig_offer_price) fee_price, trade_price = self._determine_offer_price( - energy_portion=energy / accepted_offer.energy, energy=energy, - trade_rate=trade_rate, trade_bid_info=trade_bid_info, - orig_offer_price=orig_offer_price) + energy_portion=energy / accepted_offer.energy, + energy=energy, + trade_rate=trade_rate, + trade_bid_info=trade_bid_info, + orig_offer_price=orig_offer_price, + ) offer = accepted_offer offer.update_price(trade_price) elif (offer.energy - energy) < -FLOATING_POINT_TOLERANCE: - raise InvalidTrade(f"Energy ({energy}) can't be greater than " - f"offered energy ({offer.energy})") + raise InvalidTrade( + f"Energy ({energy}) can't be greater than " f"offered energy ({offer.energy})" + ) else: # Requested energy is equal to offer's energy - just proceed normally fee_price, trade_price = self._determine_offer_price( - 1, energy, trade_rate, trade_bid_info, orig_offer_price) + 1, energy, trade_rate, trade_bid_info, orig_offer_price + ) offer.update_price(trade_price) except Exception as ex: # Exception happened - restore offer @@ -301,26 +352,40 @@ def accept_offer( # pylint: disable=too-many-locals raise trade_id, residual_offer = self.bc_interface.handle_blockchain_trade_event( - offer, buyer, original_offer, residual_offer) + offer, buyer, original_offer, residual_offer + ) # Delete the accepted offer from self.offers: self.offers.pop(offer.id, None) offer_bid_trade_info = self.fee_class.propagate_original_bid_info_on_offer_trade( - trade_original_info=trade_bid_info) - - trade = Trade(trade_id, self.now, offer.seller, - buyer=buyer, - offer=offer, - bid=bid, - traded_energy=energy, trade_price=trade_price, residual=residual_offer, - offer_bid_trade_info=offer_bid_trade_info, - fee_price=fee_price, time_slot=offer.time_slot) + trade_original_info=trade_bid_info + ) + + trade = Trade( + trade_id, + self.now, + offer.seller, + buyer=buyer, + offer=offer, + bid=bid, + traded_energy=energy, + trade_price=trade_price, + residual=residual_offer, + offer_bid_trade_info=offer_bid_trade_info, + fee_price=fee_price, + time_slot=offer.time_slot, + ) self.bc_interface.track_trade_event(self.time_slot, trade) self._update_stats_after_trade(trade, offer) - log.info("%s[TRADE][OFFER] [%s] [%s] %s", - self._debug_log_market_type_identifier, self.name, trade.time_slot, trade) + log.info( + "%s[TRADE][OFFER] [%s] [%s] %s", + self._debug_log_market_type_identifier, + self.name, + trade.time_slot, + trade, + ) self._notify_listeners(MarketEvent.OFFER_TRADED, trade=trade) return trade diff --git a/src/gsy_e/models/market/two_sided.py b/src/gsy_e/models/market/two_sided.py index c7769bb97..66c37cd45 100644 --- a/src/gsy_e/models/market/two_sided.py +++ b/src/gsy_e/models/market/two_sided.py @@ -15,25 +15,36 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import uuid from copy import deepcopy from logging import getLogger from math import isclose from typing import Dict, List, Union, Tuple, Optional -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, FLOATING_POINT_TOLERANCE from gsy_framework.data_classes import ( - Bid, Offer, Trade, TradeBidOfferInfo, BidOfferMatch, TraderDetails) + Bid, + Offer, + Trade, + TradeBidOfferInfo, + BidOfferMatch, + TraderDetails, +) from gsy_framework.enums import BidOfferMatchAlgoEnum from gsy_framework.matching_algorithms.requirements_validators import RequirementsSatisfiedChecker from gsy_framework.utils import limit_float_precision from pendulum import DateTime -from gsy_e.constants import FLOATING_POINT_TOLERANCE from gsy_e.events.event_structures import MarketEvent from gsy_e.gsy_e_core.exceptions import ( - BidNotFoundException, InvalidBidOfferPairException, InvalidTrade, - NegativePriceOrdersException, NegativeEnergyOrderException, NegativeEnergyTradeException) + BidNotFoundException, + InvalidBidOfferPairException, + InvalidTrade, + NegativePriceOrdersException, + NegativeEnergyOrderException, + NegativeEnergyTradeException, +) from gsy_e.gsy_e_core.util import short_offer_bid_log_str, is_external_matching_enabled from gsy_e.models.market import lock_market_action from gsy_e.models.market.one_sided import OneSidedMarket @@ -51,25 +62,43 @@ class TwoSidedMarket(OneSidedMarket): the offers and bids are being matched via some matching algorithm. """ - def __init__(self, time_slot=None, bc=None, notification_listener=None, readonly=False, - grid_fee_type=ConstSettings.MASettings.GRID_FEE_TYPE, - grid_fees=None, name=None, in_sim_duration=True): + def __init__( + self, + time_slot=None, + bc=None, + notification_listener=None, + readonly=False, + grid_fee_type=ConstSettings.MASettings.GRID_FEE_TYPE, + grid_fees=None, + name=None, + in_sim_duration=True, + ): # pylint: disable=too-many-arguments - super().__init__(time_slot, bc, notification_listener, readonly, grid_fee_type, - grid_fees, name, in_sim_duration=in_sim_duration) + super().__init__( + time_slot, + bc, + notification_listener, + readonly, + grid_fee_type, + grid_fees, + name, + in_sim_duration=in_sim_duration, + ) @property def _debug_log_market_type_identifier(self): return "[TWO_SIDED]" def __repr__(self): - return (f"<{self._class_name} {self.time_slot_str} bids: {len(self.bids)}" - f" (E: {sum(b.energy for b in self.bids.values())} kWh" - f" V:{sum(b.price for b in self.bids.values())}) " - f"offers: {len(self.offers)} (E: {sum(o.energy for o in self.offers.values())} kWh" - f" V: {sum(o.price for o in self.offers.values())}) " - f"trades: {len(self.trades)} (E: {self.accumulated_trade_energy} kWh" - f", V: {self.accumulated_trade_price})>") + return ( + f"<{self._class_name} {self.time_slot_str} bids: {len(self.bids)}" + f" (E: {sum(b.energy for b in self.bids.values())} kWh" + f" V:{sum(b.price for b in self.bids.values())}) " + f"offers: {len(self.offers)} (E: {sum(o.energy for o in self.offers.values())} kWh" + f" V: {sum(o.price for o in self.offers.values())}) " + f"trades: {len(self.trades)} (E: {self.accumulated_trade_energy} kWh" + f", V: {self.accumulated_trade_price})>" + ) @lock_market_action def get_bids(self) -> Dict: @@ -90,21 +119,28 @@ def _update_requirements_prices(self, bid): if "price" in updated_requirement: energy = updated_requirement.get("energy") or bid.energy original_bid_price = updated_requirement["price"] + bid.accumulated_grid_fees - updated_price = (self.fee_class.update_incoming_bid_with_fee( - updated_requirement["price"] / energy, - original_bid_price / energy)) * energy + updated_price = ( + self.fee_class.update_incoming_bid_with_fee( + updated_requirement["price"] / energy, original_bid_price / energy + ) + ) * energy updated_requirement["price"] = updated_price requirements.append(updated_requirement) return requirements @lock_market_action - def bid(self, price: float, energy: float, buyer: TraderDetails, - bid_id: Optional[str] = None, - original_price: Optional[float] = None, - adapt_price_with_fees: bool = True, - add_to_history: bool = True, - dispatch_event: bool = True, - time_slot: Optional[DateTime] = None) -> Bid: + def bid( + self, + price: float, + energy: float, + buyer: TraderDetails, + bid_id: Optional[str] = None, + original_price: Optional[float] = None, + adapt_price_with_fees: bool = True, + add_to_history: bool = True, + dispatch_event: bool = True, + time_slot: Optional[DateTime] = None, + ) -> Bid: """Create bid object.""" # pylint: disable=too-many-arguments if energy <= FLOATING_POINT_TOLERANCE: @@ -117,25 +153,37 @@ def bid(self, price: float, energy: float, buyer: TraderDetails, original_price = price if adapt_price_with_fees: - price = self.fee_class.update_incoming_bid_with_fee( - price/energy, original_price/energy) * energy + price = ( + self.fee_class.update_incoming_bid_with_fee( + price / energy, original_price / energy + ) + * energy + ) if price < 0.0: - raise NegativePriceOrdersException( - "Negative price after taxes, bid cannot be posted.") - - bid = Bid(str(uuid.uuid4()) if bid_id is None else bid_id, - self.now, price, energy, - buyer, original_price, - time_slot=time_slot) + raise NegativePriceOrdersException("Negative price after taxes, bid cannot be posted.") + + bid = Bid( + str(uuid.uuid4()) if bid_id is None else bid_id, + self.now, + price, + energy, + buyer, + original_price, + time_slot=time_slot, + ) self.bids[bid.id] = bid if add_to_history is True: self.bid_history.append(bid) if dispatch_event is True: self.dispatch_market_bid_event(bid) - log.debug("%s[BID][NEW][%s] %s", self._debug_log_market_type_identifier, - self.time_slot_str or bid.time_slot, bid) + log.debug( + "%s[BID][NEW][%s] %s", + self._debug_log_market_type_identifier, + self.time_slot_str or bid.time_slot, + bid, + ) self.no_new_order = False return bid @@ -151,8 +199,12 @@ def delete_bid(self, bid_or_id: Union[str, Bid]): bid = self.bids.pop(bid_or_id, None) if not bid: raise BidNotFoundException(bid_or_id) - log.debug("%s[BID][DEL][%s] %s", - self._debug_log_market_type_identifier, self.time_slot_str or bid.time_slot, bid) + log.debug( + "%s[BID][DEL][%s] %s", + self._debug_log_market_type_identifier, + self.time_slot_str or bid.time_slot, + bid, + ) self._notify_listeners(MarketEvent.BID_DELETED, bid=bid) def split_bid(self, original_bid: Bid, energy: float, orig_bid_price: float): @@ -162,57 +214,72 @@ def split_bid(self, original_bid: Bid, energy: float, orig_bid_price: float): # same bid id is used for the new accepted_bid original_accepted_price = energy / original_bid.energy * orig_bid_price - accepted_bid = self.bid(bid_id=original_bid.id, - price=original_bid.price * (energy / original_bid.energy), - energy=energy, - buyer=original_bid.buyer, - original_price=original_accepted_price, - adapt_price_with_fees=False, - add_to_history=False, - dispatch_event=False, - time_slot=original_bid.time_slot) + accepted_bid = self.bid( + bid_id=original_bid.id, + price=original_bid.price * (energy / original_bid.energy), + energy=energy, + buyer=original_bid.buyer, + original_price=original_accepted_price, + adapt_price_with_fees=False, + add_to_history=False, + dispatch_event=False, + time_slot=original_bid.time_slot, + ) residual_price = (1 - energy / original_bid.energy) * original_bid.price residual_energy = original_bid.energy - energy - original_residual_price = ((original_bid.energy - energy) / - original_bid.energy) * orig_bid_price - - residual_bid = self.bid(price=residual_price, - energy=residual_energy, - buyer=original_bid.buyer, - original_price=original_residual_price, - adapt_price_with_fees=False, - add_to_history=True, - dispatch_event=False, - time_slot=original_bid.time_slot) - - log.debug("%s[BID][SPLIT][%s, %s] (%s into %s and %s", - self._debug_log_market_type_identifier, - self.time_slot_str or residual_bid.time_slot, self.name, - short_offer_bid_log_str(original_bid), short_offer_bid_log_str(accepted_bid), - short_offer_bid_log_str(residual_bid)) - - self._notify_listeners(MarketEvent.BID_SPLIT, - original_bid=original_bid, - accepted_bid=accepted_bid, - residual_bid=residual_bid) + original_residual_price = ( + (original_bid.energy - energy) / original_bid.energy + ) * orig_bid_price + + residual_bid = self.bid( + price=residual_price, + energy=residual_energy, + buyer=original_bid.buyer, + original_price=original_residual_price, + adapt_price_with_fees=False, + add_to_history=True, + dispatch_event=False, + time_slot=original_bid.time_slot, + ) + + log.debug( + "%s[BID][SPLIT][%s, %s] (%s into %s and %s", + self._debug_log_market_type_identifier, + self.time_slot_str or residual_bid.time_slot, + self.name, + short_offer_bid_log_str(original_bid), + short_offer_bid_log_str(accepted_bid), + short_offer_bid_log_str(residual_bid), + ) + + self._notify_listeners( + MarketEvent.BID_SPLIT, + original_bid=original_bid, + accepted_bid=accepted_bid, + residual_bid=residual_bid, + ) return accepted_bid, residual_bid def _determine_bid_price(self, trade_offer_info, energy): _, grid_fee_rate, final_trade_rate = self.fee_class.calculate_trade_price_and_fees( - trade_offer_info) + trade_offer_info + ) return grid_fee_rate * energy, energy * final_trade_rate @lock_market_action - def accept_bid(self, bid: Bid, - energy: Optional[float] = None, - seller: Optional[TraderDetails] = None, - buyer: Optional[TraderDetails] = None, - trade_offer_info: Optional[TradeBidOfferInfo] = None, - offer: Offer = None) -> Trade: + def accept_bid( + self, + bid: Bid, + energy: Optional[float] = None, + seller: Optional[TraderDetails] = None, + buyer: Optional[TraderDetails] = None, + trade_offer_info: Optional[TradeBidOfferInfo] = None, + offer: Offer = None, + ) -> Trade: """Accept bid and create Trade object.""" # pylint: disable=too-many-arguments, too-many-locals market_bid = self.bids.pop(bid.id, None) @@ -230,8 +297,10 @@ def accept_bid(self, bid: Bid, if energy <= 0: raise NegativeEnergyTradeException("Energy cannot be negative or zero.") if market_bid.energy - energy < -FLOATING_POINT_TOLERANCE: - raise InvalidTrade(f"Traded energy ({energy}) cannot be more than the " - f"bid energy ({market_bid.energy}).") + raise InvalidTrade( + f"Traded energy ({energy}) cannot be more than the " + f"bid energy ({market_bid.energy})." + ) if market_bid.energy - energy > FLOATING_POINT_TOLERANCE: # partial bid trade accepted_bid, residual_bid = self.split_bid(market_bid, energy, orig_price) @@ -242,7 +311,8 @@ def accept_bid(self, bid: Bid, self.bids.pop(accepted_bid.id) except KeyError as exception: raise BidNotFoundException( - f"Bid {accepted_bid.id} not found in self.bids ({self.name}).") from exception + f"Bid {accepted_bid.id} not found in self.bids ({self.name})." + ) from exception else: # full bid trade, nothing further to do here pass @@ -258,46 +328,67 @@ def accept_bid(self, bid: Bid, trade_offer_info, ignore_fees=True ) - trade = Trade(str(uuid.uuid4()), self.now, - seller, - bid.buyer, - bid=bid, offer=offer, traded_energy=energy, trade_price=trade_price, - residual=residual_bid, - offer_bid_trade_info=updated_bid_trade_info, - fee_price=fee_price, time_slot=bid.time_slot) + trade = Trade( + str(uuid.uuid4()), + self.now, + seller, + bid.buyer, + bid=bid, + offer=offer, + traded_energy=energy, + trade_price=trade_price, + residual=residual_bid, + offer_bid_trade_info=updated_bid_trade_info, + fee_price=fee_price, + time_slot=bid.time_slot, + ) if not offer: # This is a chain trade, therefore needs to be tracked. For the trade on the market # that the match is performed, the tracking should have already been done by the offer # trade. self._update_stats_after_trade(trade, bid) - log.info("%s[TRADE][BID] [%s] [%s] {%s}", - self._debug_log_market_type_identifier, self.name, trade.time_slot, trade) + log.info( + "%s[TRADE][BID] [%s] [%s] {%s}", + self._debug_log_market_type_identifier, + self.name, + trade.time_slot, + trade, + ) self._notify_listeners(MarketEvent.BID_TRADED, bid_trade=trade) if residual_bid: self.dispatch_market_bid_event(residual_bid) return trade - def accept_bid_offer_pair(self, bid: Bid, offer: Offer, clearing_rate: float, - trade_bid_info: TradeBidOfferInfo, - selected_energy: float) -> Tuple[Trade, Trade]: + def accept_bid_offer_pair( + self, + bid: Bid, + offer: Offer, + clearing_rate: float, + trade_bid_info: TradeBidOfferInfo, + selected_energy: float, + ) -> Tuple[Trade, Trade]: """Accept bid and offers in pair when a trade is happening.""" # pylint: disable=too-many-arguments assert isclose(clearing_rate, trade_bid_info.trade_rate) assert bid.buyer.uuid != offer.seller.uuid - trade = self.accept_offer(offer_or_id=offer, - buyer=bid.buyer, - energy=selected_energy, - trade_bid_info=trade_bid_info, - bid=bid) - - bid_trade = self.accept_bid(bid=bid, - energy=selected_energy, - seller=offer.seller, - buyer=bid.buyer, - trade_offer_info=trade_bid_info, - offer=offer) + trade = self.accept_offer( + offer_or_id=offer, + buyer=bid.buyer, + energy=selected_energy, + trade_bid_info=trade_bid_info, + bid=bid, + ) + + bid_trade = self.accept_bid( + bid=bid, + energy=selected_energy, + seller=offer.seller, + buyer=bid.buyer, + trade_offer_info=trade_bid_info, + offer=offer, + ) return bid_trade, trade def _get_offer_from_seller_origin_id(self, seller_origin_id): @@ -307,21 +398,30 @@ def _get_offer_from_seller_origin_id(self, seller_origin_id): # inaccurate. return None - return next(iter( - [offer for offer in self.offers.values() - if offer.seller.origin_uuid == seller_origin_id]), None) + return next( + iter( + [ + offer + for offer in self.offers.values() + if offer.seller.origin_uuid == seller_origin_id + ] + ), + None, + ) def _get_bid_from_buyer_origin_id(self, buyer_origin_id): if buyer_origin_id is None: # Many bids may have buyer_origin_id=None; Avoid looking for them as it is inaccurate. return None - return next(iter( - [bid for bid in self.bids.values() - if bid.buyer.origin_uuid == buyer_origin_id]), None) + return next( + iter([bid for bid in self.bids.values() if bid.buyer.origin_uuid == buyer_origin_id]), + None, + ) def match_recommendations( - self, recommendations: List[BidOfferMatch.serializable_dict]) -> bool: + self, recommendations: List[BidOfferMatch.serializable_dict] + ) -> bool: """Match a list of bid/offer pairs, create trades and residual offers/bids. Returns True if trades were actually performed, False otherwise.""" were_trades_performed = False @@ -335,7 +435,8 @@ def match_recommendations( # by seller / buyer. if not market_offer: market_offer = self._get_offer_from_seller_origin_id( - recommended_pair.offer["seller"]["origin_uuid"]) + recommended_pair.offer["seller"]["origin_uuid"] + ) if market_offer is None: raise InvalidBidOfferPairException("Offer does not exist in the market") recommended_pair.offer = market_offer.serializable_dict() @@ -343,7 +444,8 @@ def match_recommendations( market_bid = self.bids.get(recommended_pair.bid["id"]) if not market_bid: market_bid = self._get_bid_from_buyer_origin_id( - recommended_pair.bid["buyer"]["origin_uuid"]) + recommended_pair.bid["buyer"]["origin_uuid"] + ) if market_bid is None: raise InvalidBidOfferPairException("Bid does not exist in the market") recommended_pair.bid = market_bid.serializable_dict() @@ -362,13 +464,17 @@ def match_recommendations( raise invalid_bop_exception continue original_bid_rate = recommended_pair.bid_energy_rate + ( - market_bid.accumulated_grid_fees / recommended_pair.bid_energy) - if ConstSettings.MASettings.BID_OFFER_MATCH_TYPE == \ - BidOfferMatchAlgoEnum.PAY_AS_BID.value: + market_bid.accumulated_grid_fees / recommended_pair.bid_energy + ) + if ( + ConstSettings.MASettings.BID_OFFER_MATCH_TYPE + == BidOfferMatchAlgoEnum.PAY_AS_BID.value + ): trade_rate = original_bid_rate else: trade_rate = self.fee_class.calculate_original_trade_rate_from_clearing_rate( - original_bid_rate, market_bid.energy_rate, recommended_pair.trade_rate) + original_bid_rate, market_bid.energy_rate, recommended_pair.trade_rate + ) trade_bid_info = TradeBidOfferInfo( original_bid_rate=original_bid_rate, propagated_bid_rate=recommended_pair.bid_energy_rate, @@ -378,19 +484,20 @@ def match_recommendations( ) bid_trade, offer_trade = self.accept_bid_offer_pair( - market_bid, market_offer, trade_rate, - trade_bid_info, min(recommended_pair.selected_energy, - market_offer.energy, market_bid.energy)) + market_bid, + market_offer, + trade_rate, + trade_bid_info, + min(recommended_pair.selected_energy, market_offer.energy, market_bid.energy), + ) were_trades_performed = True - recommendations = ( - self._replace_offers_bids_with_residual_in_recommendations_list( - recommendations, offer_trade, bid_trade) + recommendations = self._replace_offers_bids_with_residual_in_recommendations_list( + recommendations, offer_trade, bid_trade ) return were_trades_performed @staticmethod - def _validate_requirements_satisfied( - recommendation: BidOfferMatch) -> None: + def _validate_requirements_satisfied(recommendation: BidOfferMatch) -> None: """Validate if both trade parties satisfy each other's requirements. :raises: @@ -400,20 +507,26 @@ def _validate_requirements_satisfied( if (recommendation.matching_requirements or {}).get("offer_requirement"): offer_requirement = recommendation.matching_requirements["offer_requirement"] requirements_satisfied &= RequirementsSatisfiedChecker.is_offer_requirement_satisfied( - recommendation.offer, recommendation.bid, offer_requirement, - recommendation.trade_rate, recommendation.selected_energy) + recommendation.offer, + recommendation.bid, + offer_requirement, + recommendation.trade_rate, + recommendation.selected_energy, + ) if (recommendation.matching_requirements or {}).get("bid_requirement"): bid_requirement = recommendation.matching_requirements["bid_requirement"] requirements_satisfied &= RequirementsSatisfiedChecker.is_bid_requirement_satisfied( - recommendation.offer, recommendation.bid, bid_requirement, - recommendation.trade_rate, recommendation.selected_energy) + recommendation.offer, + recommendation.bid, + bid_requirement, + recommendation.trade_rate, + recommendation.selected_energy, + ) if not requirements_satisfied: # If requirements are not satisfied - raise InvalidBidOfferPairException( - "The requirements failed the validation.") + raise InvalidBidOfferPairException("The requirements failed the validation.") - def validate_bid_offer_match( - self, recommendation: BidOfferMatch) -> None: + def validate_bid_offer_match(self, recommendation: BidOfferMatch) -> None: """Basic validation function for a bid against an offer. Raises: @@ -431,21 +544,26 @@ def validate_bid_offer_match( offer_energy = market_offer.energy if selected_energy <= 0: raise InvalidBidOfferPairException( - f"Energy traded {selected_energy} should be more than 0.") + f"Energy traded {selected_energy} should be more than 0." + ) if selected_energy > bid_energy: raise InvalidBidOfferPairException( - f"Energy traded {selected_energy} is higher than bids energy {bid_energy}.") + f"Energy traded {selected_energy} is higher than bids energy {bid_energy}." + ) if selected_energy > offer_energy: raise InvalidBidOfferPairException( - f"Energy traded {selected_energy} is higher than offers energy {offer_energy}.") + f"Energy traded {selected_energy} is higher than offers energy {offer_energy}." + ) if recommendation.bid_energy_rate + FLOATING_POINT_TOLERANCE < clearing_rate: raise InvalidBidOfferPairException( f"Trade rate {clearing_rate} is higher than bid energy rate " - f"{recommendation.bid_energy_rate}.") + f"{recommendation.bid_energy_rate}." + ) if market_offer.energy_rate > clearing_rate + FLOATING_POINT_TOLERANCE: raise InvalidBidOfferPairException( f"Trade rate {clearing_rate} is lower than offer energy rate " - f"{market_offer.energy_rate}.") + f"{market_offer.energy_rate}." + ) self._validate_matching_requirements(recommendation) self._validate_requirements_satisfied(recommendation) @@ -467,7 +585,8 @@ def _validate_matching_requirements(recommendation: BidOfferMatch) -> None: if bid_matching_requirement not in bid_requirements: raise InvalidBidOfferPairException( f"Matching requirement {bid_matching_requirement} doesn't exist in the Bid" - " object.") + " object." + ) offer_matching_requirement = recommendation.matching_requirements.get("offer_requirement") if offer_matching_requirement: @@ -475,11 +594,12 @@ def _validate_matching_requirements(recommendation: BidOfferMatch) -> None: if offer_matching_requirement not in offer_requirements: raise InvalidBidOfferPairException( f"Matching requirement {offer_matching_requirement} doesn't exist in the Offer" - f" object.") + f" object." + ) @classmethod def _replace_offers_bids_with_residual_in_recommendations_list( - cls, recommendations: List[Dict], offer_trade: Trade, bid_trade: Trade + cls, recommendations: List[Dict], offer_trade: Trade, bid_trade: Trade ) -> List[BidOfferMatch.serializable_dict]: """ If a trade resulted in a residual offer/bid, upcoming matching list with same offer/bid @@ -493,29 +613,37 @@ def _replace_offers_bids_with_residual_in_recommendations_list( def _adapt_matching_requirements_in_residuals(recommendation): if "energy" in (recommendation.get("matching_requirements") or {}).get( - "bid_requirement", {}): + "bid_requirement", {} + ): for index, requirement in enumerate(recommendation["bid"]["requirements"]): if requirement == recommendation["matching_requirements"]["bid_requirement"]: bid_requirement = deepcopy(requirement) bid_requirement["energy"] -= bid_trade.traded_energy recommendation["bid"]["requirements"][index] = bid_requirement recommendation["matching_requirements"][ - "bid_requirement"] = bid_requirement + "bid_requirement" + ] = bid_requirement return recommendation return recommendation def replace_recommendations_with_residuals(recommendation: Dict): - if (recommendation["offer"]["id"] == offer_trade.match_details["offer"].id and - offer_trade.residual is not None): + if ( + recommendation["offer"]["id"] == offer_trade.match_details["offer"].id + and offer_trade.residual is not None + ): recommendation["offer"] = offer_trade.residual.serializable_dict() - if (recommendation["bid"]["id"] == bid_trade.match_details["bid"].id and - bid_trade.residual is not None): + if ( + recommendation["bid"]["id"] == bid_trade.match_details["bid"].id + and bid_trade.residual is not None + ): recommendation["bid"] = bid_trade.residual.serializable_dict() recommendation = _adapt_matching_requirements_in_residuals(recommendation) return recommendation if offer_trade.residual or bid_trade.residual: - recommendations = [replace_recommendations_with_residuals(recommendation) - for recommendation in recommendations] + recommendations = [ + replace_recommendations_with_residuals(recommendation) + for recommendation in recommendations + ] return recommendations diff --git a/src/gsy_e/models/strategy/__init__.py b/src/gsy_e/models/strategy/__init__.py index 93b5cc565..ceb7ed18d 100644 --- a/src/gsy_e/models/strategy/__init__.py +++ b/src/gsy_e/models/strategy/__init__.py @@ -26,14 +26,14 @@ from typing import TYPE_CHECKING, Callable, Dict, Generator, List, Optional, Union from uuid import uuid4 -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, FLOATING_POINT_TOLERANCE from gsy_framework.data_classes import Bid, Offer, Trade, TraderDetails from gsy_framework.enums import SpotMarketTypeEnum from gsy_framework.utils import limit_float_precision from pendulum import DateTime from gsy_e import constants -from gsy_e.constants import FLOATING_POINT_TOLERANCE, REDIS_PUBLISH_RESPONSE_TIMEOUT +from gsy_e.constants import REDIS_PUBLISH_RESPONSE_TIMEOUT from gsy_e.events import EventMixin from gsy_e.events.event_structures import AreaEvent, MarketEvent from gsy_e.gsy_e_core.device_registry import DeviceRegistry diff --git a/src/gsy_e/models/strategy/energy_parameters/energy_params_eb.py b/src/gsy_e/models/strategy/energy_parameters/energy_params_eb.py index 861f9f0a7..f3f142950 100644 --- a/src/gsy_e/models/strategy/energy_parameters/energy_params_eb.py +++ b/src/gsy_e/models/strategy/energy_parameters/energy_params_eb.py @@ -26,12 +26,12 @@ from typing import Optional, TYPE_CHECKING, DefaultDict import pendulum -from gsy_framework.constants_limits import GlobalConfig +from gsy_framework.constants_limits import GlobalConfig, FLOATING_POINT_TOLERANCE from gsy_framework.enums import AvailableMarketTypes from gsy_framework.forward_markets.forward_profile import ForwardTradeProfileGenerator from gsy_framework.utils import convert_kW_to_kWh -from gsy_e.constants import FLOATING_POINT_TOLERANCE, FORWARD_MARKET_MAX_DURATION_YEARS +from gsy_e.constants import FORWARD_MARKET_MAX_DURATION_YEARS from gsy_e.models.strategy.state import LoadState, PVState if TYPE_CHECKING: @@ -45,6 +45,7 @@ class _BaseMarketEnergyParams(ABC): The children of these classes should not be instantiated by classes other than the ForwardEnergyParams child classes. """ + def __init__(self, posted_energy_kWh: DefaultDict): self._posted_energy_kWh = posted_energy_kWh self._area: Optional["AreaBase"] = None @@ -63,35 +64,35 @@ def get_posted_energy_kWh(self, market_slot: pendulum.DateTime) -> float: """Get the already posted energy from the asset for this market slot.""" @abstractmethod - def increment_posted_energy( - self, market_slot: pendulum.DateTime, posted_energy_kWh: float): + def increment_posted_energy(self, market_slot: pendulum.DateTime, posted_energy_kWh: float): """Increment the posted energy of the asset.""" @abstractmethod - def decrement_posted_energy( - self, market_slot: pendulum.DateTime, posted_energy_kWh: float): + def decrement_posted_energy(self, market_slot: pendulum.DateTime, posted_energy_kWh: float): """Decrement the posted energy of the asset.""" @abstractmethod def get_available_load_energy_kWh( - self, market_slot: pendulum.DateTime, state: "LoadState", - peak_energy_kWh: float) -> float: + self, market_slot: pendulum.DateTime, state: "LoadState", peak_energy_kWh: float + ) -> float: """Get the available (not traded) energy of a load asset.""" @abstractmethod def get_available_pv_energy_kWh( - self, market_slot: pendulum.DateTime, state: "PVState", - peak_energy_kWh: float) -> float: + self, market_slot: pendulum.DateTime, state: "PVState", peak_energy_kWh: float + ) -> float: """Get the available (not traded) energy of a PV asset.""" @abstractmethod def event_load_traded_energy( - self, energy_kWh: float, market_slot: pendulum.DateTime, state: "LoadState"): + self, energy_kWh: float, market_slot: pendulum.DateTime, state: "LoadState" + ): """Trigger actions on a trade event of a load asset.""" @abstractmethod def event_pv_traded_energy( - self, energy_kWh: float, market_slot: pendulum.DateTime, state: "PVState"): + self, energy_kWh: float, market_slot: pendulum.DateTime, state: "PVState" + ): """Trigger actions on a trade event of a PV asset.""" @@ -100,40 +101,39 @@ class _IntradayEnergyParams(_BaseMarketEnergyParams): Energy parameters for the intraday market. Should only be instantiated by the ForwardEnergyParams and its child classes. """ + def get_posted_energy_kWh(self, market_slot: pendulum.DateTime) -> float: return self._posted_energy_kWh[market_slot] - def increment_posted_energy( - self, market_slot: pendulum.DateTime, posted_energy_kWh: float): + def increment_posted_energy(self, market_slot: pendulum.DateTime, posted_energy_kWh: float): self._posted_energy_kWh[market_slot] += posted_energy_kWh - def decrement_posted_energy( - self, market_slot: pendulum.DateTime, posted_energy_kWh: float): + def decrement_posted_energy(self, market_slot: pendulum.DateTime, posted_energy_kWh: float): self._posted_energy_kWh[market_slot] -= posted_energy_kWh def get_available_load_energy_kWh( - self, market_slot: pendulum.DateTime, state: "LoadState", - peak_energy_kWh: float) -> float: + self, market_slot: pendulum.DateTime, state: "LoadState", peak_energy_kWh: float + ) -> float: return state.get_energy_requirement_Wh(market_slot) / 1000.0 def get_available_pv_energy_kWh( - self, market_slot: pendulum.DateTime, state: "PVState", - peak_energy_kWh: float) -> float: + self, market_slot: pendulum.DateTime, state: "PVState", peak_energy_kWh: float + ) -> float: return state.get_available_energy_kWh(market_slot) def event_load_traded_energy( - self, energy_kWh: float, market_slot: pendulum.DateTime, state: "LoadState"): + self, energy_kWh: float, market_slot: pendulum.DateTime, state: "LoadState" + ): state.decrement_energy_requirement( - purchased_energy_Wh=energy_kWh * 1000, - time_slot=market_slot, - area_name=self._area.name) + purchased_energy_Wh=energy_kWh * 1000, time_slot=market_slot, area_name=self._area.name + ) def event_pv_traded_energy( - self, energy_kWh: float, market_slot: pendulum.DateTime, state: "PVState"): + self, energy_kWh: float, market_slot: pendulum.DateTime, state: "PVState" + ): state.decrement_available_energy( - sold_energy_kWh=energy_kWh, - time_slot=market_slot, - area_name=self._area.name) + sold_energy_kWh=energy_kWh, time_slot=market_slot, area_name=self._area.name + ) class _DayForwardEnergyParams(_BaseMarketEnergyParams): @@ -141,6 +141,7 @@ class _DayForwardEnergyParams(_BaseMarketEnergyParams): Energy parameters for the day forward market. Should only be instantiated by the ForwardEnergyParams and its child classes. """ + @staticmethod def _day_forward_slots(market_slot: pendulum.DateTime): """Get the market slots for the day forward market.""" @@ -148,55 +149,52 @@ def _day_forward_slots(market_slot: pendulum.DateTime): market_slot, market_slot + pendulum.duration(minutes=15), market_slot + pendulum.duration(minutes=30), - market_slot + pendulum.duration(minutes=45)] + market_slot + pendulum.duration(minutes=45), + ] def get_posted_energy_kWh(self, market_slot: pendulum.DateTime) -> float: - return max( - self._posted_energy_kWh[slot] - for slot in self._day_forward_slots(market_slot) - ) + return max(self._posted_energy_kWh[slot] for slot in self._day_forward_slots(market_slot)) def increment_posted_energy(self, market_slot: pendulum.DateTime, posted_energy_kWh: float): slots = self._day_forward_slots(market_slot) for slot in slots: self._posted_energy_kWh[slot] += posted_energy_kWh - def decrement_posted_energy( - self, market_slot: pendulum.DateTime, posted_energy_kWh: float): + def decrement_posted_energy(self, market_slot: pendulum.DateTime, posted_energy_kWh: float): for slot in self._day_forward_slots(market_slot): self._posted_energy_kWh[slot] -= posted_energy_kWh def get_available_load_energy_kWh( - self, market_slot: pendulum.DateTime, state: "LoadState", - peak_energy_kWh: float) -> float: + self, market_slot: pendulum.DateTime, state: "LoadState", peak_energy_kWh: float + ) -> float: return min( state.get_energy_requirement_Wh(slot) / 1000.0 for slot in self._day_forward_slots(market_slot) ) def get_available_pv_energy_kWh( - self, market_slot: pendulum.DateTime, state: "PVState", - peak_energy_kWh: float) -> float: + self, market_slot: pendulum.DateTime, state: "PVState", peak_energy_kWh: float + ) -> float: return min( state.get_energy_production_forecast_kWh(slot) for slot in self._day_forward_slots(market_slot) ) def event_load_traded_energy( - self, energy_kWh: float, market_slot: pendulum.DateTime, state: "LoadState"): + self, energy_kWh: float, market_slot: pendulum.DateTime, state: "LoadState" + ): for slot in self._day_forward_slots(market_slot): state.decrement_energy_requirement( - purchased_energy_Wh=energy_kWh * 1000, - time_slot=slot, - area_name=self._area.name) + purchased_energy_Wh=energy_kWh * 1000, time_slot=slot, area_name=self._area.name + ) def event_pv_traded_energy( - self, energy_kWh: float, market_slot: pendulum.DateTime, state: "PVState"): + self, energy_kWh: float, market_slot: pendulum.DateTime, state: "PVState" + ): for slot in self._day_forward_slots(market_slot): state.decrement_available_energy( - sold_energy_kWh=energy_kWh, - time_slot=slot, - area_name=self._area.name) + sold_energy_kWh=energy_kWh, time_slot=slot, area_name=self._area.name + ) class _LongForwardEnergyParameters(_BaseMarketEnergyParams): @@ -204,24 +202,23 @@ class _LongForwardEnergyParameters(_BaseMarketEnergyParams): Energy parameters for the week / month / year forward markets. Should only be instantiated by the ForwardEnergyParams and its child classes. """ + def __init__(self, posted_energy_kWh: DefaultDict, product_type: AvailableMarketTypes): super().__init__(posted_energy_kWh) self._product_type = product_type def get_posted_energy_kWh(self, market_slot: pendulum.DateTime) -> float: - return 0. + return 0.0 - def increment_posted_energy( - self, market_slot: pendulum.DateTime, posted_energy_kWh: float): + def increment_posted_energy(self, market_slot: pendulum.DateTime, posted_energy_kWh: float): return - def decrement_posted_energy( - self, market_slot: pendulum.DateTime, posted_energy_kWh: float): + def decrement_posted_energy(self, market_slot: pendulum.DateTime, posted_energy_kWh: float): return def get_available_load_energy_kWh( - self, market_slot: pendulum.DateTime, state: "LoadState", - peak_energy_kWh: float) -> float: + self, market_slot: pendulum.DateTime, state: "LoadState", peak_energy_kWh: float + ) -> float: # This is the part where the week / month / year forward markets' available energy is # calculated. In order for the available energy for a bid to be calculated, the available @@ -234,16 +231,16 @@ def get_available_load_energy_kWh( reference_slot = market_slot.set(hour=12, minute=0) if state.get_desired_energy_Wh(reference_slot) <= FLOATING_POINT_TOLERANCE: - scaling_factor = 0. + scaling_factor = 0.0 else: - scaling_factor = ( - state.get_energy_requirement_Wh(reference_slot) / - state.get_desired_energy_Wh(reference_slot)) + scaling_factor = state.get_energy_requirement_Wh( + reference_slot + ) / state.get_desired_energy_Wh(reference_slot) return abs(scaling_factor) * peak_energy_kWh def get_available_pv_energy_kWh( - self, market_slot: pendulum.DateTime, state: "PVState", - peak_energy_kWh: float) -> float: + self, market_slot: pendulum.DateTime, state: "PVState", peak_energy_kWh: float + ) -> float: # This is the part where the week / month / year forward markets' available energy is # calculated. In order for the available energy for a bid to be calculated, the available @@ -255,88 +252,100 @@ def get_available_pv_energy_kWh( # multiplying the scaling factor by the already known peak energy of the asset. reference_slot = market_slot.set(hour=12, minute=0) - if state.get_energy_production_forecast_kWh( - reference_slot) <= FLOATING_POINT_TOLERANCE: - scaling_factor = 0. + if state.get_energy_production_forecast_kWh(reference_slot) <= FLOATING_POINT_TOLERANCE: + scaling_factor = 0.0 else: - scaling_factor = ( - state.get_available_energy_kWh(reference_slot) / - state.get_energy_production_forecast_kWh(reference_slot)) + scaling_factor = state.get_available_energy_kWh( + reference_slot + ) / state.get_energy_production_forecast_kWh(reference_slot) return abs(scaling_factor) * peak_energy_kWh def event_load_traded_energy( - self, energy_kWh: float, market_slot: pendulum.DateTime, state: "LoadState"): + self, energy_kWh: float, market_slot: pendulum.DateTime, state: "LoadState" + ): trade_profile = self._profile_generator.generate_trade_profile( - energy_kWh=energy_kWh, - market_slot=market_slot, - product_type=self._product_type) + energy_kWh=energy_kWh, market_slot=market_slot, product_type=self._product_type + ) for time_slot, energy_value_kWh in trade_profile.items(): state.decrement_energy_requirement( purchased_energy_Wh=energy_value_kWh * 1000, time_slot=time_slot, - area_name=self._area.name) + area_name=self._area.name, + ) def event_pv_traded_energy( - self, energy_kWh: float, market_slot: pendulum.DateTime, state: "PVState"): + self, energy_kWh: float, market_slot: pendulum.DateTime, state: "PVState" + ): # Create a new profile that spreads the trade energy across multiple slots. The values # of this new profile are obtained by scaling the values of the standard solar profile trade_profile = self._profile_generator.generate_trade_profile( - energy_kWh=energy_kWh, - market_slot=market_slot, - product_type=self._product_type) + energy_kWh=energy_kWh, market_slot=market_slot, product_type=self._product_type + ) for time_slot, energy_value_kWh in trade_profile.items(): state.decrement_available_energy( - sold_energy_kWh=energy_value_kWh, - time_slot=time_slot, - area_name=self._area.name) + sold_energy_kWh=energy_value_kWh, time_slot=time_slot, area_name=self._area.name + ) class ForwardEnergyParams(ABC): """Common abstract base class for the energy parameters of the forward strategies.""" + def __init__(self): - self._posted_energy_kWh: DefaultDict = defaultdict(lambda: 0.) + self._posted_energy_kWh: DefaultDict = defaultdict(lambda: 0.0) self._forward_energy_params = { AvailableMarketTypes.INTRADAY: _IntradayEnergyParams(self._posted_energy_kWh), AvailableMarketTypes.DAY_FORWARD: _DayForwardEnergyParams(self._posted_energy_kWh), AvailableMarketTypes.WEEK_FORWARD: _LongForwardEnergyParameters( - self._posted_energy_kWh, AvailableMarketTypes.WEEK_FORWARD), + self._posted_energy_kWh, AvailableMarketTypes.WEEK_FORWARD + ), AvailableMarketTypes.MONTH_FORWARD: _LongForwardEnergyParameters( - self._posted_energy_kWh, AvailableMarketTypes.MONTH_FORWARD), + self._posted_energy_kWh, AvailableMarketTypes.MONTH_FORWARD + ), AvailableMarketTypes.YEAR_FORWARD: _LongForwardEnergyParameters( - self._posted_energy_kWh, AvailableMarketTypes.YEAR_FORWARD) + self._posted_energy_kWh, AvailableMarketTypes.YEAR_FORWARD + ), } self._area = None self._profile_generator: Optional[ForwardTradeProfileGenerator] = None def get_posted_energy_kWh( - self, market_slot: pendulum.DateTime, product_type: AvailableMarketTypes) -> float: + self, market_slot: pendulum.DateTime, product_type: AvailableMarketTypes + ) -> float: """Retrieve the already posted energy on this market slot.""" return self._forward_energy_params[product_type].get_posted_energy_kWh(market_slot) def increment_posted_energy( - self, market_slot: pendulum.DateTime, posted_energy_kWh: float, - market_type: AvailableMarketTypes): + self, + market_slot: pendulum.DateTime, + posted_energy_kWh: float, + market_type: AvailableMarketTypes, + ): """ Increase the posted energy of the strategy. Needs to handle only intraday and day ahead since these are the only 2 market types that can operate at the same market slots concurrently. """ self._forward_energy_params[market_type].increment_posted_energy( - market_slot, posted_energy_kWh) + market_slot, posted_energy_kWh + ) def decrement_posted_energy( - self, market_slot: pendulum.DateTime, posted_energy_kWh: float, - market_type: AvailableMarketTypes): + self, + market_slot: pendulum.DateTime, + posted_energy_kWh: float, + market_type: AvailableMarketTypes, + ): """ Decrease the posted energy of the strategy. Needs to handle only intraday and day ahead since these are the only 2 market types that can operate at the same market slots concurrently. """ self._forward_energy_params[market_type].decrement_posted_energy( - market_slot, posted_energy_kWh) + market_slot, posted_energy_kWh + ) @abstractmethod def serialize(self): @@ -344,7 +353,8 @@ def serialize(self): @abstractmethod def get_available_energy_kWh( - self, market_slot: pendulum.DateTime, market_type: AvailableMarketTypes): + self, market_slot: pendulum.DateTime, market_type: AvailableMarketTypes + ): """Get the available offer energy of the PV.""" def event_activate_energy(self, area): @@ -354,8 +364,8 @@ def event_activate_energy(self, area): @abstractmethod def event_traded_energy( - self, energy_kWh: float, market_slot: pendulum.DateTime, - product_type: AvailableMarketTypes): + self, energy_kWh: float, market_slot: pendulum.DateTime, product_type: AvailableMarketTypes + ): """ When a trade happens, we want to split the energy through the entire year following a standard solar profile. @@ -388,9 +398,7 @@ def state(self) -> LoadState: def serialize(self): """Return dict with the current energy parameter values.""" - return { - "capacity_kW": self.capacity_kW - } + return {"capacity_kW": self.capacity_kW} @property def peak_energy_kWh(self): @@ -398,33 +406,35 @@ def peak_energy_kWh(self): return convert_kW_to_kWh(self.capacity_kW, self._area.config.slot_length) def get_available_energy_kWh( - self, market_slot: pendulum.DateTime, market_type: AvailableMarketTypes) -> float: + self, market_slot: pendulum.DateTime, market_type: AvailableMarketTypes + ) -> float: """ Get the available energy of the Load for one market slot. The available energy is the energy that the Load can consumed, but has not been traded yet. """ return self._forward_energy_params[market_type].get_available_load_energy_kWh( - market_slot, self.state, self.peak_energy_kWh) + market_slot, self.state, self.peak_energy_kWh + ) def event_activate_energy(self, area): """Initialize values that are required to compute the energy values of the asset.""" self._area = area - self._profile_generator = ForwardTradeProfileGenerator( - peak_kWh=self.peak_energy_kWh) + self._profile_generator = ForwardTradeProfileGenerator(peak_kWh=self.peak_energy_kWh) for i in range(FORWARD_MARKET_MAX_DURATION_YEARS + 1): capacity_profile = self._profile_generator.generate_trade_profile( energy_kWh=self.peak_energy_kWh, market_slot=GlobalConfig.start_date.start_of("year").add(years=i), - product_type=AvailableMarketTypes.YEAR_FORWARD) + product_type=AvailableMarketTypes.YEAR_FORWARD, + ) for time_slot, energy_kWh in capacity_profile.items(): self.state.set_desired_energy(energy_kWh * 1000, time_slot) super().event_activate_energy(area) def event_traded_energy( - self, energy_kWh: float, market_slot: pendulum.DateTime, - product_type: AvailableMarketTypes): + self, energy_kWh: float, market_slot: pendulum.DateTime, product_type: AvailableMarketTypes + ): """ When a trade happens, we want to split the energy through the entire year following a standard solar profile. @@ -437,7 +447,8 @@ def event_traded_energy( """ assert self._profile_generator is not None self._forward_energy_params[product_type].event_load_traded_energy( - energy_kWh, market_slot, self.state) + energy_kWh, market_slot, self.state + ) class ProductionStandardProfileEnergyParameters(ForwardEnergyParams): @@ -458,9 +469,7 @@ def state(self) -> PVState: def serialize(self): """Return dict with the current energy parameter values.""" - return { - "capacity_kW": self.capacity_kW - } + return {"capacity_kW": self.capacity_kW} @property def peak_energy_kWh(self): @@ -468,33 +477,35 @@ def peak_energy_kWh(self): return convert_kW_to_kWh(self.capacity_kW, self._area.config.slot_length) def get_available_energy_kWh( - self, market_slot: pendulum.DateTime, market_type: AvailableMarketTypes) -> float: + self, market_slot: pendulum.DateTime, market_type: AvailableMarketTypes + ) -> float: """ Get the available energy of the PV for one market slot. The available energy is the energy that the PV can produce, but has not been traded yet. """ return self._forward_energy_params[market_type].get_available_pv_energy_kWh( - market_slot, self.state, self.peak_energy_kWh) + market_slot, self.state, self.peak_energy_kWh + ) def event_activate_energy(self, area): """Initialize values that are required to compute the energy values of the asset.""" self._area = area - self._profile_generator = ForwardTradeProfileGenerator( - peak_kWh=self.peak_energy_kWh) + self._profile_generator = ForwardTradeProfileGenerator(peak_kWh=self.peak_energy_kWh) for i in range(FORWARD_MARKET_MAX_DURATION_YEARS + 1): capacity_profile = self._profile_generator.generate_trade_profile( energy_kWh=self.peak_energy_kWh, market_slot=GlobalConfig.start_date.start_of("year").add(years=i), - product_type=AvailableMarketTypes.YEAR_FORWARD) + product_type=AvailableMarketTypes.YEAR_FORWARD, + ) for time_slot, energy_kWh in capacity_profile.items(): self.state.set_available_energy(energy_kWh, time_slot) super().event_activate_energy(area) def event_traded_energy( - self, energy_kWh: float, market_slot: pendulum.DateTime, - product_type: AvailableMarketTypes): + self, energy_kWh: float, market_slot: pendulum.DateTime, product_type: AvailableMarketTypes + ): """ Spread the traded energy over the market duration following a standard solar profile. @@ -509,4 +520,5 @@ def event_traded_energy( """ assert self._profile_generator is not None self._forward_energy_params[product_type].event_pv_traded_energy( - energy_kWh, market_slot, self.state) + energy_kWh, market_slot, self.state + ) diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/__init__.py b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/__init__.py new file mode 100644 index 000000000..66ff14ff7 --- /dev/null +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/__init__.py @@ -0,0 +1,7 @@ +from gsy_e.models.strategy.energy_parameters.heatpump.cop_models.cop_models import ( + COPModelType, + cop_model_factory, +) + + +__all__ = ["COPModelType", "cop_model_factory"] diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/cop_models.py b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/cop_models.py new file mode 100644 index 000000000..5f3e05805 --- /dev/null +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/cop_models.py @@ -0,0 +1,141 @@ +import json +import os +from abc import abstractmethod +from enum import Enum +from typing import Optional +from logging import getLogger + +from gsy_framework.enums import HeatPumpSourceType + +log = getLogger(__name__) + + +class COPModelType(Enum): + """Selection of supported COP models""" + + UNIVERSAL = 0 + ELCO_AEROTOP_S09_IR = 1 + ELCO_AEROTOP_G07_14M = 2 + HOVAL_ULTRASOURCE_B_COMFORT_C11 = 3 + + +MODEL_FILE_DIR = os.path.join(os.path.dirname(__file__), "model_data") + +MODEL_TYPE_FILENAME_MAPPING = { + COPModelType.ELCO_AEROTOP_S09_IR: "Elco_Aerotop_S09M-IR_model_parameters.json", + COPModelType.ELCO_AEROTOP_G07_14M: "Elco_Aerotop_G07-14M_model_parameters.json", + COPModelType.HOVAL_ULTRASOURCE_B_COMFORT_C11: "hoval_UltraSource_B_comfort_C_11_model_" + "parameters.json", +} + + +class BaseCOPModel: + """Base clas for COP models""" + + @abstractmethod + def calc_cop(self, source_temp_C: float, tank_temp_C: float, heat_demand_kW: Optional[float]): + """Return COP value for provided inputs""" + + +class IndividualCOPModel(BaseCOPModel): + """Handles cop models for specific heat pump models""" + + def __init__(self, model_type: COPModelType): + with open( + os.path.join(MODEL_FILE_DIR, MODEL_TYPE_FILENAME_MAPPING[model_type]), + "r", + encoding="utf-8", + ) as fp: + self._model = json.load(fp) + self.model_type = model_type + + def _calc_power(self, source_temp_C: float, tank_temp_C: float, heat_demand_kW: float): + CAPFT = ( + self._model["CAPFT"][0] + + self._model["CAPFT"][1] * source_temp_C + + self._model["CAPFT"][3] * source_temp_C**2 + + self._model["CAPFT"][2] * tank_temp_C + + self._model["CAPFT"][5] * tank_temp_C**2 + + self._model["CAPFT"][4] * source_temp_C * tank_temp_C + ) + + HEIRFT = ( + self._model["HEIRFT"][0] + + self._model["HEIRFT"][1] * source_temp_C + + self._model["HEIRFT"][3] * source_temp_C**2 + + self._model["HEIRFT"][2] * tank_temp_C + + self._model["HEIRFT"][5] * tank_temp_C**2 + + self._model["HEIRFT"][4] * source_temp_C * tank_temp_C + ) + + # Partial Load Ratio (PLR) + PLR = heat_demand_kW / (self._model["Qref"] * CAPFT) + + # HEIRFPLR calculation + HEIRFPLR = ( + self._model["HEIRFPLR"][0] + + self._model["HEIRFPLR"][1] * PLR + + self._model["HEIRFPLR"][2] * PLR**2 + ) + + # Power consumption (P) calculation + return self._model["Pref"] * CAPFT * HEIRFT * HEIRFPLR + + def calc_cop(self, source_temp_C: float, tank_temp_C: float, heat_demand_kW: float): + assert heat_demand_kW is not None, "heat demand should be provided" + if heat_demand_kW == 0: + return 0 + electrical_power_kW = self._calc_power(source_temp_C, tank_temp_C, heat_demand_kW) + if electrical_power_kW <= 0: + log.error( + "calculated power is negative: " + "hp model: %s source_temp: %s, " + "tank_temp: %s, heat_demand_kW: %s, calculated power: %s", + self.model_type.name, + round(source_temp_C, 2), + round(tank_temp_C, 2), + round(heat_demand_kW, 2), + round(electrical_power_kW, 2), + ) + return 0 + cop = heat_demand_kW / electrical_power_kW + if cop > 6: + log.error( + "calculated COP (%s) is unrealistic: " + "hp model: %s source_temp: %s, " + "tank_temp: %s, heat_demand_kW: %s, calculated power: %s", + round(cop, 2), + self.model_type.name, + round(source_temp_C, 2), + round(tank_temp_C, 2), + round(heat_demand_kW, 2), + round(electrical_power_kW, 2), + ) + return cop + + +class UniversalCOPModel(BaseCOPModel): + """Handle cop calculation independent of the heat pump model""" + + def __init__(self, source_type: int = HeatPumpSourceType.AIR.value): + self._source_type = source_type + + def calc_cop( + self, source_temp_C: float, tank_temp_C: float, heat_demand_kW: Optional[float] + ) -> float: + """COP model following https://www.nature.com/articles/s41597-019-0199-y""" + delta_temp = tank_temp_C - source_temp_C + if self._source_type == HeatPumpSourceType.AIR.value: + return 6.08 - 0.09 * delta_temp + 0.0005 * delta_temp**2 + if self._source_type == HeatPumpSourceType.GROUND.value: + return 10.29 - 0.21 * delta_temp + 0.0012 * delta_temp**2 + assert False, "Source type not supported" + + +def cop_model_factory( + model_type: COPModelType, source_type: int = HeatPumpSourceType.AIR.value +) -> BaseCOPModel: + """Return the correct COP model.""" + if model_type == COPModelType.UNIVERSAL: + return UniversalCOPModel(source_type) + return IndividualCOPModel(model_type) diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/Elco_Aerotop_G07-14M_model_parameters.json b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/Elco_Aerotop_G07-14M_model_parameters.json new file mode 100644 index 000000000..d9ce3117b --- /dev/null +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/Elco_Aerotop_G07-14M_model_parameters.json @@ -0,0 +1 @@ +{"CAPFT": [1.3674619178648815, 0.03804659465962401, -0.019681855839990714, 0.00035564628176876223, -0.0003621972557350528, 0.00020130672570195518], "HEIRFT": [0.0013367484492077253, -0.005549193693871857, 0.020206838944944794, -6.42720175278999e-05, -0.00023069214578574915, -7.601014281322094e-06], "HEIRFPLR": [-0.194398846570212, 1.468913188968992, -0.2833766224760481], "Qref": 16.18, "Pref": 7.6, "PLR_min": 0.2} diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/Elco_Aerotop_S09M-IR_model_parameters.json b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/Elco_Aerotop_S09M-IR_model_parameters.json new file mode 100644 index 000000000..50ded42b7 --- /dev/null +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/Elco_Aerotop_S09M-IR_model_parameters.json @@ -0,0 +1 @@ +{"CAPFT": [0.9300492302752958, 0.03526591169765436, -0.004968669510962087, 0.00041747226581356767, -0.0003022915023160877, 9.773660370027137e-06], "HEIRFT": [0.3556666343811108, -0.02211409627372074, 0.0089215929159423, 5.995243647605175e-05, -3.4550553532408657e-05, 0.00014357037230472436], "HEIRFPLR": [-0.049758215126849414, 1.5407231145753069, -0.4967040777633469], "Qref": 16.2, "Pref": 6.47, "PLR_min": 0.1} diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/hoval_UltraSource_B_comfort_C_11_model_parameters.json b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/hoval_UltraSource_B_comfort_C_11_model_parameters.json new file mode 100644 index 000000000..e0a522201 --- /dev/null +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/cop_models/model_data/hoval_UltraSource_B_comfort_C_11_model_parameters.json @@ -0,0 +1 @@ +{"CAPFT": [0.5152305055168107, 0.016858287502234393, 0.04002915190886247, -0.00029916924915052157, 6.765446186174362e-05, -0.000496776482422856], "HEIRFT": [0.8811301356647324, -0.0019346380513800977, -0.024648245650783343, -0.0003083507655603219, -0.0001586422096891705, 0.00039208468135561403], "HEIRFPLR": [-0.3720222858742652, 2.375345746851973, -1.0047114539738535], "Qref": 8.3, "Pref": 5.7, "PLR_min": 0.1} diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/heat_pump.py b/src/gsy_e/models/strategy/energy_parameters/heatpump/heat_pump.py index 731720949..99757b070 100644 --- a/src/gsy_e/models/strategy/energy_parameters/heatpump/heat_pump.py +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/heat_pump.py @@ -1,12 +1,15 @@ from abc import ABC, abstractmethod from typing import Optional, Dict, Union, List -from gsy_framework.constants_limits import ConstSettings, GlobalConfig -from gsy_framework.enums import HeatPumpSourceType +from gsy_framework.constants_limits import ConstSettings, GlobalConfig, FLOATING_POINT_TOLERANCE from gsy_framework.read_user_profile import InputProfileTypes +from gsy_framework.utils import convert_kJ_to_kWh, convert_kWh_to_kJ from pendulum import DateTime -from gsy_e.constants import FLOATING_POINT_TOLERANCE +from gsy_e.models.strategy.energy_parameters.heatpump.cop_models import ( + COPModelType, + cop_model_factory, +) from gsy_e.models.strategy.energy_parameters.heatpump.tank import ( TankParameters, AllTanksEnergyParameters, @@ -177,6 +180,7 @@ def __init__( consumption_kWh_measurement_uuid: Optional[str] = None, source_type: int = ConstSettings.HeatPumpSettings.SOURCE_TYPE, heat_demand_Q_profile: Optional[Union[str, float, Dict]] = None, + cop_model_type: COPModelType = COPModelType.UNIVERSAL, ): super().__init__(maximum_power_rating_kW, tank_parameters) @@ -210,7 +214,7 @@ def __init__( None, source_temp_C_measurement_uuid, profile_type=InputProfileTypes.IDENTITY ) - # self.min_temp_C = min_temp_C # for usage in the strategy + self._cop_model = cop_model_factory(cop_model_type, source_type) def serialize(self): """Return dict with the current energy parameter values.""" @@ -233,8 +237,8 @@ def event_traded_energy(self, time_slot: DateTime, energy_kWh: float): """React to an event_traded_energy.""" self._decrement_posted_energy(time_slot, energy_kWh) - traded_heat_energy = self._calc_Q_from_energy_kWh(time_slot, energy_kWh) - self._state.tanks.increase_tanks_temp_from_heat_energy(traded_heat_energy, time_slot) + traded_heat_energy_kJ = self._calc_Q_kJ_from_energy_kWh(time_slot, energy_kWh) + self._state.tanks.increase_tanks_temp_from_heat_energy(traded_heat_energy_kJ, time_slot) self._calculate_and_set_unmatched_demand(time_slot) @@ -247,6 +251,8 @@ def _rotate_profiles(self, current_time_slot: Optional[DateTime] = None): def _calc_energy_to_buy_maximum(self, time_slot: DateTime) -> float: cop = self._state.heatpump.get_cop(time_slot) + if cop == 0: + return 0 max_energy_consumption_kWh = self._state.tanks.get_max_energy_consumption(cop, time_slot) assert max_energy_consumption_kWh > -FLOATING_POINT_TOLERANCE if max_energy_consumption_kWh > self._max_energy_consumption_kWh: @@ -255,6 +261,8 @@ def _calc_energy_to_buy_maximum(self, time_slot: DateTime) -> float: def _calc_energy_to_buy_minimum(self, time_slot: DateTime) -> float: cop = self._state.heatpump.get_cop(time_slot) + if cop == 0: + return 0 min_energy_consumption_kWh = self._state.tanks.get_min_energy_consumption(cop, time_slot) if min_energy_consumption_kWh > self._max_energy_consumption_kWh: return self._max_energy_consumption_kWh @@ -265,26 +273,29 @@ def _populate_state(self, time_slot: DateTime): self._state.heatpump.set_cop(time_slot, self._calc_cop(time_slot)) if not self._heat_demand_Q_J: - produced_heat_energy_KJ = self._calc_Q_from_energy_kWh( + produced_heat_energy_kJ = self._calc_Q_kJ_from_energy_kWh( time_slot, self._consumption_kWh.profile[time_slot] ) else: - produced_heat_energy_KJ = self._heat_demand_Q_J.get_value(time_slot) / 1000.0 - energy_demand_kWh = self._calc_energy_kWh_from_Q(time_slot, produced_heat_energy_KJ) + produced_heat_energy_kJ = self._heat_demand_Q_J.get_value(time_slot) / 1000.0 + energy_demand_kWh = self._calc_energy_kWh_from_Q_kJ(time_slot, produced_heat_energy_kJ) self._consumption_kWh.profile[time_slot] = energy_demand_kWh - self._state.heatpump.set_heat_demand(time_slot, produced_heat_energy_KJ * 1000) - self._state.tanks.decrease_tanks_temp_from_heat_energy(produced_heat_energy_KJ, time_slot) + self._state.heatpump.set_heat_demand(time_slot, produced_heat_energy_kJ * 1000) + self._state.tanks.decrease_tanks_temp_from_heat_energy(produced_heat_energy_kJ, time_slot) super()._populate_state(time_slot) self._state.heatpump.set_energy_consumption_kWh( time_slot, self._consumption_kWh.get_value(time_slot) ) - def _calc_Q_from_energy_kWh(self, time_slot: DateTime, energy_kWh: float) -> float: - return self._state.heatpump.get_cop(time_slot) * energy_kWh + def _calc_Q_kJ_from_energy_kWh(self, time_slot: DateTime, energy_kWh: float) -> float: + return convert_kWh_to_kJ(self._state.heatpump.get_cop(time_slot) * energy_kWh) - def _calc_energy_kWh_from_Q(self, time_slot: DateTime, Q_energy_KJ: float) -> float: - return Q_energy_KJ / self._state.heatpump.get_cop(time_slot) + def _calc_energy_kWh_from_Q_kJ(self, time_slot: DateTime, Q_energy_kJ: float) -> float: + cop = self._state.heatpump.get_cop(time_slot) + if cop == 0: + return 0 + return convert_kJ_to_kWh(Q_energy_kJ / self._state.heatpump.get_cop(time_slot)) def _calc_cop(self, time_slot: DateTime) -> float: """ @@ -294,20 +305,17 @@ def _calc_cop(self, time_slot: DateTime) -> float: Generally, the higher the temperature difference between the source and the sink, the lower the efficiency of the heat pump (the lower COP). """ - return self._cop_model( - self._state.tanks.get_average_tank_temperature(time_slot), - self._source_temp_C.get_value(time_slot), + # 1 J = 1 W s + heat_demand_kW = ( + self._heat_demand_Q_J.get_value(time_slot) / self._slot_length.total_seconds() / 1000 + if self._heat_demand_Q_J + else None + ) + return self._cop_model.calc_cop( + source_temp_C=self._source_temp_C.get_value(time_slot), + tank_temp_C=self._state.tanks.get_average_tank_temperature(time_slot), + heat_demand_kW=heat_demand_kW, ) - - def _cop_model(self, temp_current: float, temp_ambient: float) -> float: - """COP model following https://www.nature.com/articles/s41597-019-0199-y""" - delta_temp = temp_current - temp_ambient - if self._source_type == HeatPumpSourceType.AIR.value: - return 6.08 - 0.09 * delta_temp + 0.0005 * delta_temp**2 - if self._source_type == HeatPumpSourceType.GROUND.value: - return 10.29 - 0.21 * delta_temp + 0.0012 * delta_temp**2 - - raise HeatPumpEnergyParametersException("HeatPumpSourceType not supported") def _calculate_and_set_unmatched_demand(self, time_slot: DateTime) -> None: unmatched_energy_demand = self._state.tanks.get_unmatched_demand_kWh(time_slot) diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/tank.py b/src/gsy_e/models/strategy/energy_parameters/heatpump/tank.py index bd2800237..d141390f6 100644 --- a/src/gsy_e/models/strategy/energy_parameters/heatpump/tank.py +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/tank.py @@ -3,10 +3,11 @@ from statistics import mean from typing import Dict, Union, List -from gsy_framework.constants_limits import ConstSettings, GlobalConfig from pendulum import DateTime -from gsy_e.constants import FLOATING_POINT_TOLERANCE +from gsy_framework.constants_limits import ConstSettings, GlobalConfig, FLOATING_POINT_TOLERANCE +from gsy_framework.utils import convert_kJ_to_kWh + from gsy_e.models.strategy.energy_parameters.heatpump.constants import ( WATER_DENSITY, SPECIFIC_HEAT_CONST_WATER, @@ -54,14 +55,14 @@ def get_results_dict(self, current_time_slot: DateTime): """Results dict with the results from the tank.""" return self._state.get_results_dict(current_time_slot) - def increase_tank_temp_from_heat_energy(self, heat_energy: float, time_slot: DateTime): + def increase_tank_temp_from_heat_energy(self, heat_energy_kWh: float, time_slot: DateTime): """Increase the temperature of the water tank with the provided heat energy.""" - temp_increase_K = self._Q_kWh_to_temp_diff(heat_energy) + temp_increase_K = self._Q_kWh_to_temp_diff(heat_energy_kWh) self._state.update_temp_increase_K(time_slot, temp_increase_K) - def decrease_tank_temp_from_heat_energy(self, heat_energy: float, time_slot: DateTime): + def decrease_tank_temp_from_heat_energy(self, heat_energy_kWh: float, time_slot: DateTime): """Decrease the temperature of the water tank with the provided heat energy.""" - temp_decrease_K = self._Q_kWh_to_temp_diff(heat_energy) + temp_decrease_K = self._Q_kWh_to_temp_diff(heat_energy_kWh) self._state.set_temp_decrease_K(time_slot, temp_decrease_K) def increase_tank_temp_from_temp_delta(self, temp_diff: float, time_slot: DateTime): @@ -109,18 +110,20 @@ def __init__(self, tank_parameters: List[TankParameters]): TankEnergyParameters(tank, GlobalConfig.slot_length) for tank in tank_parameters ] - def increase_tanks_temp_from_heat_energy(self, heat_energy: float, time_slot: DateTime): + def increase_tanks_temp_from_heat_energy(self, heat_energy_kJ: float, time_slot: DateTime): """Increase the temperature of the water tanks with the provided heat energy.""" # Split heat energy equally across tanks - heat_energy_per_tank = heat_energy / len(self._tanks_energy_parameters) + heat_energy_per_tank_kJ = heat_energy_kJ / len(self._tanks_energy_parameters) + heat_energy_per_tank_kWh = convert_kJ_to_kWh(heat_energy_per_tank_kJ) for tank in self._tanks_energy_parameters: - tank.increase_tank_temp_from_heat_energy(heat_energy_per_tank, time_slot) + tank.increase_tank_temp_from_heat_energy(heat_energy_per_tank_kWh, time_slot) - def decrease_tanks_temp_from_heat_energy(self, heat_energy: float, time_slot: DateTime): + def decrease_tanks_temp_from_heat_energy(self, heat_energy_kJ: float, time_slot: DateTime): """Decrease the temperature of the water tanks with the provided heat energy.""" - heat_energy_per_tank = heat_energy / len(self._tanks_energy_parameters) + heat_energy_per_tank_kJ = heat_energy_kJ / len(self._tanks_energy_parameters) + heat_energy_per_tank_kWh = convert_kJ_to_kWh(heat_energy_per_tank_kJ) for tank in self._tanks_energy_parameters: - tank.decrease_tank_temp_from_heat_energy(heat_energy_per_tank, time_slot) + tank.decrease_tank_temp_from_heat_energy(heat_energy_per_tank_kWh, time_slot) def update_tanks_temperature(self, time_slot: DateTime): """ @@ -142,6 +145,8 @@ def get_max_energy_consumption(self, cop: float, time_slot: DateTime): def get_min_energy_consumption(self, cop: float, time_slot: DateTime): """Get min energy consumption from all water tanks.""" + if cop == 0: + return 0 min_energy_consumption_kWh = sum( tank.get_min_energy_consumption(cop, time_slot) for tank in self._tanks_energy_parameters diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/virtual_heat_pump.py b/src/gsy_e/models/strategy/energy_parameters/heatpump/virtual_heat_pump.py index af752e9f5..b7c69f9e3 100644 --- a/src/gsy_e/models/strategy/energy_parameters/heatpump/virtual_heat_pump.py +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/virtual_heat_pump.py @@ -1,11 +1,11 @@ import logging from typing import Optional, Union, Dict, List +from pendulum import DateTime +from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE from gsy_framework.constants_limits import ConstSettings from gsy_framework.read_user_profile import InputProfileTypes -from pendulum import DateTime -from gsy_e.constants import FLOATING_POINT_TOLERANCE from gsy_e.models.strategy.energy_parameters.heatpump.constants import ( WATER_SPECIFIC_HEAT_CAPACITY, DEFAULT_SOURCE_TEMPERATURE_C, diff --git a/src/gsy_e/models/strategy/energy_parameters/heatpump/virtual_heatpump_solver.py b/src/gsy_e/models/strategy/energy_parameters/heatpump/virtual_heatpump_solver.py index 8d8a378ed..4c1f66fa0 100644 --- a/src/gsy_e/models/strategy/energy_parameters/heatpump/virtual_heatpump_solver.py +++ b/src/gsy_e/models/strategy/energy_parameters/heatpump/virtual_heatpump_solver.py @@ -6,7 +6,7 @@ from gsy_framework.constants_limits import ConstSettings, GlobalConfig from gsy_framework.utils import convert_W_to_kWh, convert_kWh_to_W -from gsy_e.constants import FLOATING_POINT_TOLERANCE +from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE from gsy_e.models.strategy.energy_parameters.heatpump.constants import ( WATER_SPECIFIC_HEAT_CAPACITY, WATER_DENSITY, diff --git a/src/gsy_e/models/strategy/forward/load.py b/src/gsy_e/models/strategy/forward/load.py index 11ae3f7ee..02f83e921 100644 --- a/src/gsy_e/models/strategy/forward/load.py +++ b/src/gsy_e/models/strategy/forward/load.py @@ -1,12 +1,13 @@ from typing import Dict, TYPE_CHECKING +from pendulum import DateTime, duration from gsy_framework.data_classes import TraderDetails from gsy_framework.enums import AvailableMarketTypes -from pendulum import DateTime, duration +from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE -from gsy_e.constants import FLOATING_POINT_TOLERANCE from gsy_e.models.strategy.energy_parameters.energy_params_eb import ( - ConsumptionStandardProfileEnergyParameters) + ConsumptionStandardProfileEnergyParameters, +) from gsy_e.models.strategy.forward import ForwardStrategyBase from gsy_e.models.strategy.forward.order_updater import ForwardOrderUpdaterParameters @@ -17,16 +18,17 @@ DEFAULT_LOAD_ORDER_UPDATER_PARAMS = { - AvailableMarketTypes.INTRADAY: ForwardOrderUpdaterParameters( - duration(minutes=5), 10, 40, 10), + AvailableMarketTypes.INTRADAY: ForwardOrderUpdaterParameters(duration(minutes=5), 10, 40, 10), AvailableMarketTypes.DAY_FORWARD: ForwardOrderUpdaterParameters( - duration(minutes=30), 20, 40, 10), - AvailableMarketTypes.WEEK_FORWARD: ForwardOrderUpdaterParameters( - duration(days=1), 30, 50, 10), + duration(minutes=30), 20, 40, 10 + ), + AvailableMarketTypes.WEEK_FORWARD: ForwardOrderUpdaterParameters(duration(days=1), 30, 50, 10), AvailableMarketTypes.MONTH_FORWARD: ForwardOrderUpdaterParameters( - duration(weeks=1), 40, 60, 20), + duration(weeks=1), 40, 60, 20 + ), AvailableMarketTypes.YEAR_FORWARD: ForwardOrderUpdaterParameters( - duration(months=1), 50, 70, 50) + duration(months=1), 50, 70, 50 + ), } @@ -35,10 +37,12 @@ class ForwardLoadStrategy(ForwardStrategyBase): Strategy that models a Load that trades with a Standard Solar Profile on the forward exchanges. """ + def __init__( - self, capacity_kW: float, - order_updater_parameters: Dict[ - AvailableMarketTypes, ForwardOrderUpdaterParameters] = None): + self, + capacity_kW: float, + order_updater_parameters: Dict[AvailableMarketTypes, ForwardOrderUpdaterParameters] = None, + ): if not order_updater_parameters: order_updater_parameters = DEFAULT_LOAD_ORDER_UPDATER_PARAMS super().__init__(order_updater_parameters) @@ -52,63 +56,82 @@ def event_activate(self, **kwargs): self._energy_params.event_activate_energy(self.area) def remove_order(self, market: "ForwardMarketBase", market_slot: DateTime, order_uuid: str): - bids = [bid - for bid in market.slot_bid_mapping[market_slot] - if bid.buyer.name == self.owner.name and bid.id == order_uuid] + bids = [ + bid + for bid in market.slot_bid_mapping[market_slot] + if bid.buyer.name == self.owner.name and bid.id == order_uuid + ] if not bids: - self.log.error("Bid with id %s does not exist on the market %s %s.", - order_uuid, market.market_type, market_slot) + self.log.error( + "Bid with id %s does not exist on the market %s %s.", + order_uuid, + market.market_type, + market_slot, + ) return market.delete_bid(bids[0]) def remove_open_orders(self, market: "ForwardMarketBase", market_slot: DateTime): - bids = [bid - for bid in market.slot_bid_mapping[market_slot] - if bid.buyer.name == self.owner.name] + bids = [ + bid + for bid in market.slot_bid_mapping[market_slot] + if bid.buyer.name == self.owner.name + ] for bid in bids: market.delete_bid(bid) def post_order( - self, market: "ForwardMarketBase", market_slot: DateTime, order_rate: float = None, - **kwargs): + self, + market: "ForwardMarketBase", + market_slot: DateTime, + order_rate: float = None, + **kwargs + ): if not order_rate: - order_rate = self._order_updaters[market][market_slot].get_energy_rate( - self.area.now) + order_rate = self._order_updaters[market][market_slot].get_energy_rate(self.area.now) capacity_percent = kwargs.get("capacity_percent") if not capacity_percent: capacity_percent = self._order_updaters[market][market_slot].capacity_percent / 100.0 max_energy_kWh = self._energy_params.peak_energy_kWh * capacity_percent available_energy_kWh = self._energy_params.get_available_energy_kWh( - market_slot, market.market_type) + market_slot, market.market_type + ) posted_energy_kWh = self._energy_params.get_posted_energy_kWh( - market_slot, market.market_type) + market_slot, market.market_type + ) order_energy_kWh = min(available_energy_kWh - posted_energy_kWh, max_energy_kWh) if order_energy_kWh <= FLOATING_POINT_TOLERANCE: return market.bid( - order_rate * order_energy_kWh, order_energy_kWh, - buyer=TraderDetails(self.owner.name, self.owner.uuid, - self.owner.name, self.owner.uuid), + order_rate * order_energy_kWh, + order_energy_kWh, + buyer=TraderDetails( + self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid + ), original_price=order_rate * order_energy_kWh, - time_slot=market_slot) + time_slot=market_slot, + ) self._energy_params.increment_posted_energy( - market_slot, order_energy_kWh, market.market_type) + market_slot, order_energy_kWh, market.market_type + ) def event_bid_traded(self, *, market_id: str, bid_trade: "Trade"): """Method triggered by the MarketEvent.BID_TRADED event.""" - market = [market - for market in self.area.forward_markets.values() - if market_id == market.id] + market = [ + market for market in self.area.forward_markets.values() if market_id == market.id + ] if not market: return if bid_trade.buyer.uuid != self.owner.uuid: return - self._energy_params.event_traded_energy(bid_trade.traded_energy, - bid_trade.time_slot, market[0].market_type) + self._energy_params.event_traded_energy( + bid_trade.traded_energy, bid_trade.time_slot, market[0].market_type + ) self._energy_params.decrement_posted_energy( - bid_trade.time_slot, bid_trade.traded_energy, market[0].market_type) + bid_trade.time_slot, bid_trade.traded_energy, market[0].market_type + ) diff --git a/src/gsy_e/models/strategy/forward/pv.py b/src/gsy_e/models/strategy/forward/pv.py index ef250dd5e..30cff61ea 100644 --- a/src/gsy_e/models/strategy/forward/pv.py +++ b/src/gsy_e/models/strategy/forward/pv.py @@ -1,12 +1,13 @@ from typing import Dict, TYPE_CHECKING +from pendulum import DateTime, duration from gsy_framework.data_classes import TraderDetails from gsy_framework.enums import AvailableMarketTypes -from pendulum import DateTime, duration +from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE -from gsy_e.constants import FLOATING_POINT_TOLERANCE from gsy_e.models.strategy.energy_parameters.energy_params_eb import ( - ProductionStandardProfileEnergyParameters) + ProductionStandardProfileEnergyParameters, +) from gsy_e.models.strategy.forward import ForwardStrategyBase from gsy_e.models.strategy.forward.order_updater import ForwardOrderUpdaterParameters @@ -17,16 +18,17 @@ DEFAULT_PV_ORDER_UPDATER_PARAMS = { - AvailableMarketTypes.INTRADAY: ForwardOrderUpdaterParameters( - duration(minutes=5), 10, 10, 10), + AvailableMarketTypes.INTRADAY: ForwardOrderUpdaterParameters(duration(minutes=5), 10, 10, 10), AvailableMarketTypes.DAY_FORWARD: ForwardOrderUpdaterParameters( - duration(minutes=30), 10, 10, 10), - AvailableMarketTypes.WEEK_FORWARD: ForwardOrderUpdaterParameters( - duration(days=1), 10, 10, 10), + duration(minutes=30), 10, 10, 10 + ), + AvailableMarketTypes.WEEK_FORWARD: ForwardOrderUpdaterParameters(duration(days=1), 10, 10, 10), AvailableMarketTypes.MONTH_FORWARD: ForwardOrderUpdaterParameters( - duration(weeks=1), 10, 10, 20), + duration(weeks=1), 10, 10, 20 + ), AvailableMarketTypes.YEAR_FORWARD: ForwardOrderUpdaterParameters( - duration(months=1), 10, 10, 50) + duration(months=1), 10, 10, 50 + ), } @@ -35,10 +37,12 @@ class ForwardPVStrategy(ForwardStrategyBase): Strategy that models a PV that trades with a Standard Solar Profile on the forward exchanges. """ + def __init__( - self, capacity_kW: float, - order_updater_parameters: Dict[ - AvailableMarketTypes, ForwardOrderUpdaterParameters] = None): + self, + capacity_kW: float, + order_updater_parameters: Dict[AvailableMarketTypes, ForwardOrderUpdaterParameters] = None, + ): if not order_updater_parameters: order_updater_parameters = DEFAULT_PV_ORDER_UPDATER_PARAMS @@ -53,36 +57,50 @@ def event_activate(self, **kwargs): self._energy_params.event_activate_energy(self.area) def remove_open_orders(self, market: "ForwardMarketBase", market_slot: DateTime): - offers = [offer - for offer in market.slot_offer_mapping[market_slot] - if offer.seller.name == self.owner.name] + offers = [ + offer + for offer in market.slot_offer_mapping[market_slot] + if offer.seller.name == self.owner.name + ] for offer in offers: market.delete_offer(offer) def remove_order(self, market: "ForwardMarketBase", market_slot: DateTime, order_uuid: str): - offers = [offer - for offer in market.slot_offer_mapping[market_slot] - if offer.seller.name == self.owner.name and offer.id == order_uuid] + offers = [ + offer + for offer in market.slot_offer_mapping[market_slot] + if offer.seller.name == self.owner.name and offer.id == order_uuid + ] if not offers: - self.log.error("Bid with id %s does not exist on the market %s %s.", - order_uuid, market.market_type, market_slot) + self.log.error( + "Bid with id %s does not exist on the market %s %s.", + order_uuid, + market.market_type, + market_slot, + ) return market.delete_offer(offers[0]) - def post_order(self, market: "ForwardMarketBase", market_slot: DateTime, - order_rate: float = None, **kwargs): + def post_order( + self, + market: "ForwardMarketBase", + market_slot: DateTime, + order_rate: float = None, + **kwargs + ): if not order_rate: - order_rate = self._order_updaters[market][market_slot].get_energy_rate( - self.area.now) + order_rate = self._order_updaters[market][market_slot].get_energy_rate(self.area.now) capacity_percent = kwargs.get("capacity_percent") if not capacity_percent: capacity_percent = self._order_updaters[market][market_slot].capacity_percent / 100.0 max_energy_kWh = self._energy_params.peak_energy_kWh * capacity_percent available_energy_kWh = self._energy_params.get_available_energy_kWh( - market_slot, market.market_type) + market_slot, market.market_type + ) posted_energy_kWh = self._energy_params.get_posted_energy_kWh( - market_slot, market.market_type) + market_slot, market.market_type + ) order_energy_kWh = min(available_energy_kWh - posted_energy_kWh, max_energy_kWh) @@ -90,25 +108,30 @@ def post_order(self, market: "ForwardMarketBase", market_slot: DateTime, return market.offer( - order_rate * order_energy_kWh, order_energy_kWh, + order_rate * order_energy_kWh, + order_energy_kWh, TraderDetails(self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid), original_price=order_rate * order_energy_kWh, - time_slot=market_slot) + time_slot=market_slot, + ) self._energy_params.increment_posted_energy( - market_slot, order_energy_kWh, market.market_type) + market_slot, order_energy_kWh, market.market_type + ) def event_traded(self, *, market_id: str, trade: "Trade"): """Method triggered by the MarketEvent.OFFER_TRADED event.""" - market = [market - for market in self.area.forward_markets.values() - if market_id == market.id] + market = [ + market for market in self.area.forward_markets.values() if market_id == market.id + ] if not market: return if trade.seller.uuid != self.owner.uuid: return - self._energy_params.event_traded_energy(trade.traded_energy, - trade.time_slot, market[0].market_type) + self._energy_params.event_traded_energy( + trade.traded_energy, trade.time_slot, market[0].market_type + ) self._energy_params.decrement_posted_energy( - trade.time_slot, trade.traded_energy, market[0].market_type) + trade.time_slot, trade.traded_energy, market[0].market_type + ) diff --git a/src/gsy_e/models/strategy/heat_pump.py b/src/gsy_e/models/strategy/heat_pump.py index c0a760d4d..d1afe4143 100644 --- a/src/gsy_e/models/strategy/heat_pump.py +++ b/src/gsy_e/models/strategy/heat_pump.py @@ -9,7 +9,7 @@ from gsy_framework.utils import convert_pendulum_to_str_in_dict from gsy_framework.validators.heat_pump_validator import HeatPumpValidator -from gsy_e.constants import FLOATING_POINT_TOLERANCE +from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE from gsy_e.gsy_e_core.util import ( get_market_maker_rate_from_time_slot, get_feed_in_tariff_rate_from_time_slot, @@ -17,10 +17,11 @@ from gsy_e.models.strategy.energy_parameters.heatpump.heat_pump import ( HeatPumpEnergyParameters, TankParameters, + CombinedHeatpumpTanksState, ) from gsy_e.models.strategy.order_updater import OrderUpdaterParameters, OrderUpdater -from gsy_e.models.strategy.state import HeatPumpState from gsy_e.models.strategy.trading_strategy_base import TradingStrategyBase +from gsy_e.models.strategy.energy_parameters.heatpump.cop_models import COPModelType if TYPE_CHECKING: from gsy_e.models.market import MarketBase @@ -60,12 +61,12 @@ def serialize(self): "update_interval": self.update_interval, "initial_buying_rate": ( self.initial_rate - if isinstance(self.initial_rate, (type(None), float)) + if isinstance(self.initial_rate, (type(None), int, float)) else convert_pendulum_to_str_in_dict(self.initial_rate) ), "final_buying_rate": ( self.final_rate - if isinstance(self.final_rate, (type(None), float)) + if isinstance(self.final_rate, (type(None), int, float)) else convert_pendulum_to_str_in_dict(self.final_rate) ), "use_market_maker_rate": self.use_market_maker_rate, @@ -92,6 +93,7 @@ def __init__( ] = None, preferred_buying_rate: float = ConstSettings.HeatPumpSettings.PREFERRED_BUYING_RATE, heat_demand_Q_profile: Optional[Union[str, float, Dict]] = None, + cop_model_type: COPModelType = COPModelType.UNIVERSAL, ): assert ( @@ -109,6 +111,7 @@ def __init__( consumption_kWh_profile_uuid=consumption_kWh_profile_uuid, source_type=source_type, heat_demand_Q_profile=heat_demand_Q_profile, + cop_model_type=cop_model_type, ) for tank in tank_parameters: @@ -123,6 +126,7 @@ def __init__( consumption_kWh_profile=consumption_kWh_profile, consumption_kWh_profile_uuid=consumption_kWh_profile_uuid, source_type=source_type, + heat_demand_Q_profile=heat_demand_Q_profile, ) # needed for profile_handler @@ -186,7 +190,7 @@ def deserialize_args(constructor_args: Dict) -> Dict: return constructor_args @property - def state(self) -> HeatPumpState: + def state(self) -> CombinedHeatpumpTanksState: return self._energy_params.combined_state def event_activate(self, **kwargs): @@ -312,6 +316,7 @@ def __init__( AvailableMarketTypes, HeatPumpOrderUpdaterParameters ] = None, preferred_buying_rate: float = ConstSettings.HeatPumpSettings.PREFERRED_BUYING_RATE, + cop_model_type: COPModelType = COPModelType.UNIVERSAL, ): tank_parameters = [ TankParameters( @@ -333,4 +338,5 @@ def __init__( source_type, order_updater_parameters, preferred_buying_rate, + cop_model_type=cop_model_type, ) diff --git a/src/gsy_e/models/strategy/load_hours.py b/src/gsy_e/models/strategy/load_hours.py index 2073aec5e..9be80c118 100644 --- a/src/gsy_e/models/strategy/load_hours.py +++ b/src/gsy_e/models/strategy/load_hours.py @@ -15,34 +15,36 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from collections import namedtuple from typing import Union, Dict -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, FLOATING_POINT_TOLERANCE from gsy_framework.data_classes import Offer, TraderDetails from gsy_framework.enums import SpotMarketTypeEnum from gsy_framework.exceptions import GSyDeviceException from gsy_framework.read_user_profile import read_arbitrary_profile, InputProfileTypes from gsy_framework.utils import ( - limit_float_precision, get_from_profile_same_weekday_and_time, - is_time_slot_in_simulation_duration) + limit_float_precision, + get_from_profile_same_weekday_and_time, + is_time_slot_in_simulation_duration, +) from gsy_framework.validators.load_validator import LoadValidator from numpy import random from pendulum import duration from gsy_e import constants -from gsy_e.constants import FLOATING_POINT_TOLERANCE from gsy_e.gsy_e_core.device_registry import DeviceRegistry from gsy_e.gsy_e_core.exceptions import MarketException from gsy_e.models.base import AssetType from gsy_e.models.market import MarketBase -from gsy_e.models.strategy.state import LoadState from gsy_e.models.strategy import BidEnabledStrategy from gsy_e.models.strategy.energy_parameters.load import LoadHoursEnergyParameters from gsy_e.models.strategy.future.strategy import future_market_strategy_factory +from gsy_e.models.strategy.mixins import UseMarketMakerMixin from gsy_e.models.strategy.settlement.strategy import settlement_market_strategy_factory +from gsy_e.models.strategy.state import LoadState from gsy_e.models.strategy.update_frequency import TemplateStrategyBidUpdater -from gsy_e.models.strategy.mixins import UseMarketMakerMixin BalancingRatio = namedtuple("BalancingRatio", ("demand", "supply")) @@ -56,21 +58,29 @@ def serialize(self): **self._energy_params.serialize(), **self.bid_update.serialize(), "balancing_energy_ratio": self.balancing_energy_ratio, - "use_market_maker_rate": self.use_market_maker_rate + "use_market_maker_rate": self.use_market_maker_rate, } # pylint: disable=too-many-arguments - def __init__(self, avg_power_W, hrs_of_day=None, - fit_to_limit=True, energy_rate_increase_per_update=None, - update_interval=None, - initial_buying_rate: Union[float, Dict, str] = - ConstSettings.LoadSettings.BUYING_RATE_RANGE.initial, - final_buying_rate: Union[float, Dict, str] = - ConstSettings.LoadSettings.BUYING_RATE_RANGE.final, - balancing_energy_ratio: tuple = - (ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, - ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO), - use_market_maker_rate: bool = False): + def __init__( + self, + avg_power_W, + hrs_of_day=None, + fit_to_limit=True, + energy_rate_increase_per_update=None, + update_interval=None, + initial_buying_rate: Union[ + float, Dict, str + ] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.initial, + final_buying_rate: Union[ + float, Dict, str + ] = ConstSettings.LoadSettings.BUYING_RATE_RANGE.final, + balancing_energy_ratio: tuple = ( + ConstSettings.BalancingSettings.OFFER_DEMAND_RATIO, + ConstSettings.BalancingSettings.OFFER_SUPPLY_RATIO, + ), + use_market_maker_rate: bool = False, + ): """ Constructor of LoadHoursStrategy :param avg_power_W: Power rating of load device @@ -89,8 +99,13 @@ def __init__(self, avg_power_W, hrs_of_day=None, self.balancing_energy_ratio = BalancingRatio(*balancing_energy_ratio) self.use_market_maker_rate = use_market_maker_rate - self._init_price_update(fit_to_limit, energy_rate_increase_per_update, update_interval, - initial_buying_rate, final_buying_rate) + self._init_price_update( + fit_to_limit, + energy_rate_increase_per_update, + update_interval, + initial_buying_rate, + final_buying_rate, + ) self._calculate_active_markets() self._cycled_market = set() @@ -107,46 +122,65 @@ def _create_settlement_market_strategy(cls): def _create_future_market_strategy(self): return future_market_strategy_factory(self.asset_type) - def _init_price_update(self, fit_to_limit, energy_rate_increase_per_update, update_interval, - initial_buying_rate, final_buying_rate): + def _init_price_update( + self, + fit_to_limit, + energy_rate_increase_per_update, + update_interval, + initial_buying_rate, + final_buying_rate, + ): LoadValidator.validate_rate( fit_to_limit=fit_to_limit, - energy_rate_increase_per_update=energy_rate_increase_per_update) + energy_rate_increase_per_update=energy_rate_increase_per_update, + ) if update_interval is None: update_interval = duration( - minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL) + minutes=ConstSettings.GeneralSettings.DEFAULT_UPDATE_INTERVAL + ) if isinstance(update_interval, int): update_interval = duration(minutes=update_interval) BidEnabledStrategy.__init__(self) self.bid_update = TemplateStrategyBidUpdater( - initial_rate=initial_buying_rate, final_rate=final_buying_rate, + initial_rate=initial_buying_rate, + final_rate=final_buying_rate, fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_increase_per_update, - update_interval=update_interval, rate_limit_object=min) + update_interval=update_interval, + rate_limit_object=min, + ) - def _validate_rates(self, initial_rate, final_rate, energy_rate_change_per_update, - fit_to_limit): + def _validate_rates( + self, initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ): # all parameters have to be validated for each time slot starting from the current time for time_slot in initial_rate.keys(): if not is_time_slot_in_simulation_duration(time_slot, self.area.config): continue - if (self.area and - self.area.current_market - and time_slot < self.area.current_market.time_slot): + if ( + self.area + and self.area.current_market + and time_slot < self.area.current_market.time_slot + ): continue - rate_change = (None if fit_to_limit else - get_from_profile_same_weekday_and_time( - energy_rate_change_per_update, time_slot)) + rate_change = ( + None + if fit_to_limit + else get_from_profile_same_weekday_and_time( + energy_rate_change_per_update, time_slot + ) + ) LoadValidator.validate_rate( initial_buying_rate=initial_rate[time_slot], energy_rate_increase_per_update=rate_change, final_buying_rate=get_from_profile_same_weekday_and_time(final_rate, time_slot), - fit_to_limit=fit_to_limit) + fit_to_limit=fit_to_limit, + ) def event_activate(self, **kwargs): self._energy_params.event_activate_energy(self.area) @@ -185,32 +219,37 @@ def _set_energy_measurement_of_last_market(self): self._energy_params.set_energy_measurement_kWh(self.area.current_market.time_slot) def _delete_past_state(self): - if (constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True or - self.area.current_market is None): + if ( + constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True + or self.area.current_market is None + ): return self.state.delete_past_state_values(self.area.current_market.time_slot) self.bid_update.delete_past_state_values(self.area.current_market.time_slot) - self._future_market_strategy.delete_past_state_values( - self.area.current_market.time_slot) + self._future_market_strategy.delete_past_state_values(self.area.current_market.time_slot) def _area_reconfigure_prices(self, **kwargs): if kwargs.get("initial_buying_rate") is not None: - initial_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["initial_buying_rate"]) + initial_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["initial_buying_rate"] + ) else: initial_rate = self.bid_update.initial_rate_profile_buffer if kwargs.get("final_buying_rate") is not None: - final_rate = read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["final_buying_rate"]) + final_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["final_buying_rate"] + ) else: final_rate = self.bid_update.final_rate_profile_buffer if kwargs.get("energy_rate_increase_per_update") is not None: energy_rate_change_per_update = read_arbitrary_profile( - InputProfileTypes.IDENTITY, kwargs["energy_rate_increase_per_update"]) + InputProfileTypes.IDENTITY, kwargs["energy_rate_increase_per_update"] + ) else: - energy_rate_change_per_update = (self.bid_update. - energy_rate_change_per_update_profile_buffer) + energy_rate_change_per_update = ( + self.bid_update.energy_rate_change_per_update_profile_buffer + ) if kwargs.get("fit_to_limit") is not None: fit_to_limit = kwargs["fit_to_limit"] else: @@ -219,7 +258,8 @@ def _area_reconfigure_prices(self, **kwargs): update_interval = ( duration(minutes=kwargs["update_interval"]) if isinstance(kwargs["update_interval"], int) - else kwargs["update_interval"]) + else kwargs["update_interval"] + ) else: update_interval = self.bid_update.update_interval @@ -227,8 +267,9 @@ def _area_reconfigure_prices(self, **kwargs): self.use_market_maker_rate = kwargs["use_market_maker_rate"] try: - self._validate_rates(initial_rate, final_rate, energy_rate_change_per_update, - fit_to_limit) + self._validate_rates( + initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ) except GSyDeviceException: self.log.exception("LoadHours._area_reconfigure_prices failed. Exception: ") return @@ -238,7 +279,7 @@ def _area_reconfigure_prices(self, **kwargs): final_rate=final_rate, energy_rate_change_per_update=energy_rate_change_per_update, fit_to_limit=fit_to_limit, - update_interval=update_interval + update_interval=update_interval, ) def area_reconfigure_event(self, *args, **kwargs): @@ -252,10 +293,12 @@ def event_activate_price(self): """Update the strategy prices upon the activation and validate them afterwards.""" self._replace_rates_with_market_maker_rates() - self._validate_rates(self.bid_update.initial_rate_profile_buffer, - self.bid_update.final_rate_profile_buffer, - self.bid_update.energy_rate_change_per_update_profile_buffer, - self.bid_update.fit_to_limit) + self._validate_rates( + self.bid_update.initial_rate_profile_buffer, + self.bid_update.final_rate_profile_buffer, + self.bid_update.energy_rate_change_per_update_profile_buffer, + self.bid_update.fit_to_limit, + ) @staticmethod def _find_acceptable_offer(market): @@ -267,7 +310,8 @@ def _offer_rate_can_be_accepted(self, offer: Offer, market_slot: MarketBase): max_affordable_offer_rate = self.bid_update.get_updated_rate(market_slot.time_slot) return ( limit_float_precision(offer.energy_rate) - <= max_affordable_offer_rate + FLOATING_POINT_TOLERANCE) + <= max_affordable_offer_rate + FLOATING_POINT_TOLERANCE + ) def _one_sided_market_event_tick(self, market, offer=None): if not self.state.can_buy_more_energy(market.time_slot): @@ -284,20 +328,26 @@ def _one_sided_market_event_tick(self, market, offer=None): acceptable_offer = offer time_slot = market.time_slot - if (acceptable_offer and self._energy_params.allowed_operating_hours(time_slot) - and self._offer_rate_can_be_accepted(acceptable_offer, market)): + if ( + acceptable_offer + and self._energy_params.allowed_operating_hours(time_slot) + and self._offer_rate_can_be_accepted(acceptable_offer, market) + ): energy_Wh = self.state.calculate_energy_to_accept( - acceptable_offer.energy * 1000.0, time_slot) - self.accept_offer(market, acceptable_offer, - buyer=TraderDetails( - self.owner.name, self.owner.uuid, - self.owner.name, self.owner.uuid), - energy=energy_Wh / 1000.0) + acceptable_offer.energy * 1000.0, time_slot + ) + self.accept_offer( + market, + acceptable_offer, + buyer=TraderDetails( + self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid + ), + energy=energy_Wh / 1000.0, + ) self._energy_params.decrement_energy_requirement( - energy_kWh=energy_Wh / 1000, - time_slot=time_slot, - area_name=self.owner.name) + energy_kWh=energy_Wh / 1000, time_slot=time_slot, area_name=self.owner.name + ) except MarketException: self.log.exception("An Error occurred while buying an offer") @@ -347,16 +397,21 @@ def _post_first_bid(self): if ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.ONE_SIDED.value: return for market in self.active_markets: - if (self.state.can_buy_more_energy(market.time_slot) and - self._energy_params.allowed_operating_hours(market.time_slot) - and not self.are_bids_posted(market.id)): + if ( + self.state.can_buy_more_energy(market.time_slot) + and self._energy_params.allowed_operating_hours(market.time_slot) + and not self.are_bids_posted(market.id) + ): bid_energy = self.state.get_energy_requirement_Wh(market.time_slot) if self._is_eligible_for_balancing_market: - bid_energy -= (self.state.get_desired_energy_Wh(market.time_slot) * - self.balancing_energy_ratio.demand) + bid_energy -= ( + self.state.get_desired_energy_Wh(market.time_slot) + * self.balancing_energy_ratio.demand + ) try: - self.post_first_bid(market, bid_energy, - self.bid_update.initial_rate[market.time_slot]) + self.post_first_bid( + market, bid_energy, self.bid_update.initial_rate[market.time_slot] + ) except MarketException: pass @@ -380,7 +435,8 @@ def event_bid_traded(self, *, market_id, bid_trade): self._energy_params.decrement_energy_requirement( energy_kWh=bid_trade.traded_energy, time_slot=bid_trade.time_slot, - area_name=self.owner.name) + area_name=self.owner.name, + ) def event_offer_traded(self, *, market_id, trade): """Register the offer traded by the device and its effects. Extends the superclass method. @@ -406,19 +462,21 @@ def _demand_balancing_offer(self, market): if not self._is_eligible_for_balancing_market: return - ramp_up_energy = (self.balancing_energy_ratio.demand * - self.state.get_desired_energy_Wh(market.time_slot)) + ramp_up_energy = self.balancing_energy_ratio.demand * self.state.get_desired_energy_Wh( + market.time_slot + ) self._energy_params.decrement_energy_requirement( - energy_kWh=ramp_up_energy / 1000, - time_slot=market.time_slot, - area_name=self.owner.name) + energy_kWh=ramp_up_energy / 1000, time_slot=market.time_slot, area_name=self.owner.name + ) ramp_up_price = DeviceRegistry.REGISTRY[self.owner.name][0] * ramp_up_energy if ramp_up_energy != 0 and ramp_up_price != 0: self.area.get_balancing_market(market.time_slot).balancing_offer( - ramp_up_price, -ramp_up_energy, TraderDetails( - self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid)) + ramp_up_price, + -ramp_up_energy, + TraderDetails(self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid), + ) # committing to reduce its consumption when required def _supply_balancing_offer(self, market, trade): @@ -429,8 +487,10 @@ def _supply_balancing_offer(self, market, trade): ramp_down_energy = self.balancing_energy_ratio.supply * trade.traded_energy ramp_down_price = DeviceRegistry.REGISTRY[self.owner.name][1] * ramp_down_energy self.area.get_balancing_market(market.time_slot).balancing_offer( - ramp_down_price, ramp_down_energy, TraderDetails( - self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid)) + ramp_down_price, + ramp_down_energy, + TraderDetails(self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid), + ) @property def active_markets(self): @@ -442,16 +502,21 @@ def active_markets(self): return self._active_markets def _calculate_active_markets(self): - self._active_markets = [ - market for market in self.area.all_markets - if self._is_market_active(market) - ] if self.area else [] + self._active_markets = ( + [market for market in self.area.all_markets if self._is_market_active(market)] + if self.area + else [] + ) def _is_market_active(self, market): - return (self._energy_params.allowed_operating_hours(market.time_slot) and - market.in_sim_duration and - (not self.area.current_market or - market.time_slot >= self.area.current_market.time_slot)) + return ( + self._energy_params.allowed_operating_hours(market.time_slot) + and market.in_sim_duration + and ( + not self.area.current_market + or market.time_slot >= self.area.current_market.time_slot + ) + ) def _update_energy_requirement_future_markets(self): if not ConstSettings.FutureMarketSettings.FUTURE_MARKET_DURATION_HOURS: diff --git a/src/gsy_e/models/strategy/market_agents/balancing_agent.py b/src/gsy_e/models/strategy/market_agents/balancing_agent.py index 150ef7a8e..c1c0f3fc5 100644 --- a/src/gsy_e/models/strategy/market_agents/balancing_agent.py +++ b/src/gsy_e/models/strategy/market_agents/balancing_agent.py @@ -15,11 +15,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from gsy_framework.constants_limits import ConstSettings + +from gsy_framework.constants_limits import ConstSettings, FLOATING_POINT_TOLERANCE from gsy_framework.data_classes import TraderDetails from numpy.random import random -from gsy_e.constants import FLOATING_POINT_TOLERANCE from gsy_e.models.strategy.market_agents.one_sided_agent import OneSidedAgent from gsy_e.models.strategy.market_agents.one_sided_engine import BalancingEngine @@ -27,22 +27,31 @@ class BalancingAgent(OneSidedAgent): """Market agent for balancing market""" - def __init__(self, owner, higher_market, lower_market, - min_offer_age=ConstSettings.MASettings.MIN_OFFER_AGE): + def __init__( + self, + owner, + higher_market, + lower_market, + min_offer_age=ConstSettings.MASettings.MIN_OFFER_AGE, + ): self.balancing_spot_trade_ratio = owner.balancing_spot_trade_ratio - super().__init__(owner=owner, - higher_market=higher_market, - lower_market=lower_market, - min_offer_age=min_offer_age) + super().__init__( + owner=owner, + higher_market=higher_market, + lower_market=lower_market, + min_offer_age=min_offer_age, + ) self.name = self.owner.name def _create_engines(self): self.engines = [ - BalancingEngine("High -> Low", self.higher_market, self.lower_market, - self.min_offer_age, self), - BalancingEngine("Low -> High", self.lower_market, self.higher_market, - self.min_offer_age, self), + BalancingEngine( + "High -> Low", self.higher_market, self.lower_market, self.min_offer_age, self + ), + BalancingEngine( + "Low -> High", self.lower_market, self.higher_market, self.min_offer_age, self + ), ] def __repr__(self): @@ -50,10 +59,14 @@ def __repr__(self): def event_tick(self): super().event_tick() - if self.lower_market.unmatched_energy_downward > 0.0 or \ - self.lower_market.unmatched_energy_upward > 0.0: - self._trigger_balancing_trades(self.lower_market.unmatched_energy_upward, - self.lower_market.unmatched_energy_downward) + if ( + self.lower_market.unmatched_energy_downward > 0.0 + or self.lower_market.unmatched_energy_upward > 0.0 + ): + self._trigger_balancing_trades( + self.lower_market.unmatched_energy_upward, + self.lower_market.unmatched_energy_downward, + ) def event_offer_traded(self, *, market_id, trade): market = self.get_market_from_market_id(market_id) @@ -75,29 +88,30 @@ def event_bid_traded(self, *, market_id, bid_trade): super().event_bid_traded(market_id=market_id, bid_trade=bid_trade) def _calculate_and_buy_balancing_energy(self, market, trade): - if trade.buyer.name != self.owner.name or \ - market.time_slot != self.lower_market.time_slot: + if trade.buyer.name != self.owner.name or market.time_slot != self.lower_market.time_slot: return - positive_balancing_energy = \ - trade.traded_energy * self.balancing_spot_trade_ratio + \ - self.lower_market.unmatched_energy_upward - negative_balancing_energy = \ - trade.traded_energy * self.balancing_spot_trade_ratio + \ - self.lower_market.unmatched_energy_downward + positive_balancing_energy = ( + trade.traded_energy * self.balancing_spot_trade_ratio + + self.lower_market.unmatched_energy_upward + ) + negative_balancing_energy = ( + trade.traded_energy * self.balancing_spot_trade_ratio + + self.lower_market.unmatched_energy_downward + ) self._trigger_balancing_trades(positive_balancing_energy, negative_balancing_energy) def _trigger_balancing_trades(self, positive_balancing_energy, negative_balancing_energy): for offer in self.lower_market.sorted_offers: - if offer.energy > FLOATING_POINT_TOLERANCE and \ - positive_balancing_energy > FLOATING_POINT_TOLERANCE: - balance_trade = self._balancing_trade(offer, - positive_balancing_energy) + if ( + offer.energy > FLOATING_POINT_TOLERANCE + and positive_balancing_energy > FLOATING_POINT_TOLERANCE + ): + balance_trade = self._balancing_trade(offer, positive_balancing_energy) if balance_trade is not None: positive_balancing_energy -= abs(balance_trade.traded_energy) elif offer.energy < FLOATING_POINT_TOLERANCE < negative_balancing_energy: - balance_trade = self._balancing_trade(offer, - -negative_balancing_energy) + balance_trade = self._balancing_trade(offer, -negative_balancing_energy) if balance_trade is not None: negative_balancing_energy -= abs(balance_trade.traded_energy) @@ -107,29 +121,35 @@ def _trigger_balancing_trades(self, positive_balancing_energy, negative_balancin def _balancing_trade(self, offer, target_energy): trade = None buyer = TraderDetails( - (self.owner.name - if self.owner.name != offer.seller.name - else f"{self.owner.name} Reserve"), - self.owner.uuid) + ( + self.owner.name + if self.owner.name != offer.seller.name + else f"{self.owner.name} Reserve" + ), + self.owner.uuid, + ) if abs(offer.energy) <= abs(target_energy): - trade = self.lower_market.accept_offer(offer_or_id=offer, - buyer=buyer, - energy=offer.energy) + trade = self.lower_market.accept_offer( + offer_or_id=offer, buyer=buyer, energy=offer.energy + ) elif abs(offer.energy) >= abs(target_energy): - trade = self.lower_market.accept_offer(offer_or_id=offer, - buyer=buyer, - energy=target_energy) + trade = self.lower_market.accept_offer( + offer_or_id=offer, buyer=buyer, energy=target_energy + ) return trade def event_balancing_trade(self, *, market_id, trade, offer=None): for engine in sorted(self.engines, key=lambda _: random()): engine.event_offer_traded(trade=trade) - def event_balancing_offer_split(self, *, market_id, original_offer, accepted_offer, - residual_offer): + def event_balancing_offer_split( + self, *, market_id, original_offer, accepted_offer, residual_offer + ): for engine in sorted(self.engines, key=lambda _: random()): - engine.event_offer_split(market_id=market_id, - original_offer=original_offer, - accepted_offer=accepted_offer, - residual_offer=residual_offer) + engine.event_offer_split( + market_id=market_id, + original_offer=original_offer, + accepted_offer=accepted_offer, + residual_offer=residual_offer, + ) diff --git a/src/gsy_e/models/strategy/market_agents/market_agent.py b/src/gsy_e/models/strategy/market_agents/market_agent.py index 3d9cebaa6..2e71f823d 100644 --- a/src/gsy_e/models/strategy/market_agents/market_agent.py +++ b/src/gsy_e/models/strategy/market_agents/market_agent.py @@ -15,12 +15,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from typing import Optional, TYPE_CHECKING -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, TIME_FORMAT from numpy.random import random - -from gsy_e.constants import TIME_FORMAT from gsy_e.models.strategy import BaseStrategy, _TradeLookerUpper if TYPE_CHECKING: @@ -33,12 +32,20 @@ class MarketAgent(BaseStrategy): def serialize(self): return { - "owner": self.owner, "higher_market": self.higher_market, - "lower_market": self.lower_market, "min_offer_age": self.min_offer_age + "owner": self.owner, + "higher_market": self.higher_market, + "lower_market": self.lower_market, + "min_offer_age": self.min_offer_age, } - def __init__(self, *, owner: "Area", higher_market: "MarketBase", lower_market: "MarketBase", - min_offer_age: int = ConstSettings.MASettings.MIN_OFFER_AGE): + def __init__( + self, + *, + owner: "Area", + higher_market: "MarketBase", + lower_market: "MarketBase", + min_offer_age: int = ConstSettings.MASettings.MIN_OFFER_AGE, + ): """ :param min_offer_age: Minimum age of offer before transferring """ @@ -61,8 +68,11 @@ def _create_engines(self): @property def time_slot_str(self) -> Optional[str]: """Return time_slot of the inter area agent. For future markets it is None.""" - return (self.higher_market.time_slot.format(TIME_FORMAT) - if self.higher_market.time_slot else None) + return ( + self.higher_market.time_slot.format(TIME_FORMAT) + if self.higher_market.time_slot + else None + ) @staticmethod def _validate_constructor_arguments(min_offer_age: int): diff --git a/src/gsy_e/models/strategy/market_agents/one_sided_engine.py b/src/gsy_e/models/strategy/market_agents/one_sided_engine.py index 7a0acda3c..134bbf8b3 100644 --- a/src/gsy_e/models/strategy/market_agents/one_sided_engine.py +++ b/src/gsy_e/models/strategy/market_agents/one_sided_engine.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from collections import namedtuple from typing import Dict, Optional # noqa @@ -23,7 +24,7 @@ from gsy_framework.enums import SpotMarketTypeEnum from gsy_framework.utils import limit_float_precision -from gsy_e.constants import FLOATING_POINT_TOLERANCE +from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE from gsy_e.gsy_e_core.exceptions import MarketException, OfferNotFoundException from gsy_e.gsy_e_core.util import short_offer_bid_log_str @@ -34,6 +35,7 @@ class MAEngine: """Handle forwarding offers to the connected one-sided market.""" + # pylint: disable=too-many-arguments,too-many-instance-attributes def __init__(self, name: str, market_1, market_2, min_offer_age: int, owner): @@ -60,9 +62,12 @@ def _update_offer_requirements_prices(self, offer): if "price" in updated_requirement: energy = updated_requirement.get("energy") or offer.energy original_offer_price = updated_requirement["price"] + offer.accumulated_grid_fees - updated_price = self.markets.target.fee_class.update_forwarded_offer_with_fee( - updated_requirement["price"] / energy, - original_offer_price / energy) * energy + updated_price = ( + self.markets.target.fee_class.update_forwarded_offer_with_fee( + updated_requirement["price"] / energy, original_offer_price / energy + ) + * energy + ) updated_requirement["price"] = updated_price requirements.append(updated_requirement) return requirements @@ -70,18 +75,20 @@ def _update_offer_requirements_prices(self, offer): def _offer_in_market(self, offer): updated_price = limit_float_precision( self.markets.target.fee_class.update_forwarded_offer_with_fee( - offer.energy_rate, offer.original_energy_rate) * offer.energy) + offer.energy_rate, offer.original_energy_rate + ) + * offer.energy + ) kwargs = { "price": updated_price, "energy": offer.energy, "seller": TraderDetails( - self.owner.name, self.owner.uuid, - offer.seller.origin, offer.seller.origin_uuid + self.owner.name, self.owner.uuid, offer.seller.origin, offer.seller.origin_uuid ), "original_price": offer.original_price, "dispatch_event": False, - "time_slot": offer.time_slot + "time_slot": offer.time_slot, } return self.owner.post_offer(market=self.markets.target, replace_existing=False, **kwargs) @@ -96,8 +103,10 @@ def _forward_offer(self, offer: Offer) -> Optional[Offer]: try: forwarded_offer = self._offer_in_market(offer) except MarketException: - self.owner.log.debug("Offer is not forwarded because grid fees of the target market " - "lead to a negative offer price.") + self.owner.log.debug( + "Offer is not forwarded because grid fees of the target market " + "lead to a negative offer price." + ) return None self._add_to_forward_offers(offer, forwarded_offer) @@ -157,8 +166,10 @@ def _propagate_offer(self, current_tick): forwarded_offer = self._forward_offer(offer) if forwarded_offer: - self.owner.log.debug(f"Forwarded offer to {self.markets.source.name} " - f"{self.owner.name}, {self.name} {forwarded_offer}") + self.owner.log.debug( + f"Forwarded offer to {self.markets.source.name} " + f"{self.owner.name}, {self.name} {forwarded_offer}" + ) def event_offer_traded(self, *, trade): """Perform actions that need to be done when OFFER_TRADED event is triggered.""" @@ -171,22 +182,30 @@ def event_offer_traded(self, *, trade): # Offer was accepted in target market - buy in source source_rate = offer_info.source_offer.energy_rate target_rate = offer_info.target_offer.energy_rate - assert abs(source_rate) <= abs(target_rate) + 0.0001, \ - f"offer: source_rate ({source_rate}) is not lower than target_rate ({target_rate})" + assert ( + abs(source_rate) <= abs(target_rate) + 0.0001 + ), f"offer: source_rate ({source_rate}) is not lower than target_rate ({target_rate})" - updated_trade_bid_info = \ + updated_trade_bid_info = ( self.markets.source.fee_class.update_forwarded_offer_trade_original_info( - trade.offer_bid_trade_info, offer_info.source_offer) + trade.offer_bid_trade_info, offer_info.source_offer + ) + ) try: if ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.ONE_SIDED.value: # One sided market should subtract the fees - trade_offer_rate = trade.trade_rate - \ - trade.fee_price / trade.traded_energy + trade_offer_rate = trade.trade_rate - trade.fee_price / trade.traded_energy if not updated_trade_bid_info: updated_trade_bid_info = TradeBidOfferInfo( - None, None, ( - offer_info.source_offer.original_price / - offer_info.source_offer.energy), source_rate, 0.) + None, + None, + ( + offer_info.source_offer.original_price + / offer_info.source_offer.energy + ), + source_rate, + 0.0, + ) updated_trade_bid_info.trade_rate = trade_offer_rate trade_source = self.owner.accept_offer( @@ -194,15 +213,19 @@ def event_offer_traded(self, *, trade): offer=offer_info.source_offer, energy=trade.traded_energy, buyer=TraderDetails( - self.owner.name, self.owner.uuid, - trade.buyer.origin, trade.buyer.origin_uuid), + self.owner.name, + self.owner.uuid, + trade.buyer.origin, + trade.buyer.origin_uuid, + ), trade_bid_info=updated_trade_bid_info, ) except OfferNotFoundException as ex: raise OfferNotFoundException() from ex self.owner.log.debug( - f"[{self.markets.source.time_slot_str}] Offer accepted {trade_source}") + f"[{self.markets.source.time_slot_str}] Offer accepted {trade_source}" + ) self._delete_forwarded_offer_entries(offer_info.source_offer) self.offer_age.pop(offer_info.source_offer.id, None) @@ -258,12 +281,15 @@ def event_offer_split(self, *, market_id, original_offer, accepted_offer, residu # offer was split in target market, also split in source market local_offer = self.forwarded_offers[original_offer.id].source_offer - original_price = local_offer.original_price \ - if local_offer.original_price is not None else local_offer.price + original_price = ( + local_offer.original_price + if local_offer.original_price is not None + else local_offer.price + ) - local_split_offer, local_residual_offer = \ - self.markets.source.split_offer(local_offer, accepted_offer.energy, - original_price) + local_split_offer, local_residual_offer = self.markets.source.split_offer( + local_offer, accepted_offer.energy, original_price + ) # add the new offers to forwarded_offers self._add_to_forward_offers(local_residual_offer, residual_offer) @@ -271,18 +297,23 @@ def event_offer_split(self, *, market_id, original_offer, accepted_offer, residu elif market == self.markets.source and accepted_offer.id in self.forwarded_offers: # offer was split in source market, also split in target market - if not self.owner.usable_offer(accepted_offer) or \ - self.owner.name == accepted_offer.seller.name: + if ( + not self.owner.usable_offer(accepted_offer) + or self.owner.name == accepted_offer.seller.name + ): return local_offer = self.forwarded_offers[original_offer.id].source_offer - original_price = local_offer.original_price \ - if local_offer.original_price is not None else local_offer.price + original_price = ( + local_offer.original_price + if local_offer.original_price is not None + else local_offer.price + ) - local_split_offer, local_residual_offer = \ - self.markets.target.split_offer(local_offer, accepted_offer.energy, - original_price) + local_split_offer, local_residual_offer = self.markets.target.split_offer( + local_offer, accepted_offer.energy, original_price + ) # add the new offers to forwarded_offers self._add_to_forward_offers(residual_offer, local_residual_offer) @@ -294,9 +325,11 @@ def event_offer_split(self, *, market_id, original_offer, accepted_offer, residu if original_offer.id in self.offer_age: self.offer_age[residual_offer.id] = self.offer_age.pop(original_offer.id) - self.owner.log.debug(f"Offer {short_offer_bid_log_str(local_offer)} was split into " - f"{short_offer_bid_log_str(local_split_offer)} and " - f"{short_offer_bid_log_str(local_residual_offer)}") + self.owner.log.debug( + f"Offer {short_offer_bid_log_str(local_offer)} was split into " + f"{short_offer_bid_log_str(local_split_offer)} and " + f"{short_offer_bid_log_str(local_residual_offer)}" + ) def _add_to_forward_offers(self, source_offer, target_offer): offer_info = OfferInfo(Offer.copy(source_offer), Offer.copy(target_offer)) @@ -305,8 +338,10 @@ def _add_to_forward_offers(self, source_offer, target_offer): def event_offer(self, offer: Offer) -> None: """Perform actions on the event of the creation of a new offer.""" - if (ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.TWO_SIDED.value and - self.min_offer_age == 0): + if ( + ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.TWO_SIDED.value + and self.min_offer_age == 0 + ): # Propagate offer immediately if the MIN_OFFER_AGE is set to zero. if offer.id not in self.offer_age: self.offer_age[offer.id] = self._current_tick @@ -330,8 +365,10 @@ def event_offer(self, offer: Offer) -> None: forwarded_offer = self._forward_offer(offer) if forwarded_offer: - self.owner.log.debug(f"Forwarded offer to {self.markets.source.name} " - f"{self.owner.name}, {self.name} {forwarded_offer}") + self.owner.log.debug( + f"Forwarded offer to {self.markets.source.name} " + f"{self.owner.name}, {self.name} {forwarded_offer}" + ) class BalancingEngine(MAEngine): @@ -339,10 +376,12 @@ class BalancingEngine(MAEngine): def _forward_offer(self, offer): forwarded_balancing_offer = self.markets.target.balancing_offer( - offer.price, offer.energy, + offer.price, + offer.energy, TraderDetails( - self.owner.name, self.owner.uuid, offer.seller.origin, offer.seller.origin_uuid), - from_agent=True + self.owner.name, self.owner.uuid, offer.seller.origin, offer.seller.origin_uuid + ), + from_agent=True, ) self._add_to_forward_offers(offer, forwarded_balancing_offer) self.owner.log.trace(f"Forwarding balancing offer {offer} to {forwarded_balancing_offer}") diff --git a/src/gsy_e/models/strategy/market_agents/two_sided_engine.py b/src/gsy_e/models/strategy/market_agents/two_sided_engine.py index b56a84553..59175cb16 100644 --- a/src/gsy_e/models/strategy/market_agents/two_sided_engine.py +++ b/src/gsy_e/models/strategy/market_agents/two_sided_engine.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from collections import namedtuple from typing import Dict, TYPE_CHECKING @@ -23,7 +24,7 @@ from gsy_framework.enums import SpotMarketTypeEnum from gsy_framework.utils import limit_float_precision -from gsy_e.constants import FLOATING_POINT_TOLERANCE +from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE from gsy_e.gsy_e_core.exceptions import BidNotFoundException, MarketException from gsy_e.gsy_e_core.util import short_offer_bid_log_str from gsy_e.models.strategy.market_agents.one_sided_engine import MAEngine @@ -36,10 +37,18 @@ class TwoSidedEngine(MAEngine): """Handle forwarding offers and bids to the connected two-sided market.""" + # pylint: disable = too-many-arguments - def __init__(self, name: str, market_1, market_2, min_offer_age: int, min_bid_age: int, - owner: "MarketAgent"): + def __init__( + self, + name: str, + market_1, + market_2, + min_offer_age: int, + min_bid_age: int, + owner: "MarketAgent", + ): super().__init__(name, market_1, market_2, min_offer_age, owner) self.forwarded_bids: Dict[str, BidInfo] = {} self.bid_trade_residual: Dict[str, Bid] = {} @@ -47,8 +56,10 @@ def __init__(self, name: str, market_1, market_2, min_offer_age: int, min_bid_ag self.bid_age: Dict[str, int] = {} def __repr__(self): - return "".format(s=self) + return ( + "".format(s=self) + ) def _update_requirements_prices(self, bid): requirements = [] @@ -57,9 +68,12 @@ def _update_requirements_prices(self, bid): if "price" in updated_requirement: energy = updated_requirement.get("energy") or bid.energy original_bid_price = updated_requirement["price"] + bid.accumulated_grid_fees - updated_price = self.markets.source.fee_class.update_forwarded_bid_with_fee( - updated_requirement["price"] / energy, - original_bid_price / energy) * energy + updated_price = ( + self.markets.source.fee_class.update_forwarded_bid_with_fee( + updated_requirement["price"] / energy, original_bid_price / energy + ) + * energy + ) updated_requirement["price"] = updated_price requirements.append(updated_requirement) return requirements @@ -72,22 +86,30 @@ def _forward_bid(self, bid): self.owner.log.debug("Bid is not forwarded because price < 0") return None try: - updated_price = limit_float_precision(( - self.markets.source.fee_class.update_forwarded_bid_with_fee( - bid.energy_rate, bid.original_energy_rate)) * bid.energy) + updated_price = limit_float_precision( + ( + self.markets.source.fee_class.update_forwarded_bid_with_fee( + bid.energy_rate, bid.original_energy_rate + ) + ) + * bid.energy + ) forwarded_bid = self.markets.target.bid( price=updated_price, energy=bid.energy, buyer=TraderDetails( - self.owner.name, self.owner.uuid, bid.buyer.origin, bid.buyer.origin_uuid), + self.owner.name, self.owner.uuid, bid.buyer.origin, bid.buyer.origin_uuid + ), original_price=bid.original_price, dispatch_event=False, - time_slot=bid.time_slot + time_slot=bid.time_slot, ) except MarketException: - self.owner.log.debug("Bid is not forwarded because grid fees of the target market " - "lead to a negative bid price.") + self.owner.log.debug( + "Bid is not forwarded because grid fees of the target market " + "lead to a negative bid price." + ) return None self._add_to_forward_bids(bid, forwarded_bid) @@ -138,8 +160,9 @@ def _delete_forwarded_bids(self, bid_info): try: self.markets.target.delete_bid(bid_info.target_bid) except BidNotFoundException: - self.owner.log.trace(f"Bid {bid_info.target_bid.id} not " - f"found in the target market.") + self.owner.log.trace( + f"Bid {bid_info.target_bid.id} not " f"found in the target market." + ) self._delete_forwarded_bid_entries(bid_info.source_bid) def event_bid_traded(self, *, bid_trade): @@ -154,36 +177,43 @@ def event_bid_traded(self, *, bid_trade): if not market_bid: return - assert market_bid.energy - bid_trade.traded_energy >= -FLOATING_POINT_TOLERANCE, \ - "Traded bid on target market has more energy than the market bid." + assert ( + market_bid.energy - bid_trade.traded_energy >= -FLOATING_POINT_TOLERANCE + ), "Traded bid on target market has more energy than the market bid." source_rate = bid_info.source_bid.energy_rate target_rate = bid_info.target_bid.energy_rate - assert abs(source_rate) + FLOATING_POINT_TOLERANCE >= abs(target_rate), \ - f"bid: source_rate ({source_rate}) is not lower than target_rate ({target_rate})" + assert abs(source_rate) + FLOATING_POINT_TOLERANCE >= abs( + target_rate + ), f"bid: source_rate ({source_rate}) is not lower than target_rate ({target_rate})" if bid_trade.offer_bid_trade_info is not None: # Adapt trade_offer_info received by the trade to include source market grid fees, # which was skipped when accepting the bid during the trade operation. - updated_trade_offer_info = \ + updated_trade_offer_info = ( self.markets.source.fee_class.propagate_original_offer_info_on_bid_trade( bid_trade.offer_bid_trade_info ) + ) else: updated_trade_offer_info = bid_trade.offer_bid_trade_info - trade_offer_info = \ + trade_offer_info = ( self.markets.source.fee_class.update_forwarded_bid_trade_original_info( updated_trade_offer_info, market_bid ) + ) self.markets.source.accept_bid( bid=market_bid, energy=bid_trade.traded_energy, seller=TraderDetails( - self.owner.name, self.owner.uuid, - bid_trade.seller.origin, bid_trade.seller.origin_uuid), + self.owner.name, + self.owner.uuid, + bid_trade.seller.origin, + bid_trade.seller.origin_uuid, + ), trade_offer_info=trade_offer_info, - offer=None + offer=None, ) self._delete_forwarded_bids(bid_info) self.bid_age.pop(bid_info.source_bid.id, None) @@ -198,8 +228,10 @@ def event_bid_traded(self, *, bid_trade): if bid_trade.residual: self._forward_bid(bid_trade.residual) else: - raise Exception(f"Invalid bid state for MA {self.owner.name}: " - f"traded bid {bid_trade} was not in offered bids tuple {bid_info}") + raise Exception( + f"Invalid bid state for MA {self.owner.name}: " + f"traded bid {bid_trade} was not in offered bids tuple {bid_info}" + ) def event_bid_deleted(self, *, bid): """Perform actions that need to be done when BID_DELETED event is triggered.""" @@ -220,8 +252,9 @@ def event_bid_deleted(self, *, bid): self._delete_forwarded_bid_entries(bid_info.source_bid) self.bid_age.pop(bid_info.source_bid.id, None) - def event_bid_split(self, *, market_id: str, original_bid: Bid, - accepted_bid: Bid, residual_bid: Bid) -> None: + def event_bid_split( + self, *, market_id: str, original_bid: Bid, accepted_bid: Bid, residual_bid: Bid + ) -> None: """Perform actions that need to be done when BID_SPLIT event is triggered.""" market = self.owner.get_market_from_market_id(market_id) if market is None: @@ -232,18 +265,23 @@ def event_bid_split(self, *, market_id: str, original_bid: Bid, # in the source market local_bid = self.forwarded_bids[original_bid.id].source_bid - original_price = local_bid.original_price \ - if local_bid.original_price is not None else local_bid.price + original_price = ( + local_bid.original_price + if local_bid.original_price is not None + else local_bid.price + ) - local_split_bid, local_residual_bid = \ - self.markets.source.split_bid(local_bid, accepted_bid.energy, original_price) + local_split_bid, local_residual_bid = self.markets.source.split_bid( + local_bid, accepted_bid.energy, original_price + ) # add the new bids to forwarded_bids self._add_to_forward_bids(local_residual_bid, residual_bid) self._add_to_forward_bids(local_split_bid, accepted_bid) self.bid_age[local_residual_bid.id] = self.bid_age.pop( - local_bid.id, self._current_tick) + local_bid.id, self._current_tick + ) elif market == self.markets.source and accepted_bid.id in self.forwarded_bids: # bid in the source market was split, also split the corresponding forwarded bid @@ -253,11 +291,15 @@ def event_bid_split(self, *, market_id: str, original_bid: Bid, local_bid = self.forwarded_bids[original_bid.id].source_bid - original_price = local_bid.original_price \ - if local_bid.original_price is not None else local_bid.price + original_price = ( + local_bid.original_price + if local_bid.original_price is not None + else local_bid.price + ) - local_split_bid, local_residual_bid = \ - self.markets.target.split_bid(local_bid, accepted_bid.energy, original_price) + local_split_bid, local_residual_bid = self.markets.target.split_bid( + local_bid, accepted_bid.energy, original_price + ) # add the new bids to forwarded_bids self._add_to_forward_bids(residual_bid, local_residual_bid) @@ -268,9 +310,11 @@ def event_bid_split(self, *, market_id: str, original_bid: Bid, else: return - self.owner.log.debug(f"Bid {short_offer_bid_log_str(local_bid)} was split into " - f"{short_offer_bid_log_str(local_split_bid)} and " - f"{short_offer_bid_log_str(local_residual_bid)}") + self.owner.log.debug( + f"Bid {short_offer_bid_log_str(local_bid)} was split into " + f"{short_offer_bid_log_str(local_split_bid)} and " + f"{short_offer_bid_log_str(local_residual_bid)}" + ) def _add_to_forward_bids(self, source_bid, target_bid): bid_info = BidInfo(source_bid, target_bid) @@ -279,8 +323,10 @@ def _add_to_forward_bids(self, source_bid, target_bid): def event_bid(self, bid: Bid) -> None: """Perform actions on the event of the creation of a new bid.""" - if (ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.TWO_SIDED.value and - self.min_bid_age == 0): + if ( + ConstSettings.MASettings.MARKET_TYPE == SpotMarketTypeEnum.TWO_SIDED.value + and self.min_bid_age == 0 + ): # Propagate bid immediately if the MIN_BID_AGE is set to zero. source_bid = self.markets.source.bids.get(bid.id) if not source_bid: diff --git a/src/gsy_e/models/strategy/smart_meter.py b/src/gsy_e/models/strategy/smart_meter.py index 4ca699126..0fde6e52c 100644 --- a/src/gsy_e/models/strategy/smart_meter.py +++ b/src/gsy_e/models/strategy/smart_meter.py @@ -17,7 +17,7 @@ from pathlib import Path from typing import Dict, Union -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, FLOATING_POINT_TOLERANCE from gsy_framework.data_classes import Offer, TraderDetails from gsy_framework.enums import SpotMarketTypeEnum from gsy_framework.read_user_profile import InputProfileTypes, read_arbitrary_profile @@ -27,7 +27,6 @@ from pendulum import duration from gsy_e import constants -from gsy_e.constants import FLOATING_POINT_TOLERANCE from gsy_e.gsy_e_core.exceptions import GSyException, MarketException from gsy_e.models.base import AssetType from gsy_e.models.market import MarketBase @@ -35,8 +34,10 @@ from gsy_e.models.strategy.energy_parameters.smart_meter import SmartMeterEnergyParameters from gsy_e.models.strategy.mixins import UseMarketMakerMixin from gsy_e.models.strategy.state import SmartMeterState -from gsy_e.models.strategy.update_frequency import (TemplateStrategyBidUpdater, - TemplateStrategyOfferUpdater) +from gsy_e.models.strategy.update_frequency import ( + TemplateStrategyBidUpdater, + TemplateStrategyOfferUpdater, +) log = getLogger(__name__) @@ -52,25 +53,24 @@ def serialize(self): **self.offer_update.serialize(), # Price consumption parameters **self.bid_update.serialize(), - "use_market_maker_rate": self.use_market_maker_rate + "use_market_maker_rate": self.use_market_maker_rate, } # pylint: disable=too-many-arguments def __init__( - self, - smart_meter_profile: Union[Path, str, Dict[int, float], Dict[str, float]] = None, - initial_selling_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, - final_selling_rate: float = ConstSettings.SmartMeterSettings.SELLING_RATE_RANGE.final, - energy_rate_decrease_per_update: Union[float, None] = None, - initial_buying_rate: float = ( - ConstSettings.SmartMeterSettings.BUYING_RATE_RANGE.initial), - final_buying_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, - energy_rate_increase_per_update: Union[float, None] = None, - fit_to_limit: bool = True, - update_interval=None, - use_market_maker_rate: bool = False, - smart_meter_profile_uuid: str = None, - smart_meter_measurement_uuid: str = None + self, + smart_meter_profile: Union[Path, str, Dict[int, float], Dict[str, float]] = None, + initial_selling_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, + final_selling_rate: float = ConstSettings.SmartMeterSettings.SELLING_RATE_RANGE.final, + energy_rate_decrease_per_update: Union[float, None] = None, + initial_buying_rate: float = (ConstSettings.SmartMeterSettings.BUYING_RATE_RANGE.initial), + final_buying_rate: float = ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, + energy_rate_increase_per_update: Union[float, None] = None, + fit_to_limit: bool = True, + update_interval=None, + use_market_maker_rate: bool = False, + smart_meter_profile_uuid: str = None, + smart_meter_measurement_uuid: str = None, ): """ Args: @@ -98,7 +98,8 @@ def __init__( super().__init__() self._energy_params = SmartMeterEnergyParameters( - smart_meter_profile, smart_meter_profile_uuid, smart_meter_measurement_uuid) + smart_meter_profile, smart_meter_profile_uuid, smart_meter_measurement_uuid + ) # needed for profile_handler self.smart_meter_profile_uuid = smart_meter_profile_uuid @@ -111,7 +112,8 @@ def __init__( self.validator.validate( fit_to_limit=fit_to_limit, energy_rate_increase_per_update=energy_rate_increase_per_update, - energy_rate_decrease_per_update=energy_rate_decrease_per_update) + energy_rate_decrease_per_update=energy_rate_decrease_per_update, + ) # Instances to update the Smart Meter's bids and offers across all market slots self.bid_update = TemplateStrategyBidUpdater( @@ -120,7 +122,8 @@ def __init__( fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_increase_per_update, update_interval=update_interval, - rate_limit_object=min) + rate_limit_object=min, + ) self.offer_update = TemplateStrategyOfferUpdater( initial_rate=initial_selling_rate, @@ -128,7 +131,8 @@ def __init__( fit_to_limit=fit_to_limit, energy_rate_change_per_update=energy_rate_decrease_per_update, update_interval=update_interval, - rate_limit_object=max) + rate_limit_object=max, + ) @property def state(self) -> SmartMeterState: @@ -150,15 +154,19 @@ def event_activate_price(self): initial_rate=self.bid_update.initial_rate_profile_buffer, final_rate=self.bid_update.final_rate_profile_buffer, energy_rate_change_per_update=( - self.bid_update.energy_rate_change_per_update_profile_buffer), - fit_to_limit=self.bid_update.fit_to_limit) + self.bid_update.energy_rate_change_per_update_profile_buffer + ), + fit_to_limit=self.bid_update.fit_to_limit, + ) self._validate_production_rates( initial_rate=self.offer_update.initial_rate_profile_buffer, final_rate=self.offer_update.final_rate_profile_buffer, energy_rate_change_per_update=( - self.offer_update.energy_rate_change_per_update_profile_buffer), - fit_to_limit=self.offer_update.fit_to_limit) + self.offer_update.energy_rate_change_per_update_profile_buffer + ), + fit_to_limit=self.offer_update.fit_to_limit, + ) def event_activate_energy(self): """Read the power profile and update the energy requirements for future market slots. @@ -167,8 +175,7 @@ def event_activate_energy(self): """ self._energy_params.activate(self.owner) time_slots = [m.time_slot for m in self.area.all_markets] - self._energy_params.set_energy_forecast_for_future_markets( - time_slots, reconfigure=True) + self._energy_params.set_energy_forecast_for_future_markets(time_slots, reconfigure=True) def event_market_cycle(self): """Prepare rates and execute bids/offers when a new market slot begins. @@ -179,8 +186,7 @@ def event_market_cycle(self): self._energy_params.read_and_rotate_profiles() self._reset_rates_and_update_prices() time_slots = [m.time_slot for m in self.area.all_markets] - self._energy_params.set_energy_forecast_for_future_markets( - time_slots, reconfigure=False) + self._energy_params.set_energy_forecast_for_future_markets(time_slots, reconfigure=False) self._set_energy_measurement_of_last_market() # Create bids/offers for the expected energy consumption/production in future markets for market in self.area.all_markets: @@ -241,7 +247,8 @@ def event_offer_traded(self, *, market_id, trade): else: self._assert_if_trade_offer_price_is_too_low(market_id, trade) self.state.decrement_available_energy( - trade.traded_energy, market.time_slot, self.owner.name) + trade.traded_energy, market.time_slot, self.owner.name + ) def event_bid_traded(self, *, market_id, bid_trade): """Register the bid traded by the device. Extends the superclass method. @@ -257,7 +264,8 @@ def event_bid_traded(self, *, market_id, bid_trade): self._energy_params.decrement_energy_requirement( energy_kWh=bid_trade.traded_energy, time_slot=market.time_slot, - area_name=self.owner.name) + area_name=self.owner.name, + ) def area_reconfigure_event(self, *args, **kwargs): """Reconfigure the device properties at runtime using the provided arguments. @@ -278,23 +286,28 @@ def _area_reconfigure_production_prices(self, **kwargs): initial_rate = ( read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs["initial_selling_rate"]) if kwargs.get("initial_selling_rate") is not None - else self.offer_update.initial_rate_profile_buffer) + else self.offer_update.initial_rate_profile_buffer + ) final_rate = ( read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs["final_selling_rate"]) if kwargs.get("final_selling_rate") is not None - else self.offer_update.final_rate_profile_buffer) + else self.offer_update.final_rate_profile_buffer + ) energy_rate_change_per_update = ( - read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["energy_rate_decrease_per_update"]) + read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["energy_rate_decrease_per_update"] + ) if kwargs.get("energy_rate_decrease_per_update") is not None - else self.offer_update.energy_rate_change_per_update_profile_buffer) + else self.offer_update.energy_rate_change_per_update_profile_buffer + ) fit_to_limit = ( kwargs["fit_to_limit"] if kwargs.get("fit_to_limit") is not None - else self.offer_update.fit_to_limit) + else self.offer_update.fit_to_limit + ) if kwargs.get("update_interval") is not None: if isinstance(kwargs["update_interval"], int): @@ -309,7 +322,8 @@ def _area_reconfigure_production_prices(self, **kwargs): try: self._validate_production_rates( - initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit) + initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ) except GSyException as ex: log.exception("SmartMeterStrategy._area_reconfigure_production_prices failed: %s", ex) return @@ -319,29 +333,35 @@ def _area_reconfigure_production_prices(self, **kwargs): final_rate=final_rate, energy_rate_change_per_update=energy_rate_change_per_update, fit_to_limit=fit_to_limit, - update_interval=update_interval) + update_interval=update_interval, + ) def _area_reconfigure_consumption_prices(self, **kwargs): initial_rate = ( read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs["initial_buying_rate"]) if kwargs.get("initial_buying_rate") is not None - else self.bid_update.initial_rate_profile_buffer) + else self.bid_update.initial_rate_profile_buffer + ) final_rate = ( read_arbitrary_profile(InputProfileTypes.IDENTITY, kwargs["final_buying_rate"]) if kwargs.get("final_buying_rate") is not None - else self.bid_update.final_rate_profile_buffer) + else self.bid_update.final_rate_profile_buffer + ) energy_rate_change_per_update = ( - read_arbitrary_profile(InputProfileTypes.IDENTITY, - kwargs["energy_rate_increase_per_update"]) + read_arbitrary_profile( + InputProfileTypes.IDENTITY, kwargs["energy_rate_increase_per_update"] + ) if kwargs.get("energy_rate_increase_per_update") is not None - else self.bid_update.energy_rate_change_per_update_profile_buffer) + else self.bid_update.energy_rate_change_per_update_profile_buffer + ) fit_to_limit = ( kwargs["fit_to_limit"] if kwargs.get("fit_to_limit") is not None - else self.bid_update.fit_to_limit) + else self.bid_update.fit_to_limit + ) if kwargs.get("update_interval") is not None: if isinstance(kwargs["update_interval"], int): @@ -356,7 +376,8 @@ def _area_reconfigure_consumption_prices(self, **kwargs): try: self._validate_consumption_rates( - initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit) + initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ) except GSyException as ex: log.exception(ex) return @@ -366,7 +387,8 @@ def _area_reconfigure_consumption_prices(self, **kwargs): final_rate=final_rate, energy_rate_change_per_update=energy_rate_change_per_update, fit_to_limit=fit_to_limit, - update_interval=update_interval) + update_interval=update_interval, + ) def _reset_rates_and_update_prices(self): """Set the initial/final rates and update the price of all bids/offers consequently.""" @@ -386,9 +408,11 @@ def _post_offer(self, market): offer = market.offer( offer_price, offer_energy_kWh, - TraderDetails(self.owner.name, self.owner.uuid, self.owner.name, - self.owner.uuid), - original_price=offer_price) + TraderDetails( + self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid + ), + original_price=offer_price, + ) self.offers.post(offer, market.id) except MarketException: pass @@ -404,8 +428,9 @@ def _post_first_bid(self, market): # self.balancing_energy_ratio.demand try: if not self.are_bids_posted(market.id): - self.post_first_bid(market, bid_energy, - self.bid_update.initial_rate[market.time_slot]) + self.post_first_bid( + market, bid_energy, self.bid_update.initial_rate[market.time_slot] + ) except MarketException: pass @@ -420,8 +445,10 @@ def _convert_update_interval_to_duration(update_interval): return None def _delete_past_state(self): - if (constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True or - self.area.current_market is None): + if ( + constants.RETAIN_PAST_MARKET_STRATEGIES_STATE is True + or self.area.current_market is None + ): return # Delete past energy requirements and availability @@ -431,39 +458,53 @@ def _delete_past_state(self): # Delete offer rates for previous market slots self.offer_update.delete_past_state_values(self.area.current_market.time_slot) # Delete the state of the current slot from the future market cache - self._future_market_strategy.delete_past_state_values( - self.area.current_market.time_slot) + self._future_market_strategy.delete_past_state_values(self.area.current_market.time_slot) def _validate_consumption_rates( - self, initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit): + self, initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ): for time_slot in initial_rate.keys(): - rate_change = None if fit_to_limit else get_from_profile_same_weekday_and_time( - energy_rate_change_per_update, time_slot) + rate_change = ( + None + if fit_to_limit + else get_from_profile_same_weekday_and_time( + energy_rate_change_per_update, time_slot + ) + ) self.validator.validate_rate( initial_buying_rate=initial_rate[time_slot], energy_rate_increase_per_update=rate_change, final_buying_rate=get_from_profile_same_weekday_and_time(final_rate, time_slot), - fit_to_limit=fit_to_limit) + fit_to_limit=fit_to_limit, + ) def _validate_production_rates( - self, initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit): + self, initial_rate, final_rate, energy_rate_change_per_update, fit_to_limit + ): for time_slot in initial_rate.keys(): - rate_change = None if fit_to_limit else get_from_profile_same_weekday_and_time( - energy_rate_change_per_update, time_slot) + rate_change = ( + None + if fit_to_limit + else get_from_profile_same_weekday_and_time( + energy_rate_change_per_update, time_slot + ) + ) self.validator.validate_rate( initial_selling_rate=initial_rate[time_slot], final_selling_rate=get_from_profile_same_weekday_and_time(final_rate, time_slot), energy_rate_decrease_per_update=rate_change, - fit_to_limit=fit_to_limit) + fit_to_limit=fit_to_limit, + ) def _offer_rate_can_be_accepted(self, offer: Offer, market_slot: MarketBase): """Check if the offer rate is less than what the device wants to pay.""" max_affordable_offer_rate = self.bid_update.get_updated_rate(market_slot.time_slot) return ( limit_float_precision(offer.energy_rate) - <= max_affordable_offer_rate + FLOATING_POINT_TOLERANCE) + <= max_affordable_offer_rate + FLOATING_POINT_TOLERANCE + ) def _event_tick_consumption(self): for market in self.area.all_markets: @@ -500,15 +541,20 @@ def _one_sided_market_event_tick(self, market, offer=None): if acceptable_offer and self._offer_rate_can_be_accepted(acceptable_offer, market): # If the device can still buy more energy energy_Wh = self.state.calculate_energy_to_accept( - acceptable_offer.energy * 1000.0, time_slot) - self.accept_offer(market, acceptable_offer, buyer=TraderDetails( - self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid - ), energy=energy_Wh / 1000.0) + acceptable_offer.energy * 1000.0, time_slot + ) + self.accept_offer( + market, + acceptable_offer, + buyer=TraderDetails( + self.owner.name, self.owner.uuid, self.owner.name, self.owner.uuid + ), + energy=energy_Wh / 1000.0, + ) self._energy_params.decrement_energy_requirement( - energy_kWh=energy_Wh / 1000, - time_slot=time_slot, - area_name=self.owner.name) + energy_kWh=energy_Wh / 1000, time_slot=time_slot, area_name=self.owner.name + ) except MarketException: self.log.exception("An Error occurred while buying an offer.") diff --git a/src/gsy_e/models/strategy/state/base_states.py b/src/gsy_e/models/strategy/state/base_states.py index 2381ed6ae..641039eb3 100644 --- a/src/gsy_e/models/strategy/state/base_states.py +++ b/src/gsy_e/models/strategy/state/base_states.py @@ -15,15 +15,16 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from abc import ABC, abstractmethod from math import copysign from typing import Dict, Optional -from gsy_framework.utils import ( - convert_pendulum_to_str_in_dict, convert_str_to_pendulum_in_dict) from pendulum import DateTime -from gsy_e.constants import FLOATING_POINT_TOLERANCE +from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE +from gsy_framework.utils import convert_pendulum_to_str_in_dict, convert_str_to_pendulum_in_dict + from gsy_e.gsy_e_core.util import is_time_slot_in_past_markets @@ -99,7 +100,8 @@ def __init__(self): # pylint: disable=unused-argument, no-self-use def _calculate_unsettled_energy_kWh( - self, measured_energy_kWh: float, time_slot: DateTime) -> float: + self, measured_energy_kWh: float, time_slot: DateTime + ) -> float: """ Calculates the unsettled energy (produced or consumed) in kWh. Args: @@ -123,9 +125,11 @@ def set_energy_measurement_kWh(self, energy_kWh: float, time_slot: DateTime) -> """ self._energy_measurement_kWh[time_slot] = energy_kWh self._forecast_measurement_deviation_kWh[time_slot] = self._calculate_unsettled_energy_kWh( - energy_kWh, time_slot) - self._unsettled_deviation_kWh[time_slot] = ( - abs(self._forecast_measurement_deviation_kWh[time_slot])) + energy_kWh, time_slot + ) + self._unsettled_deviation_kWh[time_slot] = abs( + self._forecast_measurement_deviation_kWh[time_slot] + ) def get_energy_measurement_kWh(self, time_slot: DateTime) -> float: """ @@ -186,8 +190,7 @@ def get_unsettled_deviation_kWh(self, time_slot: DateTime) -> float: """ return self._unsettled_deviation_kWh.get(time_slot) - def get_signed_unsettled_deviation_kWh( - self, time_slot: DateTime) -> Optional[float]: + def get_signed_unsettled_deviation_kWh(self, time_slot: DateTime) -> Optional[float]: """ Get the unsettled energy deviation of forecasted energy from measurement by the device in the given market slot including the correct sign that shows the direction @@ -205,7 +208,8 @@ def get_signed_unsettled_deviation_kWh( return None def decrement_unsettled_deviation( - self, purchased_energy_kWh: float, time_slot: DateTime) -> None: + self, purchased_energy_kWh: float, time_slot: DateTime + ) -> None: """ Decrease the device unsettled energy in a specific market slot. Args: @@ -218,7 +222,8 @@ def decrement_unsettled_deviation( self._unsettled_deviation_kWh[time_slot] -= purchased_energy_kWh assert self._unsettled_deviation_kWh[time_slot] >= -FLOATING_POINT_TOLERANCE, ( f"Unsettled energy deviation fell below zero " - f"({self._unsettled_deviation_kWh[time_slot]}).") + f"({self._unsettled_deviation_kWh[time_slot]})." + ) class ConsumptionState(ProsumptionInterface): @@ -237,7 +242,8 @@ def get_state(self) -> Dict: state = super().get_state() consumption_state = { "desired_energy_Wh": convert_pendulum_to_str_in_dict(self._desired_energy_Wh), - "total_energy_demanded_Wh": self._total_energy_demanded_Wh} + "total_energy_demanded_Wh": self._total_energy_demanded_Wh, + } # The inherited state should not have keys with the same name (to avoid overwriting them) conflicting_keys = state.keys() & consumption_state.keys() assert not conflicting_keys, f"Conflicting state values found for {conflicting_keys}." @@ -248,8 +254,9 @@ def get_state(self) -> Dict: def restore_state(self, state_dict: Dict): super().restore_state(state_dict) - self._desired_energy_Wh.update(convert_str_to_pendulum_in_dict( - state_dict["desired_energy_Wh"])) + self._desired_energy_Wh.update( + convert_str_to_pendulum_in_dict(state_dict["desired_energy_Wh"]) + ) self._total_energy_demanded_Wh = state_dict["total_energy_demanded_Wh"] def get_energy_requirement_Wh(self, time_slot: DateTime, default_value: float = 0.0) -> float: @@ -265,7 +272,7 @@ def set_desired_energy(self, energy: float, time_slot: DateTime, overwrite=False def update_total_demanded_energy(self, time_slot: DateTime) -> None: """Accumulate the _total_energy_demanded_Wh based on the desired energy per time_slot.""" - self._total_energy_demanded_Wh += self._desired_energy_Wh.get(time_slot, 0.) + self._total_energy_demanded_Wh += self._desired_energy_Wh.get(time_slot, 0.0) def can_buy_more_energy(self, time_slot: DateTime) -> bool: """Check whether the consumer can but more energy in the passed time_slot.""" @@ -282,12 +289,14 @@ def calculate_energy_to_accept(self, offer_energy_Wh: float, time_slot: DateTime return min(offer_energy_Wh, self._energy_requirement_Wh[time_slot]) def decrement_energy_requirement( - self, purchased_energy_Wh: float, time_slot: DateTime, area_name: str) -> None: + self, purchased_energy_Wh: float, time_slot: DateTime, area_name: str + ) -> None: """Decrease the energy required by the device in a specific market slot.""" self._energy_requirement_Wh[time_slot] -= purchased_energy_Wh assert self._energy_requirement_Wh[time_slot] >= -FLOATING_POINT_TOLERANCE, ( f"Energy requirement for device {area_name} fell below zero " - f"({self._energy_requirement_Wh[time_slot]}).") + f"({self._energy_requirement_Wh[time_slot]})." + ) def delete_past_state_values(self, current_time_slot: DateTime): """Delete data regarding energy consumption for past market slots.""" @@ -318,8 +327,10 @@ def get_state(self) -> Dict: state = super().get_state() production_state = { "available_energy_kWh": convert_pendulum_to_str_in_dict(self._available_energy_kWh), - "energy_production_forecast_kWh": - convert_pendulum_to_str_in_dict(self._energy_production_forecast_kWh)} + "energy_production_forecast_kWh": convert_pendulum_to_str_in_dict( + self._energy_production_forecast_kWh + ), + } # The inherited state should not have keys with the same name (to avoid overwriting them) conflicting_keys = state.keys() & production_state.keys() assert not conflicting_keys, f"Conflicting state values found for {conflicting_keys}." @@ -332,12 +343,15 @@ def restore_state(self, state_dict: Dict): super().restore_state(state_dict) self._available_energy_kWh.update( - convert_str_to_pendulum_in_dict(state_dict["available_energy_kWh"])) + convert_str_to_pendulum_in_dict(state_dict["available_energy_kWh"]) + ) self._energy_production_forecast_kWh.update( - convert_str_to_pendulum_in_dict(state_dict["energy_production_forecast_kWh"])) + convert_str_to_pendulum_in_dict(state_dict["energy_production_forecast_kWh"]) + ) - def set_available_energy(self, energy_kWh: float, time_slot: DateTime, - overwrite: bool = False) -> None: + def set_available_energy( + self, energy_kWh: float, time_slot: DateTime, overwrite: bool = False + ) -> None: """Set the available energy in the passed time_slot. If overwrite is True, set the available energy even if the time_slot is already tracked. @@ -356,13 +370,15 @@ def get_available_energy_kWh(self, time_slot: DateTime, default_value: float = 0 assert available_energy >= -FLOATING_POINT_TOLERANCE return available_energy - def decrement_available_energy(self, sold_energy_kWh: float, time_slot: DateTime, - area_name: str) -> None: + def decrement_available_energy( + self, sold_energy_kWh: float, time_slot: DateTime, area_name: str + ) -> None: """Decrement the available energy after a successful trade.""" self._available_energy_kWh[time_slot] -= sold_energy_kWh assert self._available_energy_kWh[time_slot] >= -FLOATING_POINT_TOLERANCE, ( f"Available energy for device {area_name} fell below zero " - f"({self._available_energy_kWh[time_slot]}).") + f"({self._available_energy_kWh[time_slot]})." + ) def delete_past_state_values(self, current_time_slot: DateTime): """Delete data regarding energy production for past market slots.""" diff --git a/src/gsy_e/models/strategy/state/storage_state.py b/src/gsy_e/models/strategy/state/storage_state.py index 7fda6546d..4d34a2ec2 100644 --- a/src/gsy_e/models/strategy/state/storage_state.py +++ b/src/gsy_e/models/strategy/state/storage_state.py @@ -22,7 +22,7 @@ from typing import Dict, List, Optional from dataclasses import dataclass -from gsy_framework.constants_limits import ConstSettings, GlobalConfig +from gsy_framework.constants_limits import ConstSettings, GlobalConfig, FLOATING_POINT_TOLERANCE from gsy_framework.utils import ( convert_pendulum_to_str_in_dict, convert_str_to_pendulum_in_dict, @@ -31,7 +31,6 @@ ) from pendulum import DateTime -from gsy_e.constants import FLOATING_POINT_TOLERANCE from gsy_e.gsy_e_core.util import is_time_slot_in_past_markets, write_default_to_dict from gsy_e.models.strategy.state.base_states import StateInterface diff --git a/src/gsy_e/models/strategy/storage.py b/src/gsy_e/models/strategy/storage.py index 841572f1d..64d4c9334 100644 --- a/src/gsy_e/models/strategy/storage.py +++ b/src/gsy_e/models/strategy/storage.py @@ -21,7 +21,7 @@ from logging import getLogger from typing import Union, Optional -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, FLOATING_POINT_TOLERANCE from gsy_framework.data_classes import TraderDetails from gsy_framework.enums import SpotMarketTypeEnum from gsy_framework.exceptions import GSyException @@ -33,7 +33,6 @@ from gsy_e import constants from gsy_e.gsy_e_core.device_registry import DeviceRegistry from gsy_e.gsy_e_core.exceptions import MarketException -from gsy_e.constants import FLOATING_POINT_TOLERANCE from gsy_e.models.base import AssetType from gsy_e.models.strategy.state import ESSEnergyOrigin, StorageState, StorageLosses from gsy_e.models.strategy import BidEnabledStrategy diff --git a/src/gsy_e/resources/create_profile.py b/src/gsy_e/resources/create_profile.py index cdf4f4dda..a63501847 100755 --- a/src/gsy_e/resources/create_profile.py +++ b/src/gsy_e/resources/create_profile.py @@ -5,18 +5,18 @@ ./create_profile.py -i ./LOAD_DATA_1.csv -o ./LOAD_DATA_1_5d.csv -d 5 """ -from pendulum import from_format, DateTime -import csv import argparse +import csv -from gsy_e.constants import TIME_FORMAT, DATE_TIME_FORMAT +from gsy_framework.constants_limits import TIME_FORMAT, DATE_TIME_FORMAT +from pendulum import from_format, DateTime def read_daily_profile_todict(daily_profile_fn, separator): outdict = {} header = [] firstline = True - with open(daily_profile_fn, 'r') as csv_file: + with open(daily_profile_fn, "r") as csv_file: file_reader = csv.reader(csv_file, delimiter=separator) for row in file_reader: if firstline: @@ -29,15 +29,16 @@ def read_daily_profile_todict(daily_profile_fn, separator): def write_profile_todict(profile_dict, header, outfile, separator): - with open(outfile, 'w') as csv_file: + with open(outfile, "w") as csv_file: writer = csv.writer(csv_file, delimiter=separator) writer.writerow(header) for time, value in profile_dict.items(): writer.writerow([time.format(DATE_TIME_FORMAT), value]) -def create_profile_from_daily_profile(n_days=365, year=2019, daily_profile_fn=None, out_fn=None, - separator=","): +def create_profile_from_daily_profile( + n_days=365, year=2019, daily_profile_fn=None, out_fn=None, separator="," +): if daily_profile_fn is None: raise ValueError("No daily_profile_fn was provided") @@ -45,8 +46,9 @@ def create_profile_from_daily_profile(n_days=365, year=2019, daily_profile_fn=No profile_dict = {} for day in range(1, n_days): for time, value in daily_profile_dict.items(): - profile_dict[DateTime(year, 1, 1).add( - days=day - 1, hours=time.hour, minutes=time.minute)] = value + profile_dict[ + DateTime(year, 1, 1).add(days=day - 1, hours=time.hour, minutes=time.minute) + ] = value if out_fn is None: out_fn = daily_profile_fn.replace(".csv", "_year.csv") @@ -54,14 +56,20 @@ def create_profile_from_daily_profile(n_days=365, year=2019, daily_profile_fn=No if __name__ == "__main__": - parser = argparse.ArgumentParser(description='Creates a Load/Generation profile for a set' - 'number of days from a daily profile') - parser.add_argument('-y', '--year', help='year for timestamp', type=int, default=2019) - parser.add_argument('-d', '--n-days', help='number of days', type=int, default=365) - parser.add_argument('-s', '--separator', help='separator of csv colums', type=str, default=";") - parser.add_argument('-i', '--input-file', help='daily profile file', type=str, required=True) - parser.add_argument('-o', '--output-file', help='output profile file', type=str) + parser = argparse.ArgumentParser( + description="Creates a Load/Generation profile for a set" + "number of days from a daily profile" + ) + parser.add_argument("-y", "--year", help="year for timestamp", type=int, default=2019) + parser.add_argument("-d", "--n-days", help="number of days", type=int, default=365) + parser.add_argument("-s", "--separator", help="separator of csv colums", type=str, default=";") + parser.add_argument("-i", "--input-file", help="daily profile file", type=str, required=True) + parser.add_argument("-o", "--output-file", help="output profile file", type=str) args = vars(parser.parse_args()) - create_profile_from_daily_profile(n_days=args["n_days"], year=args["year"], - daily_profile_fn=args["input_file"], - out_fn=args["output_file"], separator=args["separator"]) + create_profile_from_daily_profile( + n_days=args["n_days"], + year=args["year"], + daily_profile_fn=args["input_file"], + out_fn=args["output_file"], + separator=args["separator"], + ) diff --git a/tests/area/test_area.py b/tests/area/test_area.py index b0e861ce3..e69e34fe8 100644 --- a/tests/area/test_area.py +++ b/tests/area/test_area.py @@ -15,11 +15,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + # pylint: disable=missing-function-docstring, protected-access from unittest.mock import MagicMock, Mock, call, patch import pytest -from gsy_framework.constants_limits import ConstSettings, GlobalConfig +from gsy_framework.constants_limits import ConstSettings, GlobalConfig, TIME_ZONE from gsy_framework.enums import AvailableMarketTypes, BidOfferMatchAlgoEnum, SpotMarketTypeEnum from pendulum import duration, today @@ -43,7 +44,7 @@ def config_fixture(): config.slot_length = duration(minutes=15) config.tick_length = duration(seconds=15) config.ticks_per_slot = int(config.slot_length.seconds / config.tick_length.seconds) - config.start_date = today(tz=constants.TIME_ZONE) + config.start_date = today(tz=TIME_ZONE) config.sim_duration = duration(days=1) config.grid_fee_type = 1 config.end_date = config.start_date + config.sim_duration @@ -71,8 +72,9 @@ def teardown_method(): @staticmethod def test_respective_area_grid_fee_is_applied(config): config.grid_fee_type = 2 - area = Area(name="Street", children=[Area(name="House")], - grid_fee_percentage=5, config=config) + area = Area( + name="Street", children=[Area(name="House")], grid_fee_percentage=5, config=config + ) area.parent = Area(name="GRID", config=config) area.activate() assert area.spot_market.fee_class.grid_fee_rate == 0.05 @@ -82,46 +84,45 @@ def test_respective_area_grid_fee_is_applied(config): @staticmethod def test_delete_past_markets_instead_of_last(config): constants.RETAIN_PAST_MARKET_STRATEGIES_STATE = False - area = Area(name="Street", children=[Area(name="House")], - config=config, grid_fee_percentage=5) + area = Area( + name="Street", children=[Area(name="House")], config=config, grid_fee_percentage=5 + ) area.activate() area._bc = None area.cycle_markets(False, False, False) assert len(area.past_markets) == 0 - current_time = today(tz=constants.TIME_ZONE).add(minutes=config.slot_length.minutes) + current_time = today(tz=TIME_ZONE).add(minutes=config.slot_length.minutes) area._markets.rotate_markets(current_time) assert len(area.past_markets) == 1 - area._markets.create_new_spot_market( - current_time, AvailableMarketTypes.SPOT, area) - current_time = today(tz=constants.TIME_ZONE).add(minutes=2 * config.slot_length.minutes) + area._markets.create_new_spot_market(current_time, AvailableMarketTypes.SPOT, area) + current_time = today(tz=TIME_ZONE).add(minutes=2 * config.slot_length.minutes) area._markets.rotate_markets(current_time) assert len(area.past_markets) == 1 - assert (list(area.past_markets)[-1].time_slot == - today(tz=constants.TIME_ZONE).add(minutes=config.slot_length.minutes)) + assert list(area.past_markets)[-1].time_slot == today(tz=TIME_ZONE).add( + minutes=config.slot_length.minutes + ) @staticmethod def test_keep_past_markets(config): constants.RETAIN_PAST_MARKET_STRATEGIES_STATE = True - area = Area(name="Street", children=[Area(name="House")], - config=config, grid_fee_percentage=5) + area = Area( + name="Street", children=[Area(name="House")], config=config, grid_fee_percentage=5 + ) area.activate() area._bc = None area.cycle_markets(False, False, False) assert len(area.past_markets) == 0 - current_time = today(tz=constants.TIME_ZONE).add( - minutes=config.slot_length.total_minutes()) + current_time = today(tz=TIME_ZONE).add(minutes=config.slot_length.total_minutes()) area._markets.rotate_markets(current_time) assert len(area.past_markets) == 1 - area._markets.create_new_spot_market( - current_time, AvailableMarketTypes.SPOT, area) - current_time = today(tz=constants.TIME_ZONE).add( - minutes=2 * config.slot_length.total_minutes()) + area._markets.create_new_spot_market(current_time, AvailableMarketTypes.SPOT, area) + current_time = today(tz=TIME_ZONE).add(minutes=2 * config.slot_length.total_minutes()) area._markets.rotate_markets(current_time) assert len(area.past_markets) == 2 @@ -159,22 +160,26 @@ def test_get_state_returns_correct_values(): bat = Area(name="battery", strategy=StorageStrategy()) house = Area(name="House", children=[bat]) - assert house.get_state() == {"current_tick": 0, - "exported_energy": {}, - "imported_energy": {}, - "rate_stats_market": {}} - assert bat.get_state() == {"battery_energy_per_slot": 0.0, - "charge_history": {}, - "charge_history_kWh": {}, - "current_tick": 0, - "energy_to_buy_dict": {}, - "energy_to_sell_dict": {}, - "offered_buy_kWh": {}, - "offered_history": {}, - "offered_sell_kWh": {}, - "pledged_buy_kWh": {}, - "pledged_sell_kWh": {}, - "used_storage": 0.12} + assert house.get_state() == { + "current_tick": 0, + "exported_energy": {}, + "imported_energy": {}, + "rate_stats_market": {}, + } + assert bat.get_state() == { + "battery_energy_per_slot": 0.0, + "charge_history": {}, + "charge_history_kWh": {}, + "current_tick": 0, + "energy_to_buy_dict": {}, + "energy_to_sell_dict": {}, + "offered_buy_kWh": {}, + "offered_history": {}, + "offered_sell_kWh": {}, + "pledged_buy_kWh": {}, + "pledged_sell_kWh": {}, + "used_storage": 0.12, + } @staticmethod @patch("gsy_e.models.area.Area._consume_commands_from_aggregator", Mock()) @@ -267,9 +272,14 @@ def test_are_dispatches_other_events_only_if_connected_and_enabled(): assert area.dispatcher._should_dispatch_to_strategies(AreaEvent.MARKET_CYCLE) @staticmethod - @pytest.mark.parametrize("event_type, area_method", [ - (AreaEvent.MARKET_CYCLE, "cycle_markets"), (AreaEvent.ACTIVATE, "activate"), - (AreaEvent.TICK, "tick")]) + @pytest.mark.parametrize( + "event_type, area_method", + [ + (AreaEvent.MARKET_CYCLE, "cycle_markets"), + (AreaEvent.ACTIVATE, "activate"), + (AreaEvent.TICK, "tick"), + ], + ) def test_event_listener_calls_area_methods_for_area_events(event_type, area_method): function_mock = MagicMock(name=area_method) area = Area(name="test_area") @@ -278,14 +288,18 @@ def test_event_listener_calls_area_methods_for_area_events(event_type, area_meth assert function_mock.call_count == 1 @staticmethod - @pytest.mark.parametrize("event_type", [ - (MarketEvent.OFFER,), - (MarketEvent.BID,), - (MarketEvent.OFFER_TRADED,), - (MarketEvent.OFFER_SPLIT,), - (MarketEvent.BID_TRADED,), - (MarketEvent.BID_DELETED,), - (MarketEvent.OFFER_DELETED,)]) + @pytest.mark.parametrize( + "event_type", + [ + (MarketEvent.OFFER,), + (MarketEvent.BID,), + (MarketEvent.OFFER_TRADED,), + (MarketEvent.OFFER_SPLIT,), + (MarketEvent.BID_TRADED,), + (MarketEvent.BID_DELETED,), + (MarketEvent.OFFER_DELETED,), + ], + ) def test_event_listener_dispatches_to_strategy_if_enabled_connected(event_type, strategy_mock): area = strategy_mock area.events.is_enabled = True @@ -294,14 +308,18 @@ def test_event_listener_dispatches_to_strategy_if_enabled_connected(event_type, assert area.strategy.event_listener.call_count == 1 @staticmethod - @pytest.mark.parametrize("event_type", [ - (MarketEvent.OFFER,), - (MarketEvent.BID,), - (MarketEvent.OFFER_TRADED,), - (MarketEvent.OFFER_SPLIT,), - (MarketEvent.BID_TRADED,), - (MarketEvent.BID_DELETED,), - (MarketEvent.OFFER_DELETED,)]) + @pytest.mark.parametrize( + "event_type", + [ + (MarketEvent.OFFER,), + (MarketEvent.BID,), + (MarketEvent.OFFER_TRADED,), + (MarketEvent.OFFER_SPLIT,), + (MarketEvent.BID_TRADED,), + (MarketEvent.BID_DELETED,), + (MarketEvent.OFFER_DELETED,), + ], + ) def test_event_listener_doesnt_dispatch_to_strategy_if_not_enabled(event_type, strategy_mock): area = strategy_mock area.events.is_enabled = False @@ -310,16 +328,21 @@ def test_event_listener_doesnt_dispatch_to_strategy_if_not_enabled(event_type, s assert area.strategy.event_listener.call_count == 0 @staticmethod - @pytest.mark.parametrize("event_type", [ - (MarketEvent.OFFER,), - (MarketEvent.BID,), - (MarketEvent.OFFER_TRADED,), - (MarketEvent.OFFER_SPLIT,), - (MarketEvent.BID_TRADED,), - (MarketEvent.BID_DELETED,), - (MarketEvent.OFFER_DELETED,)]) + @pytest.mark.parametrize( + "event_type", + [ + (MarketEvent.OFFER,), + (MarketEvent.BID,), + (MarketEvent.OFFER_TRADED,), + (MarketEvent.OFFER_SPLIT,), + (MarketEvent.BID_TRADED,), + (MarketEvent.BID_DELETED,), + (MarketEvent.OFFER_DELETED,), + ], + ) def test_event_listener_doesnt_dispatch_to_strategy_if_not_connected( - event_type, strategy_mock): + event_type, strategy_mock + ): area = strategy_mock area.events.is_enabled = True area.events.is_connected = False @@ -337,14 +360,24 @@ def test_event_on_disabled_area_triggered_for_market_cycle_on_disabled_area(stra @staticmethod def test_duplicate_area_in_the_same_parent_append(): - area = Area(name="Street", children=[Area(name="House")], ) + area = Area( + name="Street", + children=[Area(name="House")], + ) with pytest.raises(Exception) as exception: - area.children.append(Area(name="House", children=[Area(name="House")], )) + area.children.append( + Area( + name="House", + children=[Area(name="House")], + ) + ) assert exception == "Area name should be unique inside the same Parent Area" @staticmethod def test_duplicate_area_in_the_same_parent_change_name(): - child = Area(name="Street", ) + child = Area( + name="Street", + ) with pytest.raises(Exception) as exception: child.name = "Street 2" assert exception == "Area name should be unique inside the same Parent Area" @@ -355,7 +388,10 @@ class TestFunctions: @staticmethod def test_check_area_name_exists_in_parent_area(): - area = Area(name="Street", children=[Area(name="House")], ) + area = Area( + name="Street", + children=[Area(name="House")], + ) assert check_area_name_exists_in_parent_area(area, "House") is True assert check_area_name_exists_in_parent_area(area, "House 2") is False diff --git a/tests/area/test_coefficient_area.py b/tests/area/test_coefficient_area.py index 8de718d0e..5a774f2c2 100644 --- a/tests/area/test_coefficient_area.py +++ b/tests/area/test_coefficient_area.py @@ -21,7 +21,7 @@ from unittest.mock import MagicMock import pytest -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, FLOATING_POINT_TOLERANCE, TIME_ZONE from gsy_framework.enums import ( SpotMarketTypeEnum, CoefficientAlgorithm, @@ -31,7 +31,6 @@ from pendulum import duration, today from pendulum import now -from gsy_e import constants from gsy_e.models.area import CoefficientArea, CoefficientAreaException from gsy_e.models.area.scm_manager import ( SCMManager, @@ -56,7 +55,7 @@ def config_fixture(): config.slot_length = duration(minutes=15) config.tick_length = duration(seconds=15) config.ticks_per_slot = int(config.slot_length.seconds / config.tick_length.seconds) - config.start_date = today(tz=constants.TIME_ZONE) + config.start_date = today(tz=TIME_ZONE) config.sim_duration = duration(days=1) return config @@ -309,9 +308,7 @@ def test_trigger_energy_trades(_create_2_house_grid, intracommunity_base_rate): assert isclose(scm._bills[house2.uuid].gsy_energy_bill, -0.02) assert isclose(scm._bills[house2.uuid].export_grid_fees, 0.0) - assert isclose( - scm._bills[house2.uuid].savings, 0.0, abs_tol=constants.FLOATING_POINT_TOLERANCE - ) + assert isclose(scm._bills[house2.uuid].savings, 0.0, abs_tol=FLOATING_POINT_TOLERANCE) assert isclose(scm._bills[house2.uuid].savings_percent, 0.0) assert len(scm._home_data[house1.uuid].trades) == 2 trades = scm._home_data[house1.uuid].trades diff --git a/tests/market/test_market.py b/tests/market/test_market.py index 44eef9661..2e6081870 100644 --- a/tests/market/test_market.py +++ b/tests/market/test_market.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import string from copy import deepcopy from unittest.mock import MagicMock @@ -22,7 +23,7 @@ import pytest from deepdiff import DeepDiff -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, TIME_ZONE from gsy_framework.data_classes import Bid, Offer, TraderDetails from gsy_framework.utils import datetime_to_string_incl_seconds from hypothesis import strategies as st @@ -30,13 +31,17 @@ from hypothesis.stateful import Bundle, RuleBasedStateMachine, precondition, rule from pendulum import now -from gsy_e.constants import TIME_ZONE from gsy_e.events.event_structures import MarketEvent from gsy_e.gsy_e_core.blockchain_interface import NonBlockchainInterface from gsy_e.gsy_e_core.device_registry import DeviceRegistry -from gsy_e.gsy_e_core.exceptions import (DeviceNotInRegistryError, InvalidBalancingTradeException, - NegativeEnergyOrderException, InvalidTrade, - MarketReadOnlyException, OfferNotFoundException) +from gsy_e.gsy_e_core.exceptions import ( + DeviceNotInRegistryError, + InvalidBalancingTradeException, + NegativeEnergyOrderException, + InvalidTrade, + MarketReadOnlyException, + OfferNotFoundException, +) from gsy_e.gsy_e_core.util import add_or_create_key, subtract_or_create_key from gsy_e.models.market.balancing import BalancingMarket from gsy_e.models.market.one_sided import OneSidedMarket @@ -74,11 +79,17 @@ def test_device_registry(market=BalancingMarket()): market.balancing_offer(10, 10, TraderDetails("noone", "")) -@pytest.mark.parametrize("market, offer", [ - (OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer"), - (BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "balancing_offer"), - (SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer"), -]) +@pytest.mark.parametrize( + "market, offer", + [ + (OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer"), + ( + BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "balancing_offer", + ), + (SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer"), + ], +) def test_market_offer(market, offer): ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = True e_offer = getattr(market, offer)(10, 20, TraderDetails("someone", "", "someone", "")) @@ -91,10 +102,13 @@ def test_market_offer(market, offer): assert e_offer.time_slot == market.time_slot -@pytest.mark.parametrize("market", [ - TwoSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()) -]) +@pytest.mark.parametrize( + "market", + [ + TwoSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + ], +) def test_market_bid(market): ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = True bid = market.bid(10, 20, buyer_details) @@ -112,49 +126,70 @@ def test_market_offer_invalid(market: OneSidedMarket): market.offer(10, -1, buyer_details) -@pytest.mark.parametrize("market, offer", [ - (TwoSidedMarket(), "offer"), - (BalancingMarket(), "balancing_offer"), - (SettlementMarket(), "offer") -]) +@pytest.mark.parametrize( + "market, offer", + [ + (TwoSidedMarket(), "offer"), + (BalancingMarket(), "balancing_offer"), + (SettlementMarket(), "offer"), + ], +) def test_market_offer_readonly(market, offer): market.readonly = True with pytest.raises(MarketReadOnlyException): getattr(market, offer)(10, 10, seller_details) -@pytest.mark.parametrize("market", - [OneSidedMarket(bc=MagicMock()), - BalancingMarket(bc=MagicMock()), - SettlementMarket(bc=MagicMock()) - ]) +@pytest.mark.parametrize( + "market", + [ + OneSidedMarket(bc=MagicMock()), + BalancingMarket(bc=MagicMock()), + SettlementMarket(bc=MagicMock()), + ], +) def test_market_offer_delete_missing(market): with pytest.raises(OfferNotFoundException): market.delete_offer("no such offer") -@pytest.mark.parametrize("market", - [OneSidedMarket(bc=MagicMock()), - BalancingMarket(bc=MagicMock()), - SettlementMarket(bc=MagicMock())]) +@pytest.mark.parametrize( + "market", + [ + OneSidedMarket(bc=MagicMock()), + BalancingMarket(bc=MagicMock()), + SettlementMarket(bc=MagicMock()), + ], +) def test_market_offer_delete_readonly(market): market.readonly = True with pytest.raises(MarketReadOnlyException): market.delete_offer("no such offer") -@pytest.mark.parametrize("market, offer, accept_offer", [ - (OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now(tz=TIME_ZONE)), - "offer", "accept_offer"), - (BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now(tz=TIME_ZONE)), - "balancing_offer", "accept_offer"), - (SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now(tz=TIME_ZONE)), - "offer", "accept_offer") -]) +@pytest.mark.parametrize( + "market, offer, accept_offer", + [ + ( + OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now(tz=TIME_ZONE)), + "offer", + "accept_offer", + ), + ( + BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now(tz=TIME_ZONE)), + "balancing_offer", + "accept_offer", + ), + ( + SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now(tz=TIME_ZONE)), + "offer", + "accept_offer", + ), + ], +) def test_market_trade(market, offer, accept_offer): e_offer = getattr(market, offer)(20, 10, seller_details) - trade = getattr(market, accept_offer)(offer_or_id=e_offer, buyer=buyer_details, - energy=10) + trade = getattr(market, accept_offer)(offer_or_id=e_offer, buyer=buyer_details, energy=10) assert trade assert trade == market.trades[0] assert trade.id @@ -169,44 +204,56 @@ def test_orders_per_slot(market): """Test whether the orders_per_slot method returns order in format format.""" creation_time = now() market.bids = {"bid1": Bid("bid1", creation_time, 10, 10, TraderDetails("buyer", ""))} - market.offers = {"offer1": Offer( - "offer1", creation_time, 10, 10, TraderDetails("seller", ""))} - order_dict_diff = DeepDiff(market.orders_per_slot(), { - market.time_slot_str: {"bids": [{"buyer": { - "name": "buyer", - "uuid": "", - "origin": None, - "origin_uuid": None, - }, - "energy": 10, - "price": 10, - "energy_rate": 1.0, - "id": "bid1", - "original_price": 10, - "time_slot": "", - "creation_time": datetime_to_string_incl_seconds( - creation_time), - "type": "Bid"}], - "offers": [{"energy": 10, - "price": 10, - "energy_rate": 1.0, - "id": "offer1", - "original_price": 10, - "seller": { - "name": "seller", - "uuid": "", - "origin": None, - "origin_uuid": None, - }, - "time_slot": "", - "creation_time": datetime_to_string_incl_seconds( - creation_time), - "type": "Offer"}]}}) + market.offers = {"offer1": Offer("offer1", creation_time, 10, 10, TraderDetails("seller", ""))} + order_dict_diff = DeepDiff( + market.orders_per_slot(), + { + market.time_slot_str: { + "bids": [ + { + "buyer": { + "name": "buyer", + "uuid": "", + "origin": None, + "origin_uuid": None, + }, + "energy": 10, + "price": 10, + "energy_rate": 1.0, + "id": "bid1", + "original_price": 10, + "time_slot": "", + "creation_time": datetime_to_string_incl_seconds(creation_time), + "type": "Bid", + } + ], + "offers": [ + { + "energy": 10, + "price": 10, + "energy_rate": 1.0, + "id": "offer1", + "original_price": 10, + "seller": { + "name": "seller", + "uuid": "", + "origin": None, + "origin_uuid": None, + }, + "time_slot": "", + "creation_time": datetime_to_string_incl_seconds(creation_time), + "type": "Offer", + } + ], + } + }, + ) assert len(order_dict_diff) == 0 -def test_balancing_market_negative_offer_trade(market=BalancingMarket( - bc=NonBlockchainInterface(str(uuid4())))): # NOQA +def test_balancing_market_negative_offer_trade( + market=BalancingMarket(bc=NonBlockchainInterface(str(uuid4()))), +): # NOQA offer = market.balancing_offer(20, -10, seller_details) trade = market.accept_offer(offer, buyer_details, energy=-10) assert trade @@ -219,28 +266,40 @@ def test_balancing_market_negative_offer_trade(market=BalancingMarket( assert trade.buyer.name == buyer_details.name -@pytest.mark.parametrize("market, offer, accept_offer", [ - (OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - "offer", "accept_offer"), - (BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - "balancing_offer", "accept_offer"), - (SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - "offer", "accept_offer") -]) +@pytest.mark.parametrize( + "market, offer, accept_offer", + [ + ( + OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "offer", + "accept_offer", + ), + ( + BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "balancing_offer", + "accept_offer", + ), + ( + SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "offer", + "accept_offer", + ), + ], +) def test_market_trade_by_id(market, offer, accept_offer): e_offer = getattr(market, offer)(20, 10, seller_details) trade = getattr(market, accept_offer)(offer_or_id=e_offer.id, buyer=buyer_details, energy=10) assert trade -@pytest.mark.parametrize("market, offer, accept_offer", [ - (OneSidedMarket(bc=MagicMock(), time_slot=now()), - "offer", "accept_offer"), - (BalancingMarket(bc=MagicMock(), time_slot=now()), - "balancing_offer", "accept_offer"), - (SettlementMarket(bc=MagicMock(), time_slot=now()), - "offer", "accept_offer") -]) +@pytest.mark.parametrize( + "market, offer, accept_offer", + [ + (OneSidedMarket(bc=MagicMock(), time_slot=now()), "offer", "accept_offer"), + (BalancingMarket(bc=MagicMock(), time_slot=now()), "balancing_offer", "accept_offer"), + (SettlementMarket(bc=MagicMock(), time_slot=now()), "offer", "accept_offer"), + ], +) def test_market_trade_readonly(market, offer, accept_offer): e_offer = getattr(market, offer)(20, 10, seller_details) market.readonly = True @@ -248,14 +307,26 @@ def test_market_trade_readonly(market, offer, accept_offer): getattr(market, accept_offer)(e_offer, buyer_details) -@pytest.mark.parametrize("market, offer, accept_offer", [ - (OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - "offer", "accept_offer"), - (BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - "balancing_offer", "accept_offer"), - (SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - "offer", "accept_offer") -]) +@pytest.mark.parametrize( + "market, offer, accept_offer", + [ + ( + OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "offer", + "accept_offer", + ), + ( + BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "balancing_offer", + "accept_offer", + ), + ( + SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "offer", + "accept_offer", + ), + ], +) def test_market_trade_not_found(market, offer, accept_offer): e_offer = getattr(market, offer)(20, 10, seller_details) @@ -264,14 +335,26 @@ def test_market_trade_not_found(market, offer, accept_offer): getattr(market, accept_offer)(offer_or_id=e_offer, buyer=buyer_details, energy=10) -@pytest.mark.parametrize("market, offer, accept_offer", [ - (OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - "offer", "accept_offer"), - (BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - "balancing_offer", "accept_offer"), - (SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - "offer", "accept_offer") -]) +@pytest.mark.parametrize( + "market, offer, accept_offer", + [ + ( + OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "offer", + "accept_offer", + ), + ( + BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "balancing_offer", + "accept_offer", + ), + ( + SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "offer", + "accept_offer", + ), + ], +) def test_market_trade_partial(market, offer, accept_offer): e_offer = getattr(market, offer)(20, 20, seller_details) @@ -294,31 +377,62 @@ def test_market_trade_partial(market, offer, accept_offer): assert new_offer.id != e_offer.id -@pytest.mark.parametrize("market, offer, accept_offer, energy, exception", [ - (OneSidedMarket(bc=MagicMock(), time_slot=now()), - "offer", "accept_offer", 0, InvalidTrade), - (OneSidedMarket(bc=MagicMock(), time_slot=now()), - "offer", "accept_offer", 21, InvalidTrade), - (BalancingMarket(bc=MagicMock(), time_slot=now()), - "balancing_offer", "accept_offer", 0, - InvalidBalancingTradeException), - (BalancingMarket(bc=MagicMock(), time_slot=now()), - "balancing_offer", "accept_offer", 21, - InvalidBalancingTradeException), - (SettlementMarket(bc=MagicMock(), time_slot=now()), - "offer", "accept_offer", 0, InvalidTrade), - (SettlementMarket(bc=MagicMock(), time_slot=now()), - "offer", "accept_offer", 21, InvalidTrade), -]) +@pytest.mark.parametrize( + "market, offer, accept_offer, energy, exception", + [ + ( + OneSidedMarket(bc=MagicMock(), time_slot=now()), + "offer", + "accept_offer", + 0, + InvalidTrade, + ), + ( + OneSidedMarket(bc=MagicMock(), time_slot=now()), + "offer", + "accept_offer", + 21, + InvalidTrade, + ), + ( + BalancingMarket(bc=MagicMock(), time_slot=now()), + "balancing_offer", + "accept_offer", + 0, + InvalidBalancingTradeException, + ), + ( + BalancingMarket(bc=MagicMock(), time_slot=now()), + "balancing_offer", + "accept_offer", + 21, + InvalidBalancingTradeException, + ), + ( + SettlementMarket(bc=MagicMock(), time_slot=now()), + "offer", + "accept_offer", + 0, + InvalidTrade, + ), + ( + SettlementMarket(bc=MagicMock(), time_slot=now()), + "offer", + "accept_offer", + 21, + InvalidTrade, + ), + ], +) def test_market_trade_partial_invalid(market, offer, accept_offer, energy, exception): e_offer = getattr(market, offer)(20, 20, seller_details) with pytest.raises(exception): - getattr(market, accept_offer)( - offer_or_id=e_offer, buyer=buyer_details.name, energy=energy) + getattr(market, accept_offer)(offer_or_id=e_offer, buyer=buyer_details.name, energy=energy) -def test_market_acct_simple(market=OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), - time_slot=now())): +def test_market_acct_simple( + market=OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()) +): offer = market.offer(20, 10, TraderDetails("A", "", "A", "")) market.accept_offer(offer, TraderDetails("B", "", "B", "")) @@ -330,8 +444,9 @@ def test_market_acct_simple(market=OneSidedMarket(bc=NonBlockchainInterface(str( assert market.sold_energy("B") == 0 -def test_market_acct_multiple(market=OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), - time_slot=now())): +def test_market_acct_multiple( + market=OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()) +): offer1 = market.offer(10, 20, TraderDetails("A", "", "A", "")) offer2 = market.offer(10, 10, TraderDetails("A", "", "A", "")) market.accept_offer(offer1, TraderDetails("B", "", "B", "")) @@ -346,11 +461,17 @@ def test_market_acct_multiple(market=OneSidedMarket(bc=NonBlockchainInterface(st assert market.bought_energy("C") == offer2.energy == 10 -@pytest.mark.parametrize("market, offer", [ - (OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer"), - (BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "balancing_offer"), - (SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer") -]) +@pytest.mark.parametrize( + "market, offer", + [ + (OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer"), + ( + BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "balancing_offer", + ), + (SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer"), + ], +) def test_market_sorted_offers(market, offer): getattr(market, offer)(5, 1, seller_details) getattr(market, offer)(3, 1, seller_details) @@ -361,11 +482,17 @@ def test_market_sorted_offers(market, offer): assert [o.price for o in market.sorted_offers] == [1, 2, 3, 4, 5] -@pytest.mark.parametrize("market, offer", [ - (OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer"), - (BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "balancing_offer"), - (SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer") -]) +@pytest.mark.parametrize( + "market, offer", + [ + (OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer"), + ( + BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "balancing_offer", + ), + (SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer"), + ], +) def test_market_most_affordable_offers(market, offer): getattr(market, offer)(5, 1, seller_details) getattr(market, offer)(3, 1, seller_details) @@ -379,22 +506,24 @@ def test_market_most_affordable_offers(market, offer): assert {o.price for o in market.most_affordable_offers} == {1, 10, 20, 20000} -@pytest.mark.parametrize("market, offer", [ - (OneSidedMarket, "offer"), - (BalancingMarket, "balancing_offer"), - (SettlementMarket, "offer") -]) +@pytest.mark.parametrize( + "market, offer", + [(OneSidedMarket, "offer"), (BalancingMarket, "balancing_offer"), (SettlementMarket, "offer")], +) def test_market_listeners_init(market, offer, called): markt = market(bc=MagicMock(), time_slot=now(), notification_listener=called) getattr(markt, offer)(10, 20, seller_details) assert len(called.calls) == 1 -@pytest.mark.parametrize("market, offer, add_listener", [ - (OneSidedMarket(bc=MagicMock(), time_slot=now()), "offer", "add_listener"), - (BalancingMarket(bc=MagicMock(), time_slot=now()), "balancing_offer", "add_listener"), - (SettlementMarket(bc=MagicMock(), time_slot=now()), "offer", "add_listener") -]) +@pytest.mark.parametrize( + "market, offer, add_listener", + [ + (OneSidedMarket(bc=MagicMock(), time_slot=now()), "offer", "add_listener"), + (BalancingMarket(bc=MagicMock(), time_slot=now()), "balancing_offer", "add_listener"), + (SettlementMarket(bc=MagicMock(), time_slot=now()), "offer", "add_listener"), + ], +) def test_market_listeners_add(market, offer, add_listener, called): getattr(market, add_listener)(called) getattr(market, offer)(10, 20, seller_details) @@ -402,14 +531,29 @@ def test_market_listeners_add(market, offer, add_listener, called): assert len(called.calls) == 1 -@pytest.mark.parametrize("market, offer, add_listener, event", [ - (OneSidedMarket(bc=MagicMock(), time_slot=now()), - "offer", "add_listener", MarketEvent.OFFER), - (BalancingMarket(bc=MagicMock(), time_slot=now()), - "balancing_offer", "add_listener", MarketEvent.BALANCING_OFFER), - (SettlementMarket(bc=MagicMock(), time_slot=now()), - "offer", "add_listener", MarketEvent.OFFER), -]) +@pytest.mark.parametrize( + "market, offer, add_listener, event", + [ + ( + OneSidedMarket(bc=MagicMock(), time_slot=now()), + "offer", + "add_listener", + MarketEvent.OFFER, + ), + ( + BalancingMarket(bc=MagicMock(), time_slot=now()), + "balancing_offer", + "add_listener", + MarketEvent.BALANCING_OFFER, + ), + ( + SettlementMarket(bc=MagicMock(), time_slot=now()), + "offer", + "add_listener", + MarketEvent.OFFER, + ), + ], +) def test_market_listeners_offer(market, offer, add_listener, event, called): getattr(market, add_listener)(called) e_offer = getattr(market, offer)(10, 20, seller_details) @@ -418,22 +562,37 @@ def test_market_listeners_offer(market, offer, add_listener, event, called): assert called.calls[0][1] == {"offer": repr(e_offer), "market_id": repr(market.id)} -@pytest.mark.parametrize("market, offer, accept_offer, add_listener, event", [ - (OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - "offer", "accept_offer", "add_listener", - MarketEvent.OFFER_SPLIT), - (BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - "balancing_offer", "accept_offer", "add_listener", - MarketEvent.BALANCING_OFFER_SPLIT), - (SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), - "offer", "accept_offer", "add_listener", - MarketEvent.OFFER_SPLIT), -]) +@pytest.mark.parametrize( + "market, offer, accept_offer, add_listener, event", + [ + ( + OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "offer", + "accept_offer", + "add_listener", + MarketEvent.OFFER_SPLIT, + ), + ( + BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "balancing_offer", + "accept_offer", + "add_listener", + MarketEvent.BALANCING_OFFER_SPLIT, + ), + ( + SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "offer", + "accept_offer", + "add_listener", + MarketEvent.OFFER_SPLIT, + ), + ], +) def test_market_listeners_offer_split(market, offer, accept_offer, add_listener, event, called): # pylint: disable=too-many-arguments getattr(market, add_listener)(called) - e_offer = getattr(market, offer)(10., 20, seller_details) - getattr(market, accept_offer)(e_offer, buyer_details, energy=3.) + e_offer = getattr(market, offer)(10.0, 20, seller_details) + getattr(market, accept_offer)(e_offer, buyer_details, energy=3.0) assert len(called.calls) == 3 assert called.calls[1][0] == (repr(event),) call_kwargs = called.calls[1][1] @@ -444,21 +603,36 @@ def test_market_listeners_offer_split(market, offer, accept_offer, add_listener, assert call_kwargs == { "original_offer": repr(e_offer), "accepted_offer": repr(a_offer), - "residual_offer": repr(list(market.offers.values())[0]) + "residual_offer": repr(list(market.offers.values())[0]), } -@pytest.mark.parametrize("market, offer, delete_offer, add_listener, event", [ - (OneSidedMarket(bc=MagicMock(), time_slot=now()), - "offer", "delete_offer", - "add_listener", MarketEvent.OFFER_DELETED), - (BalancingMarket(bc=MagicMock(), time_slot=now()), - "balancing_offer", "delete_balancing_offer", - "add_listener", MarketEvent.BALANCING_OFFER_DELETED), - (SettlementMarket(bc=MagicMock(), time_slot=now()), - "offer", "delete_offer", - "add_listener", MarketEvent.OFFER_DELETED), -]) +@pytest.mark.parametrize( + "market, offer, delete_offer, add_listener, event", + [ + ( + OneSidedMarket(bc=MagicMock(), time_slot=now()), + "offer", + "delete_offer", + "add_listener", + MarketEvent.OFFER_DELETED, + ), + ( + BalancingMarket(bc=MagicMock(), time_slot=now()), + "balancing_offer", + "delete_balancing_offer", + "add_listener", + MarketEvent.BALANCING_OFFER_DELETED, + ), + ( + SettlementMarket(bc=MagicMock(), time_slot=now()), + "offer", + "delete_offer", + "add_listener", + MarketEvent.OFFER_DELETED, + ), + ], +) def test_market_listeners_offer_deleted(market, offer, delete_offer, add_listener, event, called): # pylint: disable=too-many-arguments getattr(market, add_listener)(called) @@ -470,14 +644,7 @@ def test_market_listeners_offer_deleted(market, offer, delete_offer, add_listene assert called.calls[1][1] == {"offer": repr(e_offer), "market_id": repr(market.id)} -@pytest.mark.parametrize( - ("last_offer_size", "traded_energy"), - ( - (20, 10), - (30, 0), - (40, -10) - ) -) +@pytest.mark.parametrize(("last_offer_size", "traded_energy"), ((20, 10), (30, 0), (40, -10))) def test_market_issuance_acct_reverse(last_offer_size, traded_energy): market = OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()) offer1 = market.offer(10, 20, TraderDetails("A", "", "A", "")) @@ -490,25 +657,40 @@ def test_market_issuance_acct_reverse(last_offer_size, traded_energy): assert market.traded_energy["A"] == traded_energy -@pytest.mark.parametrize("market, offer, accept_offer", [ - (OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer", - "accept_offer"), - (BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "balancing_offer", - "accept_offer"), - (SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), "offer", - "accept_offer") -]) +@pytest.mark.parametrize( + "market, offer, accept_offer", + [ + ( + OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "offer", + "accept_offer", + ), + ( + BalancingMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "balancing_offer", + "accept_offer", + ), + ( + SettlementMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()), + "offer", + "accept_offer", + ), + ], +) def test_market_accept_offer_yields_partial_trade(market, offer, accept_offer): """Test market accept offer returns partial trade.""" e_offer = getattr(market, offer)(2.0, 4, seller_details) trade = getattr(market, accept_offer)(e_offer, buyer_details, energy=1) - assert (trade.match_details["offer"].id == e_offer.id - and trade.traded_energy == 1 - and trade.residual.energy == 3) + assert ( + trade.match_details["offer"].id == e_offer.id + and trade.traded_energy == 1 + and trade.residual.energy == 3 + ) class MarketStateMachine(RuleBasedStateMachine): """State machine for Market""" + # pylint: disable=missing-function-docstring offers = Bundle("Offers") actors = Bundle("Actors") @@ -517,14 +699,20 @@ def __init__(self): self.market = OneSidedMarket(bc=NonBlockchainInterface(str(uuid4())), time_slot=now()) super().__init__() - @rule(target=actors, actor=st.text(min_size=1, max_size=3, - alphabet=string.ascii_letters + string.digits)) + @rule( + target=actors, + actor=st.text(min_size=1, max_size=3, alphabet=string.ascii_letters + string.digits), + ) def new_actor(self, actor): # pylint: disable=no-self-use return actor - @rule(target=offers, seller=actors, energy=st.integers(min_value=1), - price=st.integers(min_value=0)) + @rule( + target=offers, + seller=actors, + energy=st.integers(min_value=1), + price=st.integers(min_value=0), + ) def offer(self, seller, energy, price): return self.market.offer(price, energy, TraderDetails(seller, "")) diff --git a/tests/strategies/energy_parameters/test_heat_pump.py b/tests/strategies/energy_parameters/test_heat_pump.py index 73f587ddf..2d0169678 100644 --- a/tests/strategies/energy_parameters/test_heat_pump.py +++ b/tests/strategies/energy_parameters/test_heat_pump.py @@ -11,6 +11,7 @@ HeatPumpEnergyParameters, TankParameters, ) +from gsy_e.models.strategy.energy_parameters.heatpump.cop_models import COPModelType CURRENT_MARKET_SLOT = today(tz=TIME_ZONE) @@ -49,6 +50,41 @@ def fixture_heatpump_energy_params() -> HeatPumpEnergyParameters: GlobalConfig.slot_length = original_slot_length +@pytest.fixture(name="energy_params_heat_profile") +def fixture_heatpump_energy_params_heat_profile() -> HeatPumpEnergyParameters: + original_start_date = GlobalConfig.start_date + original_sim_duration = GlobalConfig.sim_duration + original_slot_length = GlobalConfig.slot_length + GlobalConfig.start_date = CURRENT_MARKET_SLOT + GlobalConfig.sim_duration = duration(days=1) + GlobalConfig.slot_length = duration(minutes=60) + + source_temp_profile = { + timestamp: 12 for timestamp in generate_market_slot_list(CURRENT_MARKET_SLOT) + } + heat_demand_profile = { + timestamp: 9000000 for timestamp in generate_market_slot_list(CURRENT_MARKET_SLOT) + } + energy_params = HeatPumpEnergyParameters( + maximum_power_rating_kW=30, + tank_parameters=[ + TankParameters( + min_temp_C=10, + max_temp_C=60, + initial_temp_C=45, + tank_volume_L=500, + ) + ], + source_temp_C_profile=source_temp_profile, + heat_demand_Q_profile=heat_demand_profile, + cop_model_type=COPModelType.HOVAL_ULTRASOURCE_B_COMFORT_C11, + ) + yield energy_params + GlobalConfig.start_date = original_start_date + GlobalConfig.sim_duration = original_sim_duration + GlobalConfig.slot_length = original_slot_length + + class TestHeatPumpEnergyParameters: @staticmethod @@ -142,3 +178,13 @@ def test_if_profiles_are_rotated_on_market_cycle(energy_params): energy_params._consumption_kWh.read_or_rotate_profiles.assert_called_once() energy_params._source_temp_C.read_or_rotate_profiles.assert_called_once() energy_params._populate_state.assert_called_once() + + @staticmethod + def test_cop_model_is_correctly_selected(energy_params_heat_profile): + energy_params_heat_profile._source_temp_C.read_or_rotate_profiles = Mock() + energy_params_heat_profile.event_market_cycle(CURRENT_MARKET_SLOT) + assert isclose( + energy_params_heat_profile._state.heatpump.get_cop(CURRENT_MARKET_SLOT), + 4.8941, + abs_tol=0.001, + ) diff --git a/tests/strategies/energy_parameters/test_tank_energy_parameters.py b/tests/strategies/energy_parameters/test_tank_energy_parameters.py index 43b1a3aef..c970ea8a7 100644 --- a/tests/strategies/energy_parameters/test_tank_energy_parameters.py +++ b/tests/strategies/energy_parameters/test_tank_energy_parameters.py @@ -28,59 +28,59 @@ def setup_method(self): self._datetime = datetime(2023, 1, 1, 0, 0) def test_increase_tanks_temp_from_heat_energy(self): - self._tanks.increase_tanks_temp_from_heat_energy(1, self._datetime) + self._tanks.increase_tanks_temp_from_heat_energy(5000, self._datetime) energy_params = self._tanks._tanks_energy_parameters assert isclose( - energy_params[0]._state._temp_increase_K[self._datetime], 0.5747, rel_tol=0.0001 + energy_params[0]._state._temp_increase_K[self._datetime], 0.7982, rel_tol=0.0001 ) assert isclose( - energy_params[1]._state._temp_increase_K[self._datetime], 0.3592, rel_tol=0.0001 + energy_params[1]._state._temp_increase_K[self._datetime], 0.49888, rel_tol=0.0001 ) assert isclose( - energy_params[2]._state._temp_increase_K[self._datetime], 0.28735, rel_tol=0.0001 + energy_params[2]._state._temp_increase_K[self._datetime], 0.399106, rel_tol=0.0001 ) def test_decrease_tanks_temp_from_heat_energy(self): - self._tanks.decrease_tanks_temp_from_heat_energy(1, self._datetime) + self._tanks.decrease_tanks_temp_from_heat_energy(5000, self._datetime) energy_params = self._tanks._tanks_energy_parameters assert isclose( - energy_params[0]._state._temp_decrease_K[self._datetime], 0.5747, rel_tol=0.0001 + energy_params[0]._state._temp_decrease_K[self._datetime], 0.7982, rel_tol=0.0001 ) assert isclose( - energy_params[1]._state._temp_decrease_K[self._datetime], 0.3592, rel_tol=0.0001 + energy_params[1]._state._temp_decrease_K[self._datetime], 0.49888, rel_tol=0.0001 ) assert isclose( - energy_params[2]._state._temp_decrease_K[self._datetime], 0.28735, rel_tol=0.0001 + energy_params[2]._state._temp_decrease_K[self._datetime], 0.399106, rel_tol=0.0001 ) def test_update_tanks_temperature(self): - self._tanks.increase_tanks_temp_from_heat_energy(2, self._datetime) - self._tanks.decrease_tanks_temp_from_heat_energy(1, self._datetime) + self._tanks.increase_tanks_temp_from_heat_energy(2000, self._datetime) + self._tanks.decrease_tanks_temp_from_heat_energy(1000, self._datetime) self._tanks.update_tanks_temperature(self._datetime) energy_params = self._tanks._tanks_energy_parameters assert isclose( - energy_params[0]._state._temp_decrease_K[self._datetime], 0.5747, rel_tol=0.0001 + energy_params[0]._state._temp_decrease_K[self._datetime], 0.159642, rel_tol=0.0001 ) assert isclose( - energy_params[1]._state._temp_decrease_K[self._datetime], 0.3592, rel_tol=0.0001 + energy_params[1]._state._temp_decrease_K[self._datetime], 0.09977, rel_tol=0.0001 ) assert isclose( - energy_params[2]._state._temp_decrease_K[self._datetime], 0.28735, rel_tol=0.0001 + energy_params[2]._state._temp_decrease_K[self._datetime], 0.079821, rel_tol=0.0001 ) @pytest.mark.parametrize("cop", (1, 4, 10, 0.5, 12)) def test_get_max_energy_consumption(self, cop): - energy_decrease = 1 - self._tanks.decrease_tanks_temp_from_heat_energy(energy_decrease, self._datetime) + energy_decrease_kJ = 1000 + self._tanks.decrease_tanks_temp_from_heat_energy(energy_decrease_kJ, self._datetime) max_energy_consumption = self._tanks.get_max_energy_consumption(cop, self._datetime) - assert isclose(max_energy_consumption, (33.06 + energy_decrease) / cop, rel_tol=0.0001) + assert isclose(max_energy_consumption, 33.3377 / cop, rel_tol=0.0001) @pytest.mark.parametrize("cop", (1, 4, 10, 0.5, 12)) def test_get_min_energy_consumption(self, cop): - self._tanks.decrease_tanks_temp_from_heat_energy(2, self._datetime) + self._tanks.decrease_tanks_temp_from_heat_energy(2000, self._datetime) min_energy_consumption = self._tanks.get_min_energy_consumption(cop, self._datetime) - assert isclose(min_energy_consumption, 2.0 / cop, rel_tol=0.0001) + assert isclose(min_energy_consumption, 0.55555 / cop, rel_tol=0.0001) def test_get_average_tank_temperature(self): self._tanks._tanks_energy_parameters[0]._state._storage_temp_C[self._datetime] = 30 @@ -89,9 +89,9 @@ def test_get_average_tank_temperature(self): assert self._tanks.get_average_tank_temperature(self._datetime) == 35 def test_get_unmatched_demand_kWh(self): - self._tanks.decrease_tanks_temp_from_heat_energy(10, self._datetime) - self._tanks.increase_tanks_temp_from_heat_energy(1, self._datetime) - assert self._tanks.get_unmatched_demand_kWh(self._datetime) == 9 + self._tanks.decrease_tanks_temp_from_heat_energy(10000, self._datetime) + self._tanks.increase_tanks_temp_from_heat_energy(1000, self._datetime) + assert self._tanks.get_unmatched_demand_kWh(self._datetime) == 2.5 def test_serialize(self): tanks_dict = self._tanks.serialize() diff --git a/tests/strategies/external/test_init.py b/tests/strategies/external/test_init.py index dbfed3950..3750be20b 100644 --- a/tests/strategies/external/test_init.py +++ b/tests/strategies/external/test_init.py @@ -23,7 +23,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest -from gsy_framework.constants_limits import ConstSettings, GlobalConfig +from gsy_framework.constants_limits import ConstSettings, GlobalConfig, DATE_TIME_FORMAT from gsy_framework.data_classes import Bid, Offer, Trade, TraderDetails from gsy_framework.utils import format_datetime from parameterized import parameterized @@ -31,7 +31,6 @@ from redis.exceptions import RedisError import gsy_e.constants -import gsy_e.gsy_e_core.util from gsy_e.gsy_e_core.global_objects_singleton import global_objects from gsy_e.models.area import Area, CoefficientArea from gsy_e.models.strategy import BidEnabledStrategy @@ -121,7 +120,7 @@ def teardown_method() -> None: def test_dispatch_tick_frequency_gets_calculated_correctly(self): self.external_strategy = LoadHoursExternalStrategy(100) self._create_and_activate_strategy_area(self.external_strategy) - gsy_e.gsy_e_core.util.gsy_e.constants.DISPATCH_EVENT_TICK_FREQUENCY_PERCENT = 20 + gsy_e.constants.DISPATCH_EVENT_TICK_FREQUENCY_PERCENT = 20 self.config.ticks_per_slot = 90 global_objects.external_global_stats(self.area, self.config.ticks_per_slot) assert ( @@ -146,7 +145,7 @@ def test_dispatch_tick_frequency_gets_calculated_correctly(self): global_objects.external_global_stats.external_tick_counter._dispatch_tick_frequency == 19 ) - gsy_e.gsy_e_core.util.gsy_e.constants.DISPATCH_EVENT_TICK_FREQUENCY_PERCENT = 50 + gsy_e.constants.DISPATCH_EVENT_TICK_FREQUENCY_PERCENT = 50 self.config.ticks_per_slot = 90 global_objects.external_global_stats(self.area, self.config.ticks_per_slot) assert ( @@ -180,7 +179,7 @@ def test_dispatch_tick_frequency_gets_calculated_correctly(self): ] ) def test_dispatch_event_tick_to_external_aggregator(self, strategy): - gsy_e.gsy_e_core.util.gsy_e.constants.DISPATCH_EVENT_TICK_FREQUENCY_PERCENT = 20 + gsy_e.constants.DISPATCH_EVENT_TICK_FREQUENCY_PERCENT = 20 self._create_and_activate_strategy_area(strategy) strategy.redis.aggregator.is_controlling_device = lambda _: True self.config.ticks_per_slot = 90 @@ -204,7 +203,7 @@ def test_dispatch_event_tick_to_external_aggregator(self, strategy): ) result = strategy.redis.aggregator.add_batch_tick_event.call_args_list[0][0][1] assert result == { - "market_slot": GlobalConfig.start_date.format(gsy_e.constants.DATE_TIME_FORMAT), + "market_slot": GlobalConfig.start_date.format(DATE_TIME_FORMAT), "slot_completion": "20%", } strategy.redis.reset_mock() @@ -221,7 +220,7 @@ def test_dispatch_event_tick_to_external_aggregator(self, strategy): ) result = strategy.redis.aggregator.add_batch_tick_event.call_args_list[0][0][1] assert result == { - "market_slot": GlobalConfig.start_date.format(gsy_e.constants.DATE_TIME_FORMAT), + "market_slot": GlobalConfig.start_date.format(DATE_TIME_FORMAT), "slot_completion": "40%", } @@ -233,7 +232,7 @@ def test_dispatch_event_tick_to_external_aggregator(self, strategy): ] ) def test_dispatch_event_tick_to_external_agent(self, strategy): - gsy_e.gsy_e_core.util.gsy_e.constants.DISPATCH_EVENT_TICK_FREQUENCY_PERCENT = 20 + gsy_e.constants.DISPATCH_EVENT_TICK_FREQUENCY_PERCENT = 20 self._create_and_activate_strategy_area(strategy) strategy.redis.aggregator.is_controlling_device = lambda _: False self.config.ticks_per_slot = 90 @@ -256,7 +255,7 @@ def test_dispatch_event_tick_to_external_agent(self, strategy): result.pop("area_uuid") assert result == { "slot_completion": "20%", - "market_slot": GlobalConfig.start_date.format(gsy_e.constants.DATE_TIME_FORMAT), + "market_slot": GlobalConfig.start_date.format(DATE_TIME_FORMAT), "event": "tick", "device_info": strategy._device_info_dict, } @@ -274,7 +273,7 @@ def test_dispatch_event_tick_to_external_agent(self, strategy): result.pop("area_uuid") assert result == { "slot_completion": "40%", - "market_slot": GlobalConfig.start_date.format(gsy_e.constants.DATE_TIME_FORMAT), + "market_slot": GlobalConfig.start_date.format(DATE_TIME_FORMAT), "event": "tick", "device_info": strategy._device_info_dict, } @@ -717,7 +716,7 @@ class TestForecastRelatedFeatures: def test_set_energy_forecast_succeeds(ext_strategy_fixture): arguments = { "transaction_id": transaction_id, - "energy_forecast": {now().format(gsy_e.constants.DATE_TIME_FORMAT): 1}, + "energy_forecast": {now().format(DATE_TIME_FORMAT): 1}, } payload = {"data": json.dumps(arguments)} assert ext_strategy_fixture.pending_requests == deque([]) @@ -772,7 +771,7 @@ def test_set_energy_forecast_fails_for_wrong_payload(ext_strategy_fixture): def test_set_energy_measurement_succeeds(ext_strategy_fixture): arguments = { "transaction_id": transaction_id, - "energy_measurement": {now().format(gsy_e.constants.DATE_TIME_FORMAT): 1}, + "energy_measurement": {now().format(DATE_TIME_FORMAT): 1}, } payload = {"data": json.dumps(arguments)} assert ext_strategy_fixture.pending_requests == deque([]) @@ -826,7 +825,7 @@ def test_set_energy_forecast_impl_succeeds(self, ext_strategy_fixture): ext_strategy_fixture.redis.publish_json = Mock() arguments = { "transaction_id": transaction_id, - "energy_forecast": {now().format(gsy_e.constants.DATE_TIME_FORMAT): 1}, + "energy_forecast": {now().format(DATE_TIME_FORMAT): 1}, } response_channel = "response_channel" ext_strategy_fixture._set_energy_forecast_impl(arguments, response_channel) @@ -881,7 +880,7 @@ def test_set_energy_forecast_impl_fails_for_negative_energy(self, ext_strategy_f response_channel = "response_channel" arguments = { "transaction_id": transaction_id, - "energy_forecast": {now().format(gsy_e.constants.DATE_TIME_FORMAT): -1}, + "energy_forecast": {now().format(DATE_TIME_FORMAT): -1}, } ext_strategy_fixture._set_energy_forecast_impl(arguments, response_channel) error_message = ( @@ -912,7 +911,7 @@ def test_set_energy_measurement_impl_succeeds(self, ext_strategy_fixture): ext_strategy_fixture.redis.publish_json = Mock() arguments = { "transaction_id": transaction_id, - "energy_measurement": {now().format(gsy_e.constants.DATE_TIME_FORMAT): 1}, + "energy_measurement": {now().format(DATE_TIME_FORMAT): 1}, } response_channel = "response_channel" ext_strategy_fixture._set_energy_measurement_impl(arguments, response_channel) @@ -970,7 +969,7 @@ def test_set_energy_measurement_impl_fails_for_negative_energy(self, ext_strateg ext_strategy_fixture.redis.publish_json.reset_mock() arguments = { "transaction_id": transaction_id, - "energy_measurement": {now().format(gsy_e.constants.DATE_TIME_FORMAT): -1}, + "energy_measurement": {now().format(DATE_TIME_FORMAT): -1}, } ext_strategy_fixture._set_energy_measurement_impl(arguments, response_channel) error_message = ( diff --git a/tests/strategies/future/test_future_strategy.py b/tests/strategies/future/test_future_strategy.py index 7f7e23ed2..64cabeed7 100644 --- a/tests/strategies/future/test_future_strategy.py +++ b/tests/strategies/future/test_future_strategy.py @@ -15,16 +15,17 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import uuid from typing import TYPE_CHECKING from unittest.mock import Mock, MagicMock import pytest -from gsy_framework.constants_limits import GlobalConfig, ConstSettings +from gsy_framework.constants_limits import GlobalConfig, ConstSettings, TIME_ZONE from gsy_framework.data_classes import TraderDetails from pendulum import today, duration -from gsy_e.constants import TIME_ZONE, FutureTemplateStrategiesConstants +from gsy_e.constants import FutureTemplateStrategiesConstants from gsy_e.models.market.future import FutureMarkets from gsy_e.models.strategy.future.strategy import FutureMarketStrategy from gsy_e.models.strategy.load_hours import LoadHoursStrategy @@ -37,12 +38,14 @@ class TestFutureMarketStrategy: """Test the FutureMarketStrategy class.""" + # pylint: disable = attribute-defined-outside-init, too-many-instance-attributes def setup_method(self) -> None: """Preparation for the tests execution""" self._original_future_markets_duration = ( - ConstSettings.FutureMarketSettings.FUTURE_MARKET_DURATION_HOURS) + ConstSettings.FutureMarketSettings.FUTURE_MARKET_DURATION_HOURS + ) ConstSettings.FutureMarketSettings.FUTURE_MARKET_DURATION_HOURS = 24 self.time_slot = today(tz=TIME_ZONE).at(hour=12, minute=0, second=0) self.area_mock = Mock() @@ -56,17 +59,20 @@ def setup_method(self) -> None: self._original_initial_buying_rate = FutureTemplateStrategiesConstants.INITIAL_BUYING_RATE self._original_final_buying_rate = FutureTemplateStrategiesConstants.FINAL_BUYING_RATE self._original_initial_selling_rate = ( - FutureTemplateStrategiesConstants.INITIAL_SELLING_RATE) + FutureTemplateStrategiesConstants.INITIAL_SELLING_RATE + ) self._original_final_selling_rate = FutureTemplateStrategiesConstants.FINAL_SELLING_RATE def teardown_method(self) -> None: """Test cleanup""" ConstSettings.FutureMarketSettings.FUTURE_MARKET_DURATION_HOURS = ( - self._original_future_markets_duration) + self._original_future_markets_duration + ) FutureTemplateStrategiesConstants.INITIAL_BUYING_RATE = self._original_initial_buying_rate FutureTemplateStrategiesConstants.FINAL_BUYING_RATE = self._original_final_buying_rate FutureTemplateStrategiesConstants.INITIAL_SELLING_RATE = ( - self._original_initial_selling_rate) + self._original_initial_selling_rate + ) FutureTemplateStrategiesConstants.FINAL_SELLING_RATE = self._original_final_selling_rate def _setup_strategy_fixture(self, future_strategy_fixture: "BaseStrategy") -> None: @@ -87,11 +93,13 @@ def test_event_market_cycle_posts_bids_load(self) -> None: load_strategy_fixture.state.set_desired_energy(1234.0, self.time_slot) future_strategy.event_market_cycle(load_strategy_fixture) self.future_markets.bid.assert_called_once_with( - 10.0 * 1.234, 1.234, TraderDetails( - self.area_mock.name, self.area_mock.uuid, - self.area_mock.name, self.area_mock.uuid), + 10.0 * 1.234, + 1.234, + TraderDetails( + self.area_mock.name, self.area_mock.uuid, self.area_mock.name, self.area_mock.uuid + ), original_price=10.0 * 1.234, - time_slot=self.time_slot + time_slot=self.time_slot, ) def test_event_market_cycle_posts_offers_pv(self) -> None: @@ -102,10 +110,12 @@ def test_event_market_cycle_posts_offers_pv(self) -> None: pv_strategy_fixture.state.set_available_energy(321.3, self.time_slot) future_strategy.event_market_cycle(pv_strategy_fixture) self.future_markets.offer.assert_called_once_with( - price=50.0 * 321.3, energy=321.3, seller=TraderDetails( - self.area_mock.name, self.area_mock.uuid, - self.area_mock.name, self.area_mock.uuid), - time_slot=self.time_slot + price=50.0 * 321.3, + energy=321.3, + seller=TraderDetails( + self.area_mock.name, self.area_mock.uuid, self.area_mock.name, self.area_mock.uuid + ), + time_slot=self.time_slot, ) def test_event_market_cycle_posts_bids_and_offers_storage(self) -> None: @@ -114,37 +124,44 @@ def test_event_market_cycle_posts_bids_and_offers_storage(self) -> None: future_strategy = FutureMarketStrategy(storage_strategy_fixture.asset_type, 10, 50, 50, 20) self._setup_strategy_fixture(storage_strategy_fixture) storage_strategy_fixture.state.activate(duration(minutes=15), self.time_slot) - storage_strategy_fixture.state.offered_sell_kWh[self.time_slot] = 0. - storage_strategy_fixture.state.offered_buy_kWh[self.time_slot] = 0. - storage_strategy_fixture.state.pledged_sell_kWh[self.time_slot] = 0. - storage_strategy_fixture.state.pledged_buy_kWh[self.time_slot] = 0. + storage_strategy_fixture.state.offered_sell_kWh[self.time_slot] = 0.0 + storage_strategy_fixture.state.offered_buy_kWh[self.time_slot] = 0.0 + storage_strategy_fixture.state.pledged_sell_kWh[self.time_slot] = 0.0 + storage_strategy_fixture.state.pledged_buy_kWh[self.time_slot] = 0.0 storage_strategy_fixture.state.get_available_energy_to_buy_kWh = Mock(return_value=3) storage_strategy_fixture.state.get_available_energy_to_sell_kWh = Mock(return_value=2) storage_strategy_fixture.state.register_energy_from_posted_offer = Mock() storage_strategy_fixture.state.register_energy_from_posted_bid = Mock() future_strategy.event_market_cycle(storage_strategy_fixture) self.future_markets.offer.assert_called_once_with( - price=50.0 * 2, energy=2, seller=TraderDetails( - self.area_mock.name, self.area_mock.uuid, - self.area_mock.name, self.area_mock.uuid), - time_slot=self.time_slot) + price=50.0 * 2, + energy=2, + seller=TraderDetails( + self.area_mock.name, self.area_mock.uuid, self.area_mock.name, self.area_mock.uuid + ), + time_slot=self.time_slot, + ) storage_strategy_fixture.state.register_energy_from_posted_offer.assert_called_once() self.future_markets.bid.assert_called_once_with( - 10.0 * 3, 3, TraderDetails( - self.area_mock.name, self.area_mock.uuid, - self.area_mock.name, self.area_mock.uuid - ), original_price=10.0 * 3, - time_slot=self.time_slot + 10.0 * 3, + 3, + TraderDetails( + self.area_mock.name, self.area_mock.uuid, self.area_mock.name, self.area_mock.uuid + ), + original_price=10.0 * 3, + time_slot=self.time_slot, ) storage_strategy_fixture.state.register_energy_from_posted_bid.assert_called_once() @pytest.mark.parametrize( - "future_strategy_fixture", [LoadHoursStrategy(100), PVStrategy(), - StorageStrategy(initial_soc=50)]) + "future_strategy_fixture", + [LoadHoursStrategy(100), PVStrategy(), StorageStrategy(initial_soc=50)], + ) def test_event_tick_updates_bids_and_offers( - self, future_strategy_fixture: "BaseStrategy") -> None: + self, future_strategy_fixture: "BaseStrategy" + ) -> None: """Validate that tick event updates existing bids and offers to the expected energy rate""" future_strategy = FutureMarketStrategy(future_strategy_fixture.asset_type, 10, 50, 50, 20) @@ -160,10 +177,10 @@ def test_event_tick_updates_bids_and_offers( future_strategy_fixture.state.activate(duration(minutes=15), self.time_slot) future_strategy_fixture.get_available_energy_to_buy_kWh = Mock(return_value=3) future_strategy_fixture.get_available_energy_to_sell_kWh = Mock(return_value=2) - future_strategy_fixture.state.offered_sell_kWh[self.time_slot] = 0. - future_strategy_fixture.state.offered_buy_kWh[self.time_slot] = 0. - future_strategy_fixture.state.pledged_sell_kWh[self.time_slot] = 0. - future_strategy_fixture.state.pledged_buy_kWh[self.time_slot] = 0. + future_strategy_fixture.state.offered_sell_kWh[self.time_slot] = 0.0 + future_strategy_fixture.state.offered_buy_kWh[self.time_slot] = 0.0 + future_strategy_fixture.state.pledged_sell_kWh[self.time_slot] = 0.0 + future_strategy_fixture.state.pledged_buy_kWh[self.time_slot] = 0.0 future_strategy_fixture.state.register_energy_from_posted_offer = Mock() future_strategy_fixture.state.register_energy_from_posted_bid = Mock() future_strategy_fixture.area.current_tick = 0 @@ -179,21 +196,27 @@ def test_event_tick_updates_bids_and_offers( future_strategy_fixture.area.current_tick = ticks_for_update future_strategy.event_tick(future_strategy_fixture) number_of_updates = ( - (ConstSettings.FutureMarketSettings.FUTURE_MARKET_DURATION_HOURS * 60 / - FutureTemplateStrategiesConstants.UPDATE_INTERVAL_MIN) - 1) + ConstSettings.FutureMarketSettings.FUTURE_MARKET_DURATION_HOURS + * 60 + / FutureTemplateStrategiesConstants.UPDATE_INTERVAL_MIN + ) - 1 bid_energy_rate = (50 - 10) / number_of_updates offer_energy_rate = (50 - 20) / number_of_updates if isinstance(future_strategy_fixture, LoadHoursStrategy): future_strategy_fixture.update_bid_rates.assert_called_once_with( - self.future_markets, 10 + bid_energy_rate, self.time_slot) + self.future_markets, 10 + bid_energy_rate, self.time_slot + ) if isinstance(future_strategy_fixture, PVStrategy): future_strategy_fixture.update_offer_rates.assert_called_once_with( - self.future_markets, 50 - offer_energy_rate, self.time_slot) + self.future_markets, 50 - offer_energy_rate, self.time_slot + ) if isinstance(future_strategy_fixture, StorageStrategy): future_strategy_fixture.update_bid_rates.assert_called_once_with( - self.future_markets, 10 + bid_energy_rate, self.time_slot) + self.future_markets, 10 + bid_energy_rate, self.time_slot + ) future_strategy_fixture.update_offer_rates.assert_called_once_with( - self.future_markets, 50 - offer_energy_rate, self.time_slot) + self.future_markets, 50 - offer_energy_rate, self.time_slot + ) @staticmethod def test_future_template_strategies_constants() -> None: diff --git a/tests/strategies/settlement/test_settlement_strategy.py b/tests/strategies/settlement/test_settlement_strategy.py index 14729fa59..b3c5cab0a 100644 --- a/tests/strategies/settlement/test_settlement_strategy.py +++ b/tests/strategies/settlement/test_settlement_strategy.py @@ -15,16 +15,16 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import uuid from unittest.mock import Mock, MagicMock import pytest -from gsy_framework.constants_limits import ConstSettings +from gsy_framework.constants_limits import ConstSettings, TIME_ZONE from gsy_framework.data_classes import Bid, Offer, Trade, TraderDetails from gsy_framework.utils import format_datetime from pendulum import today, duration -from gsy_e.constants import TIME_ZONE from gsy_e.models.market.two_sided import TwoSidedMarket from gsy_e.models.strategy.load_hours import LoadHoursStrategy from gsy_e.models.strategy.pv import PVStrategy @@ -41,8 +41,9 @@ def setup_method(self): self.market_mock.time_slot = self.time_slot self.market_mock.id = str(uuid.uuid4()) self.test_bid = Bid("123", self.time_slot, 10, 1, buyer=TraderDetails("test_name", "")) - self.test_offer = Offer("234", self.time_slot, 50, 1, - seller=TraderDetails("test_name", "")) + self.test_offer = Offer( + "234", self.time_slot, 50, 1, seller=TraderDetails("test_name", "") + ) self.market_mock.bid = MagicMock(return_value=self.test_bid) self.market_mock.offer = MagicMock(return_value=self.test_offer) self.market_mock.bids = {self.test_bid.id: self.test_bid} @@ -50,18 +51,19 @@ def setup_method(self): self.area_mock.name = "test_name" self.area_mock.uuid = str(uuid.uuid4()) self._area_trader_details = TraderDetails(self.area_mock.name, self.area_mock.uuid) - self.settlement_markets = { - self.time_slot: self.market_mock - } + self.settlement_markets = {self.time_slot: self.market_mock} def _setup_strategy_fixture( - self, strategy_fixture, can_post_settlement_bid, can_post_settlement_offer): + self, strategy_fixture, can_post_settlement_bid, can_post_settlement_offer + ): strategy_fixture.owner = self.area_mock strategy_fixture.state.set_energy_measurement_kWh(1, self.time_slot) strategy_fixture.state.can_post_settlement_bid = MagicMock( - return_value=can_post_settlement_bid) + return_value=can_post_settlement_bid + ) strategy_fixture.state.can_post_settlement_offer = MagicMock( - return_value=can_post_settlement_offer) + return_value=can_post_settlement_offer + ) strategy_fixture.area = Mock() strategy_fixture.area.settlement_markets = self.settlement_markets strategy_fixture.get_market_from_id = MagicMock(return_value=self.market_mock) @@ -77,38 +79,51 @@ def _setup_strategy_fixture( def teardown_method(): ConstSettings.SettlementMarketSettings.ENABLE_SETTLEMENT_MARKETS = False - @pytest.mark.parametrize( - "strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) + @pytest.mark.parametrize("strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) @pytest.mark.parametrize("can_post_settlement_bid", [True, False]) @pytest.mark.parametrize("can_post_settlement_offer", [True, False]) def test_event_market_cycle_posts_bids_and_offers( - self, strategy_fixture, can_post_settlement_bid, can_post_settlement_offer): + self, strategy_fixture, can_post_settlement_bid, can_post_settlement_offer + ): self._setup_strategy_fixture( - strategy_fixture, can_post_settlement_bid, can_post_settlement_offer) + strategy_fixture, can_post_settlement_bid, can_post_settlement_offer + ) self.settlement_strategy.event_market_cycle(strategy_fixture) if can_post_settlement_bid: self.market_mock.bid.assert_called_once_with( - 10.0, 1.0, TraderDetails( - self.area_mock.name, self.area_mock.uuid, - self.area_mock.name, self.area_mock.uuid), original_price=10.0, - time_slot=self.time_slot + 10.0, + 1.0, + TraderDetails( + self.area_mock.name, + self.area_mock.uuid, + self.area_mock.name, + self.area_mock.uuid, + ), + original_price=10.0, + time_slot=self.time_slot, ) if can_post_settlement_offer: self.market_mock.offer.assert_called_once_with( - price=50.0, energy=1.0, seller=TraderDetails( - self.area_mock.name, self.area_mock.uuid, - self.area_mock.name, self.area_mock.uuid), - time_slot=self.time_slot + price=50.0, + energy=1.0, + seller=TraderDetails( + self.area_mock.name, + self.area_mock.uuid, + self.area_mock.name, + self.area_mock.uuid, + ), + time_slot=self.time_slot, ) - @pytest.mark.parametrize( - "strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) + @pytest.mark.parametrize("strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) @pytest.mark.parametrize("can_post_settlement_bid", [True, False]) @pytest.mark.parametrize("can_post_settlement_offer", [True, False]) def test_event_tick_updates_bids_and_offers( - self, strategy_fixture, can_post_settlement_bid, can_post_settlement_offer): + self, strategy_fixture, can_post_settlement_bid, can_post_settlement_offer + ): self._setup_strategy_fixture( - strategy_fixture, can_post_settlement_bid, can_post_settlement_offer) + strategy_fixture, can_post_settlement_bid, can_post_settlement_offer + ) strategy_fixture.area.current_tick = 0 self.settlement_strategy.event_market_cycle(strategy_fixture) @@ -123,85 +138,122 @@ def test_event_tick_updates_bids_and_offers( self.settlement_strategy.event_tick(strategy_fixture) if can_post_settlement_bid: self.market_mock.bid.assert_called_once_with( - 30.0, 1.0, TraderDetails( - self.area_mock.name, self.area_mock.uuid, - self.area_mock.name, self.area_mock.uuid), original_price=30.0, - time_slot=self.time_slot + 30.0, + 1.0, + TraderDetails( + self.area_mock.name, + self.area_mock.uuid, + self.area_mock.name, + self.area_mock.uuid, + ), + original_price=30.0, + time_slot=self.time_slot, ) if can_post_settlement_offer: self.market_mock.offer.assert_called_once_with( - 35.0, 1, TraderDetails( - self.area_mock.name, self.area_mock.uuid), - original_price=35.0, time_slot=self.time_slot + 35.0, + 1, + TraderDetails(self.area_mock.name, self.area_mock.uuid), + original_price=35.0, + time_slot=self.time_slot, ) - @pytest.mark.parametrize( - "strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) + @pytest.mark.parametrize("strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) def test_event_trade_updates_energy_deviation(self, strategy_fixture): self._setup_strategy_fixture(strategy_fixture, False, True) strategy_fixture.state.set_energy_measurement_kWh(10, self.time_slot) self.settlement_strategy.event_market_cycle(strategy_fixture) self.settlement_strategy.event_offer_traded( - strategy_fixture, self.market_mock.id, - Trade("456", self.time_slot, self._area_trader_details, self._area_trader_details, - offer=self.test_offer, traded_energy=1, trade_price=1) + strategy_fixture, + self.market_mock.id, + Trade( + "456", + self.time_slot, + self._area_trader_details, + self._area_trader_details, + offer=self.test_offer, + traded_energy=1, + trade_price=1, + ), ) assert strategy_fixture.state.get_unsettled_deviation_kWh(self.time_slot) == 9 - @pytest.mark.parametrize( - "strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) + @pytest.mark.parametrize("strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) def test_event_trade_not_update_energy_deviation_on_bid_trade(self, strategy_fixture): self._setup_strategy_fixture(strategy_fixture, False, True) strategy_fixture.state.set_energy_measurement_kWh(10, self.time_slot) self.settlement_strategy.event_market_cycle(strategy_fixture) self.settlement_strategy.event_offer_traded( - strategy_fixture, self.market_mock.id, - Trade("456", self.time_slot, self._area_trader_details, self._area_trader_details, - bid=self.test_bid, traded_energy=1, trade_price=1) + strategy_fixture, + self.market_mock.id, + Trade( + "456", + self.time_slot, + self._area_trader_details, + self._area_trader_details, + bid=self.test_bid, + traded_energy=1, + trade_price=1, + ), ) assert strategy_fixture.state.get_unsettled_deviation_kWh(self.time_slot) == 10 - @pytest.mark.parametrize( - "strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) + @pytest.mark.parametrize("strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) def test_event_bid_trade_updates_energy_deviation(self, strategy_fixture): self._setup_strategy_fixture(strategy_fixture, True, False) strategy_fixture.state.set_energy_measurement_kWh(15, self.time_slot) self.settlement_strategy.event_market_cycle(strategy_fixture) self.settlement_strategy.event_bid_traded( - strategy_fixture, self.market_mock.id, - Trade("456", self.time_slot, self._area_trader_details, self._area_trader_details, - bid=self.test_bid, traded_energy=1, trade_price=1) + strategy_fixture, + self.market_mock.id, + Trade( + "456", + self.time_slot, + self._area_trader_details, + self._area_trader_details, + bid=self.test_bid, + traded_energy=1, + trade_price=1, + ), ) assert strategy_fixture.state.get_unsettled_deviation_kWh(self.time_slot) == 14 - @pytest.mark.parametrize( - "strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) + @pytest.mark.parametrize("strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) def test_event_bid_traded_does_not_update_energy_deviation_offer_trade(self, strategy_fixture): self._setup_strategy_fixture(strategy_fixture, True, False) strategy_fixture.state.set_energy_measurement_kWh(15, self.time_slot) self.settlement_strategy.event_market_cycle(strategy_fixture) self.settlement_strategy.event_bid_traded( - strategy_fixture, self.market_mock.id, - Trade("456", self.time_slot, self._area_trader_details, self._area_trader_details, - offer=self.test_offer, traded_energy=1, trade_price=1) + strategy_fixture, + self.market_mock.id, + Trade( + "456", + self.time_slot, + self._area_trader_details, + self._area_trader_details, + offer=self.test_offer, + traded_energy=1, + trade_price=1, + ), ) assert strategy_fixture.state.get_unsettled_deviation_kWh(self.time_slot) == 15 - @pytest.mark.parametrize( - "strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) + @pytest.mark.parametrize("strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) def test_get_unsettled_deviation_dict(self, strategy_fixture): self._setup_strategy_fixture(strategy_fixture, True, False) strategy_fixture.state.set_energy_measurement_kWh(15, self.time_slot) unsettled_deviation_dict = self.settlement_strategy.get_unsettled_deviation_dict( - strategy_fixture) + strategy_fixture + ) assert len(unsettled_deviation_dict["unsettled_deviation_kWh"]) == 1 - assert (list(unsettled_deviation_dict["unsettled_deviation_kWh"].keys()) == - [format_datetime(self.time_slot)]) - assert (list(unsettled_deviation_dict["unsettled_deviation_kWh"].values()) == - [strategy_fixture.state.get_signed_unsettled_deviation_kWh(self.time_slot)]) + assert list(unsettled_deviation_dict["unsettled_deviation_kWh"].keys()) == [ + format_datetime(self.time_slot) + ] + assert list(unsettled_deviation_dict["unsettled_deviation_kWh"].values()) == [ + strategy_fixture.state.get_signed_unsettled_deviation_kWh(self.time_slot) + ] - @pytest.mark.parametrize( - "strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) + @pytest.mark.parametrize("strategy_fixture", [LoadHoursStrategy(100), PVStrategy()]) def test_get_market_from_id_works_for_settlement_markets(self, strategy_fixture): self._setup_strategy_fixture(strategy_fixture, True, True) market = strategy_fixture.get_market_from_id(self.settlement_markets[self.time_slot].id) @@ -211,24 +263,48 @@ def test_can_get_settlement_bid_be_posted(self): strategy_fixture = LoadHoursStrategy(100) self._setup_strategy_fixture(strategy_fixture, True, False) - assert strategy_fixture.can_settlement_offer_be_posted( - 1.1, 1, self.settlement_markets[self.time_slot]) is False + assert ( + strategy_fixture.can_settlement_offer_be_posted( + 1.1, 1, self.settlement_markets[self.time_slot] + ) + is False + ) - assert strategy_fixture.can_settlement_bid_be_posted( - 0.9, 1, self.settlement_markets[self.time_slot]) is True + assert ( + strategy_fixture.can_settlement_bid_be_posted( + 0.9, 1, self.settlement_markets[self.time_slot] + ) + is True + ) - assert strategy_fixture.can_settlement_bid_be_posted( - 1.1, 1, self.settlement_markets[self.time_slot]) is False + assert ( + strategy_fixture.can_settlement_bid_be_posted( + 1.1, 1, self.settlement_markets[self.time_slot] + ) + is False + ) def test_can_get_settlement_offer_be_posted(self): strategy_fixture = PVStrategy() self._setup_strategy_fixture(strategy_fixture, False, True) - assert strategy_fixture.can_settlement_bid_be_posted( - 1.1, 1, self.settlement_markets[self.time_slot]) is False + assert ( + strategy_fixture.can_settlement_bid_be_posted( + 1.1, 1, self.settlement_markets[self.time_slot] + ) + is False + ) - assert strategy_fixture.can_settlement_offer_be_posted( - 0.9, 1, self.settlement_markets[self.time_slot]) is True + assert ( + strategy_fixture.can_settlement_offer_be_posted( + 0.9, 1, self.settlement_markets[self.time_slot] + ) + is True + ) - assert strategy_fixture.can_settlement_offer_be_posted( - 1.1, 1, self.settlement_markets[self.time_slot]) is False + assert ( + strategy_fixture.can_settlement_offer_be_posted( + 1.1, 1, self.settlement_markets[self.time_slot] + ) + is False + ) diff --git a/tests/strategies/test_strategy_base.py b/tests/strategies/test_strategy_base.py index 862ed27c8..5cd2d43bc 100644 --- a/tests/strategies/test_strategy_base.py +++ b/tests/strategies/test_strategy_base.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + # pylint: disable=missing-function-docstring, protected-access, missing-class-docstring # pylint: disable=no-self-use, redefined-builtin, unused-argument, too-many-arguments from unittest.mock import MagicMock, patch @@ -26,7 +27,7 @@ from gsy_framework.data_classes import Bid, Offer, Trade, TraderDetails from gsy_framework.enums import SpotMarketTypeEnum -from gsy_e.constants import TIME_ZONE +from gsy_framework.constants_limits import TIME_ZONE from gsy_e.gsy_e_core.blockchain_interface import NonBlockchainInterface from gsy_e.gsy_e_core.exceptions import MarketException from gsy_e.models.market.one_sided import OneSidedMarket @@ -110,13 +111,18 @@ def accept_offer(self, offer_or_id, *, buyer="", energy=None, time=None, trade_b if energy is None: energy = offer.energy offer.energy = energy - return Trade("trade", 0, offer.seller, - TraderDetails("FakeOwner", ""), - offer=offer, traded_energy=offer.energy, trade_price=offer.price) + return Trade( + "trade", + 0, + offer.seller, + TraderDetails("FakeOwner", ""), + offer=offer, + traded_energy=offer.energy, + trade_price=offer.price, + ) def bid(self, price, energy, buyer, original_price=None, time_slot=None): - return Bid(123, pendulum.now(), price, energy, buyer, - original_price, time_slot=time_slot) + return Bid(123, pendulum.now(), price, energy, buyer, original_price, time_slot=time_slot) @pytest.fixture(name="offers") @@ -188,9 +194,15 @@ def test_offers_partial_offer(offer1, offers3): accepted_offer = Offer("id", pendulum.now(), 1, 0.6, offer1.seller) residual_offer = Offer("new_id", pendulum.now(), 1, 1.2, offer1.seller) offers3.on_offer_split(offer1, accepted_offer, residual_offer, "market") - trade = Trade("trade_id", pendulum.now(tz=TIME_ZONE), offer1.seller, - TraderDetails("buyer", ""), - offer=accepted_offer, traded_energy=0.6, trade_price=1) + trade = Trade( + "trade_id", + pendulum.now(tz=TIME_ZONE), + offer1.seller, + TraderDetails("buyer", ""), + offer=accepted_offer, + traded_energy=0.6, + trade_price=1, + ) offers3.on_trade("market", trade) assert len(offers3.sold_in_market("market")) == 1 assert accepted_offer in offers3.sold_in_market("market") @@ -232,8 +244,10 @@ def test_accept_offer_handles_market_exception(base, offer_to_accept): assert len(base.offers.bought.keys()) == 0 -@patch("gsy_framework.constants_limits.ConstSettings.MASettings.MARKET_TYPE", - SpotMarketTypeEnum.TWO_SIDED.value) +@patch( + "gsy_framework.constants_limits.ConstSettings.MASettings.MARKET_TYPE", + SpotMarketTypeEnum.TWO_SIDED.value, +) def test_accept_post_bid(base): market = FakeMarket(raises=True) @@ -246,8 +260,10 @@ def test_accept_post_bid(base): assert bid.buyer.name == "FakeOwner" -@patch("gsy_framework.constants_limits.ConstSettings.MASettings.MARKET_TYPE", - SpotMarketTypeEnum.TWO_SIDED.value) +@patch( + "gsy_framework.constants_limits.ConstSettings.MASettings.MARKET_TYPE", + SpotMarketTypeEnum.TWO_SIDED.value, +) def test_remove_bid_from_pending(base): market = FakeMarket(raises=True) base.area._market = market @@ -258,8 +274,10 @@ def test_remove_bid_from_pending(base): assert not base.are_bids_posted(market.id) -@patch("gsy_framework.constants_limits.ConstSettings.MASettings.MARKET_TYPE", - SpotMarketTypeEnum.TWO_SIDED.value) +@patch( + "gsy_framework.constants_limits.ConstSettings.MASettings.MARKET_TYPE", + SpotMarketTypeEnum.TWO_SIDED.value, +) def test_add_bid_to_bought(base): market = FakeMarket(raises=True) base.area._market = market @@ -280,8 +298,9 @@ def test_bid_events_fail_for_one_sided_market(base): with pytest.raises(AssertionError): base.event_bid_deleted(market_id=123, bid=test_bid) with pytest.raises(AssertionError): - base.event_bid_split(market_id=123, original_bid=test_bid, accepted_bid=test_bid, - residual_bid=test_bid) + base.event_bid_split( + market_id=123, original_bid=test_bid, accepted_bid=test_bid, residual_bid=test_bid + ) def test_bid_deleted_removes_bid_from_posted(base): @@ -302,8 +321,9 @@ def test_bid_split_adds_bid_to_posted(base): market = FakeMarket(raises=False, id=21) base.area._market = market base._bids[market.id] = [] - base.event_bid_split(market_id=21, original_bid=test_bid, accepted_bid=accepted_bid, - residual_bid=residual_bid) + base.event_bid_split( + market_id=21, original_bid=test_bid, accepted_bid=accepted_bid, residual_bid=residual_bid + ) assert base.get_posted_bids(market) == [accepted_bid, residual_bid] @@ -323,14 +343,33 @@ def test_bid_traded_moves_bid_from_posted_to_traded(base): def test_trades_returns_market_trades(base): test_trades = [ - Trade("123", pendulum.now(), TraderDetails(base.owner.name, ""), - TraderDetails("buyer", ""), 10, 5), - Trade("123", pendulum.now(), TraderDetails("seller", ""), - TraderDetails(base.owner.name, ""), 11, 6), - Trade("123", pendulum.now(), TraderDetails("seller", ""), - TraderDetails("buyer", ""), 12, 7), - Trade("123", pendulum.now(), TraderDetails(base.owner.name, ""), - TraderDetails("buyer", ""), 13, 8), + Trade( + "123", + pendulum.now(), + TraderDetails(base.owner.name, ""), + TraderDetails("buyer", ""), + 10, + 5, + ), + Trade( + "123", + pendulum.now(), + TraderDetails("seller", ""), + TraderDetails(base.owner.name, ""), + 11, + 6, + ), + Trade( + "123", pendulum.now(), TraderDetails("seller", ""), TraderDetails("buyer", ""), 12, 7 + ), + Trade( + "123", + pendulum.now(), + TraderDetails(base.owner.name, ""), + TraderDetails("buyer", ""), + 13, + 8, + ), ] market = FakeMarket(raises=False, id=21) # pylint: disable=attribute-defined-outside-init @@ -352,35 +391,67 @@ def test_can_offer_be_posted(market_class): time_slot = pendulum.now(tz=TIME_ZONE) market = market_class(time_slot=time_slot) - base.offers.post(Offer("id", time_slot.add(seconds=1), price=1, energy=12, - seller=TraderDetails("A", ""), - time_slot=time_slot), market.id) - base.offers.post(Offer("id2", time_slot.add(seconds=2), price=1, energy=13, - seller=TraderDetails("A", ""), - time_slot=time_slot), market.id) - base.offers.post(Offer("id3", time_slot.add(seconds=3), price=1, energy=20, - seller=TraderDetails("A", ""), - time_slot=time_slot), market.id) + base.offers.post( + Offer( + "id", + time_slot.add(seconds=1), + price=1, + energy=12, + seller=TraderDetails("A", ""), + time_slot=time_slot, + ), + market.id, + ) + base.offers.post( + Offer( + "id2", + time_slot.add(seconds=2), + price=1, + energy=13, + seller=TraderDetails("A", ""), + time_slot=time_slot, + ), + market.id, + ) + base.offers.post( + Offer( + "id3", + time_slot.add(seconds=3), + price=1, + energy=20, + seller=TraderDetails("A", ""), + time_slot=time_slot, + ), + market.id, + ) assert base.can_offer_be_posted(4.999, 1, 50, market, time_slot=None) is True - assert base.can_offer_be_posted(5.0, 1, 50, market, time_slot=None) is True - assert base.can_offer_be_posted(5.001, 1, 50, market, time_slot=None) is False + assert base.can_offer_be_posted(5.0, 1, 50, market, time_slot=None) is True + assert base.can_offer_be_posted(5.001, 1, 50, market, time_slot=None) is False assert base.can_offer_be_posted(4.999, 1, 50, market, time_slot=time_slot) is True - assert base.can_offer_be_posted(5.0, 1, 50, market, time_slot=time_slot) is True - assert base.can_offer_be_posted(5.001, 1, 50, market, time_slot=time_slot) is False - - assert base.can_offer_be_posted( - 5.001, 1, 50, market, time_slot=time_slot, replace_existing=True) is True - assert base.can_offer_be_posted( - 50, 1, 50, market, time_slot=time_slot, replace_existing=True) is True - assert base.can_offer_be_posted( - 50.001, 1, 50, market, time_slot=time_slot, replace_existing=True) is False + assert base.can_offer_be_posted(5.0, 1, 50, market, time_slot=time_slot) is True + assert base.can_offer_be_posted(5.001, 1, 50, market, time_slot=time_slot) is False + + assert ( + base.can_offer_be_posted(5.001, 1, 50, market, time_slot=time_slot, replace_existing=True) + is True + ) + assert ( + base.can_offer_be_posted(50, 1, 50, market, time_slot=time_slot, replace_existing=True) + is True + ) + assert ( + base.can_offer_be_posted(50.001, 1, 50, market, time_slot=time_slot, replace_existing=True) + is False + ) @pytest.mark.parametrize("market_class", [TwoSidedMarket]) -@patch("gsy_framework.constants_limits.ConstSettings.MASettings.MARKET_TYPE", - SpotMarketTypeEnum.TWO_SIDED.value) +@patch( + "gsy_framework.constants_limits.ConstSettings.MASettings.MARKET_TYPE", + SpotMarketTypeEnum.TWO_SIDED.value, +) def test_can_bid_be_posted(market_class, base): market = market_class(time_slot=pendulum.now()) @@ -398,8 +469,10 @@ def test_can_bid_be_posted(market_class, base): @pytest.mark.parametrize("market_class", [TwoSidedMarket]) -@patch("gsy_framework.constants_limits.ConstSettings.MASettings.MARKET_TYPE", - SpotMarketTypeEnum.TWO_SIDED.value) +@patch( + "gsy_framework.constants_limits.ConstSettings.MASettings.MARKET_TYPE", + SpotMarketTypeEnum.TWO_SIDED.value, +) def test_post_bid_with_replace_existing(market_class, base): """Calling post_bid with replace_existing=True triggers the removal of the existing bids.""" @@ -415,8 +488,10 @@ def test_post_bid_with_replace_existing(market_class, base): @pytest.mark.parametrize("market_class", [TwoSidedMarket]) -@patch("gsy_framework.constants_limits.ConstSettings.MASettings.MARKET_TYPE", - SpotMarketTypeEnum.TWO_SIDED.value) +@patch( + "gsy_framework.constants_limits.ConstSettings.MASettings.MARKET_TYPE", + SpotMarketTypeEnum.TWO_SIDED.value, +) def test_post_bid_without_replace_existing(market_class, base): """Calling post_bid with replace_existing=False does not trigger the removal of the existing bids. @@ -443,8 +518,7 @@ def test_post_offer_creates_offer_with_correct_parameters(market_class): market = market_class(bc=NonBlockchainInterface(str(uuid4())), time_slot=pendulum.now()) strategy.area._market = market - offer_args = { - "price": 1, "energy": 1} + offer_args = {"price": 1, "energy": 1} offer = strategy.post_offer(market, replace_existing=False, **offer_args) @@ -467,22 +541,28 @@ def test_post_offer_with_replace_existing(market_class): # Post a first offer on the market offer_1_args = { - "price": 1, "energy": 1, "seller": TraderDetails( - "FakeOwner", "", "FakeOwnerOrigin", "")} + "price": 1, + "energy": 1, + "seller": TraderDetails("FakeOwner", "", "FakeOwnerOrigin", ""), + } offer = strategy.post_offer(market, replace_existing=False, **offer_1_args) assert strategy.offers.open_in_market(market.id) == [offer] # Post a new offer not replacing the previous ones offer_2_args = { - "price": 1, "energy": 1, "seller": TraderDetails( - "FakeOwner", "", "FakeOwnerOrigin", "")} + "price": 1, + "energy": 1, + "seller": TraderDetails("FakeOwner", "", "FakeOwnerOrigin", ""), + } offer_2 = strategy.post_offer(market, replace_existing=False, **offer_2_args) assert strategy.offers.open_in_market(market.id) == [offer, offer_2] # Post a new offer replacing the previous ones (default behavior) offer_3_args = { - "price": 1, "energy": 1, "seller": TraderDetails( - "FakeOwner", "", "FakeOwnerOrigin", "")} + "price": 1, + "energy": 1, + "seller": TraderDetails("FakeOwner", "", "FakeOwnerOrigin", ""), + } offer_3 = strategy.post_offer(market, **offer_3_args) assert strategy.offers.open_in_market(market.id) == [offer_3] diff --git a/tests/strategies/test_strategy_commercial_producer.py b/tests/strategies/test_strategy_commercial_producer.py index 2741bb3e1..1f564dcde 100644 --- a/tests/strategies/test_strategy_commercial_producer.py +++ b/tests/strategies/test_strategy_commercial_producer.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import sys from uuid import uuid4 @@ -23,7 +24,7 @@ from gsy_framework.constants_limits import ConstSettings, GlobalConfig from gsy_framework.data_classes import Offer, Trade, BalancingOffer, TraderDetails -from gsy_e.constants import TIME_ZONE, TIME_FORMAT +from gsy_framework.constants_limits import TIME_ZONE, TIME_FORMAT from gsy_e.gsy_e_core.device_registry import DeviceRegistry from gsy_e.gsy_e_core.util import change_global_config from gsy_e.models.config import create_simulation_config_from_global_config @@ -50,6 +51,7 @@ def auto_fixture(): class FakeArea: """Fake class that mimics the Area class.""" + # pylint: disable=too-many-instance-attributes,missing-function-docstring def __init__(self, _count): self.current_tick = 2 @@ -89,6 +91,7 @@ def last_past_market(self): class FakeMarket: """Fake class that mimics the Market class.""" + # pylint: disable=missing-function-docstring,too-many-arguments def __init__(self, count): self.id = str(count) @@ -97,8 +100,7 @@ def __init__(self, count): self.created_balancing_offers = [] def offer(self, price, energy, seller, original_price=None): - offer = Offer( - "id", pendulum.now(), price, energy, seller, original_price) + offer = Offer("id", pendulum.now(), price, energy, seller, original_price) self.created_offers.append(offer) offer.id = "id" return offer @@ -140,14 +142,16 @@ def test_offer_is_created_at_first_market_not_on_activate(commercial_test1, area def test_balancing_offers_are_not_sent_to_all_markets_if_device_not_in_registry( - commercial_test1, area_test1): + commercial_test1, area_test1 +): DeviceRegistry.REGISTRY = {} commercial_test1.event_activate() assert len(area_test1.test_balancing_market.created_balancing_offers) == 0 def test_balancing_offers_are_sent_to_all_markets_if_device_in_registry( - commercial_test1, area_test1): + commercial_test1, area_test1 +): ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = True DeviceRegistry.REGISTRY = {"FakeArea": (30, 40)} @@ -164,7 +168,8 @@ def test_balancing_offers_are_sent_to_all_markets_if_device_in_registry( def test_event_market_cycle_does_not_create_balancing_offer_if_not_in_registry( - commercial_test1, area_test1): + commercial_test1, area_test1 +): DeviceRegistry.REGISTRY = {} commercial_test1.event_activate() commercial_test1.event_market_cycle() @@ -173,17 +178,16 @@ def test_event_market_cycle_does_not_create_balancing_offer_if_not_in_registry( def test_event_market_cycle_creates_balancing_offer_on_last_market_if_in_registry( - commercial_test1, area_test1): + commercial_test1, area_test1 +): ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = True DeviceRegistry.REGISTRY = {"FakeArea": (40, 50)} commercial_test1.event_activate() commercial_test1.event_market_cycle() assert len(area_test1.test_balancing_market.created_balancing_offers) == 1 assert len(area_test1.test_balancing_market_2.created_balancing_offers) == 1 - assert area_test1.test_balancing_market_2.created_balancing_offers[0].energy == \ - sys.maxsize - assert area_test1.test_balancing_market_2.created_balancing_offers[0].price == \ - sys.maxsize * 50 + assert area_test1.test_balancing_market_2.created_balancing_offers[0].energy == sys.maxsize + assert area_test1.test_balancing_market_2.created_balancing_offers[0].price == sys.maxsize * 50 DeviceRegistry.REGISTRY = {} @@ -205,59 +209,105 @@ def test_event_trade(area_test2, commercial_test2): commercial_test2.event_activate() commercial_test2.event_market_cycle() traded_offer = Offer( - id="id", creation_time=pendulum.now(), price=20, energy=1, - seller=TraderDetails("FakeArea", "")) - commercial_test2.event_offer_traded(market_id=area_test2.test_market.id, - trade=Trade(id="id", - offer=traded_offer, - creation_time=pendulum.now(), - seller=TraderDetails("FakeArea", ""), - buyer=TraderDetails("buyer", ""), - traded_energy=1, trade_price=1) - ) + id="id", + creation_time=pendulum.now(), + price=20, + energy=1, + seller=TraderDetails("FakeArea", ""), + ) + commercial_test2.event_offer_traded( + market_id=area_test2.test_market.id, + trade=Trade( + id="id", + offer=traded_offer, + creation_time=pendulum.now(), + seller=TraderDetails("FakeArea", ""), + buyer=TraderDetails("buyer", ""), + traded_energy=1, + trade_price=1, + ), + ) assert len(area_test2.test_market.created_offers) == 1 assert area_test2.test_market.created_offers[-1].energy == sys.maxsize def test_on_offer_split(area_test2, commercial_test2): commercial_test2.event_activate() - original_offer = Offer(id="id", creation_time=pendulum.now(), price=20, - energy=1, seller=TraderDetails("FakeArea", "")) - accepted_offer = Offer(id="new_id", creation_time=pendulum.now(), price=15, - energy=0.75, seller=TraderDetails("FakeArea", "")) - residual_offer = Offer(id="res_id", creation_time=pendulum.now(), price=55, - energy=0.25, seller=TraderDetails("FakeArea", "")) + original_offer = Offer( + id="id", + creation_time=pendulum.now(), + price=20, + energy=1, + seller=TraderDetails("FakeArea", ""), + ) + accepted_offer = Offer( + id="new_id", + creation_time=pendulum.now(), + price=15, + energy=0.75, + seller=TraderDetails("FakeArea", ""), + ) + residual_offer = Offer( + id="res_id", + creation_time=pendulum.now(), + price=55, + energy=0.25, + seller=TraderDetails("FakeArea", ""), + ) commercial_test2.offers.post(original_offer, area_test2.test_market.id) - commercial_test2.event_offer_split(market_id=area_test2.test_market.id, - original_offer=original_offer, - accepted_offer=accepted_offer, - residual_offer=residual_offer) + commercial_test2.event_offer_split( + market_id=area_test2.test_market.id, + original_offer=original_offer, + accepted_offer=accepted_offer, + residual_offer=residual_offer, + ) assert original_offer.id in commercial_test2.offers.split assert commercial_test2.offers.split[original_offer.id] == accepted_offer def test_event_trade_after_offer_changed_partial_offer(area_test2, commercial_test2): - original_offer = Offer(id="old_id", creation_time=pendulum.now(), - price=20, energy=1, seller=TraderDetails("FakeArea", "")) - accepted_offer = Offer(id="old_id", creation_time=pendulum.now(), - price=15, energy=0.75, seller=TraderDetails("FakeArea", "")) - residual_offer = Offer(id="res_id", creation_time=pendulum.now(), - price=5, energy=0.25, seller=TraderDetails("FakeArea", "")) + original_offer = Offer( + id="old_id", + creation_time=pendulum.now(), + price=20, + energy=1, + seller=TraderDetails("FakeArea", ""), + ) + accepted_offer = Offer( + id="old_id", + creation_time=pendulum.now(), + price=15, + energy=0.75, + seller=TraderDetails("FakeArea", ""), + ) + residual_offer = Offer( + id="res_id", + creation_time=pendulum.now(), + price=5, + energy=0.25, + seller=TraderDetails("FakeArea", ""), + ) commercial_test2.offers.post(original_offer, area_test2.test_market.id) - commercial_test2.event_offer_split(market_id=area_test2.test_market.id, - original_offer=original_offer, - accepted_offer=accepted_offer, - residual_offer=residual_offer) + commercial_test2.event_offer_split( + market_id=area_test2.test_market.id, + original_offer=original_offer, + accepted_offer=accepted_offer, + residual_offer=residual_offer, + ) assert original_offer.id in commercial_test2.offers.split assert commercial_test2.offers.split[original_offer.id] == accepted_offer - commercial_test2.event_offer_traded(market_id=area_test2.test_market.id, - trade=Trade(id="id", - offer=original_offer, - creation_time=pendulum.now(), - seller=TraderDetails("FakeArea", ""), - buyer=TraderDetails("buyer", ""), - traded_energy=1, trade_price=1) - ) + commercial_test2.event_offer_traded( + market_id=area_test2.test_market.id, + trade=Trade( + id="id", + offer=original_offer, + creation_time=pendulum.now(), + seller=TraderDetails("FakeArea", ""), + buyer=TraderDetails("buyer", ""), + traded_energy=1, + trade_price=1, + ), + ) assert residual_offer in commercial_test2.offers.posted assert commercial_test2.offers.posted[residual_offer] == area_test2.test_market.id @@ -303,5 +353,4 @@ def test_event_market_cycle(commercial_test3, area_test3): def test_market_maker_strategy_constructor_modifies_global_market_maker_rate(): # pylint: disable=no-member MarketMakerStrategy(energy_rate=22) - assert all(v == 22 - for v in GlobalConfig.market_maker_rate.values()) + assert all(v == 22 for v in GlobalConfig.market_maker_rate.values()) diff --git a/tests/strategies/test_strategy_infinite_bus.py b/tests/strategies/test_strategy_infinite_bus.py index 94adc0e4d..ede061576 100644 --- a/tests/strategies/test_strategy_infinite_bus.py +++ b/tests/strategies/test_strategy_infinite_bus.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + # pylint: disable=no-member, redefined-outer-name, missing-function-docstring, protected-access # pylint: disable=too-many-instance-attributes, missing-class-docstring, unused-argument import os @@ -24,11 +25,10 @@ import pendulum import pytest -from gsy_framework.constants_limits import ConstSettings, GlobalConfig +from gsy_framework.constants_limits import ConstSettings, GlobalConfig, TIME_ZONE from gsy_framework.data_classes import Offer, Trade, BalancingOffer, Bid, TraderDetails from gsy_e import constants -from gsy_e.constants import TIME_ZONE from gsy_e.gsy_e_core.device_registry import DeviceRegistry from gsy_e.gsy_e_core.util import gsye_root_path from gsy_e.models.strategy.infinite_bus import InfiniteBusStrategy @@ -98,8 +98,10 @@ def __init__(self, count): self.count = count self.created_offers = [] self.created_balancing_offers = [] - self.sorted_offers = [Offer("id", pendulum.now(), 25., 1., TraderDetails("other", "")), - Offer("id", pendulum.now(), 26., 1., TraderDetails("other", ""))] + self.sorted_offers = [ + Offer("id", pendulum.now(), 25.0, 1.0, TraderDetails("other", "")), + Offer("id", pendulum.now(), 26.0, 1.0, TraderDetails("other", "")), + ] self.traded_offers = [] self._bids = {TIME: []} @@ -122,16 +124,21 @@ def balancing_offer(self, price, energy, seller): def accept_offer(self, offer_or_id, buyer, *, energy=None, time=None, trade_bid_info=None): offer = offer_or_id - trade = Trade("trade_id", time, offer.seller, - TraderDetails(buyer, ""), - offer=offer, traded_energy=1, trade_price=1) + trade = Trade( + "trade_id", + time, + offer.seller, + TraderDetails(buyer, ""), + offer=offer, + traded_energy=1, + trade_price=1, + ) self.traded_offers.append(trade) return trade @staticmethod def bid(price, energy, buyer, original_price=None, time_slot=None): - bid = Bid("bid_id", pendulum.now(), price, energy, buyer, - time_slot=time_slot) + bid = Bid("bid_id", pendulum.now(), price, energy, buyer, time_slot=time_slot) return bid @@ -170,8 +177,9 @@ def testing_offer_is_created_at_first_market_not_on_activate(bus_test1, area_tes assert area_test1.test_market.created_offers[0].energy == sys.maxsize -def test_balancing_offers_are_not_sent_to_all_markets_if_device_not_in_registry(bus_test1, - area_test1): +def test_balancing_offers_are_not_sent_to_all_markets_if_device_not_in_registry( + bus_test1, area_test1 +): DeviceRegistry.REGISTRY = {} bus_test1.event_activate() assert len(area_test1.test_balancing_market.created_balancing_offers) == 0 @@ -194,7 +202,8 @@ def test_balancing_offers_are_sent_to_all_markets_if_device_in_registry(bus_test def test_event_market_cycle_does_not_create_balancing_offer_if_not_in_registry( - bus_test1, area_test1): + bus_test1, area_test1 +): DeviceRegistry.REGISTRY = {} bus_test1.event_activate() bus_test1.event_market_cycle() @@ -203,17 +212,16 @@ def test_event_market_cycle_does_not_create_balancing_offer_if_not_in_registry( def test_event_market_cycle_creates_balancing_offer_on_last_market_if_in_registry( - bus_test1, area_test1): + bus_test1, area_test1 +): DeviceRegistry.REGISTRY = {"FakeArea": (40, 50)} ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = True bus_test1.event_activate() bus_test1.event_market_cycle() assert len(area_test1.test_balancing_market.created_balancing_offers) == 1 assert len(area_test1.test_balancing_market_2.created_balancing_offers) == 1 - assert area_test1.test_balancing_market_2.created_balancing_offers[0].energy == \ - sys.maxsize - assert area_test1.test_balancing_market_2.created_balancing_offers[0].price == \ - sys.maxsize * 50 + assert area_test1.test_balancing_market_2.created_balancing_offers[0].energy == sys.maxsize + assert area_test1.test_balancing_market_2.created_balancing_offers[0].price == sys.maxsize * 50 @pytest.fixture() @@ -233,16 +241,24 @@ def test_event_trade(area_test2, bus_test2): bus_test2.event_activate() bus_test2.event_market_cycle() traded_offer = Offer( - id="id", creation_time=pendulum.now(), price=20, energy=1, - seller=TraderDetails("FakeArea", "")) - bus_test2.event_offer_traded(market_id=area_test2.test_market.id, - trade=Trade(id="id", - creation_time=pendulum.now(), - offer=traded_offer, - seller=TraderDetails("FakeArea", ""), - buyer=TraderDetails("buyer", ""), - traded_energy=1, trade_price=1) - ) + id="id", + creation_time=pendulum.now(), + price=20, + energy=1, + seller=TraderDetails("FakeArea", ""), + ) + bus_test2.event_offer_traded( + market_id=area_test2.test_market.id, + trade=Trade( + id="id", + creation_time=pendulum.now(), + offer=traded_offer, + seller=TraderDetails("FakeArea", ""), + buyer=TraderDetails("buyer", ""), + traded_energy=1, + trade_price=1, + ), + ) assert len(area_test2.test_market.created_offers) == 1 assert area_test2.test_market.created_offers[-1].energy == sys.maxsize @@ -250,43 +266,79 @@ def test_event_trade(area_test2, bus_test2): def test_on_offer_changed(area_test2, bus_test2): bus_test2.event_activate() original_offer = Offer( - id="id", creation_time=pendulum.now(), price=20, energy=1, - seller=TraderDetails("FakeArea", "")) + id="id", + creation_time=pendulum.now(), + price=20, + energy=1, + seller=TraderDetails("FakeArea", ""), + ) accepted_offer = Offer( - id="new", creation_time=pendulum.now(), price=15, energy=0.75, - seller=TraderDetails("FakeArea", "")) - residual_offer = Offer(id="new_id", creation_time=pendulum.now(), price=5, - energy=0.25, seller=TraderDetails("FakeArea", "")) - bus_test2.event_offer_split(market_id=area_test2.test_market.id, - original_offer=original_offer, - accepted_offer=accepted_offer, - residual_offer=residual_offer) + id="new", + creation_time=pendulum.now(), + price=15, + energy=0.75, + seller=TraderDetails("FakeArea", ""), + ) + residual_offer = Offer( + id="new_id", + creation_time=pendulum.now(), + price=5, + energy=0.25, + seller=TraderDetails("FakeArea", ""), + ) + bus_test2.event_offer_split( + market_id=area_test2.test_market.id, + original_offer=original_offer, + accepted_offer=accepted_offer, + residual_offer=residual_offer, + ) assert original_offer.id in bus_test2.offers.split assert bus_test2.offers.split[original_offer.id] == accepted_offer def test_event_trade_after_offer_changed_partial_offer(area_test2, bus_test2): - original_offer = Offer(id="old_id", creation_time=pendulum.now(), - price=20, energy=1, seller=TraderDetails("FakeArea", "")) - accepted_offer = Offer(id="old_id", creation_time=pendulum.now(), - price=15, energy=0.75, seller=TraderDetails("FakeArea", "")) - residual_offer = Offer(id="res_id", creation_time=pendulum.now(), - price=5, energy=0.25, seller=TraderDetails("FakeArea", "")) + original_offer = Offer( + id="old_id", + creation_time=pendulum.now(), + price=20, + energy=1, + seller=TraderDetails("FakeArea", ""), + ) + accepted_offer = Offer( + id="old_id", + creation_time=pendulum.now(), + price=15, + energy=0.75, + seller=TraderDetails("FakeArea", ""), + ) + residual_offer = Offer( + id="res_id", + creation_time=pendulum.now(), + price=5, + energy=0.25, + seller=TraderDetails("FakeArea", ""), + ) bus_test2.offers.post(original_offer, area_test2.test_market.id) - bus_test2.event_offer_split(market_id=area_test2.test_market.id, - original_offer=original_offer, - accepted_offer=accepted_offer, - residual_offer=residual_offer) + bus_test2.event_offer_split( + market_id=area_test2.test_market.id, + original_offer=original_offer, + accepted_offer=accepted_offer, + residual_offer=residual_offer, + ) assert original_offer.id in bus_test2.offers.split assert bus_test2.offers.split[original_offer.id] == accepted_offer - bus_test2.event_offer_traded(market_id=area_test2.test_market.id, - trade=Trade(id="id", - creation_time=pendulum.now(), - offer=original_offer, - seller=TraderDetails("FakeArea", ""), - buyer=TraderDetails("buyer", ""), - traded_energy=1, trade_price=1) - ) + bus_test2.event_offer_traded( + market_id=area_test2.test_market.id, + trade=Trade( + id="id", + creation_time=pendulum.now(), + offer=original_offer, + seller=TraderDetails("FakeArea", ""), + buyer=TraderDetails("buyer", ""), + traded_energy=1, + trade_price=1, + ), + ) assert residual_offer in bus_test2.offers.posted assert bus_test2.offers.posted[residual_offer] == area_test2.test_market.id @@ -352,13 +404,15 @@ def test_global_market_maker_rate_single_value(bus_test4): assert isinstance(GlobalConfig.market_maker_rate, dict) assert all( v == ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE - for v in GlobalConfig.market_maker_rate.values()) + for v in GlobalConfig.market_maker_rate.values() + ) @pytest.fixture() def bus_test5(area_test1): c = InfiniteBusStrategy( - energy_rate_profile=os.path.join(gsye_root_path, "resources", "SAM_SF_Summer.csv")) + energy_rate_profile=os.path.join(gsye_root_path, "resources", "SAM_SF_Summer.csv") + ) c.area = area_test1 c.owner = area_test1 yield c @@ -377,7 +431,8 @@ def test_global_market_maker_rate_profile_and_infinite_bus_selling_rate_profile( @pytest.fixture() def bus_test6(area_test1): c = InfiniteBusStrategy( - buying_rate_profile=os.path.join(gsye_root_path, "resources", "LOAD_DATA_1.csv")) + buying_rate_profile=os.path.join(gsye_root_path, "resources", "LOAD_DATA_1.csv") + ) c.area = area_test1 c.owner = area_test1 yield c diff --git a/tests/strategies/test_strategy_load_hours.py b/tests/strategies/test_strategy_load_hours.py index e68d4289e..aa7e6682c 100644 --- a/tests/strategies/test_strategy_load_hours.py +++ b/tests/strategies/test_strategy_load_hours.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import os import unittest from copy import deepcopy @@ -23,13 +24,12 @@ from uuid import uuid4 import pytest -from gsy_framework.constants_limits import ConstSettings, GlobalConfig +from gsy_framework.constants_limits import ConstSettings, GlobalConfig, TIME_ZONE, TIME_FORMAT from gsy_framework.data_classes import Offer, BalancingOffer, Bid, Trade, TraderDetails from gsy_framework.enums import SpotMarketTypeEnum from gsy_framework.exceptions import GSyDeviceException from pendulum import DateTime, duration, today, now -from gsy_e.constants import TIME_ZONE, TIME_FORMAT from gsy_e.gsy_e_core.device_registry import DeviceRegistry from gsy_e.gsy_e_core.util import gsye_root_path from gsy_e.models.area import Area @@ -54,15 +54,17 @@ def auto_fixture(): class FakeArea: def __init__(self): self.config = create_simulation_config_from_global_config() - self.name = 'FakeArea' + self.name = "FakeArea" self.uuid = str(uuid4()) self._spot_market = FakeMarket(0) self.current_market = FakeMarket(0) self._bids = {} - self.markets = {TIME: self.current_market, - TIME + self.config.slot_length: FakeMarket(0), - TIME + 2 * self.config.slot_length: FakeMarket(0)} + self.markets = { + TIME: self.current_market, + TIME + self.config.slot_length: FakeMarket(0), + TIME + 2 * self.config.slot_length: FakeMarket(0), + } self.test_balancing_market = FakeMarket(1) self.test_balancing_market_2 = FakeMarket(2) @@ -73,7 +75,7 @@ def is_market_spot_or_future(self, _): return True def get_path_to_root_fees(self): - return 0. + return 0.0 @property def future_markets(self): @@ -100,7 +102,7 @@ def now(self) -> DateTime: In this default implementation 'current time' is defined by the number of ticks that have passed. """ - return DateTime.now(tz=TIME_ZONE).start_of('day') + ( + return DateTime.now(tz=TIME_ZONE).start_of("day") + ( duration(hours=10) + self.config.tick_length * self.current_tick ) @@ -128,48 +130,54 @@ def __init__(self, count): def get_bids(self): return deepcopy(self.bids) - def bid(self, price: float, energy: float, buyer: str, original_price=None, - time_slot=None) -> Bid: - bid = Bid(id="bid_id", creation_time=now(), price=price, energy=energy, - buyer=buyer, - original_price=original_price, - time_slot=time_slot) + def bid( + self, price: float, energy: float, buyer: str, original_price=None, time_slot=None + ) -> Bid: + bid = Bid( + id="bid_id", + creation_time=now(), + price=price, + energy=energy, + buyer=buyer, + original_price=original_price, + time_slot=time_slot, + ) self.bids[bid.id] = bid return bid @property def offers(self): - return { - o.id: o for o in self.sorted_offers - } + return {o.id: o for o in self.sorted_offers} @property def sorted_offers(self): offers = [ # Energy price is 1 - [Offer('id', now(), 1, (MIN_BUY_ENERGY/1000), TraderDetails("A", "")), - # Energy price is 2 - Offer('id', now(), 2, (MIN_BUY_ENERGY/1000), TraderDetails("A", "")), - # Energy price is 3 - Offer('id', now(), 3, (MIN_BUY_ENERGY/1000), TraderDetails("A", "")), - # Energy price is 4 - Offer('id', now(), 4, (MIN_BUY_ENERGY/1000), TraderDetails("A", "")), - ], [ - Offer('id', now(), 1, (MIN_BUY_ENERGY * 0.033 / 1000), TraderDetails("A", "")), - Offer('id', now(), 2, (MIN_BUY_ENERGY * 0.033 / 1000), TraderDetails("A", "")) + Offer("id", now(), 1, (MIN_BUY_ENERGY / 1000), TraderDetails("A", "")), + # Energy price is 2 + Offer("id", now(), 2, (MIN_BUY_ENERGY / 1000), TraderDetails("A", "")), + # Energy price is 3 + Offer("id", now(), 3, (MIN_BUY_ENERGY / 1000), TraderDetails("A", "")), + # Energy price is 4 + Offer("id", now(), 4, (MIN_BUY_ENERGY / 1000), TraderDetails("A", "")), ], [ - Offer('id', now(), 1, 5, TraderDetails("A", "")), - Offer('id2', now(), 2, (MIN_BUY_ENERGY / 1000), TraderDetails("A", "")) - ] + Offer("id", now(), 1, (MIN_BUY_ENERGY * 0.033 / 1000), TraderDetails("A", "")), + Offer("id", now(), 2, (MIN_BUY_ENERGY * 0.033 / 1000), TraderDetails("A", "")), + ], + [ + Offer("id", now(), 1, 5, TraderDetails("A", "")), + Offer("id2", now(), 2, (MIN_BUY_ENERGY / 1000), TraderDetails("A", "")), + ], ] return offers[self.count] @property def most_affordable_offers(self): - return [Offer('id_affordable', now(), 1, - self.most_affordable_energy, TraderDetails("A", ""))] + return [ + Offer("id_affordable", now(), 1, self.most_affordable_energy, TraderDetails("A", "")) + ] @property def time_slot(self): @@ -180,9 +188,9 @@ def time_slot_str(self): return self.time_slot.strftime(TIME_FORMAT) def balancing_offer(self, price, energy, seller, market=None): - offer = BalancingOffer('id', now(), price, energy, seller, market) + offer = BalancingOffer("id", now(), price, energy, seller, market) self.created_balancing_offers.append(offer) - offer.id = 'id' + offer.id = "id" return offer def accept_offer(self, **kwargs): @@ -232,8 +240,8 @@ def market_test2(): @pytest.fixture def load_hours_strategy_test(called): strategy = LoadHoursStrategy( - avg_power_W=620, hrs_of_day=[8, 9, 10, 12], - initial_buying_rate=10) + avg_power_W=620, hrs_of_day=[8, 9, 10, 12], initial_buying_rate=10 + ) strategy.accept_offer = called return strategy @@ -255,8 +263,8 @@ def load_hours_strategy_test2(load_hours_strategy_test, area_test2): @pytest.fixture def load_hours_strategy_test4(): strategy = LoadHoursStrategy( - avg_power_W=620, hrs_of_day=[8, 9, 10, 12], - initial_buying_rate=10) + avg_power_W=620, hrs_of_day=[8, 9, 10, 12], initial_buying_rate=10 + ) strategy.accept_offer = Mock() return strategy @@ -272,17 +280,29 @@ def load_hours_strategy_test5(load_hours_strategy_test4, area_test2): def test_activate_event_populates_energy_requirement(load_hours_strategy_test1): load_hours_strategy_test1.event_activate() energy_requirement = load_hours_strategy_test1.state._energy_requirement_Wh - assert all([energy == load_hours_strategy_test1._energy_params.energy_per_slot_Wh - for energy in energy_requirement.values()]) - assert all([load_hours_strategy_test1.state._desired_energy_Wh[ts] == energy - for ts, energy in energy_requirement.items()]) + assert all( + [ + energy == load_hours_strategy_test1._energy_params.energy_per_slot_Wh + for energy in energy_requirement.values() + ] + ) + assert all( + [ + load_hours_strategy_test1.state._desired_energy_Wh[ts] == energy + for ts, energy in energy_requirement.items() + ] + ) # Test if daily energy requirement is calculated correctly for the device def test_calculate_daily_energy_req(load_hours_strategy_test1): load_hours_strategy_test1.event_activate() - assert all([energy == 620/4 - for energy in load_hours_strategy_test1.state._energy_requirement_Wh.values()]) + assert all( + [ + energy == 620 / 4 + for energy in load_hours_strategy_test1.state._energy_requirement_Wh.values() + ] + ) # Test if device accepts the most affordable offer @@ -290,16 +310,16 @@ def test_device_accepts_offer(load_hours_strategy_test1, market_test1): load_hours_strategy_test1.event_activate() load_hours_strategy_test1.event_market_cycle() cheapest_offer = market_test1.most_affordable_offers[0] - load_hours_strategy_test1.state._energy_requirement_Wh = \ - {market_test1.time_slot: cheapest_offer.energy * 1000 + 1} + load_hours_strategy_test1.state._energy_requirement_Wh = { + market_test1.time_slot: cheapest_offer.energy * 1000 + 1 + } load_hours_strategy_test1.event_tick() assert load_hours_strategy_test1.accept_offer.calls[0][0][1] == repr(cheapest_offer) def test_active_markets(load_hours_strategy_test1): load_hours_strategy_test1.event_activate() - assert load_hours_strategy_test1.active_markets == \ - load_hours_strategy_test1.area.all_markets + assert load_hours_strategy_test1.active_markets == load_hours_strategy_test1.area.all_markets def test_event_tick_updates_rates(load_hours_strategy_test1, market_test1): @@ -311,8 +331,10 @@ def test_event_tick_updates_rates(load_hours_strategy_test1, market_test1): number_of_markets = len(load_hours_strategy_test1.area.all_markets) # Test for all available market types (one-sided and two-sided markets) - available_market_types = (SpotMarketTypeEnum.ONE_SIDED.value, - SpotMarketTypeEnum.TWO_SIDED.value) + available_market_types = ( + SpotMarketTypeEnum.ONE_SIDED.value, + SpotMarketTypeEnum.TWO_SIDED.value, + ) # Bids' rates should be updated both when the load can buy energy and when it cannot do it for can_buy_energy in (True, False): load_hours_strategy_test1.state.can_buy_more_energy.return_value = can_buy_energy @@ -348,8 +370,10 @@ def test_event_tick(load_hours_strategy_test1, market_test1): load_hours_strategy_test1.event_activate() load_hours_strategy_test1.area.past_markets = {TIME: market_test1} load_hours_strategy_test1.event_market_cycle() - assert isclose(load_hours_strategy_test1.state._energy_requirement_Wh[TIME], - market_test1.most_affordable_energy * 1000) + assert isclose( + load_hours_strategy_test1.state._energy_requirement_Wh[TIME], + market_test1.most_affordable_energy * 1000, + ) load_hours_strategy_test1.event_tick() assert load_hours_strategy_test1.state._energy_requirement_Wh[TIME] == 0 @@ -363,7 +387,7 @@ def test_event_tick_with_partial_offer(load_hours_strategy_test2, market_test2): requirement = load_hours_strategy_test2.state._energy_requirement_Wh[TIME] / 1000 load_hours_strategy_test2.event_tick() assert load_hours_strategy_test2.state._energy_requirement_Wh[TIME] == 0 - assert float(load_hours_strategy_test2.accept_offer.calls[0][1]['energy']) == requirement + assert float(load_hours_strategy_test2.accept_offer.calls[0][1]["energy"]) == requirement def test_load_hours_constructor_rejects_incorrect_hrs_of_day(): @@ -371,25 +395,33 @@ def test_load_hours_constructor_rejects_incorrect_hrs_of_day(): LoadHoursStrategy(100, hrs_of_day=[12, 13, 24]) -def test_device_operating_hours_deduction_with_partial_trade(load_hours_strategy_test5, - market_test2): +def test_device_operating_hours_deduction_with_partial_trade( + load_hours_strategy_test5, market_test2 +): market_test2.most_affordable_energy = 0.1 load_hours_strategy_test5.event_activate() # load_hours_strategy_test5.area.past_markets = {TIME: market_test2} load_hours_strategy_test5.event_market_cycle() load_hours_strategy_test5.event_tick() - assert round((( - float(load_hours_strategy_test5.accept_offer.call_args[0][1].energy) * - 1000 / load_hours_strategy_test5._energy_params.energy_per_slot_Wh) * - (load_hours_strategy_test5.simulation_config.slot_length / duration(hours=1))), 2) == \ - round(((0.1/0.155) * 0.25), 2) - - -@pytest.mark.parametrize("partial", [None, Bid( - 'test_id', now(), 123, 321, TraderDetails("A", ""))]) -def test_event_bid_traded_removes_bid_for_partial_and_non_trade(load_hours_strategy_test5, - called, - partial): + assert round( + ( + ( + float(load_hours_strategy_test5.accept_offer.call_args[0][1].energy) + * 1000 + / load_hours_strategy_test5._energy_params.energy_per_slot_Wh + ) + * (load_hours_strategy_test5.simulation_config.slot_length / duration(hours=1)) + ), + 2, + ) == round(((0.1 / 0.155) * 0.25), 2) + + +@pytest.mark.parametrize( + "partial", [None, Bid("test_id", now(), 123, 321, TraderDetails("A", ""))] +) +def test_event_bid_traded_removes_bid_for_partial_and_non_trade( + load_hours_strategy_test5, called, partial +): ConstSettings.MASettings.MARKET_TYPE = 2 trade_market = load_hours_strategy_test5.area.spot_market @@ -402,20 +434,29 @@ def test_event_bid_traded_removes_bid_for_partial_and_non_trade(load_hours_strat # Increase energy requirement to cover the energy from the bid load_hours_strategy_test5.state._energy_requirement_Wh[TIME] = 1000 - trade = Trade('idt', None, TraderDetails("B", ""), - TraderDetails(load_hours_strategy_test5.owner.name, ""), bid=bid, - residual=partial, time_slot=TIME, traded_energy=1, trade_price=1) + trade = Trade( + "idt", + None, + TraderDetails("B", ""), + TraderDetails(load_hours_strategy_test5.owner.name, ""), + bid=bid, + residual=partial, + time_slot=TIME, + traded_energy=1, + trade_price=1, + ) load_hours_strategy_test5.event_bid_traded(market_id=trade_market.id, bid_trade=trade) assert len(load_hours_strategy_test5.remove_bid_from_pending.calls) == 1 assert load_hours_strategy_test5.remove_bid_from_pending.calls[0][0][1] == repr(bid.id) - assert load_hours_strategy_test5.remove_bid_from_pending.calls[0][0][0] == \ - repr(trade_market.id) + assert load_hours_strategy_test5.remove_bid_from_pending.calls[0][0][0] == repr( + trade_market.id + ) -def test_event_bid_traded_removes_bid_from_pending_if_energy_req_0(load_hours_strategy_test5, - market_test2, - called): +def test_event_bid_traded_removes_bid_from_pending_if_energy_req_0( + load_hours_strategy_test5, market_test2, called +): ConstSettings.MASettings.MARKET_TYPE = 2 trade_market = load_hours_strategy_test5.area.spot_market @@ -426,15 +467,24 @@ def test_event_bid_traded_removes_bid_from_pending_if_energy_req_0(load_hours_st bid = list(load_hours_strategy_test5._bids.values())[0][0] # Increase energy requirement to cover the energy from the bid + threshold load_hours_strategy_test5.state._energy_requirement_Wh[TIME] = bid.energy * 1000 + 0.000009 - trade = Trade('idt', None, TraderDetails("B", ""), - TraderDetails(load_hours_strategy_test5.owner.name, ""), residual=True, bid=bid, - time_slot=TIME, traded_energy=bid.energy, trade_price=bid.price) + trade = Trade( + "idt", + None, + TraderDetails("B", ""), + TraderDetails(load_hours_strategy_test5.owner.name, ""), + residual=True, + bid=bid, + time_slot=TIME, + traded_energy=bid.energy, + trade_price=bid.price, + ) load_hours_strategy_test5.event_bid_traded(market_id=trade_market.id, bid_trade=trade) assert len(load_hours_strategy_test5.remove_bid_from_pending.calls) == 1 assert load_hours_strategy_test5.remove_bid_from_pending.calls[0][0][1] == repr(bid.id) - assert load_hours_strategy_test5.remove_bid_from_pending.calls[0][0][0] == \ - repr(trade_market.id) + assert load_hours_strategy_test5.remove_bid_from_pending.calls[0][0][0] == repr( + trade_market.id + ) @pytest.fixture @@ -449,60 +499,78 @@ def balancing_fixture(load_hours_strategy_test5): DeviceRegistry.REGISTRY = {} -def test_balancing_offers_are_not_created_if_device_not_in_registry( - balancing_fixture, area_test2): +def test_balancing_offers_are_not_created_if_device_not_in_registry(balancing_fixture, area_test2): DeviceRegistry.REGISTRY = {} balancing_fixture.event_activate() balancing_fixture.event_market_cycle() assert len(area_test2.test_balancing_market.created_balancing_offers) == 0 -def test_balancing_offers_are_created_if_device_in_registry( - balancing_fixture, area_test2): +def test_balancing_offers_are_created_if_device_in_registry(balancing_fixture, area_test2): ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = True - DeviceRegistry.REGISTRY = {'FakeArea': (30, 40)} + DeviceRegistry.REGISTRY = {"FakeArea": (30, 40)} balancing_fixture.event_activate() balancing_fixture.event_market_cycle() balancing_fixture.event_balancing_market_cycle() - expected_balancing_demand_energy = \ - balancing_fixture.balancing_energy_ratio.demand * \ - balancing_fixture._energy_params.energy_per_slot_Wh - actual_balancing_demand_energy = \ - area_test2.test_balancing_market.created_balancing_offers[0].energy + expected_balancing_demand_energy = ( + balancing_fixture.balancing_energy_ratio.demand + * balancing_fixture._energy_params.energy_per_slot_Wh + ) + actual_balancing_demand_energy = area_test2.test_balancing_market.created_balancing_offers[ + 0 + ].energy assert len(area_test2.test_balancing_market.created_balancing_offers) == 1 assert actual_balancing_demand_energy == -expected_balancing_demand_energy - actual_balancing_demand_price = \ - area_test2.test_balancing_market.created_balancing_offers[0].price + actual_balancing_demand_price = area_test2.test_balancing_market.created_balancing_offers[ + 0 + ].price assert actual_balancing_demand_price == expected_balancing_demand_energy * 30 selected_offer = area_test2.current_market.sorted_offers[0] - balancing_fixture.state._energy_requirement_Wh[area_test2.current_market.time_slot] = \ + balancing_fixture.state._energy_requirement_Wh[area_test2.current_market.time_slot] = ( selected_offer.energy * 1000.0 - balancing_fixture.event_offer_traded(market_id=area_test2.current_market.id, - trade=Trade(id='id', - creation_time=area_test2.now, - offer=selected_offer, - traded_energy=selected_offer.energy, - trade_price=selected_offer.price, - seller=TraderDetails("B", ""), - buyer=TraderDetails("FakeArea", ""), - time_slot=area_test2.current_market.time_slot) - ) + ) + balancing_fixture.event_offer_traded( + market_id=area_test2.current_market.id, + trade=Trade( + id="id", + creation_time=area_test2.now, + offer=selected_offer, + traded_energy=selected_offer.energy, + trade_price=selected_offer.price, + seller=TraderDetails("B", ""), + buyer=TraderDetails("FakeArea", ""), + time_slot=area_test2.current_market.time_slot, + ), + ) assert len(area_test2.test_balancing_market.created_balancing_offers) == 2 - actual_balancing_supply_energy = \ - area_test2.test_balancing_market.created_balancing_offers[1].energy - expected_balancing_supply_energy = \ + actual_balancing_supply_energy = area_test2.test_balancing_market.created_balancing_offers[ + 1 + ].energy + expected_balancing_supply_energy = ( selected_offer.energy * balancing_fixture.balancing_energy_ratio.supply + ) assert actual_balancing_supply_energy == expected_balancing_supply_energy - actual_balancing_supply_price = \ - area_test2.test_balancing_market.created_balancing_offers[1].price + actual_balancing_supply_price = area_test2.test_balancing_market.created_balancing_offers[ + 1 + ].price assert actual_balancing_supply_price == expected_balancing_supply_energy * 40 DeviceRegistry.REGISTRY = {} -@pytest.mark.parametrize("use_mmr, expected_rate", [ - [True, 9, ], [False, 33, ] -]) +@pytest.mark.parametrize( + "use_mmr, expected_rate", + [ + [ + True, + 9, + ], + [ + False, + 33, + ], + ], +) def test_use_market_maker_rate_parameter_is_respected(use_mmr, expected_rate): original_mmr = GlobalConfig.market_maker_rate GlobalConfig.market_maker_rate = 9 @@ -514,17 +582,26 @@ def test_use_market_maker_rate_parameter_is_respected(use_mmr, expected_rate): GlobalConfig.market_maker_rate = original_mmr -@pytest.mark.parametrize("use_mmr, expected_rate", [ - [True, 9, ], [False, 33, ] -]) +@pytest.mark.parametrize( + "use_mmr, expected_rate", + [ + [ + True, + 9, + ], + [ + False, + 33, + ], + ], +) def test_use_market_maker_rate_parameter_is_respected_for_load_profiles(use_mmr, expected_rate): original_mmr = GlobalConfig.market_maker_rate GlobalConfig.market_maker_rate = 9 user_profile_path = os.path.join(gsye_root_path, "resources/Solar_Curve_W_sunny.csv") load = DefinedLoadStrategy( - daily_load_profile=user_profile_path, - final_buying_rate=33, - use_market_maker_rate=use_mmr) + daily_load_profile=user_profile_path, final_buying_rate=33, use_market_maker_rate=use_mmr + ) load.area = FakeArea() load.owner = load.area load.event_activate() @@ -539,23 +616,26 @@ def test_load_constructor_rejects_incorrect_rate_parameters(): with pytest.raises(GSyDeviceException): load.event_activate() with pytest.raises(GSyDeviceException): - LoadHoursStrategy(avg_power_W=100, fit_to_limit=True, - energy_rate_increase_per_update=1) + LoadHoursStrategy(avg_power_W=100, fit_to_limit=True, energy_rate_increase_per_update=1) with pytest.raises(GSyDeviceException): - LoadHoursStrategy(avg_power_W=100, fit_to_limit=False, - energy_rate_increase_per_update=-1) + LoadHoursStrategy(avg_power_W=100, fit_to_limit=False, energy_rate_increase_per_update=-1) def test_load_hour_strategy_increases_rate_when_fit_to_limit_is_false(market_test1): - load = LoadHoursStrategy(avg_power_W=100, initial_buying_rate=0, final_buying_rate=30, - fit_to_limit=False, energy_rate_increase_per_update=10, - update_interval=5) + load = LoadHoursStrategy( + avg_power_W=100, + initial_buying_rate=0, + final_buying_rate=30, + fit_to_limit=False, + energy_rate_increase_per_update=10, + update_interval=5, + ) load.area = FakeArea() load.owner = load.area load.event_activate() load.event_market_cycle() assert load.state._energy_requirement_Wh[TIME] == 25.0 - offer = Offer('id', now(), 1, (MIN_BUY_ENERGY/500), TraderDetails("A", ""), 1) + offer = Offer("id", now(), 1, (MIN_BUY_ENERGY / 500), TraderDetails("A", ""), 1) load._one_sided_market_event_tick(market_test1, offer) assert load.bid_update.get_updated_rate(TIME) == 0 assert load.state._energy_requirement_Wh[TIME] == 25.0 @@ -576,12 +656,19 @@ def load_hours_strategy_test3(area_test1): def test_assert_if_trade_rate_is_higher_than_bid_rate(load_hours_strategy_test3): market_id = 0 - load_hours_strategy_test3._bids[market_id] = \ - [Bid("bid_id", now(), 30, 1, buyer=TraderDetails("FakeArea", ""))] + load_hours_strategy_test3._bids[market_id] = [ + Bid("bid_id", now(), 30, 1, buyer=TraderDetails("FakeArea", "")) + ] expensive_bid = Bid("bid_id", now(), 31, 1, buyer=TraderDetails("FakeArea", "")) - trade = Trade("trade_id", "time", TraderDetails(load_hours_strategy_test3.owner.name, ""), - TraderDetails(load_hours_strategy_test3.owner.name, ""), bid=expensive_bid, - traded_energy=1, trade_price=31) + trade = Trade( + "trade_id", + "time", + TraderDetails(load_hours_strategy_test3.owner.name, ""), + TraderDetails(load_hours_strategy_test3.owner.name, ""), + bid=expensive_bid, + traded_energy=1, + trade_price=31, + ) with pytest.raises(AssertionError): load_hours_strategy_test3.event_offer_traded(market_id=market_id, trade=trade) @@ -612,27 +699,34 @@ def test_set_energy_measurement_of_last_market(utils_mock, load_hours_strategy_t load_hours_strategy_test1._set_energy_measurement_of_last_market() load_hours_strategy_test1.state.set_energy_measurement_kWh.assert_called_once_with( - 100, load_hours_strategy_test1.area.current_market.time_slot) + 100, load_hours_strategy_test1.area.current_market.time_slot + ) -@pytest.mark.parametrize("use_mmr, initial_buying_rate", [ - (True, 40), (False, 40)]) +@pytest.mark.parametrize("use_mmr, initial_buying_rate", [(True, 40), (False, 40)]) def test_predefined_load_strategy_rejects_incorrect_rate_parameters(use_mmr, initial_buying_rate): user_profile_path = os.path.join(gsye_root_path, "resources/Solar_Curve_W_sunny.csv") load = DefinedLoadStrategy( daily_load_profile=user_profile_path, initial_buying_rate=initial_buying_rate, - use_market_maker_rate=use_mmr) + use_market_maker_rate=use_mmr, + ) load.area = FakeArea() load.owner = load.area with pytest.raises(GSyDeviceException): load.event_activate() with pytest.raises(GSyDeviceException): - DefinedLoadStrategy(daily_load_profile=user_profile_path, fit_to_limit=True, - energy_rate_increase_per_update=1) + DefinedLoadStrategy( + daily_load_profile=user_profile_path, + fit_to_limit=True, + energy_rate_increase_per_update=1, + ) with pytest.raises(GSyDeviceException): - DefinedLoadStrategy(daily_load_profile=user_profile_path, fit_to_limit=False, - energy_rate_increase_per_update=-1) + DefinedLoadStrategy( + daily_load_profile=user_profile_path, + fit_to_limit=False, + energy_rate_increase_per_update=-1, + ) @pytest.fixture(name="load_hours_fixture") @@ -653,9 +747,15 @@ def test_event_bid_traded_calls_settlement_market_event_bid_traded(load_hours_fi although no spot market can be found by event_bid_traded.""" load_hours_fixture._settlement_market_strategy = Mock() bid = Bid("bid", None, 1, 1, TraderDetails("buyer", "")) - trade = Trade('idt', None, TraderDetails("B", ""), - TraderDetails(load_hours_fixture.owner.name, ""), bid=bid, - traded_energy=1, trade_price=1) + trade = Trade( + "idt", + None, + TraderDetails("B", ""), + TraderDetails(load_hours_fixture.owner.name, ""), + bid=bid, + traded_energy=1, + trade_price=1, + ) load_hours_fixture.event_bid_traded(market_id="not existing", bid_trade=trade) load_hours_fixture._settlement_market_strategy.event_bid_traded.assert_called_once() @@ -665,8 +765,14 @@ def test_event_offer_traded_calls_settlement_market_event_offer_traded(load_hour although no spot market can be found by event_offer_traded.""" load_hours_fixture._settlement_market_strategy = Mock() offer = Offer("oid", None, 1, 1, TraderDetails("seller", "")) - trade = Trade('idt', None, TraderDetails("B", ""), - TraderDetails(load_hours_fixture.owner.name, ""), offer=offer, - traded_energy=1, trade_price=1) + trade = Trade( + "idt", + None, + TraderDetails("B", ""), + TraderDetails(load_hours_fixture.owner.name, ""), + offer=offer, + traded_energy=1, + trade_price=1, + ) load_hours_fixture.event_offer_traded(market_id="not existing", trade=trade) load_hours_fixture._settlement_market_strategy.event_offer_traded.assert_called_once() diff --git a/tests/strategies/test_strategy_pv.py b/tests/strategies/test_strategy_pv.py index f0d7705f9..103b92846 100644 --- a/tests/strategies/test_strategy_pv.py +++ b/tests/strategies/test_strategy_pv.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + # pylint: disable=missing-function-docstring,protected-access import os import uuid @@ -24,13 +25,12 @@ import pendulum import pytest -from gsy_framework.constants_limits import ConstSettings, GlobalConfig +from gsy_framework.constants_limits import ConstSettings, GlobalConfig, TIME_FORMAT, TIME_ZONE from gsy_framework.data_classes import Offer, Trade, TraderDetails from gsy_framework.exceptions import GSyDeviceException from gsy_framework.utils import generate_market_slot_list from parameterized import parameterized -from gsy_e.constants import TIME_FORMAT, TIME_ZONE from gsy_e.gsy_e_core.util import gsye_root_path from gsy_e.models.config import create_simulation_config_from_global_config from gsy_e.models.strategy.predefined_pv import PVPredefinedStrategy, PVUserProfileStrategy @@ -52,6 +52,7 @@ def auto_fixture(): class FakeArea: """Fake class that mimics the Area class.""" + def __init__(self): self.config = create_simulation_config_from_global_config() self.current_tick = 2 @@ -81,7 +82,7 @@ def current_market(self): @staticmethod def get_path_to_root_fees(): - return 0. + return 0.0 @property def now(self) -> pendulum.DateTime: @@ -98,9 +99,7 @@ def now(self) -> pendulum.DateTime: @property def all_markets(self): - return [self.test_market, - self.test_market, - self.test_market] + return [self.test_market, self.test_market, self.test_market] @property def spot_market(self): @@ -120,24 +119,39 @@ def create_spot_market(self, time_slot): class FakeMarketTimeSlot: """Add fake market implementation that contains the time slot.""" + def __init__(self, time_slot): self.time_slot = time_slot class FakeMarket: """Fake class that mimics the Market class.""" + def __init__(self, count): self.count = count self.id = str(count) self.created_offers = [] self.offers = { - "id": Offer(id="id", creation_time=pendulum.now(), price=10, energy=0.5, - seller=TraderDetails("A", ""))} + "id": Offer( + id="id", + creation_time=pendulum.now(), + price=10, + energy=0.5, + seller=TraderDetails("A", ""), + ) + } def offer(self, price, energy, seller, original_price=None, time_slot=None): # pylint: disable=too-many-arguments - offer = Offer(str(uuid.uuid4()), pendulum.now(), price, energy, seller, - original_price, time_slot=time_slot) + offer = Offer( + str(uuid.uuid4()), + pendulum.now(), + price, + energy, + seller, + original_price, + time_slot=time_slot, + ) self.created_offers.append(offer) self.offers[offer.id] = offer return offer @@ -157,6 +171,7 @@ def delete_offer(_offer_id): class FakeTrade: """Fake class that mimics the Trade class.""" + def __init__(self, offer): self.offer = offer self.match_details = {"offer": offer, "bid": None} @@ -219,11 +234,16 @@ def testing_event_tick(pv_test2, market_test2, area_test2): assert len(pv_test2.offers.posted.items()) == 1 offer_id1 = list(pv_test2.offers.posted.keys())[0] offer1 = market_test2.offers[offer_id1] - assert market_test2.created_offers[0].price == \ - 29.9 * pv_test2.state._energy_production_forecast_kWh[TIME] - assert pv_test2.state._energy_production_forecast_kWh[ - pendulum.today(tz=TIME_ZONE).at(hour=0, minute=0, second=2) - ] == 0 + assert ( + market_test2.created_offers[0].price + == 29.9 * pv_test2.state._energy_production_forecast_kWh[TIME] + ) + assert ( + pv_test2.state._energy_production_forecast_kWh[ + pendulum.today(tz=TIME_ZONE).at(hour=0, minute=0, second=2) + ] + == 0 + ) area_test2.current_tick_in_slot = area_test2.config.ticks_per_slot - 2 pv_test2.event_tick() offer_id2 = list(pv_test2.offers.posted.keys())[0] @@ -249,8 +269,7 @@ def fixture_pv_test3(area_test3): p.area = area_test3 p.owner = area_test3 p.offers.posted = { - Offer("id", pendulum.now(), 1, 1, TraderDetails("FakeArea", "")): - area_test3.test_market.id + Offer("id", pendulum.now(), 1, 1, TraderDetails("FakeArea", "")): area_test3.test_market.id } return p @@ -284,25 +303,34 @@ def fixture_pv_test4(area_test3): p.area = area_test3 p.owner = area_test3 p.offers.posted = { - Offer(id="id", creation_time=TIME, price=20, - energy=1, seller=TraderDetails("FakeArea", "")): area_test3.test_market.id + Offer( + id="id", creation_time=TIME, price=20, energy=1, seller=TraderDetails("FakeArea", "") + ): area_test3.test_market.id } return p def testing_event_trade(area_test3, pv_test4): pv_test4.state._available_energy_kWh[area_test3.test_market.time_slot] = 1 - pv_test4.event_offer_traded(market_id=area_test3.test_market.id, - trade=Trade( - id="id", creation_time=pendulum.now(), - traded_energy=1, trade_price=20, - offer=Offer(id="id", creation_time=TIME, - price=20, - energy=1, seller=TraderDetails("FakeArea", "")), - seller=TraderDetails(area_test3.name, ""), - buyer=TraderDetails("buyer", ""), - time_slot=area_test3.test_market.time_slot) - ) + pv_test4.event_offer_traded( + market_id=area_test3.test_market.id, + trade=Trade( + id="id", + creation_time=pendulum.now(), + traded_energy=1, + trade_price=20, + offer=Offer( + id="id", + creation_time=TIME, + price=20, + energy=1, + seller=TraderDetails("FakeArea", ""), + ), + seller=TraderDetails(area_test3.name, ""), + buyer=TraderDetails("buyer", ""), + time_slot=area_test3.test_market.time_slot, + ), + ) assert len(pv_test4.offers.open) == 0 @@ -333,7 +361,8 @@ def fixture_area_test66(): @pytest.fixture(name="pv_test66") def fixture_pv_test66(area_test66): original_future_markets_duration = ( - ConstSettings.FutureMarketSettings.FUTURE_MARKET_DURATION_HOURS) + ConstSettings.FutureMarketSettings.FUTURE_MARKET_DURATION_HOURS + ) ConstSettings.FutureMarketSettings.FUTURE_MARKET_DURATION_HOURS = 0 p = PVStrategy() p.area = area_test66 @@ -341,7 +370,8 @@ def fixture_pv_test66(area_test66): p.offers.posted = {} yield p ConstSettings.FutureMarketSettings.FUTURE_MARKET_DURATION_HOURS = ( - original_future_markets_duration) + original_future_markets_duration + ) def testing_produced_energy_forecast_real_data(pv_test66): @@ -358,25 +388,32 @@ def __init__(self, time_of_day: str): self.total = 0 self.count = 0 self.time_of_day = time_of_day + morning_counts = _Counts("morning") afternoon_counts = _Counts("afternoon") evening_counts = _Counts("evening") - for (time, _power) in pv_test66.state._energy_production_forecast_kWh.items(): + for time, _power in pv_test66.state._energy_production_forecast_kWh.items(): if time < morning_time: morning_counts.total += 1 - morning_counts.count = morning_counts.count + 1 \ - if pv_test66.state._energy_production_forecast_kWh[time] == 0 \ + morning_counts.count = ( + morning_counts.count + 1 + if pv_test66.state._energy_production_forecast_kWh[time] == 0 else morning_counts.count + ) elif morning_time < time < afternoon_time: afternoon_counts.total += 1 - afternoon_counts.count = afternoon_counts.count + 1 \ - if pv_test66.state._energy_production_forecast_kWh[time] > 0.001 \ + afternoon_counts.count = ( + afternoon_counts.count + 1 + if pv_test66.state._energy_production_forecast_kWh[time] > 0.001 else afternoon_counts.count + ) elif time > afternoon_time: evening_counts.total += 1 - evening_counts.count = evening_counts.count + 1 \ - if pv_test66.state._energy_production_forecast_kWh[time] == 0 \ + evening_counts.count = ( + evening_counts.count + 1 + if pv_test66.state._energy_production_forecast_kWh[time] == 0 else evening_counts.count + ) total_count = morning_counts.total + afternoon_counts.total + evening_counts.total assert len(list(pv_test66.state._energy_production_forecast_kWh.items())) == total_count @@ -400,15 +437,21 @@ def __init__(self, time_of_day: str): # The pv sells its whole production at once if possible. # Make sure that it doesnt offer it again after selling. + def test_does_not_offer_sold_energy_again(pv_test6, market_test3): pv_test6.event_activate() pv_test6.event_market_cycle() - assert market_test3.created_offers[0].energy == \ - pv_test6.state._energy_production_forecast_kWh[TIME] + assert ( + market_test3.created_offers[0].energy + == pv_test6.state._energy_production_forecast_kWh[TIME] + ) fake_trade = FakeTrade(market_test3.created_offers[0]) fake_trade.seller = TraderDetails( - pv_test6.owner.name, fake_trade.seller.uuid, - fake_trade.seller.origin, fake_trade.seller.origin_uuid) + pv_test6.owner.name, + fake_trade.seller.uuid, + fake_trade.seller.origin, + fake_trade.seller.origin_uuid, + ) fake_trade.time_slot = market_test3.time_slot pv_test6.event_offer_traded(market_id=market_test3.id, trade=fake_trade) market_test3.created_offers = [] @@ -434,8 +477,9 @@ def fixture_pv_test7(area_test3): p = PVStrategy(panel_count=1, initial_selling_rate=30) p.area = area_test3 p.owner = area_test3 - p.offers.posted = {Offer( - "id", pendulum.now(), 1, 1, TraderDetails("FakeArea", "")): area_test3.test_market.id} + p.offers.posted = { + Offer("id", pendulum.now(), 1, 1, TraderDetails("FakeArea", "")): area_test3.test_market.id + } return p @@ -444,8 +488,9 @@ def fixture_pv_test8(area_test3): p = PVStrategy(panel_count=1, initial_selling_rate=30) p.area = area_test3 p.owner = area_test3 - p.offers.posted = {Offer( - "id", pendulum.now(), 1, 1, TraderDetails("FakeArea", "")): area_test3.test_market.id} + p.offers.posted = { + Offer("id", pendulum.now(), 1, 1, TraderDetails("FakeArea", "")): area_test3.test_market.id + } return p @@ -499,18 +544,27 @@ def test_initial_selling_rate(pv_strategy_test10, area_test10): pv_strategy_test10.event_activate() pv_strategy_test10.event_market_cycle() created_offer = area_test10.all_markets[0].created_offers[0] - assert created_offer.price/created_offer.energy == 25 - - -@parameterized.expand([ - [PVStrategy, True, 12, ], - [PVStrategy, False, 19, ], -]) + assert created_offer.price / created_offer.energy == 25 + + +@parameterized.expand( + [ + [ + PVStrategy, + True, + 12, + ], + [ + PVStrategy, + False, + 19, + ], + ] +) def test_use_mmr_parameter_is_respected1(strategy_type, use_mmr, expected_rate): original_mmr = GlobalConfig.market_maker_rate GlobalConfig.market_maker_rate = 12 - pv = strategy_type(initial_selling_rate=19, use_market_maker_rate=use_mmr, - capacity_kW=0.2) + pv = strategy_type(initial_selling_rate=19, use_market_maker_rate=use_mmr, capacity_kW=0.2) pv.area = FakeArea() pv.owner = pv.area pv.event_activate() @@ -518,15 +572,24 @@ def test_use_mmr_parameter_is_respected1(strategy_type, use_mmr, expected_rate): GlobalConfig.market_maker_rate = original_mmr -@parameterized.expand([ - [PVPredefinedStrategy, True, 12, ], - [PVPredefinedStrategy, False, 19, ], -]) +@parameterized.expand( + [ + [ + PVPredefinedStrategy, + True, + 12, + ], + [ + PVPredefinedStrategy, + False, + 19, + ], + ] +) def test_use_mmr_parameter_is_respected2(strategy_type, use_mmr, expected_rate): original_mmr = GlobalConfig.market_maker_rate GlobalConfig.market_maker_rate = 12 - pv = strategy_type(initial_selling_rate=19, use_market_maker_rate=use_mmr, - cloud_coverage=1) + pv = strategy_type(initial_selling_rate=19, use_market_maker_rate=use_mmr, cloud_coverage=1) pv.area = FakeArea() pv.owner = pv.area pv.event_activate() @@ -534,16 +597,25 @@ def test_use_mmr_parameter_is_respected2(strategy_type, use_mmr, expected_rate): GlobalConfig.market_maker_rate = original_mmr -@parameterized.expand([ - [True, 13, ], - [False, 17, ], -]) +@parameterized.expand( + [ + [ + True, + 13, + ], + [ + False, + 17, + ], + ] +) def test_use_mmr_parameter_is_respected_for_pv_profiles(use_mmr, expected_rate): original_mmr = GlobalConfig.market_maker_rate GlobalConfig.market_maker_rate = 13 user_profile_path = os.path.join(gsye_root_path, "resources/Solar_Curve_W_sunny.csv") pv = PVUserProfileStrategy( - power_profile=user_profile_path, initial_selling_rate=17, use_market_maker_rate=use_mmr) + power_profile=user_profile_path, initial_selling_rate=17, use_market_maker_rate=use_mmr + ) pv.area = FakeArea() pv.owner = pv.area pv.event_activate() @@ -562,12 +634,18 @@ def fixture_pv_test11(area_test3): def test_assert_if_trade_rate_is_lower_than_offer_rate(pv_test11): market_id = "market_id" pv_test11.offers.sold[market_id] = [ - Offer("offer_id", pendulum.now(), 30, 1, TraderDetails("FakeArea", ""))] + Offer("offer_id", pendulum.now(), 30, 1, TraderDetails("FakeArea", "")) + ] too_cheap_offer = Offer("offer_id", pendulum.now(), 29, 1, TraderDetails("FakeArea", "")) trade = Trade( - "trade_id", "time", TraderDetails(pv_test11.owner.name, ""), - TraderDetails("buyer", ""), offer=too_cheap_offer, - traded_energy=1, trade_price=1) + "trade_id", + "time", + TraderDetails(pv_test11.owner.name, ""), + TraderDetails("buyer", ""), + offer=too_cheap_offer, + traded_energy=1, + trade_price=1, + ) with pytest.raises(AssertionError): pv_test11.event_offer_traded(market_id=market_id, trade=trade) @@ -599,4 +677,5 @@ def test_set_energy_measurement_of_last_market(utils_mock, pv_strategy): pv_strategy._set_energy_measurement_of_last_market() pv_strategy.state.set_energy_measurement_kWh.assert_called_once_with( - 100, pv_strategy.area.current_market.time_slot) + 100, pv_strategy.area.current_market.time_slot + ) diff --git a/tests/strategies/test_strategy_pvpredefined.py b/tests/strategies/test_strategy_pvpredefined.py index 0a346071e..0a0dd1102 100644 --- a/tests/strategies/test_strategy_pvpredefined.py +++ b/tests/strategies/test_strategy_pvpredefined.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + # pylint: disable=redefined-outer-name, protected-access, missing-function-docstring # pylint: disable=missing-class-docstring, pointless-string-statement, no-self-use,global-statement import os @@ -25,7 +26,7 @@ import pendulum import pytest -from gsy_framework.constants_limits import ConstSettings, GlobalConfig +from gsy_framework.constants_limits import ConstSettings, GlobalConfig, TIME_ZONE, TIME_FORMAT from gsy_framework.data_classes import Offer, TraderDetails from gsy_framework.exceptions import GSyDeviceException from gsy_framework.read_user_profile import read_arbitrary_profile, InputProfileTypes @@ -33,7 +34,6 @@ from gsy_framework.enums import ConfigurationType from pendulum import DateTime, duration, datetime -from gsy_e.constants import TIME_ZONE, TIME_FORMAT from gsy_e.gsy_e_core.util import gsye_root_path, change_global_config from gsy_e.models.config import create_simulation_config_from_global_config from gsy_e.models.strategy.predefined_pv import PVPredefinedStrategy, PVUserProfileStrategy @@ -125,13 +125,26 @@ def __init__(self, count): self.id = str(count) self.created_offers = [] self.offers = { - "id": Offer(id="id", creation_time=pendulum.now(), price=10, energy=0.5, - seller=TraderDetails("A", ""))} + "id": Offer( + id="id", + creation_time=pendulum.now(), + price=10, + energy=0.5, + seller=TraderDetails("A", ""), + ) + } self._time_slot = TIME def offer(self, price, energy, seller, original_price=None, time_slot=None): - offer = Offer(str(uuid.uuid4()), pendulum.now(), price, energy, seller, - original_price, time_slot=time_slot) + offer = Offer( + str(uuid.uuid4()), + pendulum.now(), + price, + energy, + seller, + original_price, + time_slot=time_slot, + ) self.created_offers.append(offer) self.offers[offer.id] = offer return offer @@ -216,8 +229,11 @@ def pv_test3(area_test3): p = PVPredefinedStrategy(cloud_coverage=ConstSettings.PVSettings.DEFAULT_POWER_PROFILE) p.area = area_test3 p.owner = area_test3 - p.offers.posted = {Offer("id", pendulum.now(), 30, 1, - TraderDetails("FakeArea", "")): area_test3.test_market.id} + p.offers.posted = { + Offer( + "id", pendulum.now(), 30, 1, TraderDetails("FakeArea", "") + ): area_test3.test_market.id + } return p @@ -244,8 +260,13 @@ def pv_test4(area_test3, _called): p.area = area_test3 p.owner = area_test3 p.offers.posted = { - Offer(id="id", creation_time=pendulum.now(), price=20, energy=1, - seller=TraderDetails("FakeArea", "")): area_test3.test_market.id + Offer( + id="id", + creation_time=pendulum.now(), + price=20, + energy=1, + seller=TraderDetails("FakeArea", ""), + ): area_test3.test_market.id } return p @@ -304,26 +325,33 @@ def __init__(self, time): self.total = 0 self.count = 0 self.time = time + morning_counts = Counts("morning") afternoon_counts = Counts("afternoon") evening_counts = Counts("evening") - for (time, _) in pv_test66.state._energy_production_forecast_kWh.items(): + for time, _ in pv_test66.state._energy_production_forecast_kWh.items(): if time < morning_time: morning_counts.total += 1 - morning_counts.count = morning_counts.count + 1 \ - if pv_test66.state._energy_production_forecast_kWh[time] == 0 \ + morning_counts.count = ( + morning_counts.count + 1 + if pv_test66.state._energy_production_forecast_kWh[time] == 0 else morning_counts.count + ) elif morning_time < time < afternoon_time: afternoon_counts.total += 1 - afternoon_counts.count = afternoon_counts.count + 1 \ - if pv_test66.state._energy_production_forecast_kWh[time] > 0.001 \ + afternoon_counts.count = ( + afternoon_counts.count + 1 + if pv_test66.state._energy_production_forecast_kWh[time] > 0.001 else afternoon_counts.count + ) elif time > afternoon_time: evening_counts.total += 1 - evening_counts.count = evening_counts.count + 1 \ - if pv_test66.state._energy_production_forecast_kWh[time] == 0 \ + evening_counts.count = ( + evening_counts.count + 1 + if pv_test66.state._energy_production_forecast_kWh[time] == 0 else evening_counts.count + ) total_count = morning_counts.total + afternoon_counts.total + evening_counts.total assert len(list(pv_test66.state._energy_production_forecast_kWh.items())) == total_count @@ -343,12 +371,17 @@ def test_does_not_offer_sold_energy_again(pv_test6, market_test3): # pylint: disable = attribute-defined-outside-init pv_test6.event_activate() pv_test6.event_market_cycle() - assert market_test3.created_offers[0].energy == \ - pv_test6.state._energy_production_forecast_kWh[TIME] + assert ( + market_test3.created_offers[0].energy + == pv_test6.state._energy_production_forecast_kWh[TIME] + ) fake_trade = FakeTrade(market_test3.created_offers[0]) fake_trade.seller = TraderDetails( - pv_test6.owner.name, fake_trade.seller.uuid, - fake_trade.seller.origin, fake_trade.seller.origin_uuid) + pv_test6.owner.name, + fake_trade.seller.uuid, + fake_trade.seller.origin, + fake_trade.seller.origin_uuid, + ) fake_trade.time_slot = market_test3.time_slot pv_test6.event_offer_traded(market_id=market_test3.id, trade=fake_trade) market_test3.created_offers = [] @@ -396,8 +429,8 @@ def test_correct_interpolation_power_profile(): profile_path = pathlib.Path(gsye_root_path + "/resources/Solar_Curve_W_sunny.csv") profile = read_arbitrary_profile(InputProfileTypes.POWER_W, str(profile_path)) times = list(profile) - for ii in range(len(times)-1): - assert abs((times[ii]-times[ii+1]).in_seconds()) == slot_length * 60 + for ii in range(len(times) - 1): + assert abs((times[ii] - times[ii + 1]).in_seconds()) == slot_length * 60 GlobalConfig.slot_length = original_slot_length @@ -418,15 +451,18 @@ def test_pv_user_profile_constructor_rejects_incorrect_parameters(): with pytest.raises(GSyDeviceException): PVUserProfileStrategy(power_profile=user_profile_path, panel_count=-1) with pytest.raises(GSyDeviceException): - pv = PVUserProfileStrategy(power_profile=user_profile_path, - initial_selling_rate=5, final_selling_rate=15) + pv = PVUserProfileStrategy( + power_profile=user_profile_path, initial_selling_rate=5, final_selling_rate=15 + ) pv.event_activate() with pytest.raises(GSyDeviceException): - PVUserProfileStrategy(power_profile=user_profile_path, - fit_to_limit=True, energy_rate_decrease_per_update=1) + PVUserProfileStrategy( + power_profile=user_profile_path, fit_to_limit=True, energy_rate_decrease_per_update=1 + ) with pytest.raises(GSyDeviceException): - PVUserProfileStrategy(power_profile=user_profile_path, - fit_to_limit=False, energy_rate_decrease_per_update=-1) + PVUserProfileStrategy( + power_profile=user_profile_path, fit_to_limit=False, energy_rate_decrease_per_update=-1 + ) @pytest.mark.parametrize("is_canary", [False, True]) diff --git a/tests/strategies/test_strategy_storage.py b/tests/strategies/test_strategy_storage.py index a9054097a..4ebd7904b 100644 --- a/tests/strategies/test_strategy_storage.py +++ b/tests/strategies/test_strategy_storage.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + import logging from copy import deepcopy from logging import getLogger @@ -22,14 +23,18 @@ from uuid import uuid4 import pytest -from gsy_framework.constants_limits import ConstSettings, GlobalConfig +from gsy_framework.constants_limits import ( + ConstSettings, + GlobalConfig, + TIME_FORMAT, + FLOATING_POINT_TOLERANCE, + TIME_ZONE, +) from gsy_framework.data_classes import Offer, Trade, BalancingOffer, Bid, TraderDetails from gsy_framework.exceptions import GSyDeviceException from gsy_framework.read_user_profile import read_arbitrary_profile, InputProfileTypes from pendulum import Duration, DateTime, now -from gsy_e.constants import TIME_FORMAT, FLOATING_POINT_TOLERANCE -from gsy_e.constants import TIME_ZONE from gsy_e.gsy_e_core.device_registry import DeviceRegistry from gsy_e.gsy_e_core.util import change_global_config from gsy_e.models.config import SimulationConfig @@ -93,7 +98,7 @@ def cheapest_offers(self): [Offer("id", now(), 12, 0.4, TraderDetails("A", ""))], [Offer("id", now(), 20, 1, TraderDetails("A", ""))], [Offer("id", now(), 20, 5.1, TraderDetails("A", ""))], - [Offer("id", now(), 20, 5.1, TraderDetails("A", ""))] + [Offer("id", now(), 20, 5.1, TraderDetails("A", ""))], ] return offers[self.count] @@ -104,7 +109,7 @@ def past_markets(self): @property def now(self): return DateTime.now(tz=TIME_ZONE).start_of("day") + ( - self.config.tick_length * self.current_tick + self.config.tick_length * self.current_tick ) @property @@ -118,7 +123,7 @@ def config(self): slot_length=Duration(minutes=15), tick_length=Duration(seconds=15), market_maker_rate=ConstSettings.GeneralSettings.DEFAULT_MARKET_MAKER_RATE, - external_connection_enabled=False + external_connection_enabled=False, ) change_global_config(**configuration.__dict__) return configuration @@ -128,15 +133,23 @@ class FakeMarket: def __init__(self, count): self.count = count self.id = str(count) - self.trade = Trade("id", now(), TraderDetails("FakeArea", ""), - TraderDetails("buyer", ""), time_slot=self.time_slot, - offer=Offer("id", now(), 11.8, 0.5, TraderDetails("FakeArea", "")), - traded_energy=0.5, trade_price=11.8) + self.trade = Trade( + "id", + now(), + TraderDetails("FakeArea", ""), + TraderDetails("buyer", ""), + time_slot=self.time_slot, + offer=Offer("id", now(), 11.8, 0.5, TraderDetails("FakeArea", "")), + traded_energy=0.5, + trade_price=11.8, + ) self.created_offers = [] - self.offers = {"id": Offer("id", now(), 11.8, 0.5, TraderDetails("FakeArea", "")), - "id2": Offer("id2", now(), 20, 0.5, TraderDetails("A", "")), - "id3": Offer("id3", now(), 20, 1, TraderDetails("A", "")), - "id4": Offer("id4", now(), 19, 5.1, TraderDetails("A", ""))} + self.offers = { + "id": Offer("id", now(), 11.8, 0.5, TraderDetails("FakeArea", "")), + "id2": Offer("id2", now(), 20, 0.5, TraderDetails("A", "")), + "id3": Offer("id3", now(), 20, 1, TraderDetails("A", "")), + "id4": Offer("id4", now(), 19, 5.1, TraderDetails("A", "")), + } self.bids = {} self.created_balancing_offers = [] @@ -146,7 +159,7 @@ def sorted_offers(self): [Offer("id", now(), 11.8, 0.5, TraderDetails("A", ""))], [Offer("id2", now(), 20, 0.5, TraderDetails("A", ""))], [Offer("id3", now(), 20, 1, TraderDetails("A", ""))], - [Offer("id4", now(), 19, 5.1, TraderDetails("A", ""))] + [Offer("id4", now(), 19, 5.1, TraderDetails("A", ""))], ] return offers[self.count] @@ -184,6 +197,7 @@ def bid(self, price, energy, buyer, market=None, original_price=None): # Test if storage buys cheap energy + @pytest.fixture() def area_test1(): return FakeArea(0) @@ -191,9 +205,13 @@ def area_test1(): @pytest.fixture() def storage_strategy_test1(area_test1, called): - s = StorageStrategy(max_abs_battery_power_kW=2.01, - initial_buying_rate=23.6, final_buying_rate=23.6, - initial_selling_rate=23.7, final_selling_rate=23.7) + s = StorageStrategy( + max_abs_battery_power_kW=2.01, + initial_buying_rate=23.6, + final_buying_rate=23.6, + initial_selling_rate=23.7, + final_selling_rate=23.7, + ) s.owner = area_test1 s.area = area_test1 s.accept_offer = called @@ -207,7 +225,8 @@ def test_if_storage_buys_cheap_energy(storage_strategy_test1, area_test1): area_test1.current_tick += 310 storage_strategy_test1.event_tick() assert storage_strategy_test1.accept_offer.calls[0][0][1] == repr( - FakeMarket(0).sorted_offers[0]) + FakeMarket(0).sorted_offers[0] + ) """TEST2""" @@ -215,6 +234,7 @@ def test_if_storage_buys_cheap_energy(storage_strategy_test1, area_test1): # Test if storage doesn't buy energy for more than 30ct + @pytest.fixture() def area_test2(): return FakeArea(1) @@ -238,14 +258,14 @@ def test_if_storage_doesnt_buy_30ct(storage_strategy_test2, area_test2): def test_if_storage_doesnt_buy_above_break_even_point(storage_strategy_test2, area_test2): storage_strategy_test2.event_activate() storage_strategy_test2.break_even_buy = 10.0 - area_test2.current_market.offers = {"id": Offer("id", now(), 10.1, 1, - TraderDetails("FakeArea", ""), - 10.1)} + area_test2.current_market.offers = { + "id": Offer("id", now(), 10.1, 1, TraderDetails("FakeArea", ""), 10.1) + } storage_strategy_test2.event_tick() assert len(storage_strategy_test2.accept_offer.calls) == 0 - area_test2.current_market.offers = {"id": Offer("id", now(), 9.9, 1, - TraderDetails("FakeArea", ""), - 9.9)} + area_test2.current_market.offers = { + "id": Offer("id", now(), 9.9, 1, TraderDetails("FakeArea", ""), 9.9) + } storage_strategy_test2.event_tick() assert len(storage_strategy_test2.accept_offer.calls) == 0 @@ -256,6 +276,7 @@ def test_if_storage_doesnt_buy_above_break_even_point(storage_strategy_test2, ar # Test if storage doesn't buy for over avg price + @pytest.fixture() def area_test3(): return FakeArea(2) @@ -271,10 +292,12 @@ def storage_strategy_test3(area_test3, called): def test_if_storage_doesnt_buy_too_expensive(storage_strategy_test3, area_test3): - storage_strategy_test3.bid_update.initial_rate = \ - read_arbitrary_profile(InputProfileTypes.IDENTITY, 0) - storage_strategy_test3.bid_update.final_rate = \ - read_arbitrary_profile(InputProfileTypes.IDENTITY, 1) + storage_strategy_test3.bid_update.initial_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, 0 + ) + storage_strategy_test3.bid_update.final_rate = read_arbitrary_profile( + InputProfileTypes.IDENTITY, 1 + ) storage_strategy_test3.event_activate() storage_strategy_test3.event_tick() assert len(storage_strategy_test3.accept_offer.calls) == 0 @@ -309,8 +332,7 @@ def area_test4(): @pytest.fixture() def storage_strategy_test4(area_test4, called): - s = StorageStrategy(initial_soc=100, - battery_capacity_kWh=2.1) + s = StorageStrategy(initial_soc=100, battery_capacity_kWh=2.1) s.owner = area_test4 s.area = area_test4 s.accept_offer = called @@ -328,6 +350,7 @@ def test_if_storage_pays_respect_to_capacity_limits(storage_strategy_test4, area # Test if internal storage is handled correctly + @pytest.fixture() def area_test5(): return FakeArea(4) @@ -342,7 +365,7 @@ def storage_strategy_test5(area_test5, called): area_test5.past_market.offers = { "id": Offer("id", now(), 20, 1, TraderDetails("A", "")), "id2": Offer("id2", now(), 20, 3, TraderDetails("FakeArea", "")), - "id3": Offer("id3", now(), 100, 1, TraderDetails("FakeArea", "")) + "id3": Offer("id3", now(), 100, 1, TraderDetails("FakeArea", "")), } s.offers.bought_offer(area_test5.past_market.offers["id"], area_test5.past_market.id) @@ -365,6 +388,7 @@ def test_if_storage_handles_capacity_correctly(storage_strategy_test5, area_test # Test if trades are handled correctly + @pytest.fixture() def area_test6(): return FakeArea(0) @@ -387,13 +411,17 @@ def storage_strategy_test6(area_test6, market_test6, called): def test_if_trades_are_handled_correctly(storage_strategy_test6, market_test6): - storage_strategy_test6.area.get_future_market_from_id = ( - lambda _id: market_test6 if _id == market_test6.id else None) + storage_strategy_test6.area.get_future_market_from_id = lambda _id: ( + market_test6 if _id == market_test6.id else None + ) storage_strategy_test6.state.add_default_values_to_state_profiles( - [storage_strategy_test6.spot_market_time_slot]) + [storage_strategy_test6.spot_market_time_slot] + ) storage_strategy_test6.event_offer_traded(market_id=market_test6.id, trade=market_test6.trade) - assert (market_test6.trade.match_details["offer"] in - storage_strategy_test6.offers.sold[market_test6.id]) + assert ( + market_test6.trade.match_details["offer"] + in storage_strategy_test6.offers.sold[market_test6.id] + ) assert market_test6.trade.match_details["offer"] not in storage_strategy_test6.offers.open @@ -408,10 +436,15 @@ def area_test7(): @pytest.fixture() def storage_strategy_test7(area_test7): - s = StorageStrategy(initial_soc=99.667, battery_capacity_kWh=3.01, - max_abs_battery_power_kW=5.21, initial_buying_rate=31, - final_buying_rate=31, initial_selling_rate=32, - final_selling_rate=32) + s = StorageStrategy( + initial_soc=99.667, + battery_capacity_kWh=3.01, + max_abs_battery_power_kW=5.21, + initial_buying_rate=31, + final_buying_rate=31, + initial_selling_rate=32, + final_selling_rate=32, + ) s.owner = area_test7 s.area = area_test7 return s @@ -420,12 +453,16 @@ def storage_strategy_test7(area_test7): def test_sell_energy_function(storage_strategy_test7, area_test7: FakeArea): storage_strategy_test7.event_activate() sell_market = area_test7.spot_market - energy_sell_dict = \ - storage_strategy_test7.state._clamp_energy_to_sell_kWh([sell_market.time_slot]) + energy_sell_dict = storage_strategy_test7.state._clamp_energy_to_sell_kWh( + [sell_market.time_slot] + ) storage_strategy_test7.event_market_cycle() - assert (isclose(storage_strategy_test7.state.offered_sell_kWh[sell_market.time_slot], - energy_sell_dict[sell_market.time_slot], rel_tol=1e-03)) - assert (isclose(storage_strategy_test7.state.used_storage, 3.0, rel_tol=1e-03)) + assert isclose( + storage_strategy_test7.state.offered_sell_kWh[sell_market.time_slot], + energy_sell_dict[sell_market.time_slot], + rel_tol=1e-03, + ) + assert isclose(storage_strategy_test7.state.used_storage, 3.0, rel_tol=1e-03) assert len(storage_strategy_test7.offers.posted_in_market(sell_market.id)) > 0 @@ -434,15 +471,20 @@ def test_calculate_sell_energy_rate_lower_bound(storage_strategy_test7): storage_strategy_test7.event_activate() market = storage_strategy_test7.area.current_market final_selling_rate = storage_strategy_test7.offer_update.final_rate - assert (isclose(storage_strategy_test7.calculate_selling_rate(market), - final_selling_rate[market.time_slot])) + assert isclose( + storage_strategy_test7.calculate_selling_rate(market), final_selling_rate[market.time_slot] + ) @pytest.fixture() def storage_strategy_test7_1(area_test7): - s = StorageStrategy(initial_soc=99.67, battery_capacity_kWh=3.01, - max_abs_battery_power_kW=5.21, final_buying_rate=26, - final_selling_rate=27) + s = StorageStrategy( + initial_soc=99.67, + battery_capacity_kWh=3.01, + max_abs_battery_power_kW=5.21, + final_buying_rate=26, + final_selling_rate=27, + ) s.owner = area_test7 s.area = area_test7 return s @@ -451,37 +493,46 @@ def storage_strategy_test7_1(area_test7): def test_calculate_initial_sell_energy_rate_upper_bound(storage_strategy_test7_1): storage_strategy_test7_1.event_activate() market = storage_strategy_test7_1.area.current_market - market_maker_rate = \ - storage_strategy_test7_1.simulation_config.market_maker_rate[market.time_slot] + market_maker_rate = storage_strategy_test7_1.simulation_config.market_maker_rate[ + market.time_slot + ] assert storage_strategy_test7_1.calculate_selling_rate(market) == market_maker_rate @pytest.fixture() def storage_strategy_test7_3(area_test7): - s = StorageStrategy(initial_soc=19.96, battery_capacity_kWh=5.01, - max_abs_battery_power_kW=5.21, final_selling_rate=17, - initial_buying_rate=15, final_buying_rate=16) + s = StorageStrategy( + initial_soc=19.96, + battery_capacity_kWh=5.01, + max_abs_battery_power_kW=5.21, + final_selling_rate=17, + initial_buying_rate=15, + final_buying_rate=16, + ) s.owner = area_test7 s.area = area_test7 - s.offers.posted = {Offer("id", now(), - 30, 1, TraderDetails("FakeArea", "")): area_test7.current_market.id} + s.offers.posted = { + Offer("id", now(), 30, 1, TraderDetails("FakeArea", "")): area_test7.current_market.id + } s.market = area_test7.current_market return s -def test_calculate_energy_amount_to_sell_respects_min_allowed_soc(storage_strategy_test7_3, - area_test7): +def test_calculate_energy_amount_to_sell_respects_min_allowed_soc( + storage_strategy_test7_3, area_test7 +): storage_strategy_test7_3.event_activate() time_slot = area_test7.current_market.time_slot - energy_sell_dict = storage_strategy_test7_3.state._clamp_energy_to_sell_kWh( - [time_slot]) - target_energy = (storage_strategy_test7_3.state.used_storage - - storage_strategy_test7_3.state.pledged_sell_kWh[time_slot] - - storage_strategy_test7_3.state.offered_sell_kWh[time_slot] - - storage_strategy_test7_3.state.capacity - * storage_strategy_test7_3.state.min_allowed_soc_ratio) + energy_sell_dict = storage_strategy_test7_3.state._clamp_energy_to_sell_kWh([time_slot]) + target_energy = ( + storage_strategy_test7_3.state.used_storage + - storage_strategy_test7_3.state.pledged_sell_kWh[time_slot] + - storage_strategy_test7_3.state.offered_sell_kWh[time_slot] + - storage_strategy_test7_3.state.capacity + * storage_strategy_test7_3.state.min_allowed_soc_ratio + ) - assert (isclose(energy_sell_dict[time_slot], target_energy, rel_tol=1e-03)) + assert isclose(energy_sell_dict[time_slot], target_energy, rel_tol=1e-03) def test_clamp_energy_to_buy(storage_strategy_test7_3): @@ -489,8 +540,10 @@ def test_clamp_energy_to_buy(storage_strategy_test7_3): storage_strategy_test7_3.state._battery_energy_per_slot = 0.5 time_slot = storage_strategy_test7_3.market.time_slot storage_strategy_test7_3.state._clamp_energy_to_buy_kWh([time_slot]) - assert storage_strategy_test7_3.state.energy_to_buy_dict[time_slot] == \ - storage_strategy_test7_3.state._battery_energy_per_slot + assert ( + storage_strategy_test7_3.state.energy_to_buy_dict[time_slot] + == storage_strategy_test7_3.state._battery_energy_per_slot + ) # Reduce used storage below battery_energy_per_slot @@ -511,8 +564,7 @@ def area_test8(): @pytest.fixture() def storage_strategy_test8(area_test8): - s = StorageStrategy(initial_soc=99, battery_capacity_kWh=101, - max_abs_battery_power_kW=401) + s = StorageStrategy(initial_soc=99, battery_capacity_kWh=101, max_abs_battery_power_kW=401) s.owner = area_test8 s.area = area_test8 return s @@ -522,37 +574,51 @@ def test_sell_energy_function_with_stored_capacity(storage_strategy_test8, area_ storage_strategy_test8.event_activate() storage_strategy_test8.event_market_cycle() sell_market = area_test8.spot_market - assert abs(storage_strategy_test8.state.used_storage - - storage_strategy_test8.state.offered_sell_kWh[sell_market.time_slot] - - storage_strategy_test8.state.capacity * - storage_strategy_test8.state.min_allowed_soc_ratio) < FLOATING_POINT_TOLERANCE - assert (isclose(storage_strategy_test8.state.offered_sell_kWh[sell_market.time_slot], - 100 - storage_strategy_test8.state.capacity * - storage_strategy_test8.state.min_allowed_soc_ratio, rel_tol=1e-02)) - - assert (isclose(area_test8.spot_market.created_offers[0].energy, - 100 - storage_strategy_test8.state.capacity * - storage_strategy_test8.state.min_allowed_soc_ratio, rel_tol=1e-02)) - assert len(storage_strategy_test8.offers.posted_in_market( - area_test8.spot_market.id) - ) > 0 + assert ( + abs( + storage_strategy_test8.state.used_storage + - storage_strategy_test8.state.offered_sell_kWh[sell_market.time_slot] + - storage_strategy_test8.state.capacity + * storage_strategy_test8.state.min_allowed_soc_ratio + ) + < FLOATING_POINT_TOLERANCE + ) + assert isclose( + storage_strategy_test8.state.offered_sell_kWh[sell_market.time_slot], + 100 + - storage_strategy_test8.state.capacity + * storage_strategy_test8.state.min_allowed_soc_ratio, + rel_tol=1e-02, + ) + + assert isclose( + area_test8.spot_market.created_offers[0].energy, + 100 + - storage_strategy_test8.state.capacity + * storage_strategy_test8.state.min_allowed_soc_ratio, + rel_tol=1e-02, + ) + assert len(storage_strategy_test8.offers.posted_in_market(area_test8.spot_market.id)) > 0 """TEST9""" # Test if initial capacity is sold -def test_first_market_cycle_with_initial_capacity(storage_strategy_test8: StorageStrategy, - area_test8: FakeArea): +def test_first_market_cycle_with_initial_capacity( + storage_strategy_test8: StorageStrategy, area_test8: FakeArea +): storage_strategy_test8.event_activate() storage_strategy_test8.event_market_cycle() sell_market = area_test8.spot_market - assert (isclose(storage_strategy_test8.state.offered_sell_kWh[sell_market.time_slot], - 100.0 - storage_strategy_test8.state.capacity * - storage_strategy_test8.state.min_allowed_soc_ratio, rel_tol=1e-02)) - assert len(storage_strategy_test8.offers.posted_in_market( - area_test8.spot_market.id) - ) > 0 + assert isclose( + storage_strategy_test8.state.offered_sell_kWh[sell_market.time_slot], + 100.0 + - storage_strategy_test8.state.capacity + * storage_strategy_test8.state.min_allowed_soc_ratio, + rel_tol=1e-02, + ) + assert len(storage_strategy_test8.offers.posted_in_market(area_test8.spot_market.id)) > 0 """TEST10""" @@ -604,11 +670,13 @@ def test_free_storage_calculation_takes_into_account_storage_capacity(storage_st storage_strategy_test1.state.offered_buy_kWh[time_slot] = 14.0 storage_strategy_test1.state.capacity = capacity - assert storage_strategy_test1.state.free_storage(time_slot) == \ - storage_strategy_test1.state.capacity \ - + storage_strategy_test1.state.pledged_sell_kWh[time_slot] \ - - storage_strategy_test1.state.pledged_buy_kWh[time_slot] \ - - storage_strategy_test1.state.used_storage + assert ( + storage_strategy_test1.state.free_storage(time_slot) + == storage_strategy_test1.state.capacity + + storage_strategy_test1.state.pledged_sell_kWh[time_slot] + - storage_strategy_test1.state.pledged_buy_kWh[time_slot] + - storage_strategy_test1.state.used_storage + ) """TEST11""" @@ -621,9 +689,15 @@ def area_test11(): @pytest.fixture() def storage_strategy_test11(area_test11, called): - s = StorageStrategy(battery_capacity_kWh=100, initial_soc=50, - max_abs_battery_power_kW=1, initial_buying_rate=30, - final_buying_rate=30, initial_selling_rate=33, final_selling_rate=32) + s = StorageStrategy( + battery_capacity_kWh=100, + initial_soc=50, + max_abs_battery_power_kW=1, + initial_buying_rate=30, + final_buying_rate=30, + initial_selling_rate=33, + final_selling_rate=32, + ) s.owner = area_test11 s.area = area_test11 s.accept_offer = called @@ -631,8 +705,9 @@ def storage_strategy_test11(area_test11, called): return s -def test_storage_buys_partial_offer_and_respecting_battery_power(storage_strategy_test11, - area_test11): +def test_storage_buys_partial_offer_and_respecting_battery_power( + storage_strategy_test11, area_test11 +): storage_strategy_test11.event_activate() buy_market = area_test11.spot_market storage_strategy_test11.event_tick() @@ -642,7 +717,7 @@ def test_storage_buys_partial_offer_and_respecting_battery_power(storage_strateg # storage should not be able to buy energy after this tick because # self.state._battery_energy_per_slot is exceeded te = storage_strategy_test11.state.energy_to_buy_dict[buy_market.time_slot] - assert te == 0. + assert te == 0.0 assert len(storage_strategy_test11.accept_offer.calls) >= 1 @@ -653,10 +728,14 @@ def test_has_battery_reached_max_power(storage_strategy_test11): storage_strategy_test11.state.offered_sell_kWh[time_slot] = 5 storage_strategy_test11.state.pledged_buy_kWh[time_slot] = 5 storage_strategy_test11.state.offered_buy_kWh[time_slot] = 5 - assert storage_strategy_test11.state._has_battery_reached_max_discharge_power( - 1, time_slot) is True - assert storage_strategy_test11.state._has_battery_reached_max_discharge_power( - 0.25, time_slot) is False + assert ( + storage_strategy_test11.state._has_battery_reached_max_discharge_power(1, time_slot) + is True + ) + assert ( + storage_strategy_test11.state._has_battery_reached_max_discharge_power(0.25, time_slot) + is False + ) """TEST12""" @@ -674,8 +753,9 @@ def market_test7(): @pytest.fixture() def storage_strategy_test12(area_test12): - s = StorageStrategy(battery_capacity_kWh=5, max_abs_battery_power_kW=5, - cap_price_strategy=True) + s = StorageStrategy( + battery_capacity_kWh=5, max_abs_battery_power_kW=5, cap_price_strategy=True + ) s.owner = area_test12 s.area = area_test12 return s @@ -708,9 +788,14 @@ def market_test13(): @pytest.fixture() def storage_strategy_test13(area_test13, called): - s = StorageStrategy(battery_capacity_kWh=5, max_abs_battery_power_kW=5, - initial_selling_rate=35.1, final_selling_rate=35, - initial_buying_rate=34, final_buying_rate=34) + s = StorageStrategy( + battery_capacity_kWh=5, + max_abs_battery_power_kW=5, + initial_selling_rate=35.1, + final_selling_rate=35, + initial_buying_rate=34, + final_buying_rate=34, + ) s.owner = area_test13 s.area = area_test13 s.accept_offer = called @@ -719,17 +804,24 @@ def storage_strategy_test13(area_test13, called): def test_storage_event_trade(storage_strategy_test11, market_test13): storage_strategy_test11.state.add_default_values_to_state_profiles( - [storage_strategy_test11.spot_market_time_slot]) - storage_strategy_test11.event_offer_traded(market_id=market_test13.id, - trade=market_test13.trade) - assert storage_strategy_test11.state.pledged_sell_kWh[market_test13.time_slot] == \ - market_test13.trade.traded_energy - assert storage_strategy_test11.state.offered_sell_kWh[ - market_test13.time_slot] == -market_test13.trade.traded_energy + [storage_strategy_test11.spot_market_time_slot] + ) + storage_strategy_test11.event_offer_traded( + market_id=market_test13.id, trade=market_test13.trade + ) + assert ( + storage_strategy_test11.state.pledged_sell_kWh[market_test13.time_slot] + == market_test13.trade.traded_energy + ) + assert ( + storage_strategy_test11.state.offered_sell_kWh[market_test13.time_slot] + == -market_test13.trade.traded_energy + ) def test_balancing_offers_are_not_created_if_device_not_in_registry( - storage_strategy_test13, area_test13): + storage_strategy_test13, area_test13 +): DeviceRegistry.REGISTRY = {} ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = True storage_strategy_test13.event_activate() @@ -739,8 +831,7 @@ def test_balancing_offers_are_not_created_if_device_not_in_registry( ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = False -def test_balancing_offers_are_created_if_device_in_registry( - storage_strategy_test13, area_test13): +def test_balancing_offers_are_created_if_device_in_registry(storage_strategy_test13, area_test13): ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = True DeviceRegistry.REGISTRY = {"FakeArea": (30, 40)} storage_strategy_test13.event_activate() @@ -748,24 +839,31 @@ def test_balancing_offers_are_created_if_device_in_registry( storage_strategy_test13.event_balancing_market_cycle() storage_slot_market = storage_strategy_test13.area.spot_market assert len(area_test13.test_balancing_market.created_balancing_offers) == 2 - actual_balancing_demand_energy = \ - area_test13.test_balancing_market.created_balancing_offers[0].energy - - expected_balancing_demand_energy = \ - -1 * storage_strategy_test13.balancing_energy_ratio.demand * \ - storage_strategy_test13.state.free_storage(storage_slot_market.time_slot) + actual_balancing_demand_energy = area_test13.test_balancing_market.created_balancing_offers[ + 0 + ].energy + + expected_balancing_demand_energy = ( + -1 + * storage_strategy_test13.balancing_energy_ratio.demand + * storage_strategy_test13.state.free_storage(storage_slot_market.time_slot) + ) assert actual_balancing_demand_energy == expected_balancing_demand_energy - actual_balancing_demand_price = \ - area_test13.test_balancing_market.created_balancing_offers[0].price + actual_balancing_demand_price = area_test13.test_balancing_market.created_balancing_offers[ + 0 + ].price assert actual_balancing_demand_price == abs(expected_balancing_demand_energy) * 30 - actual_balancing_supply_energy = \ - area_test13.test_balancing_market.created_balancing_offers[1].energy - expected_balancing_supply_energy = \ - storage_strategy_test13.state.used_storage * \ - storage_strategy_test13.balancing_energy_ratio.supply + actual_balancing_supply_energy = area_test13.test_balancing_market.created_balancing_offers[ + 1 + ].energy + expected_balancing_supply_energy = ( + storage_strategy_test13.state.used_storage + * storage_strategy_test13.balancing_energy_ratio.supply + ) assert actual_balancing_supply_energy == expected_balancing_supply_energy - actual_balancing_supply_price = \ - area_test13.test_balancing_market.created_balancing_offers[1].price + actual_balancing_supply_price = area_test13.test_balancing_market.created_balancing_offers[ + 1 + ].price assert actual_balancing_supply_price == expected_balancing_supply_energy * 40 DeviceRegistry.REGISTRY = {} ConstSettings.BalancingSettings.ENABLE_BALANCING_MARKET = False @@ -786,9 +884,15 @@ def market_test14(): @pytest.fixture() def storage_strategy_test14(area_test14, called): - s = StorageStrategy(initial_soc=50, battery_capacity_kWh=30, - max_abs_battery_power_kW=10, initial_selling_rate=25, - final_selling_rate=24, initial_buying_rate=0, final_buying_rate=23.9) + s = StorageStrategy( + initial_soc=50, + battery_capacity_kWh=30, + max_abs_battery_power_kW=10, + initial_selling_rate=25, + final_selling_rate=24, + initial_buying_rate=0, + final_buying_rate=23.9, + ) s.owner = area_test14 s.area = area_test14 s.accept_offer = called @@ -817,10 +921,15 @@ def market_test15(): @pytest.fixture() def storage_strategy_test15(area_test15, called): - s = StorageStrategy(initial_soc=50, battery_capacity_kWh=30, - max_abs_battery_power_kW=10, initial_selling_rate=25, - final_selling_rate=25, initial_buying_rate=24, - final_buying_rate=24) + s = StorageStrategy( + initial_soc=50, + battery_capacity_kWh=30, + max_abs_battery_power_kW=10, + initial_selling_rate=25, + final_selling_rate=25, + initial_buying_rate=24, + final_buying_rate=24, + ) s.owner = area_test15 s.area = deepcopy(area_test15) s.area.parent = deepcopy(area_test15) @@ -836,55 +945,86 @@ def test_energy_origin(storage_strategy_test15, market_test15): storage_strategy_test15.event_activate() assert len(storage_strategy_test15.state._used_storage_share) == 1 assert storage_strategy_test15.state._used_storage_share[0] == EnergyOrigin( - ESSEnergyOrigin.EXTERNAL, 15) + ESSEnergyOrigin.EXTERNAL, 15 + ) # Validate that local energy origin is correctly registered current_market = storage_strategy_test15.area.current_market offer = Offer("id", now(), 20, 1.0, TraderDetails("OtherChildArea", "")) storage_strategy_test15._try_to_buy_offer(offer, current_market, 21) storage_strategy_test15.area.current_market.trade = Trade( - "id", now(), TraderDetails("OtherChildArea", ""), TraderDetails("Storage", ""), - offer=offer, traded_energy=1, trade_price=20) - storage_strategy_test15.event_offer_traded(market_id=market_test15.id, - trade=current_market.trade) + "id", + now(), + TraderDetails("OtherChildArea", ""), + TraderDetails("Storage", ""), + offer=offer, + traded_energy=1, + trade_price=20, + ) + storage_strategy_test15.event_offer_traded( + market_id=market_test15.id, trade=current_market.trade + ) assert len(storage_strategy_test15.state._used_storage_share) == 2 - assert storage_strategy_test15.state._used_storage_share == [EnergyOrigin( - ESSEnergyOrigin.EXTERNAL, 15), EnergyOrigin(ESSEnergyOrigin.LOCAL, 1)] + assert storage_strategy_test15.state._used_storage_share == [ + EnergyOrigin(ESSEnergyOrigin.EXTERNAL, 15), + EnergyOrigin(ESSEnergyOrigin.LOCAL, 1), + ] # Validate that local energy origin with the same seller / buyer is correctly registered offer = Offer("id", now(), 20, 2.0, TraderDetails("Storage", "")) storage_strategy_test15._try_to_buy_offer(offer, current_market, 21) storage_strategy_test15.area.current_market.trade = Trade( - "id", now(), TraderDetails("Storage", ""), TraderDetails("A", ""), + "id", + now(), + TraderDetails("Storage", ""), + TraderDetails("A", ""), time_slot=current_market.time_slot, - offer=offer, traded_energy=2, trade_price=20) + offer=offer, + traded_energy=2, + trade_price=20, + ) storage_strategy_test15.event_offer_traded( - market_id=market_test15.id, - trade=current_market.trade) + market_id=market_test15.id, trade=current_market.trade + ) assert len(storage_strategy_test15.state._used_storage_share) == 2 - assert storage_strategy_test15.state._used_storage_share == [EnergyOrigin( - ESSEnergyOrigin.EXTERNAL, 13), EnergyOrigin(ESSEnergyOrigin.LOCAL, 1)] + assert storage_strategy_test15.state._used_storage_share == [ + EnergyOrigin(ESSEnergyOrigin.EXTERNAL, 13), + EnergyOrigin(ESSEnergyOrigin.LOCAL, 1), + ] # Validate that external energy origin is correctly registered offer = Offer("id", now(), 20, 1.0, TraderDetails("FakeArea", "")) storage_strategy_test15._try_to_buy_offer(offer, current_market, 21) current_market.trade = Trade( - "id", now(), TraderDetails("FakeArea", ""), TraderDetails("Storage", ""), - offer=offer, traded_energy=1, trade_price=20) + "id", + now(), + TraderDetails("FakeArea", ""), + TraderDetails("Storage", ""), + offer=offer, + traded_energy=1, + trade_price=20, + ) storage_strategy_test15.event_offer_traded( - market_id=market_test15.id, - trade=current_market.trade) + market_id=market_test15.id, trade=current_market.trade + ) assert len(storage_strategy_test15.state._used_storage_share) == 3 - assert storage_strategy_test15.state._used_storage_share == [EnergyOrigin( - ESSEnergyOrigin.EXTERNAL, 13.0), EnergyOrigin(ESSEnergyOrigin.LOCAL, 1.0), - EnergyOrigin(ESSEnergyOrigin.EXTERNAL, 1.0)] + assert storage_strategy_test15.state._used_storage_share == [ + EnergyOrigin(ESSEnergyOrigin.EXTERNAL, 13.0), + EnergyOrigin(ESSEnergyOrigin.LOCAL, 1.0), + EnergyOrigin(ESSEnergyOrigin.EXTERNAL, 1.0), + ] def test_storage_strategy_increases_rate_when_fit_to_limit_is_false(): storage = StorageStrategy( fit_to_limit=False, - initial_selling_rate=30, final_selling_rate=25, energy_rate_decrease_per_update=1, - initial_buying_rate=10, final_buying_rate=20, energy_rate_increase_per_update=1) + initial_selling_rate=30, + final_selling_rate=25, + energy_rate_decrease_per_update=1, + initial_buying_rate=10, + final_buying_rate=20, + energy_rate_increase_per_update=1, + ) storage.area = FakeArea(1) storage.event_activate() assert all([rate == -1 for rate in storage.bid_update.energy_rate_change_per_update.values()]) @@ -905,10 +1045,18 @@ def storage_test11(area_test3): def test_assert_if_trade_rate_is_lower_than_offer_rate(storage_test11): market_id = "market_id" storage_test11.offers.sold[market_id] = [ - Offer("offer_id", now(), 30, 1, TraderDetails("FakeArea", ""))] + Offer("offer_id", now(), 30, 1, TraderDetails("FakeArea", "")) + ] too_cheap_offer = Offer("offer_id", now(), 29, 1, TraderDetails("FakeArea", "")) - trade = Trade("trade_id", now(), TraderDetails("FakeArea", ""), TraderDetails("buyer", ""), - offer=too_cheap_offer, traded_energy=1, trade_price=29) + trade = Trade( + "trade_id", + now(), + TraderDetails("FakeArea", ""), + TraderDetails("buyer", ""), + offer=too_cheap_offer, + traded_energy=1, + trade_price=29, + ) with pytest.raises(AssertionError): storage_test11.event_offer_traded(market_id=market_id, trade=trade) @@ -918,10 +1066,18 @@ def test_assert_if_trade_rate_is_higher_than_bid_rate(storage_test11): market_id = "2" storage_test11.area.spot_market.id = market_id storage_test11._bids[market_id] = [ - Bid("bid_id", now(), 30, 1, buyer=TraderDetails("FakeArea", ""))] + Bid("bid_id", now(), 30, 1, buyer=TraderDetails("FakeArea", "")) + ] expensive_bid = Bid("bid_id", now(), 31, 1, buyer=TraderDetails("FakeArea", "")) - trade = Trade("trade_id", now(), TraderDetails("FakeArea", ""), TraderDetails("FakeArea", ""), - bid=expensive_bid, traded_energy=1, trade_price=31, - time_slot=storage_test11.area.spot_market.time_slot) + trade = Trade( + "trade_id", + now(), + TraderDetails("FakeArea", ""), + TraderDetails("FakeArea", ""), + bid=expensive_bid, + traded_energy=1, + trade_price=31, + time_slot=storage_test11.area.spot_market.time_slot, + ) with pytest.raises(AssertionError): storage_test11.event_offer_traded(market_id=market_id, trade=trade) diff --git a/tests/strategies/test_virtual_heatpump.py b/tests/strategies/test_virtual_heatpump.py index 85a31fee7..dc3b48465 100644 --- a/tests/strategies/test_virtual_heatpump.py +++ b/tests/strategies/test_virtual_heatpump.py @@ -1,10 +1,9 @@ from math import isclose import pytest -from gsy_framework.constants_limits import ConstSettings, GlobalConfig +from gsy_framework.constants_limits import ConstSettings, GlobalConfig, FLOATING_POINT_TOLERANCE from pendulum import DateTime, UTC -from gsy_e.constants import FLOATING_POINT_TOLERANCE from gsy_e.models.area import Area from gsy_e.models.strategy.virtual_heatpump import VirtualHeatpumpStrategy from src.gsy_e.models.strategy.strategy_profile import global_objects diff --git a/tests/test_balancing_agent.py b/tests/test_balancing_agent.py index e766ab96c..ac052a52b 100644 --- a/tests/test_balancing_agent.py +++ b/tests/test_balancing_agent.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + # pylint: disable=missing-class-docstring # pylint: disable=missing-function-docstring @@ -26,7 +27,7 @@ from gsy_framework.constants_limits import ConstSettings from gsy_framework.data_classes import BalancingOffer, BalancingTrade, Offer, Trade, TraderDetails -from gsy_e.constants import TIME_ZONE +from gsy_framework.constants_limits import TIME_ZONE from gsy_e.gsy_e_core.exceptions import InvalidBalancingTradeException from gsy_e.models.strategy.market_agents.balancing_agent import BalancingAgent from tests.test_market_agent import FakeMarket @@ -71,22 +72,34 @@ def accept_offer(self, offer_or_id, buyer, energy=None, time=None, trade_rate: f if abs(energy) < abs(offer.energy): residual_energy = offer.energy - energy - residual = BalancingOffer("res", pendulum.now(), offer.price, residual_energy, - offer.seller) + residual = BalancingOffer( + "res", pendulum.now(), offer.price, residual_energy, offer.seller + ) traded = BalancingOffer(offer.id, pendulum.now(), offer.price, energy, offer.seller) return BalancingTrade( - "trade_id", time, traded.seller, buyer, residual=residual, offer=traded, - traded_energy=1, trade_price=1) + "trade_id", + time, + traded.seller, + buyer, + residual=residual, + offer=traded, + traded_energy=1, + trade_price=1, + ) - return BalancingTrade("trade_id", time, offer.seller, buyer, offer=offer, - traded_energy=1, trade_price=1) + return BalancingTrade( + "trade_id", time, offer.seller, buyer, offer=offer, traded_energy=1, trade_price=1 + ) @pytest.fixture(name="balancing_agent") def balancing_agent_fixture(): - lower_market = FakeBalancingMarket([ - BalancingOffer("id", pendulum.now(), 2, 2, TraderDetails("other", "")), - BalancingOffer("id", pendulum.now(), 2, -2, TraderDetails("other", ""))]) + lower_market = FakeBalancingMarket( + [ + BalancingOffer("id", pendulum.now(), 2, 2, TraderDetails("other", "")), + BalancingOffer("id", pendulum.now(), 2, -2, TraderDetails("other", "")), + ] + ) higher_market = FakeBalancingMarket([]) owner = FakeArea("owner") baa = BalancingAgent(owner=owner, lower_market=lower_market, higher_market=higher_market) @@ -94,12 +107,15 @@ def balancing_agent_fixture(): def test_baa_event_trade(balancing_agent): - trade = Trade("trade_id", - balancing_agent.lower_market.time_slot, - TraderDetails("someone_else", ""), - TraderDetails("MA owner", ""), - offer=Offer("A", pendulum.now(), 2, 2, TraderDetails("B", "")), - traded_energy=1, trade_price=1) + trade = Trade( + "trade_id", + balancing_agent.lower_market.time_slot, + TraderDetails("someone_else", ""), + TraderDetails("MA owner", ""), + offer=Offer("A", pendulum.now(), 2, 2, TraderDetails("B", "")), + traded_energy=1, + trade_price=1, + ) fake_spot_market = FakeMarket([]) fake_spot_market.set_time_slot(balancing_agent.lower_market.time_slot) balancing_agent.event_offer_traded(trade=trade, market_id=fake_spot_market.id) @@ -110,9 +126,12 @@ def test_baa_event_trade(balancing_agent): @pytest.fixture(name="balancing_agent_2") def balancing_agent_2_fixture(): - lower_market = FakeBalancingMarket([ - BalancingOffer("id", pendulum.now(), 2, 0.2, TraderDetails("other", "")), - BalancingOffer("id", pendulum.now(), 2, -0.2, TraderDetails("other", ""))]) + lower_market = FakeBalancingMarket( + [ + BalancingOffer("id", pendulum.now(), 2, 0.2, TraderDetails("other", "")), + BalancingOffer("id", pendulum.now(), 2, -0.2, TraderDetails("other", "")), + ] + ) higher_market = FakeBalancingMarket([]) owner = FakeArea("owner") baa = BalancingAgent(owner=owner, lower_market=lower_market, higher_market=higher_market) @@ -121,12 +140,15 @@ def balancing_agent_2_fixture(): def test_baa_unmatched_event_trade(balancing_agent_2): - trade = Trade("trade_id", - pendulum.now(tz=TIME_ZONE), - TraderDetails("someone_else", ""), - TraderDetails("owner", ""), - offer=Offer("A", pendulum.now(), 2, 2, TraderDetails("B", "")), - traded_energy=1, trade_price=1) + trade = Trade( + "trade_id", + pendulum.now(tz=TIME_ZONE), + TraderDetails("someone_else", ""), + TraderDetails("owner", ""), + offer=Offer("A", pendulum.now(), 2, 2, TraderDetails("B", "")), + traded_energy=1, + trade_price=1, + ) fake_spot_market = FakeMarket([]) fake_spot_market.set_time_slot(balancing_agent_2.lower_market.time_slot) balancing_agent_2.owner.fake_spot_market = fake_spot_market diff --git a/tests/test_global_objects.py b/tests/test_global_objects.py index 48dab6b4d..915449322 100644 --- a/tests/test_global_objects.py +++ b/tests/test_global_objects.py @@ -15,13 +15,14 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + from unittest.mock import MagicMock from pendulum import duration, today from gsy_framework.constants_limits import ConstSettings from gsy_framework.enums import SpotMarketTypeEnum -from gsy_e.constants import TIME_ZONE +from gsy_framework.constants_limits import TIME_ZONE from gsy_e.gsy_e_core.global_stats import ExternalConnectionGlobalStatistics from gsy_e.gsy_e_core.redis_connections.area_market import ExternalConnectionCommunicator from gsy_e.models.area import Area @@ -39,23 +40,38 @@ def setup_method(self): self.config = MagicMock(spec=SimulationConfig) self.config.slot_length = duration(minutes=15) self.config.tick_length = duration(seconds=15) - self.config.ticks_per_slot = int(self.config.slot_length.seconds / - self.config.tick_length.seconds) + self.config.ticks_per_slot = int( + self.config.slot_length.seconds / self.config.tick_length.seconds + ) self.config.start_date = today(tz=TIME_ZONE) self.config.sim_duration = duration(days=1) self.config.grid_fee_type = 1 self.config.end_date = self.config.start_date + self.config.sim_duration self.config.capacity_kW = 1 - self.config.external_redis_communicator = \ - MagicMock(spec=ExternalConnectionCommunicator(True)) - self.storage = Area("Storage", strategy=StorageExternalStrategy(), config=self.config, - external_connection_available=True) - self.load = Area("Load", strategy=LoadHoursExternalStrategy(avg_power_W=100), - config=self.config, external_connection_available=True) - self.pv = Area("PV", strategy=PVExternalStrategy(), config=self.config, - external_connection_available=True) - self.house_area = Area("House", children=[self.storage, self.load, self.pv], - config=self.config) + self.config.external_redis_communicator = MagicMock( + spec=ExternalConnectionCommunicator(True) + ) + self.storage = Area( + "Storage", + strategy=StorageExternalStrategy(), + config=self.config, + external_connection_available=True, + ) + self.load = Area( + "Load", + strategy=LoadHoursExternalStrategy(avg_power_W=100), + config=self.config, + external_connection_available=True, + ) + self.pv = Area( + "PV", + strategy=PVExternalStrategy(), + config=self.config, + external_connection_available=True, + ) + self.house_area = Area( + "House", children=[self.storage, self.load, self.pv], config=self.config + ) self.grid_area = Area("Grid", children=[self.house_area], config=self.config) self.grid_area.activate() @@ -69,60 +85,73 @@ def test_global_objects_area_stats_tree_dict_general_structure(self): go.update() expected_area_stats_tree_dict = { - self.grid_area.uuid: - {"last_market_bill": {"accumulated_trades": {}, "external_trades": {}}, - "last_market_stats": {"min_trade_rate": None, "max_trade_rate": None, - "avg_trade_rate": None, "median_trade_rate": None, - "total_traded_energy_kWh": None}, - "last_market_fee": 0.0, - "current_market_fee": None, - "area_name": "Grid", - "children": { - self.house_area.uuid: { - "last_market_bill": {"accumulated_trades": {}, - "external_trades": {}}, - "last_market_stats": {"min_trade_rate": None, - "max_trade_rate": None, - "avg_trade_rate": None, - "median_trade_rate": None, - "total_traded_energy_kWh": None}, - "last_market_fee": 0.0, - "current_market_fee": None, - "area_name": "House", - "children": { - self.storage.uuid: { - "asset_info": {"energy_to_sell": 0.0, - "energy_active_in_bids": 0, - "energy_to_buy": 1.08, - "energy_active_in_offers": 0, - "free_storage": 1.08, - "used_storage": 0.12, - "energy_traded": 0.0, - "total_cost": 0.0}, - "last_slot_asset_info": {"energy_traded": 0.0, - "total_cost": 0.0}, - "asset_bill": None, - "area_name": "Storage"}, - self.load.uuid: {"asset_info": { - "energy_requirement_kWh": 0.025, - "energy_active_in_bids": 0.0, - "energy_traded": 0.0, - "total_cost": 0.0}, - "last_slot_asset_info": { - "energy_traded": 0.0, - "total_cost": 0.0}, - "asset_bill": None, - "area_name": "Load"}, - self.pv.uuid: {"asset_info": { - "available_energy_kWh": 0.0, - "energy_active_in_offers": 0, - "energy_traded": 0, - "total_cost": 0}, - "last_slot_asset_info": { - "energy_traded": 0, - "total_cost": 0}, - "asset_bill": None, - "area_name": "PV" - }}}}}} + self.grid_area.uuid: { + "last_market_bill": {"accumulated_trades": {}, "external_trades": {}}, + "last_market_stats": { + "min_trade_rate": None, + "max_trade_rate": None, + "avg_trade_rate": None, + "median_trade_rate": None, + "total_traded_energy_kWh": None, + }, + "last_market_fee": 0.0, + "current_market_fee": None, + "area_name": "Grid", + "children": { + self.house_area.uuid: { + "last_market_bill": {"accumulated_trades": {}, "external_trades": {}}, + "last_market_stats": { + "min_trade_rate": None, + "max_trade_rate": None, + "avg_trade_rate": None, + "median_trade_rate": None, + "total_traded_energy_kWh": None, + }, + "last_market_fee": 0.0, + "current_market_fee": None, + "area_name": "House", + "children": { + self.storage.uuid: { + "asset_info": { + "energy_to_sell": 0.0, + "energy_active_in_bids": 0, + "energy_to_buy": 1.08, + "energy_active_in_offers": 0, + "free_storage": 1.08, + "used_storage": 0.12, + "energy_traded": 0.0, + "total_cost": 0.0, + }, + "last_slot_asset_info": {"energy_traded": 0.0, "total_cost": 0.0}, + "asset_bill": None, + "area_name": "Storage", + }, + self.load.uuid: { + "asset_info": { + "energy_requirement_kWh": 0.025, + "energy_active_in_bids": 0.0, + "energy_traded": 0.0, + "total_cost": 0.0, + }, + "last_slot_asset_info": {"energy_traded": 0.0, "total_cost": 0.0}, + "asset_bill": None, + "area_name": "Load", + }, + self.pv.uuid: { + "asset_info": { + "available_energy_kWh": 0.0, + "energy_active_in_offers": 0, + "energy_traded": 0, + "total_cost": 0, + }, + "last_slot_asset_info": {"energy_traded": 0, "total_cost": 0}, + "asset_bill": None, + "area_name": "PV", + }, + }, + } + }, + } + } assert expected_area_stats_tree_dict == go.area_stats_tree_dict diff --git a/tests/test_gsy_core/test_rq_job_handler.py b/tests/test_gsy_core/test_rq_job_handler.py index f7c241c8a..43e93328d 100644 --- a/tests/test_gsy_core/test_rq_job_handler.py +++ b/tests/test_gsy_core/test_rq_job_handler.py @@ -5,7 +5,7 @@ import pytest from pendulum import duration, now, datetime -from gsy_framework.constants_limits import GlobalConfig, ConstSettings +from gsy_framework.constants_limits import GlobalConfig, ConstSettings, TIME_ZONE from gsy_framework.enums import ConfigurationType, CoefficientAlgorithm import gsy_e.constants @@ -119,9 +119,7 @@ def test_past_market_slots_handles_settings_correctly(run_sim_mock: Mock): assert config.tick_length == duration(seconds=20) assert config.start_date == datetime(2023, 1, 1) expected_end_date = ( - now(tz=gsy_e.constants.TIME_ZONE) - .subtract(hours=ConstSettings.SCMSettings.HOURS_OF_DELAY) - .add(hours=4) + now(tz=TIME_ZONE).subtract(hours=ConstSettings.SCMSettings.HOURS_OF_DELAY).add(hours=4) ) assert config.end_date.replace(second=0, microsecond=0) == expected_end_date.replace( second=0, microsecond=0 diff --git a/tests/test_market_agent.py b/tests/test_market_agent.py index 0ce037d1e..bd5ce18e8 100644 --- a/tests/test_market_agent.py +++ b/tests/test_market_agent.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ + # pylint: disable=missing-class-docstring # pylint: disable=missing-function-docstring @@ -28,7 +29,7 @@ from gsy_framework.constants_limits import ConstSettings, GlobalConfig from gsy_framework.data_classes import Bid, MarketClearingState, Offer, Trade, TraderDetails -from gsy_e.constants import TIME_FORMAT, TIME_ZONE +from gsy_framework.constants_limits import TIME_FORMAT, TIME_ZONE from gsy_e.models.market import GridFee from gsy_e.models.market.grid_fees.base_model import GridFees from gsy_e.models.strategy.market_agents.one_sided_agent import OneSidedAgent @@ -102,21 +103,35 @@ def accept_offer(self, offer_or_id, buyer, *, energy=None, time=None, trade_bid_ if energy < offer.energy: residual_energy = offer.energy - energy residual = Offer( - "res", offer.creation_time, offer.price, residual_energy, offer.seller) + "res", offer.creation_time, offer.price, residual_energy, offer.seller + ) traded = Offer(offer.id, offer.creation_time, offer.price, energy, offer.seller) - return Trade("trade_id", time, traded.seller, - TraderDetails(buyer, ""), - residual=residual, offer=traded, - traded_energy=1, trade_price=1) - - return Trade("trade_id", time, offer.seller, - TraderDetails(buyer, ""), - offer=offer, traded_energy=1, trade_price=1) + return Trade( + "trade_id", + time, + traded.seller, + TraderDetails(buyer, ""), + residual=residual, + offer=traded, + traded_energy=1, + trade_price=1, + ) + + return Trade( + "trade_id", + time, + offer.seller, + TraderDetails(buyer, ""), + offer=offer, + traded_energy=1, + trade_price=1, + ) # pylint: disable=unused-argument # pylint: disable=too-many-locals - def accept_bid(self, bid, energy, seller, buyer=None, *, - time=None, trade_offer_info=None, offer=None): + def accept_bid( + self, bid, energy, seller, buyer=None, *, time=None, trade_offer_info=None, offer=None + ): self.calls_energy_bids.append(energy) self.calls_bids.append(bid) self.calls_bids_price.append(bid.price) @@ -132,13 +147,21 @@ def accept_bid(self, bid, energy, seller, buyer=None, *, residual_energy = bid.energy - energy residual = Bid("res", bid.creation_time, bid.price, residual_energy, bid.buyer) traded = Bid(bid.id, bid.creation_time, (trade_rate * energy), energy, bid.buyer) - return Trade("trade_id", time, seller, - bid.buyer, bid=traded, residual=residual, - traded_energy=1, trade_price=1) + return Trade( + "trade_id", + time, + seller, + bid.buyer, + bid=traded, + residual=residual, + traded_energy=1, + trade_price=1, + ) traded = Bid(bid.id, bid.creation_time, (trade_rate * energy), energy, bid.buyer) - return Trade("trade_id", time, seller, - bid.buyer, bid=traded, traded_energy=1, trade_price=1) + return Trade( + "trade_id", time, seller, bid.buyer, bid=traded, traded_energy=1, trade_price=1 + ) def delete_offer(self, *args): pass @@ -150,12 +173,19 @@ def _update_new_offer_price_with_fee(self, offer_price, original_price, energy): return offer_price + self.fee_class.grid_fee_rate * original_price def _update_new_bid_price_with_fee(self, bid_price, original_price): - return self.fee_class.update_incoming_bid_with_fee( - bid_price, original_price) - - def offer(self, price: float, energy: float, seller: TraderDetails, offer_id=None, - original_price=None, dispatch_event=True, adapt_price_with_fees=True, - time_slot=None) -> Offer: + return self.fee_class.update_incoming_bid_with_fee(bid_price, original_price) + + def offer( + self, + price: float, + energy: float, + seller: TraderDetails, + offer_id=None, + original_price=None, + dispatch_event=True, + adapt_price_with_fees=True, + time_slot=None, + ) -> Offer: self.offer_call_count += 1 if original_price is None: @@ -176,9 +206,17 @@ def dispatch_market_offer_event(self, offer): def dispatch_market_bid_event(self, bid): pass - def bid(self, price: float, energy: float, buyer: TraderDetails, - bid_id: str = None, original_price=None, dispatch_event=True, - adapt_price_with_fees=True, time_slot=None): + def bid( + self, + price: float, + energy: float, + buyer: TraderDetails, + bid_id: str = None, + original_price=None, + dispatch_event=True, + adapt_price_with_fees=True, + time_slot=None, + ): self.bid_call_count += 1 if original_price is None: @@ -190,8 +228,7 @@ def bid(self, price: float, energy: float, buyer: TraderDetails, if adapt_price_with_fees: price = self._update_new_bid_price_with_fee(price, original_price) - bid = Bid(bid_id, pendulum.now(), price, energy, buyer, - original_price=original_price) + bid = Bid(bid_id, pendulum.now(), price, energy, buyer, original_price=original_price) self._bids.append(bid) self.forwarded_bid = bid @@ -200,43 +237,53 @@ def bid(self, price: float, energy: float, buyer: TraderDetails, def split_offer(self, original_offer, energy, orig_offer_price): self.offers.pop(original_offer.id, None) # same offer id is used for the new accepted_offer - accepted_offer = self.offer(offer_id=original_offer.id, - price=original_offer.price * (energy / original_offer.energy), - energy=energy, - seller=original_offer.seller, - dispatch_event=False) + accepted_offer = self.offer( + offer_id=original_offer.id, + price=original_offer.price * (energy / original_offer.energy), + energy=energy, + seller=original_offer.seller, + dispatch_event=False, + ) residual_price = (1 - energy / original_offer.energy) * original_offer.price residual_energy = original_offer.energy - energy - original_residual_price = \ - ((original_offer.energy - energy) / original_offer.energy) * orig_offer_price - - residual_offer = self.offer(price=residual_price, - energy=residual_energy, - seller=original_offer.seller, - original_price=original_residual_price, - dispatch_event=False, - adapt_price_with_fees=False) + original_residual_price = ( + (original_offer.energy - energy) / original_offer.energy + ) * orig_offer_price + + residual_offer = self.offer( + price=residual_price, + energy=residual_energy, + seller=original_offer.seller, + original_price=original_residual_price, + dispatch_event=False, + adapt_price_with_fees=False, + ) return accepted_offer, residual_offer def split_bid(self, original_bid, energy, orig_bid_price): self.offers.pop(original_bid.id, None) # same offer id is used for the new accepted_offer - accepted_bid = self.bid(bid_id=original_bid.id, - buyer=original_bid.buyer, - price=original_bid.price * (energy / original_bid.energy), - energy=energy) + accepted_bid = self.bid( + bid_id=original_bid.id, + buyer=original_bid.buyer, + price=original_bid.price * (energy / original_bid.energy), + energy=energy, + ) residual_price = (1 - energy / original_bid.energy) * original_bid.price residual_energy = original_bid.energy - energy - original_residual_price = \ - ((original_bid.energy - energy) / original_bid.energy) * orig_bid_price - - residual_bid = self.bid(price=residual_price, - buyer=original_bid.buyer, - energy=residual_energy, - original_price=original_residual_price, - adapt_price_with_fees=False) + original_residual_price = ( + (original_bid.energy - energy) / original_bid.energy + ) * orig_bid_price + + residual_bid = self.bid( + price=residual_price, + buyer=original_bid.buyer, + energy=residual_energy, + original_price=original_residual_price, + adapt_price_with_fees=False, + ) return accepted_bid, residual_bid @@ -250,17 +297,21 @@ def teardown_method(): @staticmethod @pytest.fixture(name="market_agent") def market_agent_fixture(): - lower_market = FakeMarket([ - Offer("id", pendulum.now(), 1, 1, TraderDetails("other", ""), 1)], - transfer_fees=GridFee(grid_fee_percentage=0.1, - grid_fee_const=2)) - higher_market = FakeMarket([ - Offer("id2", pendulum.now(), 3, 3, TraderDetails("owner", ""), 3), - Offer("id3", pendulum.now(), 0.5, 1, TraderDetails("owner", ""), 0.5)], - transfer_fees=GridFee(grid_fee_percentage=0.1, grid_fee_const=2)) + lower_market = FakeMarket( + [Offer("id", pendulum.now(), 1, 1, TraderDetails("other", ""), 1)], + transfer_fees=GridFee(grid_fee_percentage=0.1, grid_fee_const=2), + ) + higher_market = FakeMarket( + [ + Offer("id2", pendulum.now(), 3, 3, TraderDetails("owner", ""), 3), + Offer("id3", pendulum.now(), 0.5, 1, TraderDetails("owner", ""), 0.5), + ], + transfer_fees=GridFee(grid_fee_percentage=0.1, grid_fee_const=2), + ) owner = FakeArea("owner") market_agent = OneSidedAgent( - owner=owner, higher_market=higher_market, lower_market=lower_market) + owner=owner, higher_market=higher_market, lower_market=lower_market + ) market_agent.event_tick() market_agent.owner.current_tick = 14 market_agent.event_tick() @@ -270,9 +321,11 @@ def market_agent_fixture(): @staticmethod def test_ma_forwarded_offers_complied_to_transfer_fee(market_agent): source_offer = [ - offer for offer in market_agent.lower_market.sorted_offers if offer.id == "id"][0] + offer for offer in market_agent.lower_market.sorted_offers if offer.id == "id" + ][0] target_offer = [ - offer for offer in market_agent.higher_market.sorted_offers if offer.id == "uuid"][0] + offer for offer in market_agent.higher_market.sorted_offers if offer.id == "uuid" + ][0] earned_ma_fee = target_offer.price - source_offer.price expected_ma_fee = market_agent.higher_market.fee_class.grid_fee_rate @@ -282,41 +335,48 @@ def test_ma_forwarded_offers_complied_to_transfer_fee(market_agent): @pytest.mark.parametrize("market_agent_fee", [0.1, 0, 0.5, 0.75, 0.05, 0.02, 0.03]) def test_ma_forwards_bids_according_to_percentage(market_agent_fee): ConstSettings.MASettings.MARKET_TYPE = 2 - lower_market = FakeMarket([], [ - Bid("id", pendulum.now(), 1, 1, TraderDetails("this", ""), 1)], - transfer_fees=GridFee(grid_fee_percentage=market_agent_fee, - grid_fee_const=0), - name="FakeMarket") - higher_market = FakeMarket([], [ - Bid("id2", pendulum.now(), 3, 3, TraderDetails("child", ""), 3)], - transfer_fees=GridFee(grid_fee_percentage=market_agent_fee, - grid_fee_const=0), - name="FakeMarket") + lower_market = FakeMarket( + [], + [Bid("id", pendulum.now(), 1, 1, TraderDetails("this", ""), 1)], + transfer_fees=GridFee(grid_fee_percentage=market_agent_fee, grid_fee_const=0), + name="FakeMarket", + ) + higher_market = FakeMarket( + [], + [Bid("id2", pendulum.now(), 3, 3, TraderDetails("child", ""), 3)], + transfer_fees=GridFee(grid_fee_percentage=market_agent_fee, grid_fee_const=0), + name="FakeMarket", + ) market_agent = TwoSidedAgent( - owner=FakeArea("owner"), higher_market=higher_market, lower_market=lower_market) + owner=FakeArea("owner"), higher_market=higher_market, lower_market=lower_market + ) market_agent.event_tick() market_agent.owner.current_tick = 14 market_agent.event_tick() assert market_agent.higher_market.bid_call_count == 1 - assert (market_agent.higher_market.forwarded_bid.price == - list(market_agent.lower_market.bids.values())[-1].price * (1 - market_agent_fee)) + assert market_agent.higher_market.forwarded_bid.price == list( + market_agent.lower_market.bids.values() + )[-1].price * (1 - market_agent_fee) @staticmethod @pytest.mark.parametrize("market_agent_fee_const", [0.5, 1, 5, 10]) @pytest.mark.skip("need to define if we need a constant fee") def test_ma_forwards_bids_according_to_constantfee(market_agent_fee_const): ConstSettings.MASettings.MARKET_TYPE = 2 - lower_market = FakeMarket([], [ - Bid("id", pendulum.now(), 15, 1, TraderDetails("this", ""), 15)], - transfer_fees=GridFee(grid_fee_percentage=0, - grid_fee_const=market_agent_fee_const)) - higher_market = FakeMarket([], [ - Bid("id2", pendulum.now(), 35, 3, TraderDetails("child", ""), 35)], - transfer_fees=GridFee(grid_fee_percentage=0, - grid_fee_const=market_agent_fee_const)) + lower_market = FakeMarket( + [], + [Bid("id", pendulum.now(), 15, 1, TraderDetails("this", ""), 15)], + transfer_fees=GridFee(grid_fee_percentage=0, grid_fee_const=market_agent_fee_const), + ) + higher_market = FakeMarket( + [], + [Bid("id2", pendulum.now(), 35, 3, TraderDetails("child", ""), 35)], + transfer_fees=GridFee(grid_fee_percentage=0, grid_fee_const=market_agent_fee_const), + ) market_agent = TwoSidedAgent( - owner=FakeArea("owner"), higher_market=higher_market, lower_market=lower_market) + owner=FakeArea("owner"), higher_market=higher_market, lower_market=lower_market + ) market_agent.event_tick() market_agent.owner.current_tick = 14 market_agent.event_tick() @@ -324,17 +384,21 @@ def test_ma_forwards_bids_according_to_constantfee(market_agent_fee_const): assert market_agent.higher_market.bid_call_count == 1 bid = list(market_agent.lower_market.bids.values())[-1] assert market_agent.higher_market.forwarded_bid.price == ( - bid.price - market_agent_fee_const * bid.energy) + bid.price - market_agent_fee_const * bid.energy + ) @pytest.fixture(name="market_agent_bid", params=[TwoSidedAgent, SettlementAgent]) def market_agent_bid_fixture(request): ConstSettings.MASettings.MARKET_TYPE = 2 - lower_market = FakeMarket([], [ - Bid("id", pendulum.now(), 1, 1, TraderDetails("this", ""), 1)]) - higher_market = FakeMarket([], [ - Bid("id2", pendulum.now(), 1, 1, TraderDetails("child", ""), 1), - Bid("id3", pendulum.now(), 0.5, 1, TraderDetails("child", ""), 1)]) + lower_market = FakeMarket([], [Bid("id", pendulum.now(), 1, 1, TraderDetails("this", ""), 1)]) + higher_market = FakeMarket( + [], + [ + Bid("id2", pendulum.now(), 1, 1, TraderDetails("child", ""), 1), + Bid("id3", pendulum.now(), 0.5, 1, TraderDetails("child", ""), 1), + ], + ) owner = FakeArea("owner") agent_class = request.param @@ -351,12 +415,15 @@ def market_agent_double_sided_fixture(): lower_market = FakeMarket( offers=[Offer("id", pendulum.now(), 2, 2, TraderDetails("other", ""), 2)], bids=[Bid("bid_id", pendulum.now(), 10, 10, TraderDetails("B", ""), 10)], - transfer_fees=GridFee(grid_fee_percentage=0.01, grid_fee_const=0)) - higher_market = FakeMarket([], [], transfer_fees=GridFee(grid_fee_percentage=0.01, - grid_fee_const=0)) + transfer_fees=GridFee(grid_fee_percentage=0.01, grid_fee_const=0), + ) + higher_market = FakeMarket( + [], [], transfer_fees=GridFee(grid_fee_percentage=0.01, grid_fee_const=0) + ) owner = FakeArea("owner") market_agent = TwoSidedAgent( - owner=owner, lower_market=lower_market, higher_market=higher_market) + owner=owner, lower_market=lower_market, higher_market=higher_market + ) market_agent.event_tick() market_agent.owner.current_tick += 2 market_agent.event_tick() @@ -379,42 +446,52 @@ def test_ma_forwards_bids(market_agent_bid): @staticmethod def test_ma_forwarded_bids_adhere_to_ma_overhead(market_agent_bid): assert market_agent_bid.higher_market.bid_call_count == 1 - expected_price = ( - list(market_agent_bid.lower_market.bids.values())[-1].price * - (1 - market_agent_bid.lower_market.fee_class.grid_fee_rate)) + expected_price = list(market_agent_bid.lower_market.bids.values())[-1].price * ( + 1 - market_agent_bid.lower_market.fee_class.grid_fee_rate + ) assert market_agent_bid.higher_market.forwarded_bid.price == expected_price @staticmethod def test_ma_event_trade_bid_deletes_forwarded_bid_when_sold(market_agent_bid, called): market_agent_bid.lower_market.delete_bid = called market_agent_bid.event_bid_traded( - bid_trade=Trade("trade_id", - pendulum.now(tz=TIME_ZONE), - TraderDetails("someone_else", ""), - TraderDetails("owner", ""), - bid=market_agent_bid.higher_market.bids["id3"], - traded_energy=1, trade_price=1), - market_id=market_agent_bid.higher_market.id) + bid_trade=Trade( + "trade_id", + pendulum.now(tz=TIME_ZONE), + TraderDetails("someone_else", ""), + TraderDetails("owner", ""), + bid=market_agent_bid.higher_market.bids["id3"], + traded_energy=1, + trade_price=1, + ), + market_id=market_agent_bid.higher_market.id, + ) assert len(market_agent_bid.lower_market.delete_bid.calls) == 1 @staticmethod def test_ma_event_trade_bid_does_not_delete_forwarded_bid_of_counterpart( - market_agent_bid, called): + market_agent_bid, called + ): market_agent_bid.lower_market.delete_bid = called high_to_low_engine = market_agent_bid.engines[1] high_to_low_engine.event_bid_traded( - bid_trade=Trade("trade_id", - pendulum.now(tz=TIME_ZONE), - bid=market_agent_bid.higher_market.bids["id3"], - seller=TraderDetails("owner", ""), - buyer=TraderDetails("someone_else", ""), - traded_energy=1, trade_price=1)) + bid_trade=Trade( + "trade_id", + pendulum.now(tz=TIME_ZONE), + bid=market_agent_bid.higher_market.bids["id3"], + seller=TraderDetails("owner", ""), + buyer=TraderDetails("someone_else", ""), + traded_energy=1, + trade_price=1, + ) + ) assert len(market_agent_bid.lower_market.delete_bid.calls) == 0 @staticmethod @pytest.mark.parametrize("partial", [True, False]) def test_ma_event_bid_split_and_trade_correctly_populate_forwarded_bid_entries( - market_agent_bid, called, partial): + market_agent_bid, called, partial + ): market_agent_bid.lower_market.delete_bid = called low_to_high_engine = market_agent_bid.engines[0] market_agent_bid._get_market_from_market_id = lambda x: low_to_high_engine.markets.target @@ -431,8 +508,9 @@ def test_ma_event_bid_split_and_trade_correctly_populate_forwarded_bid_entries( original_bid = low_to_high_engine.markets.target._bids[0] accepted_bid = deepcopy(original_bid) - accepted_bid.update_price((original_bid.energy - residual_energy) * ( - original_bid.energy_rate)) + accepted_bid.update_price( + (original_bid.energy - residual_energy) * (original_bid.energy_rate) + ) accepted_bid.update_energy(original_bid.energy - residual_energy) residual_bid = deepcopy(original_bid) @@ -440,30 +518,45 @@ def test_ma_event_bid_split_and_trade_correctly_populate_forwarded_bid_entries( residual_bid.update_price(residual_energy * original_bid.energy_rate) residual_bid.update_energy(residual_energy) - low_to_high_engine.event_bid_split(market_id=low_to_high_engine.markets.target.id, - original_bid=original_bid, - accepted_bid=accepted_bid, - residual_bid=residual_bid) - assert set(low_to_high_engine.forwarded_bids.keys()) == \ - {original_bid.id, accepted_bid.id, residual_bid.id, "uuid", "id3", "id2"} + low_to_high_engine.event_bid_split( + market_id=low_to_high_engine.markets.target.id, + original_bid=original_bid, + accepted_bid=accepted_bid, + residual_bid=residual_bid, + ) + assert set(low_to_high_engine.forwarded_bids.keys()) == { + original_bid.id, + accepted_bid.id, + residual_bid.id, + "uuid", + "id3", + "id2", + } else: original_bid = low_to_high_engine.markets.target._bids[0] accepted_bid = deepcopy(original_bid) residual_bid = None low_to_high_engine.event_bid_traded( - bid_trade=Trade("trade_id", - pendulum.now(tz=TIME_ZONE), - bid=accepted_bid, - seller=TraderDetails("someone_else", ""), - buyer=TraderDetails("owner", ""), - residual=residual_bid, - traded_energy=1, trade_price=1)) + bid_trade=Trade( + "trade_id", + pendulum.now(tz=TIME_ZONE), + bid=accepted_bid, + seller=TraderDetails("someone_else", ""), + buyer=TraderDetails("owner", ""), + residual=residual_bid, + traded_energy=1, + trade_price=1, + ) + ) if partial: # "id" gets traded in the target market, "id2" gets split in the source market, too - assert set( - low_to_high_engine.forwarded_bids.keys()) == {residual_bid.id, "uuid", "id3"} + assert set(low_to_high_engine.forwarded_bids.keys()) == { + residual_bid.id, + "uuid", + "id3", + } else: # "id" and "id2" get traded in both target and source, # left over is id3 and its forwarded instance uuid @@ -471,16 +564,21 @@ def test_ma_event_bid_split_and_trade_correctly_populate_forwarded_bid_entries( @staticmethod def test_ma_event_trade_buys_accepted_bid(market_agent_double_sided): - market_agent_double_sided.higher_market.forwarded_bid = \ + market_agent_double_sided.higher_market.forwarded_bid = ( market_agent_double_sided.higher_market.forwarded_bid + ) market_agent_double_sided.event_bid_traded( - bid_trade=Trade("trade_id", - pendulum.now(tz=TIME_ZONE), - TraderDetails("owner", ""), - TraderDetails("someone_else", ""), - bid=market_agent_double_sided.higher_market.forwarded_bid, - traded_energy=1, trade_price=1), - market_id=market_agent_double_sided.higher_market.id) + bid_trade=Trade( + "trade_id", + pendulum.now(tz=TIME_ZONE), + TraderDetails("owner", ""), + TraderDetails("someone_else", ""), + bid=market_agent_double_sided.higher_market.forwarded_bid, + traded_energy=1, + trade_price=1, + ), + market_id=market_agent_double_sided.higher_market.id, + ) assert len(market_agent_double_sided.lower_market.calls_energy_bids) == 1 expected_price = 10 * (1 - market_agent_double_sided.lower_market.fee_class.grid_fee_rate) @@ -490,13 +588,17 @@ def test_ma_event_trade_buys_accepted_bid(market_agent_double_sided): @staticmethod def test_ma_event_bid_trade_increases_bid_price(market_agent_double_sided): market_agent_double_sided.event_bid_traded( - bid_trade=Trade("trade_id", - pendulum.now(tz=TIME_ZONE), - TraderDetails("owner", ""), - TraderDetails("someone_else", ""), - bid=market_agent_double_sided.higher_market.forwarded_bid, - traded_energy=1, trade_price=1), - market_id=market_agent_double_sided.higher_market.id) + bid_trade=Trade( + "trade_id", + pendulum.now(tz=TIME_ZONE), + TraderDetails("owner", ""), + TraderDetails("someone_else", ""), + bid=market_agent_double_sided.higher_market.forwarded_bid, + traded_energy=1, + trade_price=1, + ), + market_id=market_agent_double_sided.higher_market.id, + ) assert len(market_agent_double_sided.lower_market.calls_energy_bids) == 1 expected_price = 10 * (1 - market_agent_double_sided.lower_market.fee_class.grid_fee_rate) assert market_agent_double_sided.higher_market.forwarded_bid.price == expected_price @@ -506,49 +608,71 @@ def test_ma_event_bid_trade_increases_bid_price(market_agent_double_sided): @staticmethod def test_ma_event_trade_buys_partial_accepted_bid(market_agent_double_sided): market_agent_double_sided._get_market_from_market_id = ( - lambda x: market_agent_double_sided.higher_market) + lambda x: market_agent_double_sided.higher_market + ) original_bid = market_agent_double_sided.higher_market.forwarded_bid - accepted_bid_price = (original_bid.price/original_bid.energy) * 1 - residual_bid_price = (original_bid.price/original_bid.energy) * 0.1 - accepted_bid = Bid(original_bid.id, original_bid.creation_time, accepted_bid_price, 1, - original_bid.buyer) - residual_bid = Bid("residual_bid", original_bid.creation_time, residual_bid_price, 0.1, - original_bid.buyer) + accepted_bid_price = (original_bid.price / original_bid.energy) * 1 + residual_bid_price = (original_bid.price / original_bid.energy) * 0.1 + accepted_bid = Bid( + original_bid.id, original_bid.creation_time, accepted_bid_price, 1, original_bid.buyer + ) + residual_bid = Bid( + "residual_bid", original_bid.creation_time, residual_bid_price, 0.1, original_bid.buyer + ) market_agent_double_sided.event_bid_split( market_id=market_agent_double_sided.higher_market.id, original_bid=original_bid, accepted_bid=accepted_bid, - residual_bid=residual_bid) + residual_bid=residual_bid, + ) market_agent_double_sided.event_bid_traded( - bid_trade=Trade("trade_id", - pendulum.now(tz=TIME_ZONE), - TraderDetails("owner", ""), - TraderDetails("someone_else", ""), - bid=accepted_bid, - residual="residual_offer", traded_energy=1, trade_price=1), - market_id=market_agent_double_sided.higher_market.id) + bid_trade=Trade( + "trade_id", + pendulum.now(tz=TIME_ZONE), + TraderDetails("owner", ""), + TraderDetails("someone_else", ""), + bid=accepted_bid, + residual="residual_offer", + traded_energy=1, + trade_price=1, + ), + market_id=market_agent_double_sided.higher_market.id, + ) assert market_agent_double_sided.lower_market.calls_energy_bids[0] == 1 @staticmethod def test_ma_forwards_partial_bid_from_source_market(market_agent_double_sided): market_agent_double_sided._get_market_from_market_id = ( - lambda x: market_agent_double_sided.lower_market) + lambda x: market_agent_double_sided.lower_market + ) original_bid = market_agent_double_sided.lower_market._bids[0] residual_energy = 0.1 - accepted_bid = Bid(original_bid.id, original_bid.creation_time, original_bid.price, - original_bid.energy - residual_energy, original_bid.buyer, - original_bid.price) + accepted_bid = Bid( + original_bid.id, + original_bid.creation_time, + original_bid.price, + original_bid.energy - residual_energy, + original_bid.buyer, + original_bid.price, + ) residual_bid = Bid( - "residual_bid", original_bid.creation_time, original_bid.price, residual_energy, - original_bid.buyer, original_bid.price) + "residual_bid", + original_bid.creation_time, + original_bid.price, + residual_energy, + original_bid.buyer, + original_bid.price, + ) market_agent_double_sided.usable_bid = lambda s: True market_agent_double_sided.event_bid_split( market_id=market_agent_double_sided.lower_market.id, original_bid=original_bid, accepted_bid=accepted_bid, - residual_bid=residual_bid) + residual_bid=residual_bid, + ) assert isclose( - market_agent_double_sided.higher_market.forwarded_bid.energy, residual_energy) + market_agent_double_sided.higher_market.forwarded_bid.energy, residual_energy + ) class TestMAOffer: @@ -561,15 +685,17 @@ def teardown_method(): @staticmethod @pytest.fixture(name="market_agent") def market_agent_fixture(): - lower_market = FakeMarket([ - Offer("id", pendulum.now(), 1, 1, TraderDetails("other", ""), 1)]) - higher_market = FakeMarket([ - Offer("id2", pendulum.now(), 3, 3, TraderDetails("higher", ""), 3), - Offer("id3", pendulum.now(), 0.5, 1, TraderDetails("higher", ""), 0.5)]) + lower_market = FakeMarket( + [Offer("id", pendulum.now(), 1, 1, TraderDetails("other", ""), 1)] + ) + higher_market = FakeMarket( + [ + Offer("id2", pendulum.now(), 3, 3, TraderDetails("higher", ""), 3), + Offer("id3", pendulum.now(), 0.5, 1, TraderDetails("higher", ""), 0.5), + ] + ) owner = FakeArea("owner") - maa = OneSidedAgent(owner=owner, - higher_market=higher_market, - lower_market=lower_market) + maa = OneSidedAgent(owner=owner, higher_market=higher_market, lower_market=lower_market) maa.event_tick() maa.owner.current_tick = 14 maa.event_tick() @@ -579,8 +705,9 @@ def market_agent_fixture(): @staticmethod @pytest.fixture(name="market_agent_2") def market_agent_2_fixture(): - lower_market = FakeMarket([ - Offer("id", pendulum.now(), 2, 2, TraderDetails("other", ""), 2)], m_id=123) + lower_market = FakeMarket( + [Offer("id", pendulum.now(), 2, 2, TraderDetails("other", ""), 2)], m_id=123 + ) higher_market = FakeMarket([], m_id=234) owner = FakeArea("owner") owner.future_market = lower_market @@ -604,9 +731,13 @@ def test_ma_event_trade_deletes_forwarded_offer_when_sold(market_agent, called): "trade_id", pendulum.now(tz=TIME_ZONE), TraderDetails("higher", ""), - TraderDetails("someone_else", ""), offer=market_agent.higher_market.offers["id3"], - traded_energy=1, trade_price=1), - market_id=market_agent.higher_market.id) + TraderDetails("someone_else", ""), + offer=market_agent.higher_market.offers["id3"], + traded_energy=1, + trade_price=1, + ), + market_id=market_agent.higher_market.id, + ) assert len(market_agent.lower_market.delete_offer.calls) == 1 @staticmethod @@ -618,8 +749,12 @@ def test_ma_event_trade_buys_accepted_offer(market_agent_2): TraderDetails("owner", ""), TraderDetails("someone_else", ""), offer=market_agent_2.higher_market.forwarded_offer, - fee_price=0.0, traded_energy=1, trade_price=1), - market_id=market_agent_2.higher_market.id) + fee_price=0.0, + traded_energy=1, + trade_price=1, + ), + market_id=market_agent_2.higher_market.id, + ) assert len(market_agent_2.lower_market.calls_energy) == 1 @staticmethod @@ -634,15 +769,20 @@ def test_ma_event_trade_buys_partial_accepted_offer(market_agent_2): pendulum.now(tz=TIME_ZONE), TraderDetails("owner", ""), TraderDetails("someone_else", ""), - offer=accepted_offer, traded_energy=1, trade_price=1, + offer=accepted_offer, + traded_energy=1, + trade_price=1, residual="residual_offer", - fee_price=0.0, ), - market_id=market_agent_2.higher_market.id) + fee_price=0.0, + ), + market_id=market_agent_2.higher_market.id, + ) assert market_agent_2.lower_market.calls_energy[0] == 1 @staticmethod def test_ma_event_offer_split_and_trade_correctly_populate_forwarded_offer_entries( - market_agent_2): + market_agent_2, + ): residual_offer_id = "res_id" original_offer_id = "id" original = market_agent_2.higher_market.forwarded_offer @@ -655,15 +795,20 @@ def test_ma_event_offer_split_and_trade_correctly_populate_forwarded_offer_entri market_id=market_agent_2.higher_market.id, original_offer=original, accepted_offer=accepted, - residual_offer=residual) + residual_offer=residual, + ) engine = next( - (e for e in market_agent_2.engines if residual_offer_id in e.forwarded_offers), None) + (e for e in market_agent_2.engines if residual_offer_id in e.forwarded_offers), None + ) assert engine is not None, "Residual of forwarded offers not found in forwarded_offers" # after the split event: # all three offer ids are part of the forwarded_offer member - assert set(engine.forwarded_offers.keys()) == {residual_offer_id, original_offer_id, - "uuid"} + assert set(engine.forwarded_offers.keys()) == { + residual_offer_id, + original_offer_id, + "uuid", + } # and the accepted offer was added assert engine.forwarded_offers[original_offer_id].target_offer.energy == accepted.energy # and the residual offer was added @@ -676,10 +821,13 @@ def test_ma_event_offer_split_and_trade_correctly_populate_forwarded_offer_entri TraderDetails("owner", ""), TraderDetails("someone_else", ""), offer=accepted, - traded_energy=1, trade_price=1, + traded_energy=1, + trade_price=1, residual=residual, - fee_price=0.0,), - market_id=market_agent_2.lower_market.id) + fee_price=0.0, + ), + market_id=market_agent_2.lower_market.id, + ) # after the trade event: # the forwarded_offers only contain the residual offer diff --git a/tests/test_state/test_consumption_state.py b/tests/test_state/test_consumption_state.py index 5fb5855a6..8617e6758 100644 --- a/tests/test_state/test_consumption_state.py +++ b/tests/test_state/test_consumption_state.py @@ -3,13 +3,14 @@ import pytest from pendulum import now, DateTime -from gsy_e.constants import FLOATING_POINT_TOLERANCE +from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE from gsy_e.models.strategy.state.base_states import ConsumptionState, StateInterface from tests.test_state.test_prosumption_interface import TestProsumptionInterface class ConsumptionInterfaceHelper(ConsumptionState): """Add the get_results_dict abstract method to the ConsumptionState.""" + def get_results_dict(self, current_time_slot: DateTime) -> dict: raise NotImplementedError @@ -22,17 +23,14 @@ def _setup_base_configuration(): return ConsumptionInterfaceHelper(), now() def test_get_state_raise_error_if_conflicting_keys(self): - with patch.object(StateInterface, "get_state", - return_value={"desired_energy_Wh": {-1.0}}): + with patch.object(StateInterface, "get_state", return_value={"desired_energy_Wh": {-1.0}}): consumption_state, _ = self._setup_base_configuration() with pytest.raises(AssertionError) as error: consumption_state.get_state() - assert ("Conflicting state values found for {'desired_energy_Wh'}." - in str(error.value)) + assert "Conflicting state values found for {'desired_energy_Wh'}." in str(error.value) def test_get_state_keys_in_state_dict(self): - expected_keys = ["desired_energy_Wh", - "total_energy_demanded_Wh"] + expected_keys = ["desired_energy_Wh", "total_energy_demanded_Wh"] consumption_state, _ = self._setup_base_configuration() state_dict = consumption_state.get_state() assert set(expected_keys).issubset(state_dict.keys()) @@ -40,39 +38,26 @@ def test_get_state_keys_in_state_dict(self): def test_restore_state_values_set_properly(self): time_slot, consumption_state = self._setup_default_test_consumption_state() past_state = consumption_state.get_state() - consumption_state.set_desired_energy( - energy=2.0, - time_slot=time_slot - ) - consumption_state.update_total_demanded_energy( - time_slot=time_slot - ) + consumption_state.set_desired_energy(energy=2.0, time_slot=time_slot) + consumption_state.update_total_demanded_energy(time_slot=time_slot) assert past_state != consumption_state.get_state() consumption_state.restore_state(state_dict=past_state) assert past_state == consumption_state.get_state() def _setup_default_test_consumption_state(self): consumption_state, time_slot = self._setup_base_configuration() - consumption_state.set_desired_energy( - energy=1.0, - time_slot=time_slot - ) - consumption_state.update_total_demanded_energy( - time_slot=time_slot - ) + consumption_state.set_desired_energy(energy=1.0, time_slot=time_slot) + consumption_state.update_total_demanded_energy(time_slot=time_slot) return time_slot, consumption_state @pytest.mark.parametrize("overwrite, expected_energy", [(True, 2), (False, 1)]) def test_set_desired_energy_setting_and_overwriting_mechanism( - self, overwrite, expected_energy): + self, overwrite, expected_energy + ): time_slot, consumption_state = self._setup_default_test_consumption_state() - consumption_state.set_desired_energy(energy=2, - time_slot=time_slot, - overwrite=overwrite) - assert consumption_state.get_energy_requirement_Wh( - time_slot) == expected_energy - assert consumption_state.get_desired_energy_Wh( - time_slot) == expected_energy + consumption_state.set_desired_energy(energy=2, time_slot=time_slot, overwrite=overwrite) + assert consumption_state.get_energy_requirement_Wh(time_slot) == expected_energy + assert consumption_state.get_desired_energy_Wh(time_slot) == expected_energy def test_update_total_demanded_energy_respects_addition(self): time_slot, consumption_state = self._setup_default_test_consumption_state() @@ -81,61 +66,67 @@ def test_update_total_demanded_energy_respects_addition(self): consumption_state.update_total_demanded_energy(time_slot) consumption_state.update_total_demanded_energy(time_slot) consumption_state.update_total_demanded_energy(time_slot) - assert (consumption_state.get_state()["total_energy_demanded_Wh"] - == 4 * initial_demanded_energy) + assert ( + consumption_state.get_state()["total_energy_demanded_Wh"] + == 4 * initial_demanded_energy + ) # For non-pre-existing time_slots consumption_state.update_total_demanded_energy(time_slot.add(minutes=15)) - assert (consumption_state.get_state()["total_energy_demanded_Wh"] - == 4 * initial_demanded_energy) + assert ( + consumption_state.get_state()["total_energy_demanded_Wh"] + == 4 * initial_demanded_energy + ) def test_can_buy_more_energy_time_slot_not_registered_default_false(self): time_slot, consumption_state = self._setup_default_test_consumption_state() - assert consumption_state.can_buy_more_energy( - time_slot=time_slot.add(minutes=15)) is False + assert consumption_state.can_buy_more_energy(time_slot=time_slot.add(minutes=15)) is False @pytest.mark.parametrize( - "set_energy, expected_bool", [(1, True), (FLOATING_POINT_TOLERANCE, False)]) + "set_energy, expected_bool", [(1, True), (FLOATING_POINT_TOLERANCE, False)] + ) def test_can_buy_more_energy_set_energy_requirement_return_expected( - self, set_energy, expected_bool): + self, set_energy, expected_bool + ): time_slot, consumption_state = self._setup_default_test_consumption_state() - consumption_state.set_desired_energy(energy=set_energy, - time_slot=time_slot, - overwrite=True) - assert consumption_state.can_buy_more_energy( - time_slot=time_slot) is expected_bool + consumption_state.set_desired_energy( + energy=set_energy, time_slot=time_slot, overwrite=True + ) + assert consumption_state.can_buy_more_energy(time_slot=time_slot) is expected_bool def test_decrement_energy_requirement_respects_subtraction(self): time_slot, consumption_state = self._setup_default_test_consumption_state() initial_energy_req = consumption_state.get_energy_requirement_Wh(time_slot) purchased_energy = 0.3 - consumption_state.decrement_energy_requirement(purchased_energy_Wh=purchased_energy, - time_slot=time_slot, - area_name="test_area") - consumption_state.decrement_energy_requirement(purchased_energy_Wh=purchased_energy, - time_slot=time_slot, - area_name="test_area") + consumption_state.decrement_energy_requirement( + purchased_energy_Wh=purchased_energy, time_slot=time_slot, area_name="test_area" + ) + consumption_state.decrement_energy_requirement( + purchased_energy_Wh=purchased_energy, time_slot=time_slot, area_name="test_area" + ) assert isclose( consumption_state.get_energy_requirement_Wh(time_slot), - initial_energy_req - 2 * purchased_energy + initial_energy_req - 2 * purchased_energy, ) def test_decrement_energy_requirement_purchase_more_than_possible_raise_error(self): time_slot, consumption_state = self._setup_default_test_consumption_state() purchased_energy = 1.2 with pytest.raises(AssertionError) as error: - consumption_state.decrement_energy_requirement(purchased_energy_Wh=purchased_energy, - time_slot=time_slot, - area_name="test_area") - assert ("Energy requirement for device test_area fell below zero " - ) in str(error.value) + consumption_state.decrement_energy_requirement( + purchased_energy_Wh=purchased_energy, time_slot=time_slot, area_name="test_area" + ) + assert "Energy requirement for device test_area fell below zero " in str(error.value) def test_delete_past_state_values_market_slot_not_in_past(self): past_time_slot, consumption_state = self._setup_default_test_consumption_state() current_time_slot = past_time_slot.add(minutes=15) consumption_state.set_desired_energy(energy=1, time_slot=past_time_slot) consumption_state.set_desired_energy(energy=1, time_slot=current_time_slot) - with patch("gsy_e.gsy_e_core.util.ConstSettings.SettlementMarketSettings." - "ENABLE_SETTLEMENT_MARKETS", True): + with patch( + "gsy_e.gsy_e_core.util.ConstSettings.SettlementMarketSettings." + "ENABLE_SETTLEMENT_MARKETS", + True, + ): consumption_state.delete_past_state_values(past_time_slot) assert consumption_state.get_energy_requirement_Wh(past_time_slot) is not None assert consumption_state.get_desired_energy_Wh(past_time_slot) is not None @@ -145,10 +136,16 @@ def test_delete_past_state_values_market_slot_in_past(self): current_time_slot = past_time_slot.add(minutes=15) consumption_state.set_desired_energy(energy=1, time_slot=past_time_slot) consumption_state.set_desired_energy(energy=1, time_slot=current_time_slot) - with patch("gsy_e.gsy_e_core.util.ConstSettings.SettlementMarketSettings." - "ENABLE_SETTLEMENT_MARKETS", False): + with patch( + "gsy_e.gsy_e_core.util.ConstSettings.SettlementMarketSettings." + "ENABLE_SETTLEMENT_MARKETS", + False, + ): consumption_state.delete_past_state_values(current_time_slot) - assert consumption_state.get_energy_requirement_Wh(past_time_slot, - default_value=100) == 100 - assert consumption_state.get_desired_energy_Wh(past_time_slot, - default_value=100) == 100 + assert ( + consumption_state.get_energy_requirement_Wh(past_time_slot, default_value=100) + == 100 + ) + assert ( + consumption_state.get_desired_energy_Wh(past_time_slot, default_value=100) == 100 + ) diff --git a/tests/test_state/test_storage_state.py b/tests/test_state/test_storage_state.py index 609566bdf..6dde38d98 100644 --- a/tests/test_state/test_storage_state.py +++ b/tests/test_state/test_storage_state.py @@ -4,7 +4,7 @@ import pytest from pendulum import now, duration -from gsy_e.constants import FLOATING_POINT_TOLERANCE +from gsy_framework.constants_limits import FLOATING_POINT_TOLERANCE from gsy_e.models.strategy.state import StorageState, ESSEnergyOrigin, EnergyOrigin SAMPLE_STATE = { @@ -45,21 +45,24 @@ def test_market_cycle_reset_orders(self): storage_state.add_default_values_to_state_profiles(active_market_slot_time_list) storage_state.offered_buy_kWh[future_time_slots[0]] = 10 storage_state.offered_sell_kWh[future_time_slots[0]] = 10 - with patch("gsy_e.models.strategy.state.storage_state.ConstSettings.FutureMarketSettings." - "FUTURE_MARKET_DURATION_HOURS", 5): + with patch( + "gsy_e.models.strategy.state.storage_state.ConstSettings.FutureMarketSettings." + "FUTURE_MARKET_DURATION_HOURS", + 5, + ): storage_state.market_cycle(past_time_slot, current_time_slot, future_time_slots) # The future_time_slots[0] is in the future, so it won't reset assert storage_state.offered_buy_kWh[future_time_slots[0]] == 10 assert storage_state.offered_sell_kWh[future_time_slots[0]] == 10 storage_state.market_cycle( - current_time_slot, future_time_slots[0], future_time_slots[1:]) + current_time_slot, future_time_slots[0], future_time_slots[1:] + ) # The future_time_slots[0] is in the spot market, so it has to reset the orders assert storage_state.offered_buy_kWh[future_time_slots[0]] == 0 assert storage_state.offered_sell_kWh[future_time_slots[0]] == 0 def test_market_cycle_update_used_storage(self): - storage_state = StorageState(initial_soc=100, - capacity=100) + storage_state = StorageState(initial_soc=100, capacity=100) past_time_slot, current_time_slot, future_time_slots = self._initialize_time_slots() active_market_slot_time_list = [past_time_slot, current_time_slot, *future_time_slots] storage_state.add_default_values_to_state_profiles(active_market_slot_time_list) @@ -73,93 +76,97 @@ def test_market_cycle_update_used_storage(self): assert storage_state.used_storage == 100 def test_market_cycle_ess_share_time_series_dict(self): - storage_state = StorageState(initial_soc=100, - capacity=100, - initial_energy_origin=ESSEnergyOrigin.LOCAL, - max_abs_battery_power_kW=15) + storage_state = StorageState( + initial_soc=100, + capacity=100, + initial_energy_origin=ESSEnergyOrigin.LOCAL, + max_abs_battery_power_kW=15, + ) past_time_slot, current_time_slot, future_time_slots = self._initialize_time_slots() active_market_slot_time_list = [past_time_slot, current_time_slot, *future_time_slots] storage_state.add_default_values_to_state_profiles(active_market_slot_time_list) energy = 0.3 storage_state.activate( - slot_length=duration(minutes=15), current_time_slot=current_time_slot) + slot_length=duration(minutes=15), current_time_slot=current_time_slot + ) storage_state.register_energy_from_one_sided_market_accept_offer( - energy, past_time_slot, ESSEnergyOrigin.LOCAL) + energy, past_time_slot, ESSEnergyOrigin.LOCAL + ) storage_state.register_energy_from_one_sided_market_accept_offer( - energy, past_time_slot, ESSEnergyOrigin.UNKNOWN) + energy, past_time_slot, ESSEnergyOrigin.UNKNOWN + ) storage_state.register_energy_from_one_sided_market_accept_offer( - energy, past_time_slot, ESSEnergyOrigin.UNKNOWN) + energy, past_time_slot, ESSEnergyOrigin.UNKNOWN + ) storage_state.market_cycle(past_time_slot, current_time_slot, future_time_slots) expected_ess_share_last_market = { ESSEnergyOrigin.LOCAL: storage_state.initial_capacity_kWh + energy, ESSEnergyOrigin.EXTERNAL: 0.0, - ESSEnergyOrigin.UNKNOWN: 2 * energy + ESSEnergyOrigin.UNKNOWN: 2 * energy, } - assert (storage_state.time_series_ess_share[past_time_slot] == - expected_ess_share_last_market) + assert ( + storage_state.time_series_ess_share[past_time_slot] == expected_ess_share_last_market + ) def test_market_cycle_calculate_and_update_soc_and_set_offer_history(self): - storage_state = StorageState(initial_soc=100, - capacity=100, - min_allowed_soc=20) + storage_state = StorageState(initial_soc=100, capacity=100, min_allowed_soc=20) past_time_slot, current_time_slot, future_time_slots = self._initialize_time_slots() active_market_slot_time_list = [past_time_slot, current_time_slot, *future_time_slots] storage_state.add_default_values_to_state_profiles(active_market_slot_time_list) storage_state.activate( - slot_length=duration(minutes=15), current_time_slot=current_time_slot) + slot_length=duration(minutes=15), current_time_slot=current_time_slot + ) storage_state.pledged_sell_kWh[past_time_slot] = 10 storage_state.market_cycle(past_time_slot, current_time_slot, future_time_slots) assert storage_state.charge_history[current_time_slot] != storage_state.initial_soc - assert (storage_state.charge_history_kWh[current_time_slot] != - storage_state.initial_capacity_kWh) + assert ( + storage_state.charge_history_kWh[current_time_slot] + != storage_state.initial_capacity_kWh + ) assert storage_state.offered_history[current_time_slot] != "-" @staticmethod def _initialize_time_slots(): past_time_slot = now() current_time_slot = past_time_slot.add(minutes=15) - future_time_slots = [current_time_slot.add(minutes=15), - current_time_slot.add(minutes=30)] + future_time_slots = [current_time_slot.add(minutes=15), current_time_slot.add(minutes=30)] return past_time_slot, current_time_slot, future_time_slots @staticmethod def test_check_state_charge_less_than_min_soc_error(): - storage_state = StorageState(capacity=100, - min_allowed_soc=50, - initial_soc=30) + storage_state = StorageState(capacity=100, min_allowed_soc=50, initial_soc=30) current_time_slot = now() storage_state.add_default_values_to_state_profiles([current_time_slot]) storage_state.activate( - slot_length=duration(minutes=15), current_time_slot=current_time_slot) + slot_length=duration(minutes=15), current_time_slot=current_time_slot + ) with pytest.raises(AssertionError) as error: storage_state.check_state(current_time_slot) assert "less than min soc" in str(error.value) @staticmethod def test_check_state_storage_surpasses_capacity_error(): - storage_state = StorageState(capacity=100, - min_allowed_soc=50, - initial_soc=110) + storage_state = StorageState(capacity=100, min_allowed_soc=50, initial_soc=110) current_time_slot = now() storage_state.add_default_values_to_state_profiles([current_time_slot]) storage_state.activate( - slot_length=duration(minutes=15), current_time_slot=current_time_slot) + slot_length=duration(minutes=15), current_time_slot=current_time_slot + ) with pytest.raises(AssertionError) as error: storage_state.check_state(current_time_slot) assert "surpassed the capacity" in str(error.value) @staticmethod def test_check_state_offered_and_pledged_energy_in_range(): - storage_state = StorageState(capacity=100, - min_allowed_soc=20, - initial_soc=50) + storage_state = StorageState(capacity=100, min_allowed_soc=20, initial_soc=50) current_time_slot = now() storage_state.add_default_values_to_state_profiles([current_time_slot]) max_value = storage_state.capacity * (1 - storage_state.min_allowed_soc_ratio) storage_state.max_abs_battery_power_kW = max_value * 15 storage_state.activate( - slot_length=duration(minutes=15), current_time_slot=current_time_slot) + slot_length=duration(minutes=15), current_time_slot=current_time_slot + ) def set_attribute_value_and_test(attribute): attribute[current_time_slot] = -1 @@ -186,10 +193,12 @@ def set_attribute_value_and_test(attribute): def _setup_storage_state_for_clamp_energy(): storage_state = StorageState() current_time_slot = now() - market_slot_list = [current_time_slot, - current_time_slot + duration(minutes=15), - current_time_slot + duration(minutes=30), - current_time_slot + duration(minutes=45)] + market_slot_list = [ + current_time_slot, + current_time_slot + duration(minutes=15), + current_time_slot + duration(minutes=30), + current_time_slot + duration(minutes=45), + ] storage_state._current_market_slot = current_time_slot storage_state._used_storage = 250.0 storage_state.capacity = 500.0 @@ -207,8 +216,9 @@ def test_clamp_energy_to_sell_kWh_only_on_first_slot(self): storage_state._battery_energy_per_slot = storage_state.capacity storage_state._clamp_energy_to_sell_kWh(market_slot_list) expected_available_energy = ( - storage_state.used_storage - - storage_state.min_allowed_soc_ratio * storage_state.capacity) + storage_state.used_storage + - storage_state.min_allowed_soc_ratio * storage_state.capacity + ) expected_energy_to_sell = { market_slot_list[0]: expected_available_energy, market_slot_list[1]: 0.0, @@ -313,14 +323,13 @@ def test_get_state_keys_in_dict(): assert set(SAMPLE_STATE.keys()).issubset(storage_state.get_state().keys()) def test_restore_state(self): - storage_state = StorageState(initial_soc=100, - capacity=100, - min_allowed_soc=20) + storage_state = StorageState(initial_soc=100, capacity=100, min_allowed_soc=20) past_time_slot, current_time_slot, future_time_slots = self._initialize_time_slots() active_market_slot_time_list = [past_time_slot, current_time_slot, *future_time_slots] storage_state.add_default_values_to_state_profiles(active_market_slot_time_list) storage_state.activate( - slot_length=duration(minutes=15), current_time_slot=current_time_slot) + slot_length=duration(minutes=15), current_time_slot=current_time_slot + ) past_state = storage_state.get_state() storage_state.pledged_sell_kWh[current_time_slot] = 50 modified_state = storage_state.get_state() @@ -330,9 +339,7 @@ def test_restore_state(self): @staticmethod def test_free_storage_return_float(): - storage_state = StorageState(initial_soc=100, - capacity=100, - min_allowed_soc=20) + storage_state = StorageState(initial_soc=100, capacity=100, min_allowed_soc=20) current_time_slot = now() storage_state.add_default_values_to_state_profiles([current_time_slot]) assert isinstance(storage_state.free_storage(current_time_slot), float) @@ -343,7 +350,8 @@ def test_activate_convert_energy_to_power(): storage_state = StorageState() current_time_slot = now() storage_state.activate( - slot_length=duration(minutes=15), current_time_slot=current_time_slot) + slot_length=duration(minutes=15), current_time_slot=current_time_slot + ) mocked_func.assert_called() def test_add_default_values_to_state_profiles_set_values_for_time_slots(self): @@ -357,27 +365,40 @@ def assert_time_slot_in_dict_attribute_with_default_value(attribute, time_slot, for time_slot in future_time_slots: assert_time_slot_in_dict_attribute_with_default_value( - storage_state.pledged_sell_kWh, time_slot, 0) + storage_state.pledged_sell_kWh, time_slot, 0 + ) assert_time_slot_in_dict_attribute_with_default_value( - storage_state.pledged_buy_kWh, time_slot, 0) + storage_state.pledged_buy_kWh, time_slot, 0 + ) assert_time_slot_in_dict_attribute_with_default_value( - storage_state.offered_sell_kWh, time_slot, 0) + storage_state.offered_sell_kWh, time_slot, 0 + ) assert_time_slot_in_dict_attribute_with_default_value( - storage_state.offered_buy_kWh, time_slot, 0) + storage_state.offered_buy_kWh, time_slot, 0 + ) assert_time_slot_in_dict_attribute_with_default_value( - storage_state.charge_history, time_slot, storage_state.initial_soc) + storage_state.charge_history, time_slot, storage_state.initial_soc + ) assert_time_slot_in_dict_attribute_with_default_value( - storage_state.charge_history_kWh, time_slot, storage_state.initial_capacity_kWh) + storage_state.charge_history_kWh, time_slot, storage_state.initial_capacity_kWh + ) assert_time_slot_in_dict_attribute_with_default_value( - storage_state.energy_to_sell_dict, time_slot, 0) + storage_state.energy_to_sell_dict, time_slot, 0 + ) assert_time_slot_in_dict_attribute_with_default_value( - storage_state.energy_to_buy_dict, time_slot, 0) + storage_state.energy_to_buy_dict, time_slot, 0 + ) assert_time_slot_in_dict_attribute_with_default_value( - storage_state.offered_history, time_slot, "-") + storage_state.offered_history, time_slot, "-" + ) assert_time_slot_in_dict_attribute_with_default_value( - storage_state.time_series_ess_share, time_slot, {ESSEnergyOrigin.UNKNOWN: 0., - ESSEnergyOrigin.LOCAL: 0., - ESSEnergyOrigin.EXTERNAL: 0.} + storage_state.time_series_ess_share, + time_slot, + { + ESSEnergyOrigin.UNKNOWN: 0.0, + ESSEnergyOrigin.LOCAL: 0.0, + ESSEnergyOrigin.EXTERNAL: 0.0, + }, ) def test_delete_past_state_values_market_slot_not_in_past(self): @@ -385,8 +406,11 @@ def test_delete_past_state_values_market_slot_not_in_past(self): past_time_slot, current_time_slot, future_time_slots = self._initialize_time_slots() active_market_slot_time_list = [past_time_slot, current_time_slot, *future_time_slots] storage_state.add_default_values_to_state_profiles(active_market_slot_time_list) - with patch("gsy_e.gsy_e_core.util.ConstSettings.SettlementMarketSettings." - "ENABLE_SETTLEMENT_MARKETS", True): + with patch( + "gsy_e.gsy_e_core.util.ConstSettings.SettlementMarketSettings." + "ENABLE_SETTLEMENT_MARKETS", + True, + ): storage_state.delete_past_state_values(current_time_slot) assert storage_state.pledged_sell_kWh.get(past_time_slot) is not None @@ -395,121 +419,136 @@ def test_delete_past_state_values_market_slot_in_past(self): past_time_slot, current_time_slot, future_time_slots = self._initialize_time_slots() active_market_slot_time_list = [past_time_slot, current_time_slot, *future_time_slots] storage_state.add_default_values_to_state_profiles(active_market_slot_time_list) - with patch("gsy_e.gsy_e_core.util.ConstSettings.SettlementMarketSettings." - "ENABLE_SETTLEMENT_MARKETS", False): + with patch( + "gsy_e.gsy_e_core.util.ConstSettings.SettlementMarketSettings." + "ENABLE_SETTLEMENT_MARKETS", + False, + ): storage_state.delete_past_state_values(current_time_slot) assert storage_state.pledged_sell_kWh.get(past_time_slot) is None def test_register_energy_from_posted_bid_negative_energy_raise_error(self): storage_state, current_time_slot = self._setup_registration_test() self._assert_negative_energy_raise_error( - storage_state.register_energy_from_posted_bid, time_slot=current_time_slot) + storage_state.register_energy_from_posted_bid, time_slot=current_time_slot + ) def test_register_energy_from_posted_bid_energy_add_energy_to_offered_buy_dict(self): storage_state, current_time_slot = self._setup_registration_test() bid_energy = 1 storage_state.register_energy_from_posted_bid( - energy=bid_energy, time_slot=current_time_slot) + energy=bid_energy, time_slot=current_time_slot + ) storage_state.register_energy_from_posted_bid( - energy=bid_energy, time_slot=current_time_slot) + energy=bid_energy, time_slot=current_time_slot + ) assert storage_state.offered_buy_kWh[current_time_slot] == 2 * bid_energy def test_register_energy_from_posted_offer_negative_energy_raise_error(self): storage_state, current_time_slot = self._setup_registration_test() self._assert_negative_energy_raise_error( - storage_state.register_energy_from_posted_offer, - time_slot=current_time_slot) + storage_state.register_energy_from_posted_offer, time_slot=current_time_slot + ) def test_register_energy_from_posted_offer_energy_add_energy_to_offer_sell_dict(self): storage_state, current_time_slot = self._setup_registration_test() offered_energy = 1 storage_state.register_energy_from_posted_offer( - energy=offered_energy, time_slot=current_time_slot) + energy=offered_energy, time_slot=current_time_slot + ) storage_state.register_energy_from_posted_offer( - energy=offered_energy, time_slot=current_time_slot) + energy=offered_energy, time_slot=current_time_slot + ) assert storage_state.offered_sell_kWh[current_time_slot] == 2 * offered_energy def test_reset_offered_sell_energy_negative_energy_raise_error(self): storage_state, current_time_slot = self._setup_registration_test() self._assert_negative_energy_raise_error( - storage_state.reset_offered_sell_energy, time_slot=current_time_slot) + storage_state.reset_offered_sell_energy, time_slot=current_time_slot + ) def test_reset_offered_sell_energy_energy_in_offered_sell_dict(self): storage_state, current_time_slot = self._setup_registration_test() offered_sell_energy = 1 storage_state.reset_offered_sell_energy( - energy=offered_sell_energy, time_slot=current_time_slot) + energy=offered_sell_energy, time_slot=current_time_slot + ) assert storage_state.offered_sell_kWh[current_time_slot] == offered_sell_energy def test_reset_offered_buy_energy_negative_energy_raise_error(self): storage_state, current_time_slot = self._setup_registration_test() self._assert_negative_energy_raise_error( - storage_state.reset_offered_buy_energy, time_slot=current_time_slot) + storage_state.reset_offered_buy_energy, time_slot=current_time_slot + ) def test_reset_offered_buy_energy_energy_in_offered_buy_dict(self): storage_state, current_time_slot = self._setup_registration_test() offered_buy_energy = 1 storage_state.reset_offered_buy_energy( - energy=offered_buy_energy, time_slot=current_time_slot) + energy=offered_buy_energy, time_slot=current_time_slot + ) assert storage_state.offered_buy_kWh[current_time_slot] == offered_buy_energy def test_remove_energy_from_deleted_offer_negative_energy_raise_error(self): storage_state, current_time_slot = self._setup_registration_test() self._assert_negative_energy_raise_error( - storage_state.remove_energy_from_deleted_offer, time_slot=current_time_slot) + storage_state.remove_energy_from_deleted_offer, time_slot=current_time_slot + ) def test_remove_energy_from_deleted_offer_energy_in_offered_buy_dict(self): storage_state, current_time_slot = self._setup_registration_test() offered_energy = 1 storage_state.offered_sell_kWh[current_time_slot] = offered_energy storage_state.remove_energy_from_deleted_offer( - energy=offered_energy, time_slot=current_time_slot) + energy=offered_energy, time_slot=current_time_slot + ) assert storage_state.offered_buy_kWh[current_time_slot] == 0 def test_register_energy_from_one_sided_market_accept_offer_negative_energy_raise_error(self): storage_state, current_time_slot = self._setup_registration_test() self._assert_negative_energy_raise_error( storage_state.register_energy_from_one_sided_market_accept_offer, - time_slot=current_time_slot) + time_slot=current_time_slot, + ) def test_register_energy_from_one_sided_market_accept_offer_energy_register_in_dict(self): storage_state, current_time_slot = self._setup_registration_test() energy = 1 storage_state.register_energy_from_one_sided_market_accept_offer( - energy=energy, time_slot=current_time_slot) + energy=energy, time_slot=current_time_slot + ) storage_state.register_energy_from_one_sided_market_accept_offer( - energy=energy, time_slot=current_time_slot) + energy=energy, time_slot=current_time_slot + ) assert storage_state.pledged_buy_kWh[current_time_slot] == 2 * energy def test_register_energy_from_bid_trade_negative_energy_raise_error(self): storage_state, current_time_slot = self._setup_registration_test() self._assert_negative_energy_raise_error( - storage_state.register_energy_from_bid_trade, time_slot=current_time_slot) + storage_state.register_energy_from_bid_trade, time_slot=current_time_slot + ) def test_register_energy_from_bid_trade_energy_register_in_dict(self): storage_state, current_time_slot = self._setup_registration_test() energy = 1 storage_state.offered_buy_kWh[current_time_slot] = 2 * energy - storage_state.register_energy_from_bid_trade( - energy=energy, time_slot=current_time_slot) - storage_state.register_energy_from_bid_trade( - energy=energy, time_slot=current_time_slot) + storage_state.register_energy_from_bid_trade(energy=energy, time_slot=current_time_slot) + storage_state.register_energy_from_bid_trade(energy=energy, time_slot=current_time_slot) assert storage_state.pledged_buy_kWh[current_time_slot] == 2 * energy assert storage_state.offered_buy_kWh[current_time_slot] == 0 def test_register_energy_from_offer_trade_negative_energy_raise_error(self): storage_state, current_time_slot = self._setup_registration_test() self._assert_negative_energy_raise_error( - storage_state.register_energy_from_offer_trade, time_slot=current_time_slot) + storage_state.register_energy_from_offer_trade, time_slot=current_time_slot + ) def test_register_energy_from_offer_trade_energy_register_in_dict(self): storage_state, current_time_slot = self._setup_registration_test() energy = 1 storage_state.offered_sell_kWh[current_time_slot] = 2 * energy - storage_state.register_energy_from_offer_trade( - energy=energy, time_slot=current_time_slot) - storage_state.register_energy_from_offer_trade( - energy=energy, time_slot=current_time_slot) + storage_state.register_energy_from_offer_trade(energy=energy, time_slot=current_time_slot) + storage_state.register_energy_from_offer_trade(energy=energy, time_slot=current_time_slot) assert storage_state.pledged_sell_kWh[current_time_slot] == 2 * energy assert storage_state.offered_sell_kWh[current_time_slot] == 0 @@ -518,7 +557,8 @@ def _setup_registration_test(self): current_time_slot, _, _ = self._initialize_time_slots() storage_state.add_default_values_to_state_profiles([current_time_slot]) storage_state.activate( - slot_length=duration(minutes=15), current_time_slot=current_time_slot) + slot_length=duration(minutes=15), current_time_slot=current_time_slot + ) return storage_state, current_time_slot @staticmethod @@ -530,9 +570,11 @@ def _assert_negative_energy_raise_error(method, time_slot): @staticmethod def _setup_storage_state_for_energy_origin_tracking(): storage_state = StorageState() - used_storage_share = [EnergyOrigin(ESSEnergyOrigin.LOCAL, 0.4), - EnergyOrigin(ESSEnergyOrigin.EXTERNAL, 0.5), - EnergyOrigin(ESSEnergyOrigin.UNKNOWN, 0.1)] + used_storage_share = [ + EnergyOrigin(ESSEnergyOrigin.LOCAL, 0.4), + EnergyOrigin(ESSEnergyOrigin.EXTERNAL, 0.5), + EnergyOrigin(ESSEnergyOrigin.UNKNOWN, 0.1), + ] storage_state._used_storage_share = used_storage_share return storage_state @@ -554,7 +596,8 @@ def test_track_energy_sell_type_sell_all_energy(self): storage_state._track_energy_sell_type(energy=available_energy_for_sell) if len(storage_state._used_storage_share) != 0: assert isclose( - storage_state._used_storage_share[0].value, 0, abs_tol=FLOATING_POINT_TOLERANCE) + storage_state._used_storage_share[0].value, 0, abs_tol=FLOATING_POINT_TOLERANCE + ) else: assert len(storage_state._used_storage_share) == 0 @@ -573,22 +616,21 @@ def test_track_energy_sell_type_sell_only_first_entry_completely(self): initial_registry_number = len(storage_state._used_storage_share) original_used_storage_share = storage_state._used_storage_share.copy() storage_state._track_energy_sell_type(energy=energy_for_sale) - assert (len(storage_state._used_storage_share) == - initial_registry_number - 1) - assert (storage_state._used_storage_share[0].origin == - original_used_storage_share[1].origin) + assert len(storage_state._used_storage_share) == initial_registry_number - 1 + assert storage_state._used_storage_share[0].origin == original_used_storage_share[1].origin assert isclose( storage_state._used_storage_share[0].value, original_used_storage_share[1].value, - abs_tol=FLOATING_POINT_TOLERANCE) + abs_tol=FLOATING_POINT_TOLERANCE, + ) def test_get_soc_level_default_values_and_custom_values(self): - storage_state = StorageState(initial_soc=100, - capacity=100) + storage_state = StorageState(initial_soc=100, capacity=100) current_time_slot, _, _ = self._initialize_time_slots() storage_state.add_default_values_to_state_profiles([current_time_slot]) storage_state.activate( - slot_length=duration(minutes=15), current_time_slot=current_time_slot) + slot_length=duration(minutes=15), current_time_slot=current_time_slot + ) assert storage_state.get_soc_level(current_time_slot) == 1 storage_state.charge_history[current_time_slot] = 50 assert storage_state.get_soc_level(current_time_slot) == 0.5 @@ -607,15 +649,15 @@ def test_to_dict_keys_in_return_dict(self): storage_state._used_storage = used_storage_mock assert set(SAMPLE_STATS.keys()).issubset(storage_state.to_dict(current_time_slot).keys()) - assert (storage_state.to_dict(current_time_slot)["energy_to_sell"] == - "test_energy_to_sell") - assert (storage_state.to_dict(current_time_slot)["energy_active_in_bids"] == - "test_energy_active_in_bids") - assert (storage_state.to_dict(current_time_slot)["energy_to_buy"] == - "test_energy_to_buy") - assert (storage_state.to_dict(current_time_slot)["energy_active_in_offers"] == - "test_energy_active_in_offers") - assert (storage_state.to_dict(current_time_slot)["free_storage"] == - "test_free_storage") - assert (storage_state.to_dict(current_time_slot)["used_storage"] == - used_storage_mock) + assert storage_state.to_dict(current_time_slot)["energy_to_sell"] == "test_energy_to_sell" + assert ( + storage_state.to_dict(current_time_slot)["energy_active_in_bids"] + == "test_energy_active_in_bids" + ) + assert storage_state.to_dict(current_time_slot)["energy_to_buy"] == "test_energy_to_buy" + assert ( + storage_state.to_dict(current_time_slot)["energy_active_in_offers"] + == "test_energy_active_in_offers" + ) + assert storage_state.to_dict(current_time_slot)["free_storage"] == "test_free_storage" + assert storage_state.to_dict(current_time_slot)["used_storage"] == used_storage_mock diff --git a/tests/test_stats.py b/tests/test_stats.py index a9fa1f96a..b3f005a25 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -22,6 +22,7 @@ from uuid import uuid4 import pytest +from gsy_framework.constants_limits import TIME_ZONE, DATE_TIME_FORMAT from gsy_framework.data_classes import Trade, TraderDetails from gsy_framework.sim_results.bills import MarketEnergyBills from gsy_framework.unit_test_utils import ( @@ -93,13 +94,11 @@ class FakeMarket: def __init__(self, trades, name="Area", fees=0.0): self.name = name self.trades = trades - self.time_slot = today(tz=constants.TIME_ZONE) + self.time_slot = today(tz=TIME_ZONE) self.market_fee = fees self.const_fee_rate = fees self.time_slot_str = ( - self.time_slot.format(constants.DATE_TIME_FORMAT) - if self.time_slot is not None - else None + self.time_slot.format(DATE_TIME_FORMAT) if self.time_slot is not None else None ) self.offer_history = [] self.bid_history = [] @@ -123,7 +122,7 @@ def serializable_dict(self): def _trade(price, buyer, energy=1, seller=None, fee_price=0.0): return Trade( "id", - now(tz=constants.TIME_ZONE), + now(tz=TIME_ZONE), TraderDetails(seller, ""), TraderDetails(buyer, ""), energy,