From c1d8f4631a489c55b74d01f4aa2fbfaec5698075 Mon Sep 17 00:00:00 2001 From: Ahmad Wahid Date: Fri, 22 Mar 2024 10:09:52 +0100 Subject: [PATCH 01/13] create an soc sensor and a schedule for it Signed-off-by: Ahmad Wahid --- flexmeasures/data/models/planning/storage.py | 16 ++++++++++++- flexmeasures/data/models/planning/utils.py | 24 ++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 76976c5385..3d42beb430 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -21,6 +21,7 @@ get_power_values, fallback_charging_policy, get_continuous_series_sensor_or_quantity, + create_soc_schedule, ) from flexmeasures.data.models.planning.exceptions import InfeasibleProblemException from flexmeasures.data.schemas.scheduling.storage import StorageFlexModelSchema @@ -547,10 +548,12 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: storage_schedule = fallback_charging_policy( sensor, device_constraints[0], start, end, resolution ) + soc_schedule, soc_sensor = create_soc_schedule(sensor, storage_schedule, soc_at_start) # Round schedule if self.round_to_decimals: storage_schedule = storage_schedule.round(self.round_to_decimals) + soc_schedule = soc_schedule.round(self.round_to_decimals) if self.return_multiple: return [ @@ -558,6 +561,11 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: "name": "storage_schedule", "sensor": sensor, "data": storage_schedule, + }, + { + "name": "soc_schedule", + "sensor": soc_sensor, + "data": soc_schedule, } ] else: @@ -604,10 +612,11 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: # Obtain the storage schedule from all device schedules within the EMS storage_schedule = ems_schedule[0] - + soc_schedule, soc_sensor = create_soc_schedule(sensor, storage_schedule, soc_at_start) # Round schedule if self.round_to_decimals: storage_schedule = storage_schedule.round(self.round_to_decimals) + soc_schedule = soc_schedule.round(self.round_to_decimals) if self.return_multiple: return [ @@ -615,6 +624,11 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: "name": "storage_schedule", "sensor": sensor, "data": storage_schedule, + }, + { + "name": "soc_schedule", + "sensor": soc_sensor, + "data": soc_schedule, } ] else: diff --git a/flexmeasures/data/models/planning/utils.py b/flexmeasures/data/models/planning/utils.py index 3c0aedc9dd..82808c7659 100644 --- a/flexmeasures/data/models/planning/utils.py +++ b/flexmeasures/data/models/planning/utils.py @@ -8,6 +8,7 @@ from pandas.tseries.frequencies import to_offset import numpy as np import timely_beliefs as tb +from sqlalchemy import select from flexmeasures.data import db from flexmeasures.data.models.time_series import Sensor, TimedBelief @@ -21,6 +22,9 @@ from flexmeasures.utils.unit_utils import ur, convert_units from pint.errors import UndefinedUnitError, DimensionalityError +from flexmeasures.data.services.utils import get_or_create_model +from flexmeasures.utils.calculations import integrate_time_series + def initialize_df( columns: list[str], @@ -425,3 +429,23 @@ def nanmin_of_series_and_value(s: pd.Series, value: float | pd.Series) -> pd.Ser # [right]: datetime64[ns, UTC] value = value.tz_convert("UTC") return s.fillna(value).clip(upper=value) + + +def create_soc_schedule(sensor: Sensor, data: pd.Series, soc_at_start: float) -> tuple[pd.Series, Sensor]: + """Generates a state of charge (SOC) schedule from provided data and creates or retrieves an SOC sensor.""" + asset = db.session.scalars( + select(Asset).filter_by(id=sensor.generic_asset_id)).one_or_none() + + soc_schedule = integrate_time_series( + data, soc_at_start, decimal_precision=6) + + soc_sensor = get_or_create_model( + Sensor, + name="soc schedule", + generic_asset=asset, + unit="MWh", + timezone=sensor.timezone, + event_resolution=timedelta(minutes=15), + ) + + return soc_schedule, soc_sensor From 23160b950b2ac612f0be79cbda18642143b570b4 Mon Sep 17 00:00:00 2001 From: Ahmad Wahid Date: Fri, 22 Mar 2024 10:11:03 +0100 Subject: [PATCH 02/13] run pre-commit Signed-off-by: Ahmad Wahid --- flexmeasures/data/models/planning/storage.py | 12 ++++++++---- flexmeasures/data/models/planning/utils.py | 10 ++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 3d42beb430..5f1741831f 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -548,7 +548,9 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: storage_schedule = fallback_charging_policy( sensor, device_constraints[0], start, end, resolution ) - soc_schedule, soc_sensor = create_soc_schedule(sensor, storage_schedule, soc_at_start) + soc_schedule, soc_sensor = create_soc_schedule( + sensor, storage_schedule, soc_at_start + ) # Round schedule if self.round_to_decimals: @@ -566,7 +568,7 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: "name": "soc_schedule", "sensor": soc_sensor, "data": soc_schedule, - } + }, ] else: return storage_schedule @@ -612,7 +614,9 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: # Obtain the storage schedule from all device schedules within the EMS storage_schedule = ems_schedule[0] - soc_schedule, soc_sensor = create_soc_schedule(sensor, storage_schedule, soc_at_start) + soc_schedule, soc_sensor = create_soc_schedule( + sensor, storage_schedule, soc_at_start + ) # Round schedule if self.round_to_decimals: storage_schedule = storage_schedule.round(self.round_to_decimals) @@ -629,7 +633,7 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: "name": "soc_schedule", "sensor": soc_sensor, "data": soc_schedule, - } + }, ] else: return storage_schedule diff --git a/flexmeasures/data/models/planning/utils.py b/flexmeasures/data/models/planning/utils.py index 82808c7659..f845d5e559 100644 --- a/flexmeasures/data/models/planning/utils.py +++ b/flexmeasures/data/models/planning/utils.py @@ -431,13 +431,15 @@ def nanmin_of_series_and_value(s: pd.Series, value: float | pd.Series) -> pd.Ser return s.fillna(value).clip(upper=value) -def create_soc_schedule(sensor: Sensor, data: pd.Series, soc_at_start: float) -> tuple[pd.Series, Sensor]: +def create_soc_schedule( + sensor: Sensor, data: pd.Series, soc_at_start: float +) -> tuple[pd.Series, Sensor]: """Generates a state of charge (SOC) schedule from provided data and creates or retrieves an SOC sensor.""" asset = db.session.scalars( - select(Asset).filter_by(id=sensor.generic_asset_id)).one_or_none() + select(Asset).filter_by(id=sensor.generic_asset_id) + ).one_or_none() - soc_schedule = integrate_time_series( - data, soc_at_start, decimal_precision=6) + soc_schedule = integrate_time_series(data, soc_at_start, decimal_precision=6) soc_sensor = get_or_create_model( Sensor, From 91eb16e419597f287313cc317e86b936c1ec4122 Mon Sep 17 00:00:00 2001 From: Ahmad Wahid Date: Fri, 5 Apr 2024 09:23:58 +0200 Subject: [PATCH 03/13] add soc param to get soc sensor value and save the soc schedule Signed-off-by: Ahmad Wahid --- flexmeasures/cli/data_add.py | 16 +++- flexmeasures/data/models/planning/storage.py | 73 +++++++++++++------ .../data/models/planning/tests/test_solver.py | 21 +++--- flexmeasures/data/models/planning/utils.py | 32 +++----- .../data/schemas/scheduling/storage.py | 3 +- flexmeasures/data/schemas/sensors.py | 11 +++ 6 files changed, 98 insertions(+), 58 deletions(-) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index bdd253ca19..34e2f56a49 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -1168,9 +1168,16 @@ def create_schedule(ctx): "--soc-at-start", "soc_at_start", type=QuantityField("%", validate=validate.Range(min=0, max=1)), - required=True, + required=False, help="State of charge (e.g 32.8%, or 0.328) at the start of the schedule.", ) +@click.option( + "--soc", + "soc", + type=QuantityOrSensor("MWh"), + required=False, + help="State of charge (e.g sensor:, or 0.328) at the start of the schedule.", +) @click.option( "--soc-target", "soc_target_strings", @@ -1304,7 +1311,8 @@ def add_schedule_for_storage( # noqa C901 site_production_capacity: ur.Quantity | Sensor | None, start: datetime, duration: timedelta, - soc_at_start: ur.Quantity, + soc_at_start: ur.Quantity | None, + soc: ur.Quantity | Sensor | None, charging_efficiency: ur.Quantity | Sensor | None, discharging_efficiency: ur.Quantity | Sensor | None, soc_gain: ur.Quantity | Sensor | None, @@ -1354,7 +1362,8 @@ def add_schedule_for_storage( # noqa C901 ) raise click.Abort() capacity_str = f"{power_sensor.get_attribute('max_soc_in_mwh')} MWh" - soc_at_start = convert_units(soc_at_start.magnitude, soc_at_start.units, "MWh", capacity=capacity_str) # type: ignore + if soc_at_start is not None: + soc_at_start = convert_units(soc_at_start.magnitude, soc_at_start.units, "MWh", capacity=capacity_str) # type: ignore soc_targets = [] for soc_target_tuple in soc_target_strings: soc_target_value_str, soc_target_datetime_str = soc_target_tuple @@ -1381,6 +1390,7 @@ def add_schedule_for_storage( # noqa C901 belief_time=server_now(), resolution=power_sensor.event_resolution, flex_model={ + "soc": soc, "soc-at-start": soc_at_start, "soc-targets": soc_targets, "soc-min": soc_min, diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 5f1741831f..182bf2e90d 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -21,7 +21,7 @@ get_power_values, fallback_charging_policy, get_continuous_series_sensor_or_quantity, - create_soc_schedule, + get_sensor_soc_value, ) from flexmeasures.data.models.planning.exceptions import InfeasibleProblemException from flexmeasures.data.schemas.scheduling.storage import StorageFlexModelSchema @@ -29,6 +29,7 @@ from flexmeasures.utils.time_utils import get_max_planning_horizon from flexmeasures.utils.coding_utils import deprecated from flexmeasures.utils.unit_utils import ur +from flexmeasures.utils.calculations import integrate_time_series class MetaStorageScheduler(Scheduler): @@ -80,6 +81,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 sensor = self.sensor soc_at_start = self.flex_model.get("soc_at_start") + soc = self.flex_model.get("soc") soc_targets = self.flex_model.get("soc_targets") soc_min = self.flex_model.get("soc_min") soc_max = self.flex_model.get("soc_max") @@ -93,7 +95,12 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 inflexible_device_sensors = self.flex_context.get( "inflexible_device_sensors", [] ) - + soc_sensor = None + if isinstance(soc, Sensor): + soc_sensor = soc + soc_at_start = get_sensor_soc_value(soc, start) + elif (isinstance(soc, float) or isinstance(soc, int)) and soc > 0: + soc_at_start = soc # Check for required Sensor attributes power_capacity_in_mw = self.flex_model.get( "power_capacity_in_mw", @@ -369,6 +376,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 end, resolution, soc_at_start, + soc_sensor, device_constraints, ems_constraints, commitment_quantities, @@ -410,6 +418,16 @@ def deserialize_flex_config(self): # Otherwise, we try to retrieve the current state of charge from the asset (if that is the valid one at the start). # If that doesn't work, we set the starting soc to 0 (some assets don't use the concept of a state of charge, # and without soc targets and limits the starting soc doesn't matter). + + if "soc" in self.flex_model: + if ( + self.flex_model["soc"] is not None + and self.flex_model["soc-at-start"] is not None + ): + raise Exception( + "Both 'soc-at-start' and 'soc' parameters are provided, however, only one of them is necessary." + ) + if ( "soc-at-start" not in self.flex_model or self.flex_model["soc-at-start"] is None @@ -423,6 +441,10 @@ def deserialize_flex_config(self): ) else: self.flex_model["soc-at-start"] = 0 + + if "soc" not in self.flex_model or self.flex_model["soc"] is None: + self.flex_model["soc"] = 0 + # soc-unit if "soc-unit" not in self.flex_model or self.flex_model["soc-unit"] is None: if self.sensor.unit in ("MWh", "kWh"): @@ -537,6 +559,7 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: end, resolution, soc_at_start, + soc_sensor, device_constraints, ems_constraints, commitment_quantities, @@ -548,9 +571,7 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: storage_schedule = fallback_charging_policy( sensor, device_constraints[0], start, end, resolution ) - soc_schedule, soc_sensor = create_soc_schedule( - sensor, storage_schedule, soc_at_start - ) + soc_schedule = integrate_time_series(storage_schedule, soc_at_start) # Round schedule if self.round_to_decimals: @@ -558,18 +579,22 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: soc_schedule = soc_schedule.round(self.round_to_decimals) if self.return_multiple: - return [ + data_list = [ { "name": "storage_schedule", "sensor": sensor, "data": storage_schedule, - }, - { - "name": "soc_schedule", - "sensor": soc_sensor, - "data": soc_schedule, - }, + } ] + if soc_sensor is not None: + data_list.append( + { + "name": "soc_schedule", + "sensor": soc_sensor, + "data": soc_schedule, + } + ) + return data_list else: return storage_schedule @@ -594,6 +619,7 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: end, resolution, soc_at_start, + soc_sensor, device_constraints, ems_constraints, commitment_quantities, @@ -614,27 +640,30 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: # Obtain the storage schedule from all device schedules within the EMS storage_schedule = ems_schedule[0] - soc_schedule, soc_sensor = create_soc_schedule( - sensor, storage_schedule, soc_at_start - ) + soc_schedule = integrate_time_series(storage_schedule, soc_at_start) + # Round schedule if self.round_to_decimals: storage_schedule = storage_schedule.round(self.round_to_decimals) soc_schedule = soc_schedule.round(self.round_to_decimals) if self.return_multiple: - return [ + data_list = [ { "name": "storage_schedule", "sensor": sensor, "data": storage_schedule, - }, - { - "name": "soc_schedule", - "sensor": soc_sensor, - "data": soc_schedule, - }, + } ] + if soc_sensor is not None: + data_list.append( + { + "name": "soc_schedule", + "sensor": soc_sensor, + "data": soc_schedule, + } + ) + return data_list else: return storage_schedule diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 53358d2eb3..0d209b60f7 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -223,6 +223,7 @@ def run_test_charge_discharge_sign( end, resolution, soc_at_start, + soc_sensor, device_constraints, ems_constraints, commitment_quantities, @@ -1074,6 +1075,7 @@ def test_numerical_errors(app_with_each_solver, setup_planning_test_data, db): end, resolution, soc_at_start, + soc_sensor, device_constraints, ems_constraints, commitment_quantities, @@ -1256,6 +1258,7 @@ def set_if_not_none(dictionary, key, value): end, resolution, soc_at_start, + soc_sensor, device_constraints, ems_constraints, commitment_quantities, @@ -1546,8 +1549,8 @@ def test_battery_power_capacity_as_sensor( ) data_to_solver = scheduler._prepare() - device_constraints = data_to_solver[5][0] - ems_constraints = data_to_solver[6] + device_constraints = data_to_solver[6][0] + ems_constraints = data_to_solver[7] assert all(device_constraints["derivative min"].values == expected_production) assert all(device_constraints["derivative max"].values == expected_consumption) @@ -1586,7 +1589,7 @@ def test_battery_bothways_power_capacity_as_sensor( ) data_to_solver = scheduler._prepare() - device_constraints = data_to_solver[5][0] + device_constraints = data_to_solver[6][0] max_capacity = ( capacity_sensors["power_capacity"] @@ -1616,7 +1619,7 @@ def get_efficiency_problem_device_constraints( ) scheduler_data = scheduler._prepare() - return scheduler_data[5][0] + return scheduler_data[6][0] def test_dis_charging_efficiency_as_sensor( @@ -1755,9 +1758,9 @@ def test_battery_stock_delta_quantity( scheduler_info = scheduler._prepare() if expected_delta is not None: - assert all(scheduler_info[5][0]["stock delta"] == expected_delta) + assert all(scheduler_info[6][0]["stock delta"] == expected_delta) else: - assert all(scheduler_info[5][0]["stock delta"].isna()) + assert all(scheduler_info[6][0]["stock delta"].isna()) @pytest.mark.parametrize( @@ -1810,9 +1813,9 @@ def test_battery_efficiency_quantity( scheduler_info = scheduler._prepare() if efficiency is not None: - assert all(scheduler_info[5][0]["efficiency"] == expected_efficiency) + assert all(scheduler_info[6][0]["efficiency"] == expected_efficiency) else: - assert all(scheduler_info[5][0]["efficiency"].isna()) + assert all(scheduler_info[6][0]["efficiency"].isna()) @pytest.mark.parametrize( @@ -1869,4 +1872,4 @@ def test_battery_storage_efficiency_sensor( ) scheduler_info = scheduler._prepare() - assert all(scheduler_info[5][0]["efficiency"] == expected_efficiency) + assert all(scheduler_info[6][0]["efficiency"] == expected_efficiency) diff --git a/flexmeasures/data/models/planning/utils.py b/flexmeasures/data/models/planning/utils.py index f845d5e559..a2a0fbe2a4 100644 --- a/flexmeasures/data/models/planning/utils.py +++ b/flexmeasures/data/models/planning/utils.py @@ -22,9 +22,6 @@ from flexmeasures.utils.unit_utils import ur, convert_units from pint.errors import UndefinedUnitError, DimensionalityError -from flexmeasures.data.services.utils import get_or_create_model -from flexmeasures.utils.calculations import integrate_time_series - def initialize_df( columns: list[str], @@ -431,23 +428,12 @@ def nanmin_of_series_and_value(s: pd.Series, value: float | pd.Series) -> pd.Ser return s.fillna(value).clip(upper=value) -def create_soc_schedule( - sensor: Sensor, data: pd.Series, soc_at_start: float -) -> tuple[pd.Series, Sensor]: - """Generates a state of charge (SOC) schedule from provided data and creates or retrieves an SOC sensor.""" - asset = db.session.scalars( - select(Asset).filter_by(id=sensor.generic_asset_id) - ).one_or_none() - - soc_schedule = integrate_time_series(data, soc_at_start, decimal_precision=6) - - soc_sensor = get_or_create_model( - Sensor, - name="soc schedule", - generic_asset=asset, - unit="MWh", - timezone=sensor.timezone, - event_resolution=timedelta(minutes=15), - ) - - return soc_schedule, soc_sensor +def get_sensor_soc_value(sensor: Sensor, start: datetime): + soc_value = db.session.execute( + select(TimedBelief).filter_by(sensor_id=sensor.id, event_start=start) + ).scalar_one_or_none() + if soc_value is not None: + soc_value = soc_value.event_value + else: + soc_value = 0 + return soc_value diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index bb8f18fd56..650cc09745 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -135,7 +135,8 @@ class StorageFlexModelSchema(Schema): You can use StorageScheduler.deserialize_flex_config to get that filled in. """ - soc_at_start = fields.Float(required=True, data_key="soc-at-start") + soc_at_start = fields.Float(required=False, data_key="soc-at-start") + soc = QuantityOrSensor("MWh", data_key="soc", required=False) soc_min = fields.Float(validate=validate.Range(min=0), data_key="soc-min") soc_max = fields.Float(data_key="soc-max") diff --git a/flexmeasures/data/schemas/sensors.py b/flexmeasures/data/schemas/sensors.py index 70efff73cd..3044f901a7 100644 --- a/flexmeasures/data/schemas/sensors.py +++ b/flexmeasures/data/schemas/sensors.py @@ -145,11 +145,22 @@ def _deserialize( elif isinstance(value, str): try: + try: + float(value) + return value + except ValueError: + pass return ur.Quantity(value).to(self.to_unit) except DimensionalityError as e: raise FMValidationError( f"Cannot convert value `{value}` to '{self.to_unit}'" ) from e + elif ( + isinstance(value, Sensor) + or isinstance(value, int) + or isinstance(value, float) + ): + return value else: if self.default_src_unit is not None: return self._deserialize( From 7f01658cf413ffdf31dac4ba0ede83a2a922a0b2 Mon Sep 17 00:00:00 2001 From: Ahmad Wahid Date: Fri, 5 Apr 2024 09:40:13 +0200 Subject: [PATCH 04/13] rename get-soc-sensor-value function Signed-off-by: Ahmad Wahid --- flexmeasures/data/models/planning/storage.py | 4 ++-- flexmeasures/data/models/planning/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index ea582f3d3c..7f3d77c964 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -21,7 +21,7 @@ get_power_values, fallback_charging_policy, get_continuous_series_sensor_or_quantity, - get_sensor_soc_value, + get_soc_sensor_value, ) from flexmeasures.data.models.planning.exceptions import InfeasibleProblemException from flexmeasures.data.schemas.scheduling.storage import StorageFlexModelSchema @@ -98,7 +98,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 soc_sensor = None if isinstance(soc, Sensor): soc_sensor = soc - soc_at_start = get_sensor_soc_value(soc, start) + soc_at_start = get_soc_sensor_value(soc, start) elif (isinstance(soc, float) or isinstance(soc, int)) and soc > 0: soc_at_start = soc # Check for required Sensor attributes diff --git a/flexmeasures/data/models/planning/utils.py b/flexmeasures/data/models/planning/utils.py index 34089bbf7e..8b448e9731 100644 --- a/flexmeasures/data/models/planning/utils.py +++ b/flexmeasures/data/models/planning/utils.py @@ -444,7 +444,7 @@ def nanmin_of_series_and_value(s: pd.Series, value: float | pd.Series) -> pd.Ser return s.fillna(value).clip(upper=value) -def get_sensor_soc_value(sensor: Sensor, start: datetime): +def get_soc_sensor_value(sensor: Sensor, start: datetime): soc_value = db.session.execute( select(TimedBelief).filter_by(sensor_id=sensor.id, event_start=start) ).scalar_one_or_none() From d17fe37d5af1a787e63d75f718745c7ca204951e Mon Sep 17 00:00:00 2001 From: Ahmad Wahid Date: Tue, 9 Apr 2024 06:02:48 +0200 Subject: [PATCH 05/13] refactor the schema and cli inputs Signed-off-by: Ahmad Wahid --- flexmeasures/cli/data_add.py | 2 +- flexmeasures/data/models/planning/storage.py | 53 ++++++++++++------- .../data/models/planning/tests/test_solver.py | 2 +- .../data/schemas/scheduling/storage.py | 2 +- flexmeasures/data/schemas/sensors.py | 11 +--- 5 files changed, 39 insertions(+), 31 deletions(-) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index 3e170db051..1643299ba8 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -1311,7 +1311,7 @@ def add_schedule_for_storage( # noqa C901 site_production_capacity: ur.Quantity | Sensor | None, start: datetime, duration: timedelta, - soc_at_start: ur.Quantity | None, + soc_at_start: ur.Quantity, soc: ur.Quantity | Sensor | None, charging_efficiency: ur.Quantity | Sensor | None, discharging_efficiency: ur.Quantity | Sensor | None, diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 7f3d77c964..350e8cbc40 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -21,7 +21,7 @@ get_power_values, fallback_charging_policy, get_continuous_series_sensor_or_quantity, - get_soc_sensor_value, + # get_soc_sensor_value, ) from flexmeasures.data.models.planning.exceptions import InfeasibleProblemException from flexmeasures.data.schemas.scheduling.storage import StorageFlexModelSchema @@ -98,9 +98,10 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 soc_sensor = None if isinstance(soc, Sensor): soc_sensor = soc - soc_at_start = get_soc_sensor_value(soc, start) - elif (isinstance(soc, float) or isinstance(soc, int)) and soc > 0: - soc_at_start = soc + # soc_at_start = get_soc_sensor_value(soc, start) + # elif (isinstance(soc, float) or isinstance(soc, int)) and soc > 0: + # soc_at_start = soc + # Check for required Sensor attributes power_capacity_in_mw = self.flex_model.get( "power_capacity_in_mw", @@ -454,14 +455,14 @@ def deserialize_flex_config(self): # If that doesn't work, we set the starting soc to 0 (some assets don't use the concept of a state of charge, # and without soc targets and limits the starting soc doesn't matter). - if "soc" in self.flex_model: - if ( - self.flex_model["soc"] is not None - and self.flex_model["soc-at-start"] is not None - ): - raise Exception( - "Both 'soc-at-start' and 'soc' parameters are provided, however, only one of them is necessary." - ) + # if "soc" in self.flex_model: + # if ( + # self.flex_model["soc"] is not None + # and self.flex_model["soc-at-start"] is not None + # ): + # raise Exception( + # "Both 'soc-at-start' and 'soc' parameters are provided, however, only one of them is necessary." + # ) if ( "soc-at-start" not in self.flex_model @@ -477,8 +478,8 @@ def deserialize_flex_config(self): else: self.flex_model["soc-at-start"] = 0 - if "soc" not in self.flex_model or self.flex_model["soc"] is None: - self.flex_model["soc"] = 0 + if self.flex_model.get("soc") is None: + self.flex_model["soc"] = "0 MWh" # soc-unit if "soc-unit" not in self.flex_model or self.flex_model["soc-unit"] is None: @@ -612,12 +613,20 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: storage_schedule = fallback_charging_policy( sensor, device_constraints[0], start, end, resolution ) - soc_schedule = integrate_time_series(storage_schedule, soc_at_start) + if soc_sensor is not None: + soc_schedule = integrate_time_series( + storage_schedule, + soc_at_start, + down_efficiency=device_constraints[0]["derivative down efficiency"], + up_efficiency=device_constraints[0]["derivative up efficiency"], + storage_efficiency=device_constraints[0]["efficiency"], + ) # Round schedule if self.round_to_decimals: storage_schedule = storage_schedule.round(self.round_to_decimals) - soc_schedule = soc_schedule.round(self.round_to_decimals) + if soc_sensor is not None: + soc_schedule = soc_schedule.round(self.round_to_decimals) if self.return_multiple: data_list = [ @@ -681,12 +690,20 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: # Obtain the storage schedule from all device schedules within the EMS storage_schedule = ems_schedule[0] - soc_schedule = integrate_time_series(storage_schedule, soc_at_start) + if soc_sensor is not None: + soc_schedule = integrate_time_series( + storage_schedule, + soc_at_start, + down_efficiency=device_constraints[0]["derivative down efficiency"], + up_efficiency=device_constraints[0]["derivative up efficiency"], + storage_efficiency=device_constraints[0]["efficiency"], + ) # Round schedule if self.round_to_decimals: storage_schedule = storage_schedule.round(self.round_to_decimals) - soc_schedule = soc_schedule.round(self.round_to_decimals) + if soc_sensor is not None: + soc_schedule = soc_schedule.round(self.round_to_decimals) if self.return_multiple: data_list = [ diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 5e3e4ac592..873fc34667 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -1951,7 +1951,7 @@ def test_add_storage_constraint_from_sensor( ) scheduler_info = scheduler._prepare() - storage_constraints = scheduler_info[5][0] + storage_constraints = scheduler_info[6][0] expected_target_start = pd.Timedelta(expected_start) + start expected_target_end = pd.Timedelta(expected_end) + start diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index f0e78d1e01..423940e7e7 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -54,7 +54,7 @@ class StorageFlexModelSchema(Schema): You can use StorageScheduler.deserialize_flex_config to get that filled in. """ - soc_at_start = fields.Float(required=False, data_key="soc-at-start") + soc_at_start = fields.Float(required=True, data_key="soc-at-start") soc = QuantityOrSensor("MWh", data_key="soc", required=False) soc_min = fields.Float(validate=validate.Range(min=0), data_key="soc-min") diff --git a/flexmeasures/data/schemas/sensors.py b/flexmeasures/data/schemas/sensors.py index 22e6a5f4f0..4105fa3d51 100644 --- a/flexmeasures/data/schemas/sensors.py +++ b/flexmeasures/data/schemas/sensors.py @@ -242,21 +242,12 @@ def _deserialize( elif isinstance(value, str): try: - try: - float(value) - return value - except ValueError: - pass return ur.Quantity(value).to(self.to_unit) except DimensionalityError as e: raise FMValidationError( f"Cannot convert value `{value}` to '{self.to_unit}'" ) from e - elif ( - isinstance(value, Sensor) - or isinstance(value, int) - or isinstance(value, float) - ): + elif isinstance(value, Sensor): return value else: if self.default_src_unit is not None: From 5d8b14d4c850e3626e16448666ce057af9c25211 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 7 May 2024 17:39:39 +0200 Subject: [PATCH 06/13] fix: extend type annotation Signed-off-by: F.N. Claessen --- flexmeasures/data/schemas/sensors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/schemas/sensors.py b/flexmeasures/data/schemas/sensors.py index 4105fa3d51..dd1fc0f83a 100644 --- a/flexmeasures/data/schemas/sensors.py +++ b/flexmeasures/data/schemas/sensors.py @@ -227,7 +227,7 @@ def __init__( @with_appcontext_if_needed() def _deserialize( - self, value: str | dict[str, int], attr, obj, **kwargs + self, value: str | dict[str, int] | Sensor, attr, obj, **kwargs ) -> ur.Quantity | Sensor: if isinstance(value, dict): if "sensor" not in value: From 3833c2ea13ecd4205405bdfdd5916c2e2b6d0d19 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 7 May 2024 17:43:23 +0200 Subject: [PATCH 07/13] refactor: flatten if/else-block Signed-off-by: F.N. Claessen --- flexmeasures/data/schemas/sensors.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/flexmeasures/data/schemas/sensors.py b/flexmeasures/data/schemas/sensors.py index dd1fc0f83a..29e13ec4b4 100644 --- a/flexmeasures/data/schemas/sensors.py +++ b/flexmeasures/data/schemas/sensors.py @@ -237,9 +237,7 @@ def _deserialize( sensor = SensorIdField(unit=self.to_unit)._deserialize( value["sensor"], None, None ) - return sensor - elif isinstance(value, str): try: return ur.Quantity(value).to(self.to_unit) @@ -249,12 +247,11 @@ def _deserialize( ) from e elif isinstance(value, Sensor): return value + elif self.default_src_unit is not None: + return self._deserialize( + f"{value} {self.default_src_unit}", attr, obj, **kwargs + ) else: - if self.default_src_unit is not None: - return self._deserialize( - f"{value} {self.default_src_unit}", attr, obj, **kwargs - ) - raise FMValidationError( f"Unsupported value type. `{type(value)}` was provided but only dict and str are supported." ) From 42b121a0992483bb2017b2149f4df9ea2d84b23d Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 7 May 2024 17:46:36 +0200 Subject: [PATCH 08/13] feature: add unit validation Signed-off-by: F.N. Claessen --- flexmeasures/data/schemas/sensors.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flexmeasures/data/schemas/sensors.py b/flexmeasures/data/schemas/sensors.py index 29e13ec4b4..4fed422d1b 100644 --- a/flexmeasures/data/schemas/sensors.py +++ b/flexmeasures/data/schemas/sensors.py @@ -246,7 +246,12 @@ def _deserialize( f"Cannot convert value `{value}` to '{self.to_unit}'" ) from e elif isinstance(value, Sensor): - return value + sensor = value + if not units_are_convertible(sensor.unit, str(self.to_unit.units)): + raise FMValidationError( + f"Cannot convert {sensor.unit} to {self.to_unit.units}" + ) + return sensor elif self.default_src_unit is not None: return self._deserialize( f"{value} {self.default_src_unit}", attr, obj, **kwargs From 359fa130b0f6fb9f3dad4bd54ca745ec5c5772ed Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 26 Aug 2024 12:18:50 +0200 Subject: [PATCH 09/13] Refactor return value of test util using dataclasses Signed-off-by: F.N. Claessen --- .../data/models/planning/tests/test_solver.py | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 9895d17b04..880ce76c1b 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from datetime import datetime, timedelta import pytest import pytz @@ -71,9 +72,9 @@ def test_battery_solver_day_1( battery_name, db, ): - epex_da, battery = get_sensors_from_db( + battery = get_sensors_from_db( db, add_battery_assets, battery_name=battery_name - ) + ).power tz = pytz.timezone("Europe/Amsterdam") start = tz.localize(datetime(2015, 1, 1)) end = tz.localize(datetime(2015, 1, 2)) @@ -123,7 +124,7 @@ def test_battery_solver_day_2( and so we expect the scheduler to only: - completely discharge within the last 8 hours """ - _epex_da, battery = get_sensors_from_db(db, add_battery_assets) + battery = get_sensors_from_db(db, add_battery_assets).power tz = pytz.timezone("Europe/Amsterdam") start = tz.localize(datetime(2015, 1, 2)) end = tz.localize(datetime(2015, 1, 3)) @@ -666,7 +667,7 @@ def test_soc_bounds_timeseries(db, add_battery_assets): """ # get the sensors from the database - epex_da, battery = get_sensors_from_db(db, add_battery_assets) + battery = get_sensors_from_db(db, add_battery_assets).power # time parameters tz = pytz.timezone("Europe/Amsterdam") @@ -975,7 +976,7 @@ def test_infeasible_problem_error(db, add_battery_assets): """Try to create a schedule with infeasible constraints. soc-max is 4.5 and soc-target is 8.0""" # get the sensors from the database - _epex_da, battery = get_sensors_from_db(db, add_battery_assets) + battery = get_sensors_from_db(db, add_battery_assets).power # time parameters tz = pytz.timezone("Europe/Amsterdam") @@ -1020,17 +1021,23 @@ def compute_schedule(flex_model): compute_schedule(flex_model) +@dataclass +class BatterySensors: + price: Sensor + power: Sensor + + def get_sensors_from_db( db, battery_assets, battery_name="Test battery", power_sensor_name="power" ): # get the sensors from the database epex_da = get_test_sensor(db) - battery = [ + battery_power = [ s for s in battery_assets[battery_name].sensors if s.name == power_sensor_name ][0] - assert battery.get_attribute("market_id") == epex_da.id + assert battery_power.get_attribute("market_id") == epex_da.id - return epex_da, battery + return BatterySensors(price=epex_da, power=battery_power) def test_numerical_errors(app_with_each_solver, setup_planning_test_data, db): @@ -1504,9 +1511,9 @@ def test_battery_power_capacity_as_sensor( expected_site_production, expected_site_consumption, ): - epex_da, battery = get_sensors_from_db( + battery = get_sensors_from_db( db, add_battery_assets, battery_name=battery_name - ) + ).power tz = pytz.timezone("Europe/Amsterdam") start = tz.localize(datetime(2015, 1, 2)) @@ -1569,9 +1576,9 @@ def test_battery_bothways_power_capacity_as_sensor( db, add_battery_assets, add_inflexible_device_forecasts, capacity_sensors ): """Check that the charging and discharging power capacities are limited by the power capacity.""" - epex_da, battery = get_sensors_from_db( + battery = get_sensors_from_db( db, add_battery_assets, battery_name="Test battery" - ) + ).power tz = pytz.timezone("Europe/Amsterdam") start = tz.localize(datetime(2015, 1, 2)) @@ -1611,7 +1618,7 @@ def test_battery_bothways_power_capacity_as_sensor( def get_efficiency_problem_device_constraints( extra_flex_model, efficiency_sensors, add_battery_assets, db ) -> pd.DataFrame: - _, battery = get_sensors_from_db(db, add_battery_assets) + battery = get_sensors_from_db(db, add_battery_assets).power tz = pytz.timezone("Europe/Amsterdam") start = tz.localize(datetime(2015, 1, 1)) end = tz.localize(datetime(2015, 1, 2)) @@ -1691,7 +1698,7 @@ def test_battery_stock_delta_sensor( With these settings, the battery needs to charge at a power or greater than the usage forecast to keep the SOC within bounds ([0, 2 MWh]). """ - _, battery = get_sensors_from_db(db, add_battery_assets) + battery = get_sensors_from_db(db, add_battery_assets).power tz = pytz.timezone("Europe/Amsterdam") start = tz.localize(datetime(2015, 1, 1)) end = tz.localize(datetime(2015, 1, 2)) @@ -1742,7 +1749,7 @@ def test_battery_stock_delta_quantity( We expect a constant gain/usage to happen in every time period equal to the energy value provided. """ - _, battery = get_sensors_from_db(db, add_battery_assets) + battery = get_sensors_from_db(db, add_battery_assets).power tz = pytz.timezone("Europe/Amsterdam") start = tz.localize(datetime(2015, 1, 1)) end = tz.localize(datetime(2015, 1, 2)) @@ -1800,7 +1807,7 @@ def test_battery_efficiency_quantity( case where the efficiency is not defined. """ - _, battery = get_sensors_from_db(db, add_battery_assets) + battery = get_sensors_from_db(db, add_battery_assets).power tz = pytz.timezone("Europe/Amsterdam") start = tz.localize(datetime(2015, 1, 1)) end = tz.localize(datetime(2015, 1, 2)) @@ -1856,7 +1863,7 @@ def test_battery_storage_efficiency_sensor( It checks if the scheduler correctly handles regular values, values exceeding 100%, negative values, and values with different resolutions compared to the scheduling resolution. """ - _, battery = get_sensors_from_db(db, add_battery_assets) + battery = get_sensors_from_db(db, add_battery_assets).power tz = pytz.timezone("Europe/Amsterdam") start = tz.localize(datetime(2015, 1, 1)) end = tz.localize(datetime(2015, 1, 2)) @@ -1936,7 +1943,7 @@ def test_add_storage_constraint_from_sensor( """ Test the handling of different values for the target SOC constraints as sensors in the StorageScheduler. """ - _, battery = get_sensors_from_db(db, add_battery_assets) + battery = get_sensors_from_db(db, add_battery_assets).power tz = pytz.timezone("Europe/Amsterdam") start = tz.localize(datetime(2015, 1, 1)) end = tz.localize(datetime(2015, 1, 2)) @@ -2078,12 +2085,12 @@ def test_battery_storage_different_units( soc_max = soc_max.to(soc_unit).magnitude soc_at_start = soc_at_start.to(soc_unit).magnitude - epex_da, battery = get_sensors_from_db( + battery = get_sensors_from_db( db, add_battery_assets, battery_name="Test battery", power_sensor_name=power_sensor_name, - ) + ).power tz = pytz.timezone("Europe/Amsterdam") # transition from cheap to expensive (90 -> 100) @@ -2192,12 +2199,12 @@ def test_battery_storage_with_time_series_in_flex_model( soc_max = "1 MWh" soc_at_start = "100 kWh" - epex_da, battery = get_sensors_from_db( + battery = get_sensors_from_db( db, add_battery_assets, battery_name="Test battery", power_sensor_name="power", - ) + ).power tz = pytz.timezone("Europe/Amsterdam") # transition from cheap to expensive (90 -> 100) From 405012c680c8df7af2f787011e6b64e969a4fdbe Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 26 Aug 2024 12:34:50 +0200 Subject: [PATCH 10/13] fix: add test and fix unit conversion for SoC schedule Signed-off-by: F.N. Claessen --- flexmeasures/conftest.py | 12 +++++++ flexmeasures/data/models/planning/storage.py | 6 ++++ .../data/models/planning/tests/test_solver.py | 36 ++++++++++++++----- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/flexmeasures/conftest.py b/flexmeasures/conftest.py index c7795ded2d..9b036357ff 100644 --- a/flexmeasures/conftest.py +++ b/flexmeasures/conftest.py @@ -831,6 +831,18 @@ def create_test_battery_assets( ), ) db.session.add(test_battery_sensor_kw) + test_battery_sensor_kwh = Sensor( + name="state of charge (Wh)", + generic_asset=test_battery, + event_resolution=timedelta(hours=0), + unit="Wh", + attributes=dict( + daily_seasonality=True, + weekly_seasonality=True, + yearly_seasonality=True, + ), + ) + db.session.add(test_battery_sensor_kwh) test_battery_no_prices = GenericAsset( name="Test battery with no known prices", diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 464b8e9479..efabbef1e3 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -651,6 +651,9 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: up_efficiency=device_constraints[0]["derivative up efficiency"], storage_efficiency=device_constraints[0]["efficiency"], ) + soc_schedule = convert_units( + soc_schedule, f"{sensor.unit}h", soc_sensor.unit + ) # Round schedule if self.round_to_decimals: @@ -729,6 +732,9 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType: up_efficiency=device_constraints[0]["derivative up efficiency"], storage_efficiency=device_constraints[0]["efficiency"], ) + soc_schedule = convert_units( + soc_schedule, f"{sensor.unit}h", soc_sensor.unit + ) # Round schedule if self.round_to_decimals: diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 880ce76c1b..4e0202f021 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -1025,19 +1025,27 @@ def compute_schedule(flex_model): class BatterySensors: price: Sensor power: Sensor + soc: Sensor def get_sensors_from_db( - db, battery_assets, battery_name="Test battery", power_sensor_name="power" -): + db, + battery_assets, + battery_name="Test battery", + power_sensor_name="power", + soc_sensor_name="state of charge (Wh)", +) -> BatterySensors: # get the sensors from the database epex_da = get_test_sensor(db) battery_power = [ s for s in battery_assets[battery_name].sensors if s.name == power_sensor_name ][0] + battery_soc = [ + s for s in battery_assets[battery_name].sensors if s.name == soc_sensor_name + ][0] assert battery_power.get_attribute("market_id") == epex_da.id - return BatterySensors(price=epex_da, power=battery_power) + return BatterySensors(price=epex_da, power=battery_power, soc=battery_soc) def test_numerical_errors(app_with_each_solver, setup_planning_test_data, db): @@ -2199,12 +2207,14 @@ def test_battery_storage_with_time_series_in_flex_model( soc_max = "1 MWh" soc_at_start = "100 kWh" - battery = get_sensors_from_db( + battery_sensors = get_sensors_from_db( db, add_battery_assets, battery_name="Test battery", power_sensor_name="power", - ).power + ) + battery_power_sensor = battery_sensors.power + battery_soc_sensor = battery_sensors.soc tz = pytz.timezone("Europe/Amsterdam") # transition from cheap to expensive (90 -> 100) @@ -2216,6 +2226,7 @@ def test_battery_storage_with_time_series_in_flex_model( "soc-min": soc_min, "soc-max": soc_max, "soc-at-start": soc_at_start, + "soc": {"sensor": battery_soc_sensor.id}, "roundtrip-efficiency": 1, "storage-efficiency": 1, "power-capacity": "1 MW", @@ -2223,7 +2234,7 @@ def test_battery_storage_with_time_series_in_flex_model( flex_model[ts_field] = ts_specs scheduler: Scheduler = StorageScheduler( - battery, + battery_power_sensor, start, end, resolution, @@ -2231,12 +2242,15 @@ def test_battery_storage_with_time_series_in_flex_model( flex_context={ "site-power-capacity": "1 MW", }, + return_multiple=True, ) - schedule = scheduler.compute() + scheduler_results = scheduler.compute() + schedule = scheduler_results[0]["data"] + soc_schedule = scheduler_results[1]["data"] # Check if constraints were met soc_at_start = ur.Quantity(soc_at_start).to("MWh").magnitude - check_constraints(battery, schedule, soc_at_start) + check_constraints(battery_power_sensor, schedule, soc_at_start) # charge 850 kWh in the cheap price period (100 kWh -> 950kWh) assert schedule[:4].sum() * 0.25 == pytest.approx(0.85) @@ -2246,3 +2260,9 @@ def test_battery_storage_with_time_series_in_flex_model( assert schedule[4:].sum() * 0.25 == pytest.approx(-0.75) else: assert schedule[4:].sum() * 0.25 == pytest.approx(-0.85) + + # Check final state in soc_schedule (given in the unit of the SoC sensor, which is Wh) + if ts_field == "soc-minima": + assert soc_schedule[-1] == 200 * 10**3 + else: + assert soc_schedule[-1] == 100 * 10**3 From c095707857dc2b91d7c67a45f6f37d35cde95796 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 26 Aug 2024 12:45:50 +0200 Subject: [PATCH 11/13] revert: remove deserialization case, see https://github.com/FlexMeasures/flexmeasures/pull/1018#discussion_r1554631120 Signed-off-by: F.N. Claessen --- flexmeasures/data/schemas/sensors.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/flexmeasures/data/schemas/sensors.py b/flexmeasures/data/schemas/sensors.py index ab179bca3d..6403371393 100644 --- a/flexmeasures/data/schemas/sensors.py +++ b/flexmeasures/data/schemas/sensors.py @@ -295,13 +295,6 @@ def _deserialize( return self._deserialize_str(value) elif isinstance(value, numbers.Real) and self.default_src_unit is not None: return self._deserialize_numeric(value, attr, obj, **kwargs) - elif isinstance(value, Sensor): - sensor = value - if not units_are_convertible(sensor.unit, str(self.to_unit.units)): - raise FMValidationError( - f"Cannot convert {sensor.unit} to {self.to_unit.units}" - ) - return sensor else: raise FMValidationError( f"Unsupported value type. `{type(value)}` was provided but only dict, list and str are supported." From 858a7c339b132cfee182f87a0675850777e84716 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 26 Aug 2024 12:51:14 +0200 Subject: [PATCH 12/13] fix: tests Signed-off-by: F.N. Claessen --- .../data/models/planning/tests/test_solver.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 4e0202f021..318a280489 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass from datetime import datetime, timedelta import pytest @@ -1025,7 +1027,7 @@ def compute_schedule(flex_model): class BatterySensors: price: Sensor power: Sensor - soc: Sensor + soc: Sensor | None def get_sensors_from_db( @@ -1040,9 +1042,12 @@ def get_sensors_from_db( battery_power = [ s for s in battery_assets[battery_name].sensors if s.name == power_sensor_name ][0] - battery_soc = [ - s for s in battery_assets[battery_name].sensors if s.name == soc_sensor_name - ][0] + if any([s.name == soc_sensor_name for s in battery_assets[battery_name].sensors]): + battery_soc = [ + s for s in battery_assets[battery_name].sensors if s.name == soc_sensor_name + ][0] + else: + battery_soc = None assert battery_power.get_attribute("market_id") == epex_da.id return BatterySensors(price=epex_da, power=battery_power, soc=battery_soc) From 6dede5281cb333acb617e7c9bb5c5c13786713e3 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 26 Aug 2024 13:09:42 +0200 Subject: [PATCH 13/13] revert: accidentally reintroduced code block upon resolving merge conflict Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index efabbef1e3..1a06db0e12 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -508,13 +508,6 @@ def deserialize_flex_config(self): if self.flex_model.get("soc") is None: self.flex_model["soc"] = "0 MWh" - # soc-unit - if "soc-unit" not in self.flex_model or self.flex_model["soc-unit"] is None: - if self.sensor.unit in ("MWh", "kWh"): - self.flex_model["soc-unit"] = self.sensor.unit - elif self.sensor.unit in ("MW", "kW"): - self.flex_model["soc-unit"] = self.sensor.unit + "h" - self.ensure_soc_min_max() # Now it's time to check if our flex configuration holds up to schemas