-
Notifications
You must be signed in to change notification settings - Fork 51
Feat: Support shared storage (multi‑feed stock) #2001
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 65fc268
feat: build stock groups
Ahmad-Wahid ef7cf60
feat: get stock groups
Ahmad-Wahid 55ded46
feat: add a test case for multi feed stock
Ahmad-Wahid d740c0d
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Ahmad-Wahid 8bd859f
feat: add support for shared storage
Ahmad-Wahid 6658803
remove the breakpoint
Ahmad-Wahid d500052
feat: update the test case for two devices with shared stock
Ahmad-Wahid 09e9780
feat: add assertions with clear reasons
Ahmad-Wahid d4a15eb
Add support for multi-device charging of shared storage
Ahmad-Wahid 26a1993
fix: sum all devices soc contribution, and use individual device effi…
Ahmad-Wahid c98b178
update test case for multi feed stock
Ahmad-Wahid 358afb8
expect to charge the battery early to see the effect of fully discharge
Ahmad-Wahid 4932cf9
fix: update the assert statements according to the scheduler results
Ahmad-Wahid b8ff719
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Flix6x 29785fa
dev: first step in resolving merge conflicts
Flix6x cefe507
chore: code annotation
Flix6x 118587b
fix: not all flex-models have sensors
Flix6x 74b665f
fix: static method has no self
Flix6x fbcf2e5
delete: remove inapplicable fields for stock model
Flix6x 4259ffa
fix: fix interpretation of test results
Flix6x bc3991a
fix: move initialization of ems_constraints
Flix6x aefaf0d
fix: resolve merge conflicts on _build_soc_schedule, copied from Ahmad
Flix6x 63b6bd7
fix: remove redundant code block
Flix6x 0be435f
dev: use "state-of-charge" key instead of "sensor" key for stock models
Flix6x 123f543
fix: skip StockCommitment for device models that outsource their stoc…
Flix6x f02e2ee
fix: old flex models that describe a device that serves both as a fee…
Flix6x 5fb576e
fix: model stock devices using the state-of-charge field instead of t…
Flix6x cb110a9
fix: identify asset to merge with db flex-model
Flix6x b5bb77e
fix: validation
Flix6x 816eda7
fix: flex-model setup in test
Flix6x 1d5433f
fix: create stock group
Ahmad-Wahid eeffbf3
use soc-sensor in case of missing power sensor and also correct stock…
Ahmad-Wahid d8cab12
fix: create stock model for a model which has itself stock
Ahmad-Wahid ba7b433
update the assert statements
Ahmad-Wahid ea96b53
fix: merge conflicts
Ahmad-Wahid a229502
remove stock-id field
Ahmad-Wahid 8190044
fix: correct the stock groups
Ahmad-Wahid 177154c
refactor: remove unneccessary test function
Ahmad-Wahid a110f0e
fix: shared soc-gain, soc-usage, soc-minima and soc-maxima
Flix6x c7679ef
fix: shared StockCommitment for preferring a full SoC
Flix6x 53d27c7
dev: todo
Flix6x 69b5e27
dev: add "test" test case
Flix6x b95a594
fix commodity-level commitments by grouping devices and aligning devi…
Ahmad-Wahid 5bbb515
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Ahmad-Wahid 7a0e7fe
fix: use net energy costs instead of individual device costs
Ahmad-Wahid c5351e0
fix: comment out the buggy lines
Ahmad-Wahid 84ed22b
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Flix6x d2b1812
fix: merge conflicts
Flix6x 2d662b0
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Flix6x 6fea3e7
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Ahmad-Wahid 5e76191
fix: add device-model in groups if it's missing
Ahmad-Wahid 55dbced
fix: restore SOC constraints and state-of-charge handling broken by m…
Ahmad-Wahid f95ad5e
feat: Split flex-context settings by commodity for dynamic capacity s…
Ahmad-Wahid f8fb0aa
Apply suggestions from code review
Flix6x dc358f9
style: black
Flix6x File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.