Skip to content

Commitments can be scoped to specific sensors, binding their aggregate flow - #2295

Open
Flix6x wants to merge 9 commits into
mainfrom
feat/sensor-scoped-commitments
Open

Commitments can be scoped to specific sensors, binding their aggregate flow#2295
Flix6x wants to merge 9 commits into
mainfrom
feat/sensor-scoped-commitments

Conversation

@Flix6x

@Flix6x Flix6x commented Jul 11, 2026

Copy link
Copy Markdown
Member

Description

A flex-context commitment gains an optional sensors field: instead of binding each device of the matching commodity separately, the commitment binds the aggregate flow of the devices whose power sensors are listed, as one grouped commitment (using the existing device_group machinery of FlowCommitment).

Example — reserve an upward-regulation band on a site's e-heaters (their combined consumption must stay at/above the band, with downward deviation penalized):

{
  "commitments": [
    {
      "name": "reserved band",
      "sensors": [123, 124],
      "baseline": "10 MW",
      "down-price": "-10000 EUR/MWh"
    }
  ]
}

If none of the listed sensors appear in the flex-model, a warning is logged and the commitment binds nothing (rather than failing the whole schedule).

Look & Feel

New unit test test_sensor_scoped_commitment_binds_aggregate_of_selected_devices: two 8 MW heaters that would otherwise stay off (they only cost money) are held at a combined 10 MW — a level neither device could carry alone — while the rest of the planning problem is unaffected.

How to test

pytest flexmeasures/data/models/planning/tests/test_commitments.py -k scoped

🤖 Generated with Claude Code

https://claude.ai/code/session_016MLCUiSdXDqDBmg8GbYp1B

…gregate flow

A flex-context commitment gains an optional 'sensors' field: instead of
binding each device of the matching commodity separately, the commitment
binds the aggregate flow of the devices whose power sensors are listed,
as one grouped commitment (device_group machinery). Useful to commit a
band on a subset of devices, e.g. an aFRR upward-regulation band on a
site's e-heaters (aggregate consumption >= band, deviation penalized).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016MLCUiSdXDqDBmg8GbYp1B

@Flix6x Flix6x left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I only wish to clear up some documentation. And one clarification request.

The test is correct, but would be slightly stronger if the statement about 0 consumption being preferred without the extra commitment would be proven.

commitment_commodity = commitment_spec.get("commodity", "electricity")

# A commitment scoped to specific sensors binds the *aggregate* flow
# of those devices as one commitment, rather than each device separately.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Is the regular commitment not aggregating over all devices (of one grid-connected commodity), rather than applied to each device individually?

