Skip to content

feat: multi-commodity operation modes (per-commodity ranges + no-load flow)#2335

Open
Flix6x wants to merge 2 commits into
feat/2113-operation-mode-power-bandsfrom
feat/multi-commodity-operation-modes
Open

feat: multi-commodity operation modes (per-commodity ranges + no-load flow)#2335
Flix6x wants to merge 2 commits into
feat/2113-operation-mode-power-bandsfrom
feat/multi-commodity-operation-modes

Conversation

@Flix6x

@Flix6x Flix6x commented Jul 22, 2026

Copy link
Copy Markdown
Member

What

Generalises operation modes (#2278) so that a single mode can carry, on top of the device's own signed power band, per-commodity power ranges plus a per-commodity fixed (no-load) flow. This matches the S2 FRBC model (FRBC.OperationModeElement.power_ranges, a list — one range per commodity_quantity — where today's FlexMeasures version collapses to a single commodity), and lets a unit-committed affine cogeneration unit be expressed natively:

  • off: electricity [0, 0], gas [0, 0], heat [0, 0] (no no-load fuel)
  • on: electricity [Pmin, Pmax], and — tied affinely — gas = no-load base + proportional, heat = no-load base + proportional

Design route for the affine tie (route b, S2-faithful)

Two routes were on the table:

  • (a) the mode supplies only the binary + min + per-commodity no-load flow, and the affine marginal ratio is supplied by a separate inter-port coupling feature;
  • (b) the mode itself carries the per-commodity ranges (equivalently base + coefficient).

This PR takes route (b). Rationale:

  1. Self-contained. There is no inter-commodity coupling on this branch yet (a sibling effort is building that separately), so route (a) would depend on unmerged machinery. Route (b) is a coherent, reviewable increment on its own.
  2. S2-faithful. In S2 an operation-mode element carries the per-commodity power_ranges directly, and a single operation_mode_factor (0..1) interpolates every commodity's power simultaneously within its range. That interpolation is the affine tie. We model exactly this: a continuous factor lambda in [0, 1] per (banded device, band, time step), gated by the existing device_band binary, sets the banded device's own power to pmin + lambda*(pmax-pmin) and each coupled commodity to cmin + lambda*(cmax-cmin). The range minima are the per-commodity fixed no-load flows (gated by the binary); the shared factor moves the marginal parts in lockstep.

Trade-off vs. the coupling route: route (a)/coupling keeps the marginal ratio in one place (the coupling between ports) and can express couplings that exist independently of operation modes, but it needs that feature to land first and splits a cogeneration unit's description across two mechanisms. Route (b) keeps a mode's full multi-commodity behaviour in one object (closer to S2 and easier to reason about for unit commitment), at the cost of duplicating a ratio if the same physical coupling is also modelled elsewhere. Both are being prototyped so we can compare them for a release.

Schema shape

OperationModeSchema gains an optional commodity-power-ranges: a list of {"commodity": <str>, "power-range": [<low>, <high>]} (new CommodityPowerRangeSchema), mirroring S2's per-commodity power_ranges. Endpoints are ordered factor-0 → factor-1 (not min/max), so the factor-0 endpoint is the no-load flow. Duplicate commodities are rejected. Single-commodity operation-modes are unchanged.

Scheduler wiring

device_scheduler gains an optional device_mode_commodity_ranges parameter (parallel to device_power_bands). New Pyomo pieces (flexmeasures/data/models/planning/linear_optimization.py): the device_mode_factor variable, its gate factor <= device_band, the primary-power interpolation equality, and the per-commodity interpolation equality. Machinery is only built for devices that actually declare commodity ranges, so existing single-commodity behaviour is untouched.

Test

test_operation_modes.py adds a cogeneration unit expressed via multi-commodity operation modes:

  • idles fully when unprofitable — every commodity 0 and, crucially, no no-load fuel burned;
  • runs at/above the minimum when on — a small electricity stock target forces exactly three steps at the 0.4 mode minimum, with gas = 0.2 + 1.0*P and heat = 0.1 + 0.5*P (incl. no-load bases);
  • hand-computed objective — energy 1*0.4 + 1*0.4 + 10*0.4 = 4.8 + gas 2*(0.6*3) = 3.6 = 8.4.

Existing single-commodity operation-mode cases stay green; test_scheduling.py adds schema load / duplicate-commodity validation tests.

Scope

Min up/down time (minimum on/off durations) is out of scope for this prototype.

… flow)

