Skip to content

Commit

Permalink
Merge branch 'main' into documentation/add-headroom-img-tut-part-II
Browse files Browse the repository at this point in the history
  • Loading branch information
victorgarcia98 committed Sep 20, 2023
2 parents 56cbdc7 + 64beb24 commit e5b4827
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 14 deletions.
5 changes: 5 additions & 0 deletions documentation/api/change_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>/schedules/trigger` (POST).

v3.0-11 | 2023-08-02
""""""""""""""""""""

Expand Down
2 changes: 2 additions & 0 deletions documentation/api/notation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ New features

* Introduce new reporter to compute profit/loss due to electricity flows: `ProfitOrLossReporter` [see `PR #808 <https://github.com/FlexMeasures/flexmeasures/pull/808>`_ and `PR #844 <https://github.com/FlexMeasures/flexmeasures/pull/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 <https://github.com/FlexMeasures/flexmeasures/pull/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 <https://github.com/FlexMeasures/flexmeasures/pull/850>`_]
* Sensor charts showing instantaneous observations can be interpolated by setting the ``interpolate`` sensor attribute to one of the `supported Vega-Lite interpolation methods <https://vega.github.io/vega-lite/docs/area.html#properties>`_ [see `PR #851 <https://github.com/FlexMeasures/flexmeasures/pull/851>`_]

Infrastructure / Support
Expand Down
4 changes: 3 additions & 1 deletion flexmeasures/api/v3_0/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,11 +313,13 @@ def trigger_schedule( # noqa: C901
"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]
"inflexible-device-sensors": [13, 14, 15],
"site-power-capacity": "100kW"
}
}
Expand Down
3 changes: 2 additions & 1 deletion flexmeasures/api/v3_0/tests/test_sensor_schedules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>/schedules/trigger [POST] api endpoint
Expand Down
1 change: 1 addition & 0 deletions flexmeasures/api/v3_0/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
56 changes: 44 additions & 12 deletions flexmeasures/data/models/planning/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -50,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
Expand Down Expand Up @@ -86,7 +87,26 @@ def _prepare(self, skip_validation: bool = False) -> tuple:
)

# Check for required Sensor attributes
self.sensor.check_required_attributes([("capacity_in_mw", (float, int))])
power_capacity_in_mw = self.flex_model.get(
"power_capacity_in_mw",
self.sensor.get_attribute("capacity_in_mw", None),
)

if power_capacity_in_mw is None:
raise ValueError(
"Power capacity is 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

if not (
isinstance(power_capacity_in_mw, float)
or isinstance(power_capacity_in_mw, int)
):
raise ValueError(
"The only supported types for the power capacity are int and float."
)

# Check for known prices or price forecasts, trimming planning window accordingly
up_deviation_prices, (start, end) = get_prices(
Expand Down Expand Up @@ -158,15 +178,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"] = 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"] = power_capacity_in_mw

# Apply round-trip efficiency evenly to charging and discharging
device_constraints[0]["derivative down efficiency"] = (
Expand Down Expand Up @@ -199,10 +215,26 @@ 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_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)
):
raise ValueError(
"The only supported types for the ems power capacity are int and float."
)

ems_constraints["derivative min"] = ems_power_capacity_in_mw * -1
ems_constraints["derivative max"] = ems_power_capacity_in_mw

return (
sensor,
Expand Down
68 changes: 68 additions & 0 deletions flexmeasures/data/models/planning/tests/test_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1064,3 +1064,71 @@ 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"), ("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
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)
4 changes: 4 additions & 0 deletions flexmeasures/data/schemas/scheduling/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from marshmallow import Schema, fields

from flexmeasures.data.schemas.sensors import SensorIdField
from flexmeasures.data.schemas.units import QuantityField


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(
Expand Down
4 changes: 4 additions & 0 deletions flexmeasures/data/schemas/scheduling/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ class StorageFlexModelSchema(Schema):
soc_min = fields.Float(validate=validate.Range(min=0), data_key="soc-min")
soc_max = fields.Float(data_key="soc-max")

power_capacity_in_mw = QuantityField(
"MW", required=False, data_key="power-capacity"
)

soc_maxima = fields.List(fields.Nested(SOCValueSchema()), data_key="soc-maxima")
soc_minima = fields.List(
fields.Nested(SOCValueSchema(value_validator=validate.Range(min=0))),
Expand Down

0 comments on commit e5b4827

Please sign in to comment.