"""
heater_type = get_or_create_model(GenericAssetType, name="e-heater")
site = GenericAsset(
name="Band site (scoped commitment test)", generic_asset_type=heater_type

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Explain the term "band" in the docstring.

@Flix6x

Flix6x commented Jul 12, 2026

Copy link
Copy Markdown
Member Author

I think this would also close #2179, as this essentially adds support for an activation price per device, if you model the commitment with a zero baseline, a positive upwards deviation price and a negative downward deviation price. But if that is the case, then I suggest an additional test (making sure that the problem remains convex) and extending the documentation accordingly.

Flix6x and others added 7 commits July 16, 2026 09:19
Adapt the sensor-scoped commitment branch to the typed DeviceInventory
introduced in PR #2321: resolve the scoped devices' canonical solver
indices via inventory.by_sensor_id() instead of re-enumerating the raw
flex-model entry list.

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>
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>
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>
…mmitments

Resolves conflicts in CommitmentSchema (keep the new `sensors` field alongside
main's `name` length validation and updated commodity comment) and
test_commitments.py (keep both new tests).

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

Integrates #2358 (now on main), whose CommitmentSchema/scheduling changes
auto-merge cleanly with the sensor-scoped commitment work; only changelog.rst
needed resolving (keep both entries).

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

Brings in #2374 (inflexible devices as flex-model assets); clean auto-merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TaMepbxRYzJxQKFtu6Hq4p
Signed-off-by: F.N. Claessen <felix@seita.nl>
Follow-up on #2295. A commitment's scope can now be given as a 'group' reference
(the members of an electrical group, reusing group_to_devices) in addition to the
raw 'sensors' list; at most one of the two (schema-validated). A group scope
includes the group's inflexible members (total node flow), whereas a sensors
scope binds the listed flexible devices only -- documented on the schema field and
the resolver. Also: validate that a scoped set shares one commodity, tag scoped
commitments with provenance='custom' (was missing), and give each a unique
device_group label so same-named commitments never merge.

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

Flix6x commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Pushed the recommended follow-ups (on top of a merge with current main, which includes #2374):

  • Optional group scope on a commitment, alongside the existing sensors list — at most one of the two (schema-validated). A group reference reuses the group's resolved membership (group_to_devices), so you don't restate the member sensors; a sensors list stays the general cherry-pick form that can span electrical groups. No multi-group membership is introduced — the two are different relationships (topology vs. market selection).
  • Inflexible-inclusion semantics documented: a sensors scope binds the listed flexible devices only (by_sensor_id), while a group scope includes the group's inflexible members (the node's total flow). Spelled out on the schema field and _resolve_commitment_scope.
  • Commodity-share validation: a scoped set spanning more than one commodity is rejected (a commitment binds the aggregate flow of a single commodity).
  • Robustness: scoped commitments now carry provenance='custom' (was missing, so they weren't tagged for cost reporting), and each gets a unique device_group label so two same-named scoped commitments can't merge.

Tests: group-scope binding (end-to-end), the sensors/group mutual-exclusion, plus the existing 22 commitment tests still green. Changelog updated.

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

Adds optional scoping for flex-context commitments so they can bind the aggregate flow of a selected subset of devices (via a sensors list or a group reference), reusing existing FlowCommitment.device_group aggregation behavior in the planning solver.

Changes:

  • Extend the scheduling Commitment schema and OpenAPI to accept sensors and group as optional, mutually exclusive commitment scope selectors.
  • Update StorageScheduler.convert_to_commitments to build a single aggregate-flow FlowCommitment when a scope is provided, with warning-and-skip behavior when the scope matches no devices.
  • Add tests covering sensor-scoped and group-scoped aggregate-flow commitments, plus schema validation for mutual exclusivity.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
flexmeasures/ui/static/openapi-specs.json Documents new sensors / group commitment fields and (re)positions GroupReference schema.
flexmeasures/data/schemas/scheduling/init.py Adds sensors and group fields to CommitmentSchema and validates mutual exclusivity.
flexmeasures/data/models/planning/tests/test_commitments.py Adds unit tests asserting aggregate binding behavior for sensor- and group-scoped commitments.
flexmeasures/data/models/planning/storage.py Implements scoped commitment resolution and construction of aggregate-flow FlowCommitments.
documentation/changelog.rst Adds a main changelog entry describing the new scoped-commitment capability.

Comment on lines +1604 to +1612
commodities = {
self.device_inventory.by_index(d).commodity for d in scoped_devices
}
if len(commodities) > 1:
raise ValueError(
f"Commitment '{commitment_spec.get('name')}' is scoped to devices of"
f" more than one commodity ({sorted(commodities)}); a commitment binds"
" the aggregate flow of a single commodity."
)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in c35820a: _build_scoped_commitment now pins the commitment's commodity to the (single) commodity of its scoped devices, overriding the schema's electricity default, so cost attribution matches the scope. Added test_scoped_commitment_pins_commodity_to_scoped_devices.

Comment on lines +2089 to +2093
"""A commitment scoped to specific sensors (here: two e-heaters) binds their
aggregate flow as one commitment: a baseline of 10 MW with a steep penalty on
downward deviation keeps their combined consumption at 10 MW even though a
cheaper allocation (0 MW) exists, while an unscoped battery stays unaffected.
"""

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed: removed the stray "unscoped battery" clause — that test only models the two heaters.

Comment on lines +1597 to +1601
if not scoped_devices:
current_app.logger.warning(
f"Commitment '{commitment_spec.get('name')}' is scoped to"
f" {scope_description}, none of which appear in the flex-model."
" This commitment will not bind any device."

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added test_scoped_commitment_with_no_matching_devices_warns_and_binds_nothing, which asserts the warning is logged and the scheduler binds nothing (returns no commitment) rather than failing.

Comment on lines +79 to +83
# Optional scoping: bind this commitment to the aggregate flow of a subset of
# devices, rather than binding each device of the commodity separately. Give either
# a list of power `sensors` (a cherry-pick that may span electrical groups, e.g. an
# aFRR band on a site's e-heaters) or a `group` reference (the members of an
# electrical group). At most one of the two.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reflowed the comment to break only after punctuation, per the repo convention.

… + comment tidy

Addresses Copilot's review on #2295:
- A scoped commitment now takes its commodity from the (single) commodity of its
  scoped devices, overriding the schema's electricity default, so its cost is not
  misattributed to the wrong commodity.
- Add tests for the commodity pinning and for the 'scope matches no device -> warn
  and bind nothing' path.
- Fix the sensor-scoped test docstring (it mentioned a battery the test never had).
- Reflow the CommitmentSchema scoping comment to break only after punctuation.

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

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

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (6)

flexmeasures/data/models/planning/tests/test_commitments.py:2201

  • This new test docstring breaks lines mid-sentence (e.g. after "that"), which goes against the repo convention to only break lines after punctuation (see .github/instructions/docstrings.instructions.md:38). Please rewrap so each line ends at punctuation.
    """A commitment scoped to a ``group`` reference binds the aggregate flow of that
    group's members, reusing the group's resolved membership -- the same band effect as
    listing the members' sensors, but pointing at the group instead."""

