Skip to content

Support intermediate power constraints on groups of devices (flex-model group field)#2276

Merged
Flix6x merged 12 commits into
mainfrom
fix/intermediate-power-constraints
Jul 18, 2026
Merged

Support intermediate power constraints on groups of devices (flex-model group field)#2276
Flix6x merged 12 commits into
mainfrom
fix/intermediate-power-constraints

Conversation

@Flix6x

@Flix6x Flix6x commented Jul 10, 2026

Copy link
Copy Markdown
Member

Description

Adds support for intermediate power constraints: power limits on the aggregate flow of a group of devices, sitting between per-device power capacities and site-level (EMS) constraints. Closes #2092 (a hybrid inverter whose battery + PV children could jointly exceed the inverter's limit) and implements the flex-model-centric design from the discussion #1418 comment. Includes #2277.

Design

Device entries in a multi-device storage flex-model reference a group; the group's own flex-model entry constrains the group's aggregate power:

[
  {"sensor": 1, "power-capacity": "2 kW", "group": {"sensor": 5}},
  {"sensor": 2, "production-capacity": "2 kW", "consumption-capacity": "0 kW", "group": {"sensor": 5}},
  {"sensor": 5, "power-capacity": "2.5 kW"}
]
  • power-capacity on the group entry → hard constraint (both directions).
  • consumption-capacity / production-capacitysoft constraints, enforced via breach commitments with default breach prices (10000 currency/kW; no user-set group prices — consistent with the smart-defaults policy in Smarter commodity-context defaults and Commitment commodity field #2272).
  • Groups can be referenced by sensor ({"sensor": <id>}, shown above) or by asset ({"asset": <id>}). The asset variant is what DB-stored flex-models naturally produce, making the feature fully definable via an asset tree: store partial flex-models on the assets and trigger the parent with an empty flex-model.
  • Saving the group's schedule: sensor-referenced groups save the aggregate to the group power sensor; asset-referenced groups (which have no power sensor) save it via their consumption/production output sensors, following the standard conventions (consumption only → full profile consumption-positive; production only → full profile production-positive on save; both → clip-split into positive parts on each sensor). Either kind may define output sensors.
  • Nested groups are supported (cycles rejected); group members must share a commodity; groups require multi-device flex-models.

Implementation

Built entirely on the existing device-group solver machinery — hard bounds via ems_constraints/ems_constraint_groups (multi-commodity work, #1946) and soft bounds via grouped FlowCommitments (#1934). No optimizer changes.

  • Schema: new group field (sensor or asset reference, exactly one) on StorageFlexModelSchema and DBStorageFlexModelSchema; sensor variant validated to reference a power sensor.
  • MetaStorageScheduler._prepare: group entries split from device entries (validations: dangling refs, device-only fields on group entries, mixed commodities, cycles, single-sensor mode) and per-group hard/soft constraints built mirroring the site-level capacity/breach patterns.
  • UI: group added to the flex-model editor; the editor recommends the parent asset for a child's group field (one-click suggestion) and hints in both the parent's and children's editors when the parent's flex-model defines a power-capacity.
  • Docs: new "Intermediate power constraints" section in the scheduling feature docs, plus a new toy-example tutorial driven entirely by DB-stored flex-models.

Bug fixes along the way

  • Latent device-index misalignment in _prepare: per-device lists were built from the full flex-model list (including stock-only entries) instead of the filtered device models.
  • AssetTriggerSchema.check_flex_model_sensors raised KeyError on asset-only flex-model entries.
  • The freeze_server_now test fixture leaked its server_now monkeypatch into all subsequently run tests in the same process, causing order-dependent duplicate-key failures when scheduling jobs saved beliefs with identical frozen belief times.

Tests

  • Solver-level (test_group_constraints.py, 18 tests): the [FlexMeasures/Scheduler] Investigate Missing Parent-Child Physical Constraint Propagation #2092 inverter case with a control run proving the constraint binds, soft directional bounds + breach commitment costs, output-sensor saving in all three modes, nested/mixed-kind groups, cycle detection, validation errors, and a pure-DB-tree end-to-end test (all flex-models stored as asset attributes, scheduling triggered with an empty flex-model).
  • API-level: end-to-end trigger → job → beliefs on the group sensor equal the sum of member schedules; 422 for a group sensor outside the asset tree.
  • UI tests for the editor hints; schema tests for both reference variants.
  • Full serial regression (planning + schemas + UI + API schedule suites): 537 passed, 3 xfailed. Pre-commit and docs build clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TAad46Ayg86DHpL6nY54sX

Flix6x and others added 2 commits July 10, 2026 08:40
Add a new `group` field to the storage flex-model, letting device entries
reference a power sensor that represents a group of devices (e.g. a hybrid
inverter shared by a battery and PV). The group sensor's own flex-model entry
constrains the group's aggregate power:

- power-capacity: hard constraint (both directions)
- consumption-capacity / production-capacity: soft constraints, enforced via
  breach commitments with default breach prices (10000 <currency>/kW)

The group's scheduled aggregate power is saved to the group sensor. Nested
groups are supported (cycles rejected); groups require multi-device
flex-models and members must share a commodity.

Implemented on top of the existing device-group solver machinery
(ems_constraint_groups from the multi-commodity work, and grouped
FlowCommitments from PR #1934); no changes to the optimizer itself.

Also fixes a latent device-index misalignment in MetaStorageScheduler._prepare,
where per-device lists were built from the full flex-model list (including
stock-only entries) instead of the filtered device models.

Closes #2092

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAad46Ayg86DHpL6nY54sX
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAad46Ayg86DHpL6nY54sX
Flix6x and others added 4 commits July 10, 2026 10:29
Add a new `group` field to the storage flex-model, letting device entries
reference a power sensor that represents a group of devices (e.g. a hybrid
inverter shared by a battery and PV). The group sensor's own flex-model entry
constrains the group's aggregate power:

- power-capacity: hard constraint (both directions)
- consumption-capacity / production-capacity: soft constraints, enforced via
  breach commitments with default breach prices (10000 <currency>/kW)

The group's scheduled aggregate power is saved to the group sensor. Nested
groups are supported (cycles rejected); groups require multi-device
flex-models and members must share a commodity.

Implemented on top of the existing device-group solver machinery
(ems_constraint_groups from the multi-commodity work, and grouped
FlowCommitments from PR #1934); no changes to the optimizer itself.

Also fixes a latent device-index misalignment in MetaStorageScheduler._prepare,
where per-device lists were built from the full flex-model list (including
stock-only entries) instead of the filtered device models.

Closes #2092

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAad46Ayg86DHpL6nY54sX
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAad46Ayg86DHpL6nY54sX
Signed-off-by: F.N. Claessen <claessen@seita.nl>
The UI-DB flex-model schema parity test requires every DBStorageFlexModelSchema
field to have UI support.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAad46Ayg86DHpL6nY54sX
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Flix6x and others added 2 commits July 10, 2026 12:22
… asset tree (#2277)

Extend the storage flex-model's group field to also accept {"asset": <id>},
so a group can be identified by an asset-keyed flex-model entry - the form
that DB-stored flex-models naturally produce. This makes intermediate power
constraints fully definable via an asset tree with stored flex-models, with
an empty flex-model in the scheduling trigger.

- Asset-referenced group entries define no power sensor of their own; the
  group's scheduled aggregate power is saved via the entry's consumption
  and/or production output sensors, following the usual output-sensor
  conventions (full profile on a single sensor; clip-split when both given).
- Sensor-referenced groups keep saving the aggregate to the group power
  sensor, and may now also define output sensors.
- The UI asset flex-model editor recommends the parent asset when editing a
  child's group field (one-click suggestion), and hints in both the parent's
  and children's editors when the parent's flex-model defines power-capacity.
- New tutorial: toy example for intermediate power constraints, driven
  entirely by DB-stored flex-models.

Bug fixes uncovered along the way:
- AssetTriggerSchema.check_flex_model_sensors raised KeyError on asset-only
  flex-model entries.
- The freeze_server_now test fixture leaked its server_now monkeypatch into
  all subsequently run tests in the same process, causing order-dependent
  duplicate-key failures when scheduling jobs saved beliefs with identical
  (frozen) belief times.


Claude-Session: https://claude.ai/code/session_01TAad46Ayg86DHpL6nY54sX

Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Remove references to code modules, tests and GitHub issues from the tutorial;
tutorials address FlexMeasures users, not developers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAad46Ayg86DHpL6nY54sX
Signed-off-by: F.N. Claessen <claessen@seita.nl>
@Flix6x Flix6x changed the title Support intermediate power constraints on groups of devices Support intermediate power constraints on groups of devices (flex-model group field) Jul 10, 2026
Adapt the group feature to the typed DeviceInventory introduced in PR #2321:

- Classify group entries in the inventory (new DeviceRole.GROUP), as the
  first classification step, so a group entry's own power sensor or output
  sensors cannot make it pass for a device.
- Replace the scheduler's ad-hoc _group_models/_group_to_devices derivation
  with the inventory's group_entries field and group_to_devices resolution
  (transitive leaf-device resolution with cycle detection).
- Drop this branch's own copy of the stock-only misalignment fix
  (device_sensor_id_to_index and the device_models zip fix), which main
  now solves by construction via the inventory's canonical device indices.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015qxM7UZ5wHTz3ftz1Mf9yy
Signed-off-by: F.N. Claessen <felix@seita.nl>
Comment thread flexmeasures/ui/static/openapi-specs.json Outdated
Reconcile the group-field intermediate power constraints with main's
DeviceInventory/DeviceRegistry work (#2321/#2325): combine the
devices.py imports (group_key_label + _resolve_stock_key) and keep both
sides' new DeviceInventory tests. Regenerate openapi-specs.json (version
0.33.2 -> 1.0.0), addressing the review comment on the stale spec.

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 18, 2026

Copy link
Copy Markdown
Member Author

Brought in line with origin/main, reconciling the group-field intermediate power constraints with the DeviceInventory/DeviceRegistry work (#2321/#2325): combined the devices.py imports (group_key_label + _resolve_stock_key) and kept both sides’ new DeviceInventory tests. All green locally — 18 test_device_inventory + 18 test_group_constraints + the factory/shared-stock scheduler tests.

Re the OpenAPI spec: regenerated it, so the version is back to 1.0.0 (it was a stale 0.33.2 from the branch) — that addresses the openapi-specs.json:10 comment.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for intermediate power constraints by introducing a group field in storage flex-model entries, enabling hard/soft aggregate power limits over groups of devices (e.g., hybrid inverter bottlenecks) without changing the optimizer, and wiring the feature through schema validation, scheduling logic, UI hints/editor behavior, tests, and documentation.

Changes:

  • Add group (sensor- or asset-referenced) to storage flex-model schemas and OpenAPI/metadata, including validation logic.
  • Extend the StorageScheduler pipeline to classify group entries, enforce group hard/soft constraints, and save group aggregate schedules (including via output sensors for asset-referenced groups).
  • Add UI hints + extensive test coverage and documentation/tutorial updates for DB-driven asset-tree configurations.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
flexmeasures/ui/views/assets/views.py Passes parent/child + power-capacity presence flags to the asset properties template for UI hints
flexmeasures/ui/tests/test_asset_crud.py Adds UI test coverage for the new group-field hint behavior
flexmeasures/ui/templates/assets/asset_properties.html Adds hint banners and editor-side “use parent as group” behavior in the flex-model editor
flexmeasures/ui/static/openapi-specs.json Updates generated OpenAPI specs to document the new group field/schema
flexmeasures/data/schemas/tests/test_scheduling.py Adds schema tests for group sensor/asset variants and power-sensor validation
flexmeasures/data/schemas/scheduling/storage.py Introduces GroupReferenceSchema and validates group references (power-sensor check, exactly-one-of sensor/asset)
flexmeasures/data/schemas/scheduling/metadata.py Adds GROUP metadata used for docs/OpenAPI/UI schema rendering
flexmeasures/data/schemas/scheduling/init.py Adds UI schema-spec entry for group and fixes sensor-checking for asset-only flex-model entries
flexmeasures/data/models/planning/tests/test_group_constraints.py New planning-level test module covering hard/soft group constraints, nesting, output saving, and DB-tree E2E
flexmeasures/data/models/planning/tests/test_device_inventory.py Adds tests for classifying group entries, resolving leaf membership, and cycle detection
flexmeasures/data/models/planning/storage.py Implements group constraint enforcement and group schedule output handling in the scheduler
flexmeasures/data/models/planning/devices.py Extends DeviceInventory to classify group entries and resolve transitive group membership
flexmeasures/conftest.py Fixes freeze_server_now fixture leakage by restoring patched imports and server_now functions
flexmeasures/api/v3_0/tests/test_asset_schedules_fresh_db.py Adds API-level fresh-db tests for group constraint enforcement and tree membership validation
documentation/tut/toy-example-group-constraints.rst New tutorial showing a fully DB-driven asset-tree setup using group constraints
documentation/index.rst Adds the new tutorial to the documentation index
documentation/features/scheduling.rst Documents group field semantics and intermediate power constraints
documentation/concepts/commitments.rst Updates commitments docs to reflect current group-related capability and roadmap
documentation/changelog.rst Adds changelog entries for intermediate group constraints and asset-referenced groups
documentation/api/change_log.rst Adds API changelog entries describing the new group field behavior

Comment thread flexmeasures/ui/templates/assets/asset_properties.html
Comment thread flexmeasures/data/schemas/scheduling/storage.py
Comment thread flexmeasures/data/schemas/scheduling/storage.py Outdated
- asset_properties.html: HTML-escape the parent asset name before it is
  inserted into innerHTML in the group hint (it is user-controlled -> XSS).
- storage.py: attach the "group must reference a power sensor" ValidationError
  to the `group` field, not `sensor`.
- GroupReferenceSchema now inherits from SharedSensorReferenceSchema instead of
  SensorReferenceSchema, so it accepts only `sensor`/`asset` (a group is a
  device-group identifier, not a belief-query reference) and no longer exposes
  the source-* filter fields; regenerate openapi-specs.json accordingly.

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 18, 2026

Copy link
Copy Markdown
Member Author

Addressed the three review findings (commit 7686b39):

  1. XSS (asset_properties.html) — the parent asset name is user-controlled and was interpolated into innerHTML; now HTML-escaped (via a textContent DOM helper) before insertion in the group hint and button label.
  2. Error path (storage.py) — the "group must reference a power sensor" ValidationError is now attached to group instead of sensor.
  3. GroupReferenceSchema base — now inherits from SharedSensorReferenceSchema instead of SensorReferenceSchema, so it accepts only sensor/asset and no longer carries the source-* belief-query filters or the "variable quantity" description. Regenerated openapi-specs.json to match.

All green locally: 130 tests across test_group_constraints + schema test_scheduling, plus flake8/black/mypy and the OpenAPI-generation hook.

Comment thread flexmeasures/data/models/planning/storage.py Outdated
Reword _add_group_schedules' docstring: the scheduling model assembles and
returns each group's aggregate power schedule; persistence is the data
service's responsibility.

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 merged commit 0b12bad into main Jul 18, 2026
12 of 13 checks passed
@Flix6x
Flix6x deleted the fix/intermediate-power-constraints branch July 18, 2026 22:04
Flix6x added a commit that referenced this pull request Jul 19, 2026
Resolve conflicts from PR #2276 (intermediate power constraints via
flex-model 'group' field) against the CHP converter-coupling work.

All conflicts were additive (both sides added distinct helpers/methods/
docstring notes in the same regions); kept both sides:
- devices.py: DeviceRole docstring (converter ports + GROUP), the
  _is_zero_capacity/_resolve_coupling_coefficient helpers alongside the
  group-key helpers, and coupling_groups alongside group_to_devices.
- storage.py: coupling_groups wiring alongside group entry classification
  and validation.

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 added a commit that referenced this pull request Jul 19, 2026
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>
Flix6x added a commit that referenced this pull request Jul 19, 2026
…on-mode-fixed-cost

Pull in the base branch's merge with main (group-based intermediate power
constraints from PR #2276). Only the changelog conflicted (co-located
additions); both entries kept. Fixed-cost operation-mode tests stay green.

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 added a commit that referenced this pull request Jul 19, 2026
feat/chp now carries origin/main's PR #2276 (intermediate power
constraints via the flex-model 'group' field).

Single conflict in storage.py, additive: kept both the balance_groups
initialization (internal-commodity-balance) and the group-entry
classification/validation block (from the feat/chp main-merge).

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 added a commit that referenced this pull request Jul 19, 2026
Reconcile sensor-scoped commitments with main's group field (#2276) on the
DeviceInventory/DeviceRegistry rewrite. CommitmentSchema keeps both the new
`sensors` scoping field and main's `commodity` field; convert_to_commitments
keeps the scoped-aggregate branch (via DeviceInventory.by_sensor_id) alongside
main's per-commodity `bound_device_count` guard. Re-append the scoped-commitment
test onto main's version of test_commitments.py (git had interleaved it with
_shared_stock_scheduler on their shared StorageScheduler(...) kwargs). Regenerate
openapi-specs.json.

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>
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.

[FlexMeasures/Scheduler] Investigate Missing Parent-Child Physical Constraint Propagation

2 participants