Generalise operation modes (#2278) so that a mode can carry, in addition to
the device's own signed power band, per-commodity power ranges plus a
per-commodity fixed no-load flow, matching S2's FRBC.OperationModeElement.

The device scheduler ties the commodities affinely through a single
continuous operation-mode factor per (banded device, band, time step), gated
by the existing device_band binary. When a mode is active the factor sweeps
the device's own power between its band min/max and every coupled commodity
between its own (min, max) in lockstep: the range minima are the fixed
no-load flows (gated by the binary), the shared factor supplies the marginal
part. This natively expresses a unit-committed affine cogeneration unit
(off + affine-on gas/heat), with a minimum level when on. Min up/down time is
out of scope.

Single-commodity operation modes are untouched (new machinery is only built
for devices that declare commodity ranges).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MLCUiSdXDqDBmg8GbYp1B
Signed-off-by: F.N. Claessen <claessen@seita.nl>
@read-the-docs-community

read-the-docs-community Bot commented Jul 22, 2026

Copy link
Copy Markdown

Reconcile multi-commodity operation modes with the base-branch review change that
renamed the operation-mode power range to sign-explicit consumption-range /
production-range keys and added _operation_mode_signed_band.

- OperationModeSchema: keep the base's consumption_range/production_range fields,
  check_ranges validator and _operation_mode_signed_band helper; drop the old
  power_range field/validator; keep commodity_power_ranges + uniqueness check.
- CommodityPowerRangeSchema: make per-commodity ranges sign-explicit too
  (consumption-range and/or production-range), with the same non-negative /
  through-zero validation.
- storage.py: factor out a shared _signed_range_endpoints core reused by both
  _operation_mode_signed_band (device's own band) and a new
  _operation_mode_commodity_bands, which derives each commodity's directional
  (factor-0, factor-1) signed endpoints aligned to the device's operation-mode factor.
- linear_optimization.py: clarify that device_mode_commodity_ranges carries signed
  (c0, c1) factor-extreme endpoints (sign resolved upstream), not (min, max).
- tests: express the cogeneration unit sign-explicitly (electricity and heat as
  production, gas as consumption), driven through the schema + helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MLCUiSdXDqDBmg8GbYp1B
Signed-off-by: F.N. Claessen <claessen@seita.nl>
@Flix6x

Flix6x commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

Reconciled with the base-branch review change (sign-explicit consumption-range/production-range + _operation_mode_signed_band), merged in 2055338d0.

  • Schema conflict: kept the base's consumption_range/production_range fields, check_ranges validator and _operation_mode_signed_band; dropped the old power_range field/validator; kept commodity_power_ranges + its uniqueness check.
  • Per-commodity ranges are now sign-explicit too: commodity-power-ranges entries use consumption-range and/or production-range (same non-negative / through-zero validation as the mode's own band), e.g. gas as consumption-range, electricity/heat as production-range.
  • Shared derivation: factored a _signed_range_endpoints core reused by both _operation_mode_signed_band (the device's own band) and a new _operation_mode_commodity_bands, which returns each commodity's directional (value-at-factor-0, value-at-factor-1) signed endpoints, aligned to the operation-mode factor via the shared running-level ordering (so consumed and produced commodities move in the physically correct direction against the device's own power).
  • Cogeneration test now expresses the unit sign-explicitly (electricity + heat produced, gas consumed), driven through OperationModeSchema + the helpers; hand-computed objective −4.8 (−8.4 electricity revenue + 3.6 gas cost).

test_operation_modes.py + test_scheduling.py: 127 passed; flake8/black/mypy clean; openapi-specs.json regenerated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant