Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -32,7 +32,7 @@ New features
* 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>`_]
* Let storage scheduling infer missing ``power-capacity`` from directional device capacities before falling back to site capacity, and default the missing opposite capacity to zero when only a non-zero ``consumption-capacity`` or ``production-capacity`` is configured [see `PR #2222 <https://www.github.com/FlexMeasures/flexmeasures/pull/2222>`_]
* Support multiple feeders to a shared storage [see `PR #2001 <https://www.github.com/FlexMeasures/flexmeasures/pull/2001>`_, `PR #2321 <https://www.github.com/FlexMeasures/flexmeasures/pull/2321>`_, `PR #2322 <https://www.github.com/FlexMeasures/flexmeasures/pull/2322>`_ and `PR #2325 <https://www.github.com/FlexMeasures/flexmeasures/pull/2325>`_]
* The flex-context can now define multiple commodities, each specifying their own prices and grid capacities [see `PR #1946 <https://www.github.com/FlexMeasures/flexmeasures/pull/1946>`_, `PR #2172 <https://www.github.com/FlexMeasures/flexmeasures/pull/2172>`_, `PR #2235 <https://www.github.com/FlexMeasures/flexmeasures/pull/2235>`_, `PR #2271 <https://www.github.com/FlexMeasures/flexmeasures/pull/2271>`_ and `PR #2380 <https://www.github.com/FlexMeasures/flexmeasures/pull/2380>`_]
* The flex-context can now define multiple commodities, each specifying their own prices and grid capacities [see `PR #1946 <https://www.github.com/FlexMeasures/flexmeasures/pull/1946>`_, `PR #2172 <https://www.github.com/FlexMeasures/flexmeasures/pull/2172>`_, `PR #2235 <https://www.github.com/FlexMeasures/flexmeasures/pull/2235>`_, `PR #2271 <https://www.github.com/FlexMeasures/flexmeasures/pull/2271>`_, `PR #2355 <https://www.github.com/FlexMeasures/flexmeasures/pull/2355>`_ and `PR #2380 <https://www.github.com/FlexMeasures/flexmeasures/pull/2380>`_]
* In the UI, the flex-context editor supports editing commitments (name, baseline and deviation prices, each accepting a fixed value or a sensor), also within each commodity context; the commitment's commodity follows the commodity tab being edited, and new commitments start with zero-valued baseline and prices [see `PR #2287 <https://www.github.com/FlexMeasures/flexmeasures/pull/2287>`_]
* A commitment in the flex-context now requires a ``baseline`` and at least one deviation price (``up-price`` and/or ``down-price``), as already documented; commitment costs are reported in the scheduling results under the user-given name (with a ``(custom)`` suffix in the rare case the name collides with a scheduler-internal commitment name) [see `PR #2287 <https://www.github.com/FlexMeasures/flexmeasures/pull/2287>`_]
* Commodity contexts that omit grid-connection fields (prices and site capacities) now get smart defaults instead of failing or silently leaving the grid unconstrained — for instance, a bare ``{"commodity": "gas"}`` is treated as having no grid connection; see :ref:`commodity_context_defaults` for the full rules [see `PR #2272 <https://www.github.com/FlexMeasures/flexmeasures/pull/2272>`_]
Expand Down
13 changes: 11 additions & 2 deletions flexmeasures/data/models/planning/linear_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,15 @@ def ems_flow_commitment_equalities(m, c, j):
if commitments[c]["class"].iloc[0] != FlowCommitment:
return Constraint.Skip

# A device-scoped commitment is already bound, once per device group, by
# grouped_commitment_equalities. Now that this constraint family actually
# has bounds, binding such a commitment here as well would constrain the
# same deviation variables a second time, against a different device set
# (the whole EMS, or the whole commodity). Only genuinely EMS-level
# commitments -- those naming no device -- belong here.
if device_group_lookup.get(c):
return Constraint.Skip

# Legacy behavior: no commodity → sum over all devices
if "commodity" not in commitments[c].columns:
devices = m.d
Expand All @@ -793,12 +802,12 @@ def ems_flow_commitment_equalities(m, c, j):
return Constraint.Skip

return (
None,
0 if "upwards deviation price" in commitments[c].columns else None,
m.commitment_quantity[c, j]
+ m.commitment_downwards_deviation[c]
+ m.commitment_upwards_deviation[c]
- sum(m.ems_power[d, j] for d in devices),
None,
0 if "downwards deviation price" in commitments[c].columns else None,
)

def device_derivative_equalities(m, d, j):
Expand Down
49 changes: 49 additions & 0 deletions flexmeasures/data/models/planning/tests/test_commitments.py
Original file line number Diff line number Diff line change
Expand Up @@ -2029,6 +2029,55 @@ def test_stock_scoped_commitment_binds_group_stock(named_device):
np.testing.assert_allclose(total_energy, 20.0, atol=1e-6)


def test_ems_flow_commitment_binds_all_devices():
start = pd.Timestamp("2026-01-01T00:00+01")
end = pd.Timestamp("2026-01-01T01:00+01")
resolution = pd.Timedelta("PT1H")
index = initialize_index(start=start, end=end, resolution=resolution)

device_constraints = [
pd.DataFrame(
{
"min": -100.0,
"max": 100.0,
"equals": np.nan,
"derivative min": -10.0,
"derivative max": 10.0,
"derivative equals": np.nan,
"derivative down efficiency": 1.0,
"derivative up efficiency": 1.0,
},
index=index,
)
for _ in range(2)
]
ems_constraints = pd.DataFrame(
{"derivative min": -100.0, "derivative max": 100.0}, index=index
)
commitments = [
FlowCommitment(
name="EMS target",
index=index,
quantity=5,
upwards_deviation_price=10,
downwards_deviation_price=-10,
)
]

planned_power, planned_costs, results, _ = device_scheduler(
device_constraints=device_constraints,
ems_constraints=ems_constraints,
commitments=commitments,
initial_stock=0,
)

assert results.solver.termination_condition == "optimal"
np.testing.assert_allclose(
sum(schedule.iloc[0] for schedule in planned_power), 5.0, atol=1e-6
)
assert planned_costs == pytest.approx(0)


def test_commitment_commodity_does_not_bind_other_commodity_devices():
"""A commitment
listed under the flex-context's `commitments` should only bind devices of its own
Expand Down
Loading