flexmeasures/data/models/planning/tests/test_commitments.py:2276

  • This new test docstring introduces mid-sentence line breaks, which conflicts with the repo's docstring/comment wrapping convention (line breaks only after punctuation; see .github/instructions/docstrings.instructions.md:38). Rewrap the docstring so each line ends with punctuation.
    """A scoped commitment's commodity follows its scoped devices, overriding the
    schema's electricity default -- so its cost is attributed to the right commodity."""

flexmeasures/data/models/planning/tests/test_commitments.py:2323

  • This new test docstring breaks a sentence across lines without punctuation at the line break, which conflicts with the repo convention (see .github/instructions/docstrings.instructions.md:38). Rewrap so each line break happens after punctuation.
    """A scope that matches no device in the flex-model logs a warning and binds
    nothing, rather than failing the whole schedule."""

flexmeasures/data/models/planning/storage.py:1698

  • The new scoped-commitment comment block introduces mid-sentence line breaks, which conflicts with the repo convention to only break lines after punctuation (see .github/instructions/docstrings.instructions.md:38). Rewrapping improves searchability and keeps diffs stable.
            # A commitment scoped to a subset of devices binds the *aggregate* flow of
            # those devices as one commitment, rather than each device separately. The
            # scope is given either as a raw list of power `sensors` (a cherry-pick that
            # may span electrical groups, e.g. an aFRR band on a site's e-heaters) or as
            # a `group` reference (the members of an electrical group, reusing its

flexmeasures/data/models/planning/storage.py:1559

  • This docstring paragraph has several mid-sentence line breaks (e.g. after "the"), which goes against the repo's docstring/comment wrapping convention (line breaks only after punctuation; see .github/instructions/docstrings.instructions.md:38). Rewrap these sentences so each line ends with punctuation.

This issue also appears on line 1694 of the same file.

        A ``group`` scope yields the group's (leaf) members, which -- unlike the
        by-sensor scope -- include any inflexible members, so the aggregate covers the
        node's total flow (fixed load included). A ``sensors`` scope yields the listed
        *flexible* devices (``by_sensor_id`` returns schedulable devices only, so
        inflexible devices are not bound by a sensor scope). Canonical indices always

flexmeasures/data/models/planning/tests/test_commitments.py:2093

  • This new test docstring uses mid-sentence line breaks, which conflicts with the repo convention to only break lines after punctuation (see .github/instructions/docstrings.instructions.md:38). Rewrapping into full sentences per line will make future diffs and text search cleaner.

This issue also appears in the following locations of the same file:

  • line 2199
  • line 2275
  • line 2322
    """A commitment scoped to specific sensors (here: two e-heaters) binds their
    aggregate flow as one commitment: a baseline of 10 MW with a steep penalty on
    downward deviation keeps their combined consumption at 10 MW even though a
    cheaper allocation (0 MW) exists.
    """

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.

2 participants