From eabfeb5f20bc37a28aab0c4ea2b1788ac3d9006d Mon Sep 17 00:00:00 2001 From: Victor Garcia Reolid Date: Thu, 14 Sep 2023 14:07:45 +0200 Subject: [PATCH 01/13] expose storage and ems power capacities as flex-model fields Signed-off-by: Victor Garcia Reolid --- flexmeasures/data/models/planning/storage.py | 42 ++++++++++++++----- .../data/schemas/scheduling/storage.py | 7 ++++ 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 227b0712c..ecd966cd0 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -86,7 +86,18 @@ def _prepare(self, skip_validation: bool = False) -> tuple: ) # Check for required Sensor attributes - self.sensor.check_required_attributes([("capacity_in_mw", (float, int))]) + storage_power_capacity_in_mw = self.flex_model.get( + "storage_power_capacity_in_mw", + self.sensor.get_attribute("capacity_in_mw", None), + ) + + if not ( + isinstance(storage_power_capacity_in_mw, float) + or isinstance(storage_power_capacity_in_mw, int) + ): + raise ValueError( + "Storage power capacity not defined in the sensor attributes or the flex-model" + ) # Check for known prices or price forecasts, trimming planning window accordingly up_deviation_prices, (start, end) = get_prices( @@ -158,15 +169,11 @@ def _prepare(self, skip_validation: bool = False) -> tuple: if sensor.get_attribute("is_strictly_non_positive"): device_constraints[0]["derivative min"] = 0 else: - device_constraints[0]["derivative min"] = ( - sensor.get_attribute("capacity_in_mw") * -1 - ) + device_constraints[0]["derivative min"] = storage_power_capacity_in_mw * -1 if sensor.get_attribute("is_strictly_non_negative"): device_constraints[0]["derivative max"] = 0 else: - device_constraints[0]["derivative max"] = sensor.get_attribute( - "capacity_in_mw" - ) + device_constraints[0]["derivative max"] = storage_power_capacity_in_mw # Apply round-trip efficiency evenly to charging and discharging device_constraints[0]["derivative down efficiency"] = ( @@ -199,10 +206,23 @@ def _prepare(self, skip_validation: bool = False) -> tuple: ems_constraints = initialize_df( StorageScheduler.COLUMNS, start, end, resolution ) - ems_capacity = sensor.generic_asset.get_attribute("capacity_in_mw") - if ems_capacity is not None: - ems_constraints["derivative min"] = ems_capacity * -1 - ems_constraints["derivative max"] = ems_capacity + + ems_power_capacity_in_mw = self.flex_model.get( + "ems_power_capacity_in_mw", + self.sensor.get_attribute("capacity_in_mw", None), + ) + + if ems_power_capacity_in_mw is not None: + if not ( + isinstance(ems_power_capacity_in_mw, float) + or isinstance(ems_power_capacity_in_mw, int) + ): + raise ValueError( + "EMS power capacity not defined in the sensor attributes or the flex-model" + ) + + ems_constraints["derivative min"] = ems_power_capacity_in_mw * -1 + ems_constraints["derivative max"] = ems_power_capacity_in_mw return ( sensor, diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index 25f840900..0807a7fd5 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -77,6 +77,13 @@ class StorageFlexModelSchema(Schema): soc_min = fields.Float(validate=validate.Range(min=0), data_key="soc-min") soc_max = fields.Float(data_key="soc-max") + storage_power_capacity_in_mw = QuantityField( + "MW", required=False, data_key="storage-power-capacity-in-mw" + ) + ems_power_capacity_in_mw = QuantityField( + "MW", required=False, data_key="ems-power-capacity-in-mw" + ) + soc_maxima = fields.List(fields.Nested(SOCValueSchema()), data_key="soc-maxima") soc_minima = fields.List( fields.Nested(SOCValueSchema(value_validator=validate.Range(min=0))), From c583428b9970351ff931a2c0d7a1cce8657c6fc1 Mon Sep 17 00:00:00 2001 From: Victor Garcia Reolid Date: Thu, 14 Sep 2023 16:32:23 +0200 Subject: [PATCH 02/13] get attribute from generic asset Signed-off-by: Victor Garcia Reolid --- flexmeasures/data/models/planning/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index ecd966cd0..30f231ad0 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -209,7 +209,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: ems_power_capacity_in_mw = self.flex_model.get( "ems_power_capacity_in_mw", - self.sensor.get_attribute("capacity_in_mw", None), + self.sensor.generic_asset.get_attribute("capacity_in_mw", None), ) if ems_power_capacity_in_mw is not None: From 35925f3d357b707ab598fa3f06ad52a623eaf943 Mon Sep 17 00:00:00 2001 From: Victor Garcia Reolid Date: Mon, 18 Sep 2023 14:50:25 +0200 Subject: [PATCH 03/13] add test Signed-off-by: Victor Garcia Reolid --- flexmeasures/data/models/planning/storage.py | 21 ++++-- .../data/models/planning/tests/test_solver.py | 67 +++++++++++++++++++ .../data/schemas/scheduling/__init__.py | 4 ++ .../data/schemas/scheduling/storage.py | 7 +- 4 files changed, 87 insertions(+), 12 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 30f231ad0..9c854ed7b 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -22,6 +22,7 @@ from flexmeasures.data.schemas.scheduling import FlexContextSchema from flexmeasures.utils.time_utils import get_max_planning_horizon from flexmeasures.utils.coding_utils import deprecated +from flexmeasures.utils.unit_utils import ur class StorageScheduler(Scheduler): @@ -86,14 +87,17 @@ def _prepare(self, skip_validation: bool = False) -> tuple: ) # Check for required Sensor attributes - storage_power_capacity_in_mw = self.flex_model.get( - "storage_power_capacity_in_mw", + power_capacity_in_mw = self.flex_model.get( + "power_capacity_in_mw", self.sensor.get_attribute("capacity_in_mw", None), ) + if isinstance(power_capacity_in_mw, ur.Quantity): + power_capacity_in_mw = power_capacity_in_mw.magnitude + if not ( - isinstance(storage_power_capacity_in_mw, float) - or isinstance(storage_power_capacity_in_mw, int) + isinstance(power_capacity_in_mw, float) + or isinstance(power_capacity_in_mw, int) ): raise ValueError( "Storage power capacity not defined in the sensor attributes or the flex-model" @@ -169,11 +173,11 @@ def _prepare(self, skip_validation: bool = False) -> tuple: if sensor.get_attribute("is_strictly_non_positive"): device_constraints[0]["derivative min"] = 0 else: - device_constraints[0]["derivative min"] = storage_power_capacity_in_mw * -1 + device_constraints[0]["derivative min"] = power_capacity_in_mw * -1 if sensor.get_attribute("is_strictly_non_negative"): device_constraints[0]["derivative max"] = 0 else: - device_constraints[0]["derivative max"] = storage_power_capacity_in_mw + device_constraints[0]["derivative max"] = power_capacity_in_mw # Apply round-trip efficiency evenly to charging and discharging device_constraints[0]["derivative down efficiency"] = ( @@ -207,12 +211,15 @@ def _prepare(self, skip_validation: bool = False) -> tuple: StorageScheduler.COLUMNS, start, end, resolution ) - ems_power_capacity_in_mw = self.flex_model.get( + ems_power_capacity_in_mw = self.flex_context.get( "ems_power_capacity_in_mw", self.sensor.generic_asset.get_attribute("capacity_in_mw", None), ) if ems_power_capacity_in_mw is not None: + if isinstance(ems_power_capacity_in_mw, ur.Quantity): + ems_power_capacity_in_mw = ems_power_capacity_in_mw.magnitude + if not ( isinstance(ems_power_capacity_in_mw, float) or isinstance(ems_power_capacity_in_mw, int) diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 9719accd8..5efd60e00 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -1064,3 +1064,70 @@ def test_numerical_errors(app, setup_planning_test_data, solver): assert device_constraints[0]["equals"].max() > device_constraints[0]["max"].max() assert device_constraints[0]["equals"].min() < device_constraints[0]["min"].min() assert results.solver.status == "ok" + + +@pytest.mark.parametrize( + "capacity,site_capacity", [("100kW", "300kW"), ("0.1MW", "0.3MW"), (None, None)] +) +def test_capacity(app, setup_planning_test_data, capacity, site_capacity): + """Test that the power limits of the site and storage device are set properly using the + flex-model and flex-context. + """ + + expected_capacity = 2 + expected_site_capacity = 2 + + flex_model = { + "soc-at-start": 0.01, + } + + flex_context = {} + + if capacity is not None: + flex_model["power-capacity"] = capacity + expected_capacity = 0.1 + + if site_capacity is not None: + flex_context["site-power-capacity"] = site_capacity + expected_site_capacity = 0.3 + + epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none() + charging_station = setup_planning_test_data[ + "Test charging station (bidirectional)" + ].sensors[0] + + assert charging_station.generic_asset.get_attribute("capacity_in_mw") == 2 + assert charging_station.get_attribute("market_id") == epex_da.id + + tz = pytz.timezone("Europe/Amsterdam") + start = tz.localize(datetime(2015, 1, 2)) + end = tz.localize(datetime(2015, 1, 3)) + resolution = timedelta(minutes=5) + + scheduler = StorageScheduler( + charging_station, + start, + end, + resolution, + flex_model=flex_model, + flex_context=flex_context, + ) + + ( + sensor, + start, + end, + resolution, + soc_at_start, + device_constraints, + ems_constraints, + commitment_quantities, + commitment_downwards_deviation_price, + commitment_upwards_deviation_price, + ) = scheduler._prepare(skip_validation=True) + + assert all(device_constraints[0]["derivative min"] == -expected_capacity) + assert all(device_constraints[0]["derivative max"] == expected_capacity) + + assert all(ems_constraints["derivative min"] == -expected_site_capacity) + assert all(ems_constraints["derivative max"] == expected_site_capacity) diff --git a/flexmeasures/data/schemas/scheduling/__init__.py b/flexmeasures/data/schemas/scheduling/__init__.py index 382f4430a..30e76b7f3 100644 --- a/flexmeasures/data/schemas/scheduling/__init__.py +++ b/flexmeasures/data/schemas/scheduling/__init__.py @@ -1,6 +1,7 @@ from marshmallow import Schema, fields from flexmeasures.data.schemas.sensors import SensorIdField +from flexmeasures.data.schemas.units import QuantityField class FlexContextSchema(Schema): @@ -8,6 +9,9 @@ class FlexContextSchema(Schema): This schema lists fields that can be used to describe sensors in the optimised portfolio """ + ems_power_capacity_in_mw = QuantityField( + "MW", required=False, data_key="site-power-capacity" + ) consumption_price_sensor = SensorIdField(data_key="consumption-price-sensor") production_price_sensor = SensorIdField(data_key="production-price-sensor") inflexible_device_sensors = fields.List( diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index 0807a7fd5..55088c205 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -77,11 +77,8 @@ class StorageFlexModelSchema(Schema): soc_min = fields.Float(validate=validate.Range(min=0), data_key="soc-min") soc_max = fields.Float(data_key="soc-max") - storage_power_capacity_in_mw = QuantityField( - "MW", required=False, data_key="storage-power-capacity-in-mw" - ) - ems_power_capacity_in_mw = QuantityField( - "MW", required=False, data_key="ems-power-capacity-in-mw" + power_capacity_in_mw = QuantityField( + "MW", required=False, data_key="power-capacity" ) soc_maxima = fields.List(fields.Nested(SOCValueSchema()), data_key="soc-maxima") From 47574523b2b1475155378f461cbc92c12d16796b Mon Sep 17 00:00:00 2001 From: Victor Garcia Reolid Date: Tue, 19 Sep 2023 16:30:53 +0200 Subject: [PATCH 04/13] update description of fleex-model and flex-context Signed-off-by: Victor Garcia Reolid --- documentation/api/notation.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/api/notation.rst b/documentation/api/notation.rst index 63606ee27..c3b6d6bd4 100644 --- a/documentation/api/notation.rst +++ b/documentation/api/notation.rst @@ -197,6 +197,7 @@ Here are the three types of flexibility models you can expect to be built-in: - ``roundtrip-efficiency`` (defaults to 100%) - ``storage-efficiency`` (defaults to 100%) [#]_ - ``prefer-charging-sooner`` (defaults to True, also signals a preference to discharge later) + - ``power-capacity`` (defaults to the Sensor attribute ``capacity_in_mw``) .. [#] The storage efficiency (e.g. 95% or 0.95) to use for the schedule is applied over each time step equal to the sensor resolution. For example, a storage efficiency of 95 percent per (absolute) day, for scheduling a 1-hour resolution sensor, should be passed as a storage efficiency of :math:`0.95^{1/24} = 0.997865`. @@ -236,6 +237,7 @@ With the flexibility context, we aim to describe the system in which the flexibl - ``inflexible-device-sensors`` ― power sensors that are relevant, but not flexible, such as a sensor recording rooftop solar power connected behind the main meter, whose production falls under the same contract as the flexible device(s) being scheduled - ``consumption-price-sensor`` ― the sensor which defines costs/revenues of consuming energy - ``production-price-sensor`` ― the sensor which defines cost/revenues of producing energy +- ``site-power-capacity`` (defaults to the Asset attribute ``capacity_in_mw``) These should be independent on the asset type and consequently also do not depend on which scheduling algorithm is being used. From 72558bbacb7e9b6de37f2ffdbba015c5dcec152e Mon Sep 17 00:00:00 2001 From: Victor Garcia Reolid Date: Tue, 19 Sep 2023 16:59:28 +0200 Subject: [PATCH 05/13] improve validation Signed-off-by: Victor Garcia Reolid --- flexmeasures/data/models/planning/storage.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 9c854ed7b..638c8ad12 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -51,7 +51,7 @@ def compute_schedule(self) -> pd.Series | None: return self.compute() - def _prepare(self, skip_validation: bool = False) -> tuple: + def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 """This function prepares the required data to compute the schedule: - price data - device constraint @@ -92,6 +92,11 @@ def _prepare(self, skip_validation: bool = False) -> tuple: self.sensor.get_attribute("capacity_in_mw", None), ) + if power_capacity_in_mw is None: + raise ValueError( + "Storage power capacity not defined in the sensor attributes or the flex-model." + ) + if isinstance(power_capacity_in_mw, ur.Quantity): power_capacity_in_mw = power_capacity_in_mw.magnitude @@ -100,7 +105,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: or isinstance(power_capacity_in_mw, int) ): raise ValueError( - "Storage power capacity not defined in the sensor attributes or the flex-model" + "The only supported types for the storage power capacity are int and float." ) # Check for known prices or price forecasts, trimming planning window accordingly @@ -225,7 +230,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: or isinstance(ems_power_capacity_in_mw, int) ): raise ValueError( - "EMS power capacity not defined in the sensor attributes or the flex-model" + "The only supported types for the ems power capacity are int and float." ) ems_constraints["derivative min"] = ems_power_capacity_in_mw * -1 From 5dc7855f7f41b52e2c1cd752e48b8ce25fdddb41 Mon Sep 17 00:00:00 2001 From: Victor Garcia Reolid Date: Tue, 19 Sep 2023 17:01:03 +0200 Subject: [PATCH 06/13] add details into trigger_schedule docstring Signed-off-by: Victor Garcia Reolid --- flexmeasures/api/v3_0/sensors.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index 851fbbed8..2a83c3e1d 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -313,11 +313,14 @@ def trigger_schedule( # noqa: C901 "soc-max": 25, "roundtrip-efficiency": 0.98, "storage-efficiency": 0.9999, + "storage-efficiency": 0.9999, + "power-capacity" : "25kW" }, "flex-context": { "consumption-price-sensor": 9, "production-price-sensor": 10, "inflexible-device-sensors": [13, 14, 15] + "site-power-capacity": "100kW" } } From 36f0fbc894a7d4ab2fb0cf48ab9a4b374d721e5e Mon Sep 17 00:00:00 2001 From: Victor Garcia Reolid Date: Wed, 20 Sep 2023 11:59:05 +0200 Subject: [PATCH 07/13] add test case and fix docstring Signed-off-by: Victor Garcia Reolid --- flexmeasures/api/v3_0/sensors.py | 215 +++++++++--------- .../data/models/planning/tests/test_solver.py | 3 +- 2 files changed, 109 insertions(+), 109 deletions(-) diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index 2a83c3e1d..cca2b6f95 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -225,129 +225,128 @@ def trigger_schedule( # noqa: C901 **kwargs, ): """ - Trigger FlexMeasures to create a schedule. + Trigger FlexMeasures to create a schedule. - .. :quickref: Schedule; Trigger scheduling job + .. :quickref: Schedule; Trigger scheduling job - Trigger FlexMeasures to create a schedule for this sensor. - The assumption is that this sensor is the power sensor on a flexible asset. + Trigger FlexMeasures to create a schedule for this sensor. + The assumption is that this sensor is the power sensor on a flexible asset. - In this request, you can describe: + In this request, you can describe: - - the schedule's main features (when does it start, what unit should it report, prior to what time can we assume knowledge) - - the flexibility model for the sensor (state and constraint variables, e.g. current state of charge of a battery, or connection capacity) - - the flexibility context which the sensor operates in (other sensors under the same EMS which are relevant, e.g. prices) + - the schedule's main features (when does it start, what unit should it report, prior to what time can we assume knowledge) + - the flexibility model for the sensor (state and constraint variables, e.g. current state of charge of a battery, or connection capacity) + - the flexibility context which the sensor operates in (other sensors under the same EMS which are relevant, e.g. prices) - For details on flexibility model and context, see :ref:`describing_flexibility`. - Below, we'll also list some examples. + For details on flexibility model and context, see :ref:`describing_flexibility`. + Below, we'll also list some examples. - .. note:: This endpoint does not support to schedule an EMS with multiple flexible sensors at once. This will happen in another endpoint. - See https://github.com/FlexMeasures/flexmeasures/issues/485. Until then, it is possible to call this endpoint for one flexible endpoint at a time - (considering already scheduled sensors as inflexible). + .. note:: This endpoint does not support to schedule an EMS with multiple flexible sensors at once. This will happen in another endpoint. + See https://github.com/FlexMeasures/flexmeasures/issues/485. Until then, it is possible to call this endpoint for one flexible endpoint at a time + (considering already scheduled sensors as inflexible). - The length of the schedule can be set explicitly through the 'duration' field. - Otherwise, it is set by the config setting :ref:`planning_horizon_config`, which defaults to 48 hours. - If the flex-model contains targets that lie beyond the planning horizon, the length of the schedule is extended to accommodate them. - Finally, the schedule length is limited by :ref:`max_planning_horizon_config`, which defaults to 2520 steps of the sensor's resolution. - Targets that exceed the max planning horizon are not accepted. + The length of the schedule can be set explicitly through the 'duration' field. + Otherwise, it is set by the config setting :ref:`planning_horizon_config`, which defaults to 48 hours. + If the flex-model contains targets that lie beyond the planning horizon, the length of the schedule is extended to accommodate them. + Finally, the schedule length is limited by :ref:`max_planning_horizon_config`, which defaults to 2520 steps of the sensor's resolution. + Targets that exceed the max planning horizon are not accepted. - The appropriate algorithm is chosen by FlexMeasures (based on asset type). - It's also possible to use custom schedulers and custom flexibility models, see :ref:`plugin_customization`. + The appropriate algorithm is chosen by FlexMeasures (based on asset type). + It's also possible to use custom schedulers and custom flexibility models, see :ref:`plugin_customization`. - If you have ideas for algorithms that should be part of FlexMeasures, let us know: https://flexmeasures.io/get-in-touch/ + If you have ideas for algorithms that should be part of FlexMeasures, let us know: https://flexmeasures.io/get-in-touch/ - **Example request A** + **Example request A** - This message triggers a schedule for a storage asset, starting at 10.00am, at which the state of charge (soc) is 12.1 kWh. + This message triggers a schedule for a storage asset, starting at 10.00am, at which the state of charge (soc) is 12.1 kWh. - .. code-block:: json - - { - "start": "2015-06-02T10:00:00+00:00", - "flex-model": { - "soc-at-start": 12.1, - "soc-unit": "kWh" - } - } - - **Example request B** - - This message triggers a 24-hour schedule for a storage asset, starting at 10.00am, - at which the state of charge (soc) is 12.1 kWh, with a target state of charge of 25 kWh at 4.00pm. - The global minimum and maximum soc are set to 10 and 25 kWh, respectively. - To guarantee a minimum SOC in the period prior to 4.00pm, local minima constraints are imposed (via soc-minima) - at 2.00pm and 3.00pm, for 15kWh and 20kWh, respectively. - Roundtrip efficiency for use in scheduling is set to 98%. - Storage efficiency is set to 99.99%, denoting the state of charge left after each time step equal to the sensor's resolution. - Aggregate consumption (of all devices within this EMS) should be priced by sensor 9, - and aggregate production should be priced by sensor 10, - where the aggregate power flow in the EMS is described by the sum over sensors 13, 14 and 15 - (plus the flexible sensor being optimized, of course). - Note that, if forecasts for sensors 13, 14 and 15 are not available, a schedule cannot be computed. - - .. code-block:: json + .. code-block:: json - { - "start": "2015-06-02T10:00:00+00:00", - "duration": "PT24H", - "flex-model": { - "soc-at-start": 12.1, - "soc-unit": "kWh", - "soc-targets": [ - { - "value": 25, - "datetime": "2015-06-02T16:00:00+00:00" - }, - ], - "soc-minima" : [ - { - "value": 15, - "datetime" : "2015-06-02T14:00:00+00:00" + { + "start": "2015-06-02T10:00:00+00:00", + "flex-model": { + "soc-at-start": 12.1, + "soc-unit": "kWh" + } + } + + **Example request B** + + This message triggers a 24-hour schedule for a storage asset, starting at 10.00am, + at which the state of charge (soc) is 12.1 kWh, with a target state of charge of 25 kWh at 4.00pm. + The global minimum and maximum soc are set to 10 and 25 kWh, respectively. + To guarantee a minimum SOC in the period prior to 4.00pm, local minima constraints are imposed (via soc-minima) + at 2.00pm and 3.00pm, for 15kWh and 20kWh, respectively. + Roundtrip efficiency for use in scheduling is set to 98%. + Storage efficiency is set to 99.99%, denoting the state of charge left after each time step equal to the sensor's resolution. + Aggregate consumption (of all devices within this EMS) should be priced by sensor 9, + and aggregate production should be priced by sensor 10, + where the aggregate power flow in the EMS is described by the sum over sensors 13, 14 and 15 + (plus the flexible sensor being optimized, of course). + Note that, if forecasts for sensors 13, 14 and 15 are not available, a schedule cannot be computed. + + .. code-block:: json + + { + "start": "2015-06-02T10:00:00+00:00", + "duration": "PT24H", + "flex-model": { + "soc-at-start": 12.1, + "soc-unit": "kWh", + "soc-targets": [ + { + "value": 25, + "datetime": "2015-06-02T16:00:00+00:00" + }, + ], + "soc-minima" : [ + { + "value": 15, + "datetime" : "2015-06-02T14:00:00+00:00" + }, + { + "value": 20, + "datetime" : "2015-06-02T15:00:00+00:00" + } + ], + "soc-min": 10, + "soc-max": 25, + "roundtrip-efficiency": 0.98, + ç "storage-efficiency": 0.9999, + "power-capacity" : "25kW" }, - { - "value": 20, - "datetime" : "2015-06-02T15:00:00+00:00" + "flex-context": { + "consumption-price-sensor": 9, + "production-price-sensor": 10, + "inflexible-device-sensors": [13, 14, 15] + "site-power-capacity": "100kW" } - ], - "soc-min": 10, - "soc-max": 25, - "roundtrip-efficiency": 0.98, - "storage-efficiency": 0.9999, - "storage-efficiency": 0.9999, - "power-capacity" : "25kW" - }, - "flex-context": { - "consumption-price-sensor": 9, - "production-price-sensor": 10, - "inflexible-device-sensors": [13, 14, 15] - "site-power-capacity": "100kW" - } - } - - **Example response** - - This message indicates that the scheduling request has been processed without any error. - A scheduling job has been created with some Universally Unique Identifier (UUID), - which will be picked up by a worker. - The given UUID may be used to obtain the resulting schedule: see /sensors//schedules/. - - .. sourcecode:: json - - { - "status": "PROCESSED", - "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", - "message": "Request has been processed." - } - - :reqheader Authorization: The authentication token - :reqheader Content-Type: application/json - :resheader Content-Type: application/json - :status 200: PROCESSED - :status 400: INVALID_DATA - :status 401: UNAUTHORIZED - :status 403: INVALID_SENDER - :status 405: INVALID_METHOD - :status 422: UNPROCESSABLE_ENTITY + } + + **Example response** + + This message indicates that the scheduling request has been processed without any error. + A scheduling job has been created with some Universally Unique Identifier (UUID), + which will be picked up by a worker. + The given UUID may be used to obtain the resulting schedule: see /sensors//schedules/. + + .. sourcecode:: json + + { + "status": "PROCESSED", + "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", + "message": "Request has been processed." + } + + :reqheader Authorization: The authentication token + :reqheader Content-Type: application/json + :resheader Content-Type: application/json + :status 200: PROCESSED + :status 400: INVALID_DATA + :status 401: UNAUTHORIZED + :status 403: INVALID_SENDER + :status 405: INVALID_METHOD + :status 422: UNPROCESSABLE_ENTITY """ end_of_schedule = start_of_schedule + duration scheduler_kwargs = dict( diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 5efd60e00..4091ded70 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -1067,7 +1067,8 @@ def test_numerical_errors(app, setup_planning_test_data, solver): @pytest.mark.parametrize( - "capacity,site_capacity", [("100kW", "300kW"), ("0.1MW", "0.3MW"), (None, None)] + "capacity,site_capacity", + [("100kW", "300kW"), ("0.1MW", "0.3MW"), ("0.1 MW", "0.3 MW"), (None, None)], ) def test_capacity(app, setup_planning_test_data, capacity, site_capacity): """Test that the power limits of the site and storage device are set properly using the From fa6d005cb62861b44adf46d405eb51ed953e7256 Mon Sep 17 00:00:00 2001 From: Victor Garcia Reolid Date: Wed, 20 Sep 2023 13:26:07 +0200 Subject: [PATCH 08/13] =?UTF-8?q?revert=20tab=20and=20remove=20=C3=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Victor Garcia Reolid --- flexmeasures/api/v3_0/sensors.py | 214 +++++++++++++++---------------- 1 file changed, 107 insertions(+), 107 deletions(-) diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index cca2b6f95..49c61938b 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -225,128 +225,128 @@ def trigger_schedule( # noqa: C901 **kwargs, ): """ - Trigger FlexMeasures to create a schedule. + Trigger FlexMeasures to create a schedule. - .. :quickref: Schedule; Trigger scheduling job + .. :quickref: Schedule; Trigger scheduling job - Trigger FlexMeasures to create a schedule for this sensor. - The assumption is that this sensor is the power sensor on a flexible asset. + Trigger FlexMeasures to create a schedule for this sensor. + The assumption is that this sensor is the power sensor on a flexible asset. - In this request, you can describe: + In this request, you can describe: - - the schedule's main features (when does it start, what unit should it report, prior to what time can we assume knowledge) - - the flexibility model for the sensor (state and constraint variables, e.g. current state of charge of a battery, or connection capacity) - - the flexibility context which the sensor operates in (other sensors under the same EMS which are relevant, e.g. prices) + - the schedule's main features (when does it start, what unit should it report, prior to what time can we assume knowledge) + - the flexibility model for the sensor (state and constraint variables, e.g. current state of charge of a battery, or connection capacity) + - the flexibility context which the sensor operates in (other sensors under the same EMS which are relevant, e.g. prices) - For details on flexibility model and context, see :ref:`describing_flexibility`. - Below, we'll also list some examples. + For details on flexibility model and context, see :ref:`describing_flexibility`. + Below, we'll also list some examples. - .. note:: This endpoint does not support to schedule an EMS with multiple flexible sensors at once. This will happen in another endpoint. - See https://github.com/FlexMeasures/flexmeasures/issues/485. Until then, it is possible to call this endpoint for one flexible endpoint at a time - (considering already scheduled sensors as inflexible). + .. note:: This endpoint does not support to schedule an EMS with multiple flexible sensors at once. This will happen in another endpoint. + See https://github.com/FlexMeasures/flexmeasures/issues/485. Until then, it is possible to call this endpoint for one flexible endpoint at a time + (considering already scheduled sensors as inflexible). - The length of the schedule can be set explicitly through the 'duration' field. - Otherwise, it is set by the config setting :ref:`planning_horizon_config`, which defaults to 48 hours. - If the flex-model contains targets that lie beyond the planning horizon, the length of the schedule is extended to accommodate them. - Finally, the schedule length is limited by :ref:`max_planning_horizon_config`, which defaults to 2520 steps of the sensor's resolution. - Targets that exceed the max planning horizon are not accepted. + The length of the schedule can be set explicitly through the 'duration' field. + Otherwise, it is set by the config setting :ref:`planning_horizon_config`, which defaults to 48 hours. + If the flex-model contains targets that lie beyond the planning horizon, the length of the schedule is extended to accommodate them. + Finally, the schedule length is limited by :ref:`max_planning_horizon_config`, which defaults to 2520 steps of the sensor's resolution. + Targets that exceed the max planning horizon are not accepted. - The appropriate algorithm is chosen by FlexMeasures (based on asset type). - It's also possible to use custom schedulers and custom flexibility models, see :ref:`plugin_customization`. + The appropriate algorithm is chosen by FlexMeasures (based on asset type). + It's also possible to use custom schedulers and custom flexibility models, see :ref:`plugin_customization`. - If you have ideas for algorithms that should be part of FlexMeasures, let us know: https://flexmeasures.io/get-in-touch/ + If you have ideas for algorithms that should be part of FlexMeasures, let us know: https://flexmeasures.io/get-in-touch/ - **Example request A** + **Example request A** - This message triggers a schedule for a storage asset, starting at 10.00am, at which the state of charge (soc) is 12.1 kWh. + This message triggers a schedule for a storage asset, starting at 10.00am, at which the state of charge (soc) is 12.1 kWh. - .. code-block:: json + .. code-block:: json - { - "start": "2015-06-02T10:00:00+00:00", - "flex-model": { - "soc-at-start": 12.1, - "soc-unit": "kWh" - } - } - - **Example request B** - - This message triggers a 24-hour schedule for a storage asset, starting at 10.00am, - at which the state of charge (soc) is 12.1 kWh, with a target state of charge of 25 kWh at 4.00pm. - The global minimum and maximum soc are set to 10 and 25 kWh, respectively. - To guarantee a minimum SOC in the period prior to 4.00pm, local minima constraints are imposed (via soc-minima) - at 2.00pm and 3.00pm, for 15kWh and 20kWh, respectively. - Roundtrip efficiency for use in scheduling is set to 98%. - Storage efficiency is set to 99.99%, denoting the state of charge left after each time step equal to the sensor's resolution. - Aggregate consumption (of all devices within this EMS) should be priced by sensor 9, - and aggregate production should be priced by sensor 10, - where the aggregate power flow in the EMS is described by the sum over sensors 13, 14 and 15 - (plus the flexible sensor being optimized, of course). - Note that, if forecasts for sensors 13, 14 and 15 are not available, a schedule cannot be computed. - - .. code-block:: json - - { - "start": "2015-06-02T10:00:00+00:00", - "duration": "PT24H", - "flex-model": { - "soc-at-start": 12.1, - "soc-unit": "kWh", - "soc-targets": [ - { - "value": 25, - "datetime": "2015-06-02T16:00:00+00:00" - }, - ], - "soc-minima" : [ - { - "value": 15, - "datetime" : "2015-06-02T14:00:00+00:00" - }, - { - "value": 20, - "datetime" : "2015-06-02T15:00:00+00:00" - } - ], - "soc-min": 10, - "soc-max": 25, - "roundtrip-efficiency": 0.98, - ç "storage-efficiency": 0.9999, - "power-capacity" : "25kW" + { + "start": "2015-06-02T10:00:00+00:00", + "flex-model": { + "soc-at-start": 12.1, + "soc-unit": "kWh" + } + } + + **Example request B** + + This message triggers a 24-hour schedule for a storage asset, starting at 10.00am, + at which the state of charge (soc) is 12.1 kWh, with a target state of charge of 25 kWh at 4.00pm. + The global minimum and maximum soc are set to 10 and 25 kWh, respectively. + To guarantee a minimum SOC in the period prior to 4.00pm, local minima constraints are imposed (via soc-minima) + at 2.00pm and 3.00pm, for 15kWh and 20kWh, respectively. + Roundtrip efficiency for use in scheduling is set to 98%. + Storage efficiency is set to 99.99%, denoting the state of charge left after each time step equal to the sensor's resolution. + Aggregate consumption (of all devices within this EMS) should be priced by sensor 9, + and aggregate production should be priced by sensor 10, + where the aggregate power flow in the EMS is described by the sum over sensors 13, 14 and 15 + (plus the flexible sensor being optimized, of course). + Note that, if forecasts for sensors 13, 14 and 15 are not available, a schedule cannot be computed. + + .. code-block:: json + + { + "start": "2015-06-02T10:00:00+00:00", + "duration": "PT24H", + "flex-model": { + "soc-at-start": 12.1, + "soc-unit": "kWh", + "soc-targets": [ + { + "value": 25, + "datetime": "2015-06-02T16:00:00+00:00" }, - "flex-context": { - "consumption-price-sensor": 9, - "production-price-sensor": 10, - "inflexible-device-sensors": [13, 14, 15] - "site-power-capacity": "100kW" + ], + "soc-minima" : [ + { + "value": 15, + "datetime" : "2015-06-02T14:00:00+00:00" + }, + { + "value": 20, + "datetime" : "2015-06-02T15:00:00+00:00" } - } - - **Example response** - - This message indicates that the scheduling request has been processed without any error. - A scheduling job has been created with some Universally Unique Identifier (UUID), - which will be picked up by a worker. - The given UUID may be used to obtain the resulting schedule: see /sensors//schedules/. - - .. sourcecode:: json - - { - "status": "PROCESSED", - "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", - "message": "Request has been processed." - } - - :reqheader Authorization: The authentication token - :reqheader Content-Type: application/json - :resheader Content-Type: application/json - :status 200: PROCESSED - :status 400: INVALID_DATA - :status 401: UNAUTHORIZED - :status 403: INVALID_SENDER - :status 405: INVALID_METHOD - :status 422: UNPROCESSABLE_ENTITY + ], + "soc-min": 10, + "soc-max": 25, + "roundtrip-efficiency": 0.98, + "storage-efficiency": 0.9999, + "power-capacity" : "25kW" + }, + "flex-context": { + "consumption-price-sensor": 9, + "production-price-sensor": 10, + "inflexible-device-sensors": [13, 14, 15] + "site-power-capacity": "100kW" + } + } + + **Example response** + + This message indicates that the scheduling request has been processed without any error. + A scheduling job has been created with some Universally Unique Identifier (UUID), + which will be picked up by a worker. + The given UUID may be used to obtain the resulting schedule: see /sensors//schedules/. + + .. sourcecode:: json + + { + "status": "PROCESSED", + "schedule": "364bfd06-c1fa-430b-8d25-8f5a547651fb", + "message": "Request has been processed." + } + + :reqheader Authorization: The authentication token + :reqheader Content-Type: application/json + :resheader Content-Type: application/json + :status 200: PROCESSED + :status 400: INVALID_DATA + :status 401: UNAUTHORIZED + :status 403: INVALID_SENDER + :status 405: INVALID_METHOD + :status 422: UNPROCESSABLE_ENTITY """ end_of_schedule = start_of_schedule + duration scheduler_kwargs = dict( From 72139deb6127ff9c71fd97f7b19039ed292f5874 Mon Sep 17 00:00:00 2001 From: Felix Claessen <30658763+Flix6x@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:19:33 +0200 Subject: [PATCH 09/13] Update sensors.py Revert accidental indentation Signed-off-by: Felix Claessen <30658763+Flix6x@users.noreply.github.com> --- flexmeasures/api/v3_0/sensors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index 49c61938b..bc0b7eacc 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -242,8 +242,8 @@ def trigger_schedule( # noqa: C901 Below, we'll also list some examples. .. note:: This endpoint does not support to schedule an EMS with multiple flexible sensors at once. This will happen in another endpoint. - See https://github.com/FlexMeasures/flexmeasures/issues/485. Until then, it is possible to call this endpoint for one flexible endpoint at a time - (considering already scheduled sensors as inflexible). + See https://github.com/FlexMeasures/flexmeasures/issues/485. Until then, it is possible to call this endpoint for one flexible endpoint at a time + (considering already scheduled sensors as inflexible). The length of the schedule can be set explicitly through the 'duration' field. Otherwise, it is set by the config setting :ref:`planning_horizon_config`, which defaults to 48 hours. From ac709d53d9cf65d762c2959aef098ce8ef5b019a Mon Sep 17 00:00:00 2001 From: Felix Claessen <30658763+Flix6x@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:20:25 +0200 Subject: [PATCH 10/13] Update sensors.py Missing comma Signed-off-by: Felix Claessen <30658763+Flix6x@users.noreply.github.com> --- flexmeasures/api/v3_0/sensors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index bc0b7eacc..3105fed86 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -318,7 +318,7 @@ def trigger_schedule( # noqa: C901 "flex-context": { "consumption-price-sensor": 9, "production-price-sensor": 10, - "inflexible-device-sensors": [13, 14, 15] + "inflexible-device-sensors": [13, 14, 15], "site-power-capacity": "100kW" } } From 5444ef45d5b1a098dbb4c296a7c74636d7b34986 Mon Sep 17 00:00:00 2001 From: Felix Claessen <30658763+Flix6x@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:23:30 +0200 Subject: [PATCH 11/13] Update storage.py Redacted two more mentions of `storage power capacity`. Signed-off-by: Felix Claessen <30658763+Flix6x@users.noreply.github.com> --- flexmeasures/data/models/planning/storage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 638c8ad12..b730a6f26 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -94,7 +94,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 if power_capacity_in_mw is None: raise ValueError( - "Storage power capacity not defined in the sensor attributes or the flex-model." + "Power capacity is not defined in the sensor attributes or the flex-model." ) if isinstance(power_capacity_in_mw, ur.Quantity): @@ -105,7 +105,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 or isinstance(power_capacity_in_mw, int) ): raise ValueError( - "The only supported types for the storage power capacity are int and float." + "The only supported types for the power capacity are int and float." ) # Check for known prices or price forecasts, trimming planning window accordingly From 271cbdd8a5205673cb7fe624d0e83769cfd735c2 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 20 Sep 2023 14:54:06 +0200 Subject: [PATCH 12/13] feat(test): check deserialization Signed-off-by: F.N. Claessen --- flexmeasures/api/v3_0/tests/test_sensor_schedules.py | 3 ++- flexmeasures/api/v3_0/tests/utils.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py index d03f4091e..d3bec8e5c 100644 --- a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py +++ b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py @@ -177,11 +177,12 @@ def test_trigger_and_get_schedule( message, asset_name, ): - # Include the price sensor in the flex-context explicitly, to test deserialization + # Include the price sensor and site-power-capacity in the flex-context explicitly, to test deserialization price_sensor_id = add_market_prices["epex_da"].id message["flex-context"] = { "consumption-price-sensor": price_sensor_id, "production-price-sensor": price_sensor_id, + "site-power-capacity": "1 TW", # should be big enough to avoid any infeasibilities } # trigger a schedule through the /sensors//schedules/trigger [POST] api endpoint diff --git a/flexmeasures/api/v3_0/tests/utils.py b/flexmeasures/api/v3_0/tests/utils.py index c39291f0b..445a8fcd3 100644 --- a/flexmeasures/api/v3_0/tests/utils.py +++ b/flexmeasures/api/v3_0/tests/utils.py @@ -73,6 +73,7 @@ def message_for_trigger_schedule( "soc-unit": "kWh", "roundtrip-efficiency": "98%", "storage-efficiency": "99.99%", + "power-capacity": "2 MW", # same as capacity_in_mw attribute of test battery and test charging station } if with_targets: if realistic_targets: From 72d6db914067028dce9168bad00e23aa54d7b0fa Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 20 Sep 2023 15:01:43 +0200 Subject: [PATCH 13/13] docs: changelog entries Signed-off-by: F.N. Claessen --- documentation/api/change_log.rst | 5 +++++ documentation/changelog.rst | 1 + 2 files changed, 6 insertions(+) diff --git a/documentation/api/change_log.rst b/documentation/api/change_log.rst index 235c17842..f4a9badf3 100644 --- a/documentation/api/change_log.rst +++ b/documentation/api/change_log.rst @@ -5,6 +5,11 @@ API change log .. note:: The FlexMeasures API follows its own versioning scheme. This is also reflected in the URL (e.g. `/api/v3_0`), allowing developers to upgrade at their own pace. +v3.0-12 | 2023-09-20 +"""""""""""""""""""" + +- Introduced the ``power-capacity`` field under ``flex-model``, and the ``site-power-capacity`` field under ``flex-context``, for `/sensors//schedules/trigger` (POST). + v3.0-11 | 2023-08-02 """""""""""""""""""" diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 93de22424..1db4e0a0a 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -11,6 +11,7 @@ New features * Introduce new reporter to compute profit/loss due to electricity flows: `ProfitOrLossReporter` [see `PR #808 `_ and `PR #844 `_] * Charts visible in the UI can be exported to PNG or SVG formats in a more automated fashion, using the new CLI command flexmeasures show chart [see `PR #833 `_] +* API users can ask for a schedule to take into account an explicit ``power-capacity`` (flex-model) and/or ``site-power-capacity`` (flex-context), thereby overriding any existing defaults for their asset [see `PR #850 `_] Infrastructure / Support ----------------------