Confine device power to operation-mode power bands - #2278
Conversation
New storage flex-model field "operation-modes" (S2 terminology): a list of signed power ranges; the device must operate within one of them at every time step. Adds one binary per device per band per time step to the device scheduler, so devices that cannot modulate below a minimum power (or are strictly on/off) no longer receive fractional schedules that their control layer must round up, overshooting site capacity limits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the field to the storage flex-model table (via a new OPERATION_MODES MetaData entry, which the schema now also uses for its API docs), including the sign convention, an on/off device example, the MILP note, and a reference to the S2 standard's FRBC OperationMode concept. Also adds a changelog entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Documentation build overview
75 files changed ·
|
Combine the operation-modes power-bands flex-model field with the new group-based intermediate power constraints from PR #2276. Conflicts were co-located additions in the storage flex-model schema, metadata descriptions, docs, and the generated OpenAPI spec; both features kept. 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>
Signed-off-by: Felix Claessen <30658763+Flix6x@users.noreply.github.com>
Replace the single signed `power-range` on an operation mode with explicit `consumption-range` (positive = consumption) and/or `production-range` (positive = production). A mode may use either or both; combining both (each starting at 0) forms one band through zero. Document that the S2 power-range maps to the FM consumption-range (S2 fixes one sign convention; FM leaves it to the user). Also update the changelog reference to PR #2278. 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>
|
Addressed:
The unit-commitment work stays out of this PR (separate branches). I will reconcile the branches stacked on this one — #2327 (running-cost) and the operation-modes UC route — to the new key names. |
Add integration-level tests that exercise StorageScheduler.compute() end-to-end with "operation-modes" set in the flex-model, so that a regression severing the plumbing that reads operation-modes and passes device_power_bands into device_scheduler() would be caught. Previously only LP-level tests (calling device_scheduler() directly) covered this feature, which is why a recent merge could sever the wiring without any test failing. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
There was a problem hiding this comment.
Pull request overview
This PR adds support for constraining device power to discrete “operation modes” (power bands) in the storage flex-model, wiring the new field through scheduling so the device scheduler can enforce banded operation (MILP when used), along with documentation, OpenAPI updates, and regression tests.
Changes:
- Add
operation-modesto the storage flex-model schema (with validation and metadata/docs exposure). - Wire operation-modes through
StorageSchedulerintodevice_schedulervia a newdevice_power_bandsargument and corresponding MILP constraints. - Add tests covering LP-level band behavior and end-to-end wiring through
StorageScheduler, plus docs/changelog/OpenAPI updates.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| flexmeasures/ui/static/openapi-specs.json | Adds OpenAPI schemas/field docs for OperationMode and operation-modes. |
| flexmeasures/data/schemas/scheduling/storage.py | Introduces OperationModeSchema and adds operation-modes to StorageFlexModelSchema. |
| flexmeasures/data/schemas/scheduling/metadata.py | Adds OPERATION_MODES metadata used for generated docs/OpenAPI descriptions. |
| flexmeasures/data/models/planning/storage.py | Converts operation-modes to signed MW bands and forwards them into device_scheduler. |
| flexmeasures/data/models/planning/linear_optimization.py | Extends device_scheduler with optional per-device power band constraints (MILP). |
| flexmeasures/data/models/planning/tests/test_operation_modes.py | New targeted tests for band constraints and schema-to-band mapping. |
| flexmeasures/data/models/planning/tests/test_solver.py | Integration/regression tests ensuring StorageScheduler wiring enforces/forwards bands. |
| documentation/features/scheduling.rst | Documents the new operation-modes flex-model field. |
| documentation/changelog.rst | Adds changelog entry announcing the new operation-modes capability. |
- Index band choice/lower/upper constraints over a per-device set (bd) instead of anchoring them at band 0 of the (device, band) set, and explain the band-selection mechanics in inline comments - Fail fast when device_power_bands and device_constraints disagree on the number of devices - Move the signed-band conversion into OperationModeSchema.signed_band, with doctests covering consumption-only, production-only and combined modes - Clarify that an S2 signed power-range maps onto consumption-range / production-range by sign (docs, field descriptions, OpenAPI specs) - Hint the remedy in the negative-range validation error Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0111ySFfFSbFBoGXgzUfDTmP Signed-off-by: F.N. Claessen <claessen@seita.nl>
… rule - rst_to_openapi now converts external RST hyperlinks to HTML anchors, so descriptions read well in both Sphinx and OpenAPI consumers - The published OperationMode schema now declares via anyOf that at least one of consumption-range/production-range is required, matching the backend validation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0111ySFfFSbFBoGXgzUfDTmP Signed-off-by: F.N. Claessen <claessen@seita.nl>
- Add rel="noopener noreferrer" to anchors generated by rst_to_openapi (external links and docs search links) - Validate each power band is a (min, max) pair with min <= max before building the MILP - Raise an explicit ValueError instead of asserting when an operation mode declares no range at all Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0111ySFfFSbFBoGXgzUfDTmP Signed-off-by: F.N. Claessen <claessen@seita.nl>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
flexmeasures/data/schemas/scheduling/metadata.py:400
- PR description example for
operation-modesuses{"power-range": [...]}(signed range), but the implemented/public schema and docs useconsumption-range/production-range(and nopower-rangefield). To avoid confusing reviewers/API users, please update the PR description example to match the actual payload shape.
description="""Confine the device's power to one of several power ranges at every time step.
Each operation mode declares a ``consumption-range`` (non-negative, positive is consumption) and/or a ``production-range`` (non-negative, positive is production); a mode may use either or both, and combining both (each starting at 0) forms a single band through zero.
This is useful for devices that cannot modulate their power freely, such as a device that is either off or running at some minimum power (or at one fixed power).
Terminology and semantics follow the `operation modes of the S2 standard <https://docs.s2standard.org/model-reference/FRBC/FRBC.OperationMode/>`_.
S2 fixes one sign convention for power (positive is consumption), whereas FM leaves it to the user; an S2 signed power-range therefore maps onto these fields by sign: its non-negative part corresponds to the FM ``consumption-range``, negative S2 power values (production) correspond to the FM ``production-range`` (with their sign flipped to non-negative), and an S2 range spanning zero maps to a combination of both.
Prevents invalid HTML (and attribute injection) in generated anchors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0111ySFfFSbFBoGXgzUfDTmP Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ode-power-bands-wt Signed-off-by: F.N. Claessen <claessen@seita.nl> # Conflicts: # flexmeasures/ui/static/openapi-specs.json
Brings in #2295 (sensor/group-scoped commitments), #2358 (inflexible-consumption / inflexible-production replacing inflexible-device-sensors), #2374 (inflexible devices as assets), #2278 (operation-mode power bands) and #2306 (rate limiting). Conflict resolutions of note: * schemas/scheduling/storage.py -- main moved GroupReferenceSchema into the new schemas/scheduling/groups.py and storage.py now imports it, so the branch's local copy is dropped rather than merged. Kept _validate_coupling_name (still used by both flex-model schemas) alongside main's new validate_inflexible_flex_model_entry. * linear_optimization.py -- coupling groups (this branch) and operation-mode power bands (main) are independent features; both kept. * devices.py -- FlexDevice gains main's inflexible-device fields next to this branch's coupling fields. * storage.py -- the device_scheduler call passes both coupling_groups and device_power_bands. flexmeasures/data/models/planning + flexmeasures/data/schemas: 579 passed, 3 xfailed. black and flake8 clean. Note: #2306 imports `limits`, which is resolved via uv.lock but not declared in pyproject.toml, so existing venvs need `uv sync --all-groups`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEvWAj45zXaod5WjniF81D Signed-off-by: F.N. Claessen <felix@seita.nl>
Picks up feat/chp's merge of origin/main (#2295 sensor/group-scoped commitments, #2358 inflexible-consumption/-production, #2374 inflexible devices as assets, #2278 operation-mode power bands, #2306 rate limiting). Two conflicts, both additive: this branch's internal-node balance groups and feat/chp's operation-mode power bands are independent features. The device_scheduler call now passes all three feature arguments side by side: coupling_groups (converters), balance_groups (internal commodity nodes) and device_power_bands (operation modes). flexmeasures/data/models/planning + flexmeasures/data/schemas: 586 passed, 3 xfailed. black and flake8 clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JEvWAj45zXaod5WjniF81D Signed-off-by: F.N. Claessen <felix@seita.nl>
Description
Adds support for devices whose power is not free to modulate within [0, P_max], but is confined to a set of power bands — e.g. a device that is either off or running at (or above) some minimum power. Fixes #2113.
Following the S2 standard's FRBC OperationMode terminology, the storage flex-model gains an
operation-modesfield: a list of power ranges, each declared with an explicit sign convention (consumption-rangeand/orproduction-range, both non-negative). The device must operate within one of the declared ranges at every time step:In the
device_scheduler, this adds one binary variable per device per band per time step (exactly one band active; device power bounded by the active band's range), turning the LP into a MILP when the field is used. Without the field, the model is unchanged.Motivation / validation
When a control layer (e.g. an S2 CEM/RM pair) receives a fractional schedule for an on/off device, it has to round it — typically up — which can overshoot site capacity limits. In a two-house community co-simulation with on/off heaters, a community-level capacity breach could not be resolved because the scheduler kept planning fractional heater power that was implemented as full-on blocks. With this field, the schedule only contains implementable power values; in that same co-simulation the breach was fully resolved (community peak landed exactly on the inflexible-load floor), with no observed solver slowdown (~10s solves, HiGHS, 96 time steps).
Runtime impact (LP vs MILP)
Benchmarked
device_schedulerdirectly (HiGHS viaappsi_highs, wall time incl. model build): a synthetic battery-like device doing price arbitrage at 15-min resolution (±1 MW, ±2 MWh, sinusoidal day/night prices, one price commitment per device).Multiple banded devices scale roughly linearly here (T=96, 3 bands: 3 devices 1.14 s, 5 devices 1.56 s).
So on these instances the binaries cost ~1.3–2× the LP solve time (worst observed: ~4×), not orders of magnitude. Two structural reasons:
sum(b) = 1, with power bounded byΣ b·min ≤ p ≤ Σ b·max) is the tight disjunctive formulation: its LP relaxation equals the convex hull of the band union per time step, so the root relaxation is strong. A big-M formulation would be strictly weaker.Adversarial instances (near-flat prices creating many equally good band choices, tight SoC targets interacting with band gaps, several banded devices competing for site capacity) can branch harder than this. Practical levers, should that occur:
derivative min/maxbounds, skipping the MILP entirely.FLEXMEASURES_LP_SOLVER_OPTIONScan passmip_rel_gap(0.1–1% is usually invisible in schedule quality) andtime_limitto HiGHS.Scope
OperationModeSchema+operation-modesfield in the storage flex-model schema (documented via the metadata module, so it shows up in the docs table and OpenAPI specs).device_power_bandsargument ondevice_scheduler, wired throughStorageScheduler._prepare/compute.flexmeasures/data/models/planning/tests/test_operation_modes.py(on/off band, min-power band, and a no-bands sanity check).🤖 Generated with Claude Code