Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
0360d01
dev: Support commodity-specific prices and site capacities in storage…
Ahmad-Wahid May 14, 2026
775a52d
dev: Add commodity-specific flex-context schema
Ahmad-Wahid May 14, 2026
b9aece4
dev: Add dynamic commodity prices and split flex-context settings to …
Ahmad-Wahid May 14, 2026
433fe17
feat: create a shared schema for flex-context and commodity-context
Ahmad-Wahid May 26, 2026
85a105f
update the test case to have inflexible-devices-sensors for each comm…
Ahmad-Wahid May 26, 2026
4e5aee1
refactor: loop over flex-context fields and choose all fields except …
Ahmad-Wahid May 26, 2026
4a6910a
fix: add inflexible-device-sensors to the gas commodity model
Ahmad-Wahid May 26, 2026
2105d28
Merge remote-tracking branch 'origin/feat/multi-feed-stock' into dev/…
Ahmad-Wahid May 26, 2026
e9c2681
fix: remove self
Ahmad-Wahid May 26, 2026
92c3324
Merge remote-tracking branch 'origin/feat/multi-feed-stock' into dev/…
Flix6x Jun 3, 2026
9b0b9ac
Merge remote-tracking branch 'origin/feat/multi-feed-stock' into dev/…
Flix6x Jun 3, 2026
9d6986d
Merge remote-tracking branch 'origin/feat/multi-feed-stock' into dev/…
Ahmad-Wahid Jun 4, 2026
5d02af4
fix: fall back to deprecated price fields
Flix6x Jun 8, 2026
abb41ff
fix: typo
Flix6x Jun 8, 2026
134563e
feat: store commitment costs on job meta
Flix6x Jun 8, 2026
dba828e
refactor: clarify which job is which
Flix6x Jun 8, 2026
02cfbb6
fix: update test expectation: the battery could save more?
Flix6x Jun 8, 2026
b7b21c2
dev: add todo
Flix6x Jun 8, 2026
193bca4
fix: price window should match scheduling window
Flix6x Jun 8, 2026
76b5fca
fix: comment out unreasoned check
Flix6x Jun 8, 2026
b13e239
fix: update test expectation; apparently the battery could save more?
Flix6x Jun 8, 2026
478c348
dev: add todo
Flix6x Jun 8, 2026
1dd6e80
chore: flake8
Flix6x Jun 8, 2026
e4d9c67
dev: exclude commodities field from flex-context schema referencing a…
Flix6x Jun 8, 2026
48bfe1c
fix: only save commitment costs on job if we have a job to save it on
Flix6x Jun 8, 2026
455f495
fix: inflexible devices are electricity devices by default
Flix6x Jun 8, 2026
e19984c
delete: no more need for backwards-compatibility of the temporary gas…
Flix6x Jun 8, 2026
c1c7d96
chore: black
Flix6x Jun 8, 2026
a7773a8
fix: optional dict key
Flix6x Jun 8, 2026
bf09a81
fix: keep ems-constraints and fix the test cases (#2233)
Ahmad-Wahid Jun 12, 2026
926941a
fix: only raise in case of multiple EMS constraint DataFrames
Flix6x Jun 12, 2026
953d3f1
chore: the wait for https://github.com/marshmallow-code/apispec/pull/…
Flix6x Jun 12, 2026
c6b8223
feat: allow any commodity, with electricity and gas serving as examples
Flix6x Jun 12, 2026
b706f79
delete: remove unreleased flex-context field for gas price
Flix6x Jun 12, 2026
b5f2c27
fix: add all relaxation fields to the list of fields to ignore when m…
Flix6x Jun 12, 2026
bd972aa
delete: gas_price is no longer a field (remove reference to unrelease…
Flix6x Jun 12, 2026
bcd54de
delete: just treat the whole old flex-context as the electricity flex…
Flix6x Jun 12, 2026
ed17a93
chore: update openapi-specs.json
Flix6x Jun 12, 2026
625886c
feat: list the commodity field first rather than last
Flix6x Jun 12, 2026
7b92d3d
feat: commodity is a field in both flex-model and flex-context
Flix6x Jun 12, 2026
10d82d2
feat: flex-model commodity can also be more than just electricity and…
Flix6x Jun 12, 2026
a483ce8
docs: remove mention of gas-price field
Flix6x Jun 12, 2026
718e8c3
docs: adjust scheduling section for multi-commodity
Flix6x Jun 12, 2026
35b8f22
docs: adjust field descriptions for multi-commodity
Flix6x Jun 12, 2026
c43bb06
feat: list the commodity field first rather than last, also in flex-m…
Flix6x Jun 12, 2026
51ba61f
fix: wrong data-key due to wrong indentation
Flix6x Jun 12, 2026
b2657cb
docs: explain the "commodities" field
Flix6x Jun 12, 2026
5a73040
docs: no need for ill-formatted in-line code formatting
Flix6x Jun 12, 2026
7ce8b7a
remove: devices do not have to be storages anymore
Flix6x Jun 12, 2026
39c0a9d
fix: set default flex-context commodity to electricity
Flix6x Jun 12, 2026
d6911a2
fix: preserve field order in case schema is made OpenAPI compatible
Flix6x Jun 12, 2026
af42623
fix: default flex-model and flex-context to empty list and empty dict…
Flix6x Jun 13, 2026
5512411
Feature: flex-context per commodity as list (#2235)
Flix6x Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ New features
-------------
* Floor off-clock API datetimes to a non-instantaneous sensor's resolution by default when ingesting sensor data, uploading sensor data, and handling scheduler flex-model timed events; configurable with the ``floor_datetimes_to_resolution`` sensor attribute [see `PR #2146 <https://www.github.com/FlexMeasures/flexmeasures/pull/2146>`_]
* Sensor references in flex-model and flex-context support various ways of filtering by source [see `PR #2209 <https://www.github.com/FlexMeasures/flexmeasures/pull/2209>`_]

* The flex-context can now define multiple commodities, each specifying their own prices and grid capacities [see `PR #2172 <https://www.github.com/FlexMeasures/flexmeasures/pull/2172>`_ and `PR #2235 <https://www.github.com/FlexMeasures/flexmeasures/pull/2235>`_]

Infrastructure / Support
----------------------
Expand Down
23 changes: 15 additions & 8 deletions documentation/features/scheduling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Scheduling

Scheduling is the main value-drive of FlexMeasures. We have two major types of schedulers built-in, for storage devices (usually batteries or hot water storage) and processes (usually in industry).

FlexMeasures computes schedules for energy systems that consist of multiple devices that consume and/or produce electricity.
We model a device as an asset with a power sensor, and compute schedules only for flexible devices, while taking into account inflexible devices.
FlexMeasures computes schedules for energy systems that consist of multiple devices that consume and/or produce a commodity (e.g. electricity or gas).
We model a device as an asset with a consumption/production sensor recording power values, and compute schedules only for flexible devices, while taking into account inflexible devices.

.. contents::
:local:
Expand Down Expand Up @@ -39,14 +39,15 @@ The flex-context

The ``flex-context`` is independent of the type of flexible device that is optimized, or which scheduler is used.
With the flexibility context, we aim to describe the system in which the flexible assets operate, such as its physical and contractual limitations.
For multi-commodity scheduling problems, the flex-context can be defined separately per commodity (e.g. electricity and gas).

Fields can have fixed values, but some fields can also point to sensors, so they will always represent the dynamics of the asset's environment (as long as that sensor has current data).
The full list of flex-context fields follows below.
For more details on the possible formats for field values, see :ref:`variable_quantities`.

Where should you set these fields?
Within requests to the API or by editing the relevant asset in the UI.
If they are not sent in via the API (one of the endpoints triggering schedule computation), the scheduler will look them up on the `flex-context` field of the asset.
If they are not sent in via the API (one of the endpoints triggering schedule computation), the scheduler will look them up on the flex-context field of the asset.
And if the asset belongs to a larger system (a hierarchy of assets), the scheduler will also search if parent assets have them set.


Expand All @@ -58,9 +59,18 @@ And if the asset belongs to a larger system (a hierarchy of assets), the schedul
* - Field
- Example value
- Description
* - ``commodity``
- |COMMODITY_FLEX_CONTEXT.example|
- .. include:: ../_autodoc/COMMODITY_FLEX_CONTEXT.rst
* - ``inflexible-device-sensors``
- |INFLEXIBLE_DEVICE_SENSORS.example|
- .. include:: ../_autodoc/INFLEXIBLE_DEVICE_SENSORS.rst
* - ``aggregate-consumption``
- |AGGREGATE_CONSUMPTION.example|
- .. include:: ../_autodoc/AGGREGATE_CONSUMPTION.rst
* - ``aggregate-production``
- |AGGREGATE_PRODUCTION.example|
- .. include:: ../_autodoc/AGGREGATE_PRODUCTION.rst
* - ``aggregate-power``
- |AGGREGATE_POWER.example|
- .. include:: ../_autodoc/AGGREGATE_POWER.rst
Expand All @@ -70,9 +80,6 @@ And if the asset belongs to a larger system (a hierarchy of assets), the schedul
* - ``production-price``
- |PRODUCTION_PRICE.example|
- .. include:: ../_autodoc/PRODUCTION_PRICE.rst
* - ``gas-price``
- |GAS_PRICE.example|
- .. include:: ../_autodoc/GAS_PRICE.rst
* - ``site-power-capacity``
- |SITE_POWER_CAPACITY.example|
- .. include:: ../_autodoc/SITE_POWER_CAPACITY.rst
Expand Down Expand Up @@ -187,8 +194,8 @@ For more details on the possible formats for field values, see :ref:`variable_qu
- Example value
- Description
* - ``commodity``
- |COMMODITY.example|
- .. include:: ../_autodoc/COMMODITY.rst
- |COMMODITY_FLEX_MODEL.example|
- .. include:: ../_autodoc/COMMODITY_FLEX_MODEL.rst
* - ``consumption``
- |CONSUMPTION.example|
- .. include:: ../_autodoc/CONSUMPTION.rst
Expand Down
2 changes: 2 additions & 0 deletions documentation/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ In :ref:`getting_started`, we have some helpful tips how to dive into this docum
tut/toy-example-expanded
tut/toy-example-multiasset-curtailment
tut/flex-model-v2g
tut/multi-feed-storage
tut/multi-commodity
tut/toy-example-process
tut/toy-example-reporter
tut/posting_data
Expand Down
261 changes: 261 additions & 0 deletions documentation/tut/multi-commodity.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
.. _tut_multi_commodity:

A flex-modeling tutorial for storage: Multiple commodities (gas & electricity)
------------------------------------------------------------------------------

The :ref:`multi-feed storage tutorial <tut_multi_feed_storage>` showed that the ``flex-model`` can be a *list*, so that several devices are scheduled together in one call.
Those devices all acted on the same commodity (electricity). But many real sites mix commodities — electricity *and* gas, for instance — each with its own price.

FlexMeasures handles this with two ingredients:

- a ``commodity`` field on each device in the ``flex-model``, and
- a per-commodity price listing in the ``flex-context``.

In this tutorial we schedule a small hybrid site with one device on each commodity, and read back a cost breakdown that is tracked *per commodity*.
(For a more general introduction to flex modeling, see :ref:`describing_flexibility`. For the single-commodity, multi-device case, see :ref:`tut_multi_feed_storage`.)


The use case
============

A site has two flexible-ish devices, each acting on a different commodity:

- A **battery** on the ``electricity`` commodity: 20 kW power, 100 kWh capacity, 95% charging and discharging efficiency. It starts at 20 kWh and must reach 80 kWh by 23:00.
- A **gas boiler** on the ``gas`` commodity: it draws a **constant 1 kW** of gas every hour, modelled as a fixed load (it is not really flexible, but it still incurs a commodity cost we want to account for).

Prices are flat, but *different per commodity*:

- Electricity: **100 EUR/MWh** (consumption and production)
- Gas: **50 EUR/MWh**

We want the scheduler to optimise the battery against the electricity price, run the boiler at its fixed gas baseline, and report electricity and gas costs separately.


Building the flex model
=======================

As in the multi-feed tutorial, the ``flex-model`` is a **list** with one entry per device.
What is new here is the ``commodity`` field, which tells the scheduler *which price signal* applies to each device. It defaults to ``"electricity"``.

.. code-block:: json

{
"flex-model": [
{
"sensor": 1,
"commodity": "electricity",
"state-of-charge": {"sensor": 3},
"soc-at-start": 20.0,
"soc-min": 0.0,
"soc-max": 100.0,
"soc-targets": [
{"datetime": "2024-01-01T23:00:00+01:00", "value": 80.0}
],
"power-capacity": "20 kW",
"charging-efficiency": 0.95,
"discharging-efficiency": 0.95
},
{
"sensor": 2,
"commodity": "gas",
"power-capacity": "30 kW",
"consumption-capacity": "30 kW",
"production-capacity": "0 kW",
"soc-usage": ["1 kW"],
"soc-min": 0.0,
"soc-max": 0.0,
"soc-at-start": 0.0
}
]
}

Here, sensor ``1`` is the battery's power sensor, sensor ``2`` is the boiler's power sensor, and sensor ``3`` is the battery's instantaneous ``state-of-charge`` sensor (referenced from the battery entry so the scheduler records its charge level).

A few things to note:

- **The battery is a normal storage device** (``soc-at-start``, ``soc-min``, ``soc-max``, ``soc-targets``), tagged with ``"commodity": "electricity"``.
- **The boiler is modelled as a fixed load.** With ``soc-min`` and ``soc-max`` both 0, it can store nothing; ``soc-usage`` of ``1 kW`` forces it to consume exactly 1 kW of gas every hour, which the optimiser cannot change. ``production-capacity`` of 0 kW means it can never produce gas.

The prices live in the ``flex-context``. For a single commodity you would pass ``consumption-price`` and ``production-price`` directly. For **multiple commodities**, you instead provide a ``commodities`` list, one entry per commodity:

.. code-block:: json

{
"flex-context": [
{
"commodity": "electricity",
"consumption-price": "100 EUR/MWh",
"production-price": "100 EUR/MWh"
},
{
"commodity": "gas",
"consumption-price": "50 EUR/MWh"
}
]
}

Each device's costs are then evaluated against the prices of *its own* commodity: the battery against electricity, the boiler against gas.

.. note:: All commodities in one scheduling problem must share the same currency (here, EUR). The prices themselves can of course differ, and may be time series or sensors just like any other price in FlexMeasures.


Triggering the schedule
=======================

We schedule on the **site asset**, so that FlexMeasures considers both devices together in a single optimisation.

.. tabs::

.. tab:: CLI

.. code-block:: bash

$ flexmeasures add schedule \
--asset 1 \
--start 2024-01-01T00:00+01:00 \
--duration PT24H \
--flex-model flex-model-multi-commodity.json \
--flex-context flex-context-multi-commodity.json
New schedule is stored.

.. tab:: API

Example call: `[POST] http://localhost:5000/api/v3_0/assets/1/schedules/trigger <../api/v3_0.html#post--api-v3_0-assets-id-schedules-trigger>`_:

.. code-block:: json

{
"start": "2024-01-01T00:00:00+01:00",
"duration": "PT24H",
"flex-model": [
{
"sensor": 1,
"commodity": "electricity",
"state-of-charge": {"sensor": 3},
"soc-at-start": 20.0,
"soc-min": 0.0,
"soc-max": 100.0,
"soc-targets": [
{"datetime": "2024-01-01T23:00:00+01:00", "value": 80.0}
],
"power-capacity": "20 kW",
"charging-efficiency": 0.95,
"discharging-efficiency": 0.95
},
{
"sensor": 2,
"commodity": "gas",
"power-capacity": "30 kW",
"consumption-capacity": "30 kW",
"production-capacity": "0 kW",
"soc-usage": ["1 kW"],
"soc-min": 0.0,
"soc-max": 0.0,
"soc-at-start": 0.0
}
],
"flex-context": [
{
"commodity": "electricity",
"consumption-price": "100 EUR/MWh",
"production-price": "100 EUR/MWh"
},
{
"commodity": "gas",
"consumption-price": "50 EUR/MWh"
}
]
}

.. tab:: FlexMeasures Client

Using the `FlexMeasures Client <https://pypi.org/project/flexmeasures-client/>`_:

.. code-block:: python

schedule = await client.trigger_and_get_schedule(
asset_id=1, # the site asset
start="2024-01-01T00:00:00+01:00",
duration="PT24H",
flex_model=[
{
"sensor": 1, # battery power sensor
"commodity": "electricity",
"state-of-charge": {"sensor": 3}, # battery SoC sensor
"soc-at-start": 20.0,
"soc-min": 0.0,
"soc-max": 100.0,
"soc-targets": [
{"datetime": "2024-01-01T23:00:00+01:00", "value": 80.0}
],
"power-capacity": "20 kW",
"charging-efficiency": 0.95,
"discharging-efficiency": 0.95,
},
{
"sensor": 2, # boiler power sensor
"commodity": "gas",
"power-capacity": "30 kW",
"consumption-capacity": "30 kW",
"production-capacity": "0 kW",
"soc-usage": ["1 kW"],
"soc-min": 0.0,
"soc-max": 0.0,
"soc-at-start": 0.0,
},
],
flex_context=[
{
"commodity": "electricity",
"consumption-price": "100 EUR/MWh",
"production-price": "100 EUR/MWh",
},
{
"commodity": "gas",
"consumption-price": "50 EUR/MWh",
},
],
)

The scheduler returns one schedule per device (stored on sensors ``1`` and ``2``) and a single commitment-cost result that breaks the cost down per commodity.


What to expect
==============

The asset chart shows both commodities together, with the battery's stock level in between:

.. image:: https://github.com/FlexMeasures/screenshots/raw/main/tut/multi-commodity.png
:align: center
:alt: Asset-level chart of the hybrid site, showing battery power, battery state of charge, and the gas boiler.
|

Reading the chart top to bottom:

- **Battery power (electricity)** charges at its full 20 kW for the first three hours, then makes one partial-power step, which compensates for its charging efficiency losses to land exactly on the 80 kWh target, and then sits idle for the rest of the day. In the final hour it discharges at −20 kW. Because the electricity price is flat, there is no cheaper window to wait for, so it simply charges as early as possible (``prefer-charging-sooner`` is on by default).
- **Battery state of charge** makes the effect of that power schedule explicit: the stock rises from the 20 kWh ``soc-at-start``, reaches the 80 kWh target during the morning, holds there through the idle hours, and drops in the final hour as the battery discharges. This is the charge level you would otherwise have to infer from the power curve.
- **Gas boiler (gas)** runs at exactly 1 kW every single hour. The ``soc-usage`` field makes this a fixed load that the optimiser cannot shift — its only effect on the result is the gas cost it incurs.

The schedules match the cost figures reported by the scheduler:

.. code-block:: text

Electricity (battery)
Net charge needed : 80 kWh − 20 kWh = 60 kWh stored
Grid draw : 60 kWh ÷ 0.95 = 63.16 kWh
Charge cost : 63.16 kWh × 100 EUR/MWh ≈ 6.32 EUR
Discharge credit : 20 kWh × 100 EUR/MWh = −2.00 EUR
Net electricity ≈ 4.32 EUR

Gas (boiler)
Consumption : 1 kW × 24 h = 24 kWh
Gas cost : 0.024 MWh × 50 EUR/MWh = 1.20 EUR

Total = 5.52 EUR

The commitment-cost result keeps these as separate entries — ``electricity net energy`` (≈ 4.32 EUR) and ``gas net energy`` (1.20 EUR) — so you can always see how much each commodity contributed.

.. note:: This same pattern extends to more devices and more commodities. Add further entries to the ``flex-model`` list (each with its ``commodity``) and a matching entry in the ``flex-context`` ``commodities`` list. As long as all commodities share one currency, FlexMeasures optimises them together and reports each commodity's cost on its own.

We hope this demonstration helped to illustrate multi-commodity scheduling.
To revisit scheduling several devices that share a single commodity and stock, head back to :ref:`tut_multi_feed_storage`.
Loading
Loading