Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 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
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
17 changes: 9 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), using the ``commodities`` field.

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,6 +59,9 @@ 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
Expand All @@ -70,9 +74,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 +188,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
6 changes: 5 additions & 1 deletion flexmeasures/api/common/schemas/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ def make_openapi_compatible( # noqa: C901
sensor_only_validators.append(validator[-1])

new_fields = {}
tobeadded_fields = schema_cls._declared_fields
try:
# in case `schema_cls.__init__` reordered the fields, preserve their order
tobeadded_fields = schema_cls().fields
except TypeError:
tobeadded_fields = schema_cls._declared_fields
if include:
for item in include:
tobeadded_fields.update(item)
Expand Down
12 changes: 6 additions & 6 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def __init__(self, *args, **kwargs):

flex_context = fields.Nested(
flex_context_schema_openAPI,
required=True,
load_default={},
data_key="flex-context",
metadata=dict(
description="The flex-context is validated according to the scheduler's `FlexContextSchema`.",
Expand All @@ -107,11 +107,11 @@ def __init__(self, *args, **kwargs):
flex_model = fields.List(
fields.Nested(
storage_flex_model_schema_openAPI(exclude=["asset"]),
required=True,
data_key="flex-model",
metadata=dict(
description="Flex-model per device (identified by `sensor`). The flex-model validation is handled by the scheduler. What follows is the schema used by the `StorageScheduler`.",
),
),
load_default=[],
data_key="flex-model",
metadata=dict(
description="Flex-model per device (identified by `sensor`). The flex-model validation is handled by the scheduler. What follows is the schema used by the `StorageScheduler`.",
),
)

Expand Down
64 changes: 53 additions & 11 deletions flexmeasures/data/models/planning/linear_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@

def device_scheduler( # noqa C901
device_constraints: list[pd.DataFrame],
ems_constraints: pd.DataFrame,
ems_constraints: pd.DataFrame | list[pd.DataFrame],
commitment_quantities: list[pd.Series] | None = None,
commitment_downwards_deviation_price: list[pd.Series] | list[float] | None = None,
commitment_upwards_deviation_price: list[pd.Series] | list[float] | None = None,
commitments: list[pd.DataFrame] | list[Commitment] | None = None,
initial_stock: float | list[float] = 0,
stock_groups: dict[int, list[int]] | None = None,
ems_constraint_groups: list[list[int]] | None = None,
) -> tuple[list[pd.Series], float, SolverResults, ConcreteModel]:
"""This generic device scheduler is able to handle an EMS with multiple devices,
with various types of constraints on the EMS level and on the device level,
Expand All @@ -64,6 +65,13 @@ def device_scheduler( # noqa C901
:param ems_constraints: EMS constraints are on an EMS level. Handled constraints (listed by column name):
derivative max: maximum flow
derivative min: minimum flow
May be a single DataFrame (the constraint is applied to the summed flow of all devices),
or a list of DataFrames (one per device group). In the latter case, ``ems_constraint_groups``
lists the device indices each DataFrame applies to. The StorageScheduler uses one device
group per commodity, so each commodity gets its own EMS-level capacity constraint.
:param ems_constraint_groups: For each EMS constraint DataFrame, the list of device indices it applies to. When omitted,
each EMS constraint is applied to the summed flow of all devices (legacy behaviour). A device
may appear in more than one group.
:param commitments: Commitments are on an EMS level by default. Handled parameters (listed by column name):
quantity: for example, 5.5
downwards deviation price: 10.1
Expand Down Expand Up @@ -101,7 +109,27 @@ def device_scheduler( # noqa C901
resolution = pd.to_timedelta(device_constraints[0].index.freq).to_pytimedelta()
end = device_constraints[0].index.to_pydatetime()[-1] + resolution

# map device → stock group
# Normalise EMS constraints to a list of (DataFrame, device-group) pairs.
# A single DataFrame (legacy behaviour) applies to the summed flow of all devices;
# a list of DataFrames applies one EMS-level constraint per device group, as set up
# per commodity by the StorageScheduler.
all_devices = list(range(len(device_constraints)))
if isinstance(ems_constraints, pd.DataFrame):
ems_constraints_list = [ems_constraints]
ems_constraint_device_groups = [all_devices]
else:
ems_constraints_list = ems_constraints
if ems_constraint_groups is None:
if len(ems_constraints_list) > 1:
raise ValueError(
"When passing multiple EMS constraint DataFrames, you must also specify ems_constraint_groups."
)
ems_constraint_device_groups = [all_devices for _ in ems_constraints_list]
else:
ems_constraint_device_groups = ems_constraint_groups

# map device -> primary stock group (used for per-device stock bounds)
# and map stock group -> all member devices (used for stock accumulation).
device_to_group = {}

if stock_groups:
Expand Down Expand Up @@ -389,15 +417,15 @@ def device_derivative_min_select(m, d, j):
else:
return np.nanmax([min_v, equal_v])

def ems_derivative_max_select(m, j):
v = ems_constraints["derivative max"].iloc[j]
def ems_derivative_max_select(m, g, j):
v = ems_constraints_list[g]["derivative max"].iloc[j]
if np.isnan(v):
return infinity
else:
return v

def ems_derivative_min_select(m, j):
v = ems_constraints["derivative min"].iloc[j]
def ems_derivative_min_select(m, g, j):
v = ems_constraints_list[g]["derivative min"].iloc[j]
if np.isnan(v):
return -infinity
else:
Expand Down Expand Up @@ -483,8 +511,15 @@ def grouped_commitment_equalities(m, c, j, g):
model.device_derivative_min = Param(
model.d, model.j, initialize=device_derivative_min_select
)
model.ems_derivative_max = Param(model.j, initialize=ems_derivative_max_select)
model.ems_derivative_min = Param(model.j, initialize=ems_derivative_min_select)
model.eg = RangeSet(
0, len(ems_constraints_list) - 1, doc="Set of EMS constraint (device) groups"
)
model.ems_derivative_max = Param(
model.eg, model.j, initialize=ems_derivative_max_select
)
model.ems_derivative_min = Param(
model.eg, model.j, initialize=ems_derivative_min_select
)
model.device_efficiency = Param(model.d, model.j, initialize=device_efficiency)
model.device_derivative_down_efficiency = Param(
model.d, model.j, initialize=device_derivative_down_efficiency
Expand Down Expand Up @@ -628,8 +663,15 @@ def device_down_derivative_sign(m, d, j):
"""Derivative down if sign points down, derivative not down if sign points up."""
return -m.device_power_down[d, j] <= Md * (1 - m.device_power_sign[d, j])

def ems_derivative_bounds(m, j):
return m.ems_derivative_min[j], sum(m.ems_power[:, j]), m.ems_derivative_max[j]
def ems_derivative_bounds(m, g, j):
devices = ems_constraint_device_groups[g]
if not devices:
return Constraint.Skip
return (
m.ems_derivative_min[g, j],
sum(m.ems_power[d, j] for d in devices),
m.ems_derivative_max[g, j],
)

def commitment_up_derivative_sign(m, c):
"""Up deviation active only if sign points up."""
Expand Down Expand Up @@ -722,7 +764,7 @@ def device_derivative_equalities(m, d, j):
model.device_power_down_sign = Constraint(
model.d, model.j, rule=device_down_derivative_sign
)
model.ems_power_bounds = Constraint(model.j, rule=ems_derivative_bounds)
model.ems_power_bounds = Constraint(model.eg, model.j, rule=ems_derivative_bounds)
if not convex_cost_curve:
model.commitment_up_derivative_sign_con = Constraint(
model.c, rule=commitment_up_derivative_sign
Expand Down
Loading
Loading