Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
4ad215d
feat: add stock-id field in Storage and DB flex model schemas
Ahmad-Wahid Feb 25, 2026
65fc268
feat: build stock groups
Ahmad-Wahid Feb 25, 2026
ef7cf60
feat: get stock groups
Ahmad-Wahid Feb 25, 2026
55ded46
feat: add a test case for multi feed stock
Ahmad-Wahid Feb 25, 2026
d740c0d
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Ahmad-Wahid Mar 4, 2026
8bd859f
feat: add support for shared storage
Ahmad-Wahid Mar 5, 2026
6658803
remove the breakpoint
Ahmad-Wahid Mar 5, 2026
d500052
feat: update the test case for two devices with shared stock
Ahmad-Wahid Mar 5, 2026
09e9780
feat: add assertions with clear reasons
Ahmad-Wahid Mar 5, 2026
d4a15eb
Add support for multi-device charging of shared storage
Ahmad-Wahid Mar 12, 2026
26a1993
fix: sum all devices soc contribution, and use individual device effi…
Ahmad-Wahid Mar 23, 2026
c98b178
update test case for multi feed stock
Ahmad-Wahid Mar 13, 2026
358afb8
expect to charge the battery early to see the effect of fully discharge
Ahmad-Wahid Mar 23, 2026
4932cf9
fix: update the assert statements according to the scheduler results
Ahmad-Wahid Mar 23, 2026
b8ff719
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Flix6x Mar 23, 2026
29785fa
dev: first step in resolving merge conflicts
Flix6x Mar 23, 2026
cefe507
chore: code annotation
Flix6x Mar 23, 2026
118587b
fix: not all flex-models have sensors
Flix6x Mar 23, 2026
74b665f
fix: static method has no self
Flix6x Mar 31, 2026
fbcf2e5
delete: remove inapplicable fields for stock model
Flix6x Mar 31, 2026
4259ffa
fix: fix interpretation of test results
Flix6x Mar 31, 2026
bc3991a
fix: move initialization of ems_constraints
Flix6x Mar 31, 2026
aefaf0d
fix: resolve merge conflicts on _build_soc_schedule, copied from Ahmad
Flix6x Mar 31, 2026
63b6bd7
fix: remove redundant code block
Flix6x Mar 31, 2026
0be435f
dev: use "state-of-charge" key instead of "sensor" key for stock models
Flix6x Mar 31, 2026
123f543
fix: skip StockCommitment for device models that outsource their stoc…
Flix6x Mar 31, 2026
f02e2ee
fix: old flex models that describe a device that serves both as a fee…
Flix6x Mar 31, 2026
5fb576e
fix: model stock devices using the state-of-charge field instead of t…
Flix6x Mar 31, 2026
cb110a9
fix: identify asset to merge with db flex-model
Flix6x Mar 31, 2026
b5bb77e
fix: validation
Flix6x Mar 31, 2026
816eda7
fix: flex-model setup in test
Flix6x Mar 31, 2026
1d5433f
fix: create stock group
Ahmad-Wahid Apr 4, 2026
eeffbf3
use soc-sensor in case of missing power sensor and also correct stock…
Ahmad-Wahid Apr 4, 2026
d8cab12
fix: create stock model for a model which has itself stock
Ahmad-Wahid Apr 5, 2026
ba7b433
update the assert statements
Ahmad-Wahid Apr 5, 2026
ea96b53
fix: merge conflicts
Ahmad-Wahid Apr 5, 2026
a229502
remove stock-id field
Ahmad-Wahid Apr 5, 2026
8190044
fix: correct the stock groups
Ahmad-Wahid Apr 7, 2026
177154c
refactor: remove unneccessary test function
Ahmad-Wahid Apr 9, 2026
a110f0e
fix: shared soc-gain, soc-usage, soc-minima and soc-maxima
Flix6x Apr 9, 2026
c7679ef
fix: shared StockCommitment for preferring a full SoC
Flix6x Apr 9, 2026
53d27c7
dev: todo
Flix6x Apr 9, 2026
69b5e27
dev: add "test" test case
Flix6x Apr 9, 2026
b95a594
fix commodity-level commitments by grouping devices and aligning devi…
Ahmad-Wahid Apr 28, 2026
5bbb515
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Ahmad-Wahid May 26, 2026
7a0e7fe
fix: use net energy costs instead of individual device costs
Ahmad-Wahid May 26, 2026
c5351e0
fix: comment out the buggy lines
Ahmad-Wahid May 26, 2026
84ed22b
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Flix6x Jun 3, 2026
d2b1812
fix: merge conflicts
Flix6x Jun 3, 2026
2d662b0
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Flix6x Jun 3, 2026
6fea3e7
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Ahmad-Wahid Jun 4, 2026
5e76191
fix: add device-model in groups if it's missing
Ahmad-Wahid Jun 4, 2026
55dbced
fix: restore SOC constraints and state-of-charge handling broken by m…
Ahmad-Wahid Jun 4, 2026
f95ad5e
feat: Split flex-context settings by commodity for dynamic capacity s…
Ahmad-Wahid Jul 7, 2026
f8fb0aa
Apply suggestions from code review
Flix6x Jul 7, 2026
dc358f9
style: black
Flix6x Jul 7, 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
3 changes: 2 additions & 1 deletion documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ 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>`_]

* Support multiple feeders to a shared storage [see `PR #2001 <https://www.github.com/FlexMeasures/flexmeasures/pull/2001>`_ ]
* 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>`_]
Comment thread
Flix6x marked this conversation as resolved.

Infrastructure / Support
----------------------
Expand Down
26 changes: 18 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). See :ref:`tut_multi_commodity` for a hands-on example.

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 Expand Up @@ -274,6 +281,8 @@ However, here are some tips to model a buffer correctly:
- Set ``charging-efficiency`` to the sensor describing the :abbr:`COP (coefficient of performance)` values.
- Set ``storage-efficiency`` to a value below 100% to model (heat) loss.

For a hands-on example of a heat buffer fed by multiple devices, see :ref:`tut_multi_feed_storage`.

What happens if the flex model describes an infeasible problem for the storage scheduler? Excellent question!
It is highly important for a robust operation that these situations still lead to a somewhat good outcome.
From our practical experience, we derived a ``StorageFallbackScheduler``.
Expand All @@ -283,6 +292,7 @@ depending on the first target state of charge and the capabilities of the asset.
Of course, we also log a failure in the scheduling job, so it's important to take note of these failures. Often, mis-configured flex models are the reason.

For a hands-on tutorial on using some of the storage flex-model fields, head over to :ref:`tut_v2g` use case and `the API documentation for triggering schedules <../api/v3_0.html#post--api-v3_0-assets-id-schedules-trigger>`_.
For further hands-on examples, see :ref:`tut_multi_feed_storage` (multiple devices feeding one shared storage) and :ref:`tut_multi_commodity` (devices on different commodities scheduled together).

Finally, are you interested in the linear programming details behind the storage scheduler?
Then head over to :ref:`storage_device_scheduler`!
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
5 changes: 3 additions & 2 deletions documentation/tut/flex-model-v2g.rst
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,6 @@ To provide an incentive for cycling the battery in response to market prices, th
}


We hope this demonstration helped to illustrate the flex-model of the storage scheduler. Until now, optimizing storage (like batteries) has been the sole focus of these tutorial series.
In :ref:`tut_toy_schedule_process`, we'll turn to something different: the optimal timing of processes with fixed energy work and duration.
We hope this demonstration helped to illustrate the flex-model of the storage scheduler.
Until now, optimizing a single storage device (like a battery) has been the sole focus of these tutorial series.
In :ref:`tut_multi_feed_storage`, we'll cover scheduling several devices that feed into one shared storage.
262 changes: 262 additions & 0 deletions documentation/tut/multi-commodity.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
.. _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`.
Next, in :ref:`tut_toy_schedule_process`, we'll turn to something different: the optimal timing of processes with fixed energy work and duration.
Loading
Loading