Skip to content

Fix device misalignment when flex-model entries are filtered from multi-device schedules#2319

Closed
Flix6x wants to merge 2 commits into
mainfrom
fix/multi-device-flex-model-alignment
Closed

Fix device misalignment when flex-model entries are filtered from multi-device schedules#2319
Flix6x wants to merge 2 commits into
mainfrom
fix/multi-device-flex-model-alignment

Conversation

@Flix6x

@Flix6x Flix6x commented Jul 14, 2026

Copy link
Copy Markdown
Member

Summary

Fixes two confirmed bugs in StorageScheduler multi-device scheduling that surface whenever a flex-model entry is filtered from the device list. Both were reproduced in a community simulation with a heat pump and a child battery: the heat pump inherited the battery's ±2.5 kW power capacity, and the battery was never scheduled.

Bug 1 — misaligned zip in _prepare() (zip truncation since #1946 × #1429)

device_models is a filtered subset of self.flex_model (stock-only entries are dropped). When building the per-device asset list, the code zipped sensors (built from device_models) against the unfiltered self.flex_model. Whenever anything was filtered out, this misassigned assets. Combined with _get_device_power_capacity's asset.flex_model.get("power-capacity") fallback, a device could inherit another device's power capacity. The latent assumption dates to #1429; #1946 broke it by adding the stock-only filtering.

Fix: zip against device_models (the same list sensors is built from).

Bug 2 — devices with only a nested output reference silently dropped

A flex-model entry that references its power sensor only via a nested "consumption": {"sensor": N} (or "production") and has a "state-of-charge" sensor ends up with sensor = None after deserialization. It is then classified as stock-only and removed from device_models — the device is never scheduled and its consumption/production output sensor receives zero beliefs.

Fix: in _deserialize_flex_model (list branch), when a device entry has no top-level "sensor", resolve it from the consumption/production output reference. Genuine stock-only entries (no sensor and no output reference) are still filtered.

Test

Adds test_multi_device_flex_model_alignment, which schedules two devices under a common parent:

  • Device A: normal storage with a top-level sensor and a small (1 kW) capacity taken from its asset flex-model.
  • Device B (listed first): no top-level sensor, power sensor referenced only via consumption, its own state-of-charge, and a larger (9 kW) capacity.

Asserts (i) device A respects its own 1 kW capacity (fails before Bug 1 fix — A charged 4 kW) and (ii) device B's consumption output sensor receives a schedule (fails before Bug 2 fix). Verified to fail on unfixed code and pass with the fixes. Existing test_storage.py output-sensor tests still pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LQFkeGxDwErydUV5ZmHmki

Flix6x and others added 2 commits July 14, 2026 18:52
…ti-device schedules

Two bugs in StorageScheduler multi-device scheduling:

1. _prepare() zipped the (filtered) device sensors against the unfiltered
   self.flex_model when building the per-device asset list, misassigning
   assets whenever a stock-only entry was filtered out. This let a device
   inherit another device's power capacity via the asset.flex_model
   power-capacity fallback. Zip against device_models instead.

2. _deserialize_flex_model() left the top-level "sensor" as None for devices
   that reference their power sensor only through a nested "consumption" (or
   "production") output reference. Such devices were misclassified as
   stock-only and silently dropped, so their output sensor received no
   schedule. Resolve the power sensor from the output reference.

Adds an end-to-end regression test covering both bugs.

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

Copy link
Copy Markdown

Documentation build overview

📚 flexmeasures | 🛠️ Build #33586395 | 📁 Comparing 62e0ae2 against latest (c35554a)

  🔍 Preview build  

2 files changed
± changelog.html
± api/v3_0.html

Flix6x added a commit that referenced this pull request Jul 15, 2026
…2321)

* test: lock multi-device flex-model alignment (currently broken)

Add strict-xfail regression tests for four confirmed device-alignment bugs
in multi-device storage scheduling:

- A stock-only entry listed before device entries shifts every device's
  properties (power capacity, efficiencies) by one, because per-device
  properties are read from the unfiltered flex-model list.
- A device without a state-of-charge sensor silently loses its own SoC
  parameters when a stock-only entry is present, because the synthetic
  stock keys diverge between _prepare and _build_stock_groups.
- Flex-context commitments are assigned device indices by enumerating the
  unfiltered flex-model list, so stock-only entries receive commitments
  meant for devices.
- A device referencing its power sensor only via a nested consumption/
  production output reference is misclassified as stock-only and dropped
  (carried over from PR #2319, which this branch subsumes).

The xfail markers are removed in the commits that fix each bug.

Signed-off-by: F.N. Claessen <felix@seita.nl>

* feat: add a typed device inventory for schedulers

Introduce planning/devices.py: every flex-model entry is classified exactly
once into a DeviceInventory, which becomes the single source of truth for

- entry roles (device / stock-only, extensible to group entries and
  converter ports),
- canonical solver device indices (flexible devices first, then top-level
  inflexible devices, then per-commodity-context inflexible devices),
- stock groups and their SoC parameters (sharing one key space, so
  parameters can no longer be lost to diverging synthetic keys), and
- sensor-id and commodity lookups.

Power sensors are resolved from the top-level sensor field or, failing
that, from a nested consumption/production output reference, so such
devices are no longer misclassified as stock-only entries.

The raw deserialized flex-model dicts are kept as-is on each FlexDevice,
so downstream code and new flex-model fields need no dataclass changes.

Not yet wired into the StorageScheduler; see subsequent commits.

Signed-off-by: F.N. Claessen <felix@seita.nl>

* test: give the alignment regression tests their own parent site

Scheduling on the shared module-scoped building fixture is fragile: earlier
tests create sensors on it whose legacy attributes (capacity_in_mw,
max_soc_in_mwh, ...) get moved into the building's flex-model, which
collect_flex_config then injects into the test's flex-model as an extra
asset-only device entry, changing the optimization problem.

Each alignment test now schedules on its own fresh parent site.

Signed-off-by: F.N. Claessen <felix@seita.nl>

* fix: track devices via the inventory in the StorageScheduler

Build the DeviceInventory once after flex-config deserialization (with a
lazy rebuild for tests that bypass deserialization), and replace
_prepare()'s ad-hoc entry classification with it. The local flex_model
variable now holds (copies of) the classified device flex-models instead
of reverting to the unfiltered flex-model list.

This fixes, by construction:

- Devices no longer inherit a neighboring entry's properties (power
  capacity, efficiencies, output sensors) when a stock-only entry
  precedes them in the flex-model list: all per-device property lists,
  including the sensors/assets pair, now derive from the same filtered,
  index-aligned device list.
- Devices without a state-of-charge sensor no longer lose their own SoC
  parameters when a stock-only entry is present (the synthetic stock keys
  of stock parameters and stock groups now come from one counter).
- Flex-context commitments now bind the scheduled devices instead of
  enumerating the unfiltered flex-model list.
- Devices referencing their power sensor only via a nested consumption/
  production output reference are scheduled (previously misclassified as
  stock-only and silently dropped) — subsumes PR #2319.
- The dead (and misaligned) stock-groups build in _deserialize_flex_model
  and the duplicated stock-only detection are gone.

The strict-xfail alignment regression tests now pass; their markers are
removed. _prepare()'s return value and device_scheduler's interface are
unchanged.

Signed-off-by: F.N. Claessen <felix@seita.nl>

* refactor: one source of truth for the commodity device enumeration

Both _prepare() and the compute()-side result mapping now take the
commodity -> device indices mapping (flexible devices, then top-level
inflexible devices, then per-commodity-context inflexible devices) from
the device inventory, instead of maintaining two mirrored ~40-line
enumerations that had to stay in sync byte for byte. The device
constraints for inflexible devices are likewise sized and filled from the
inventory's canonical order.

_reconstruct_commodity_to_devices keeps a fallback for bare schedulers
(e.g. in tests) that classifies the stored device models on the fly.

Signed-off-by: F.N. Claessen <felix@seita.nl>

* fix: a state of charge given as a value or time series forms its own stock

Only a sensor reference can link devices into a shared stock. A
state-of-charge field holding timed values (a list of specs) is not a
stock identity: classify such devices under their own synthetic stock
key, keeping their SoC parameters.

On main, this shape crashed _prepare (unhashable stock_models key) if it
carried SoC parameters; it only went unnoticed because such flex-models
were exercised up to deserialization, never scheduling.

Signed-off-by: F.N. Claessen <felix@seita.nl>

* fix: tolerate unflushed assets when collecting the flex config

Two latent order-dependence bugs, surfaced by running the planning test
modules in a new order:

- GenericAsset.get_flex_context crashed (TypeError) when an ancestor's
  flex_context was still None, e.g. a pending asset created without one,
  before its column default is applied on flush.
- The charging station test fixture never flushed, so its assets had no
  IDs unless an earlier test happened to trigger a flush; scheduler
  config collection then silently failed to match the flex-model to its
  asset. The affected test (test_deserialize_storage_soc_at_start_from_
  state_of_charge_time_series) failed when run in isolation.

Both are now fixed; the test passes in any order.

Signed-off-by: F.N. Claessen <felix@seita.nl>

* docs: changelog entries and _build_stock_groups deprecation note

Signed-off-by: F.N. Claessen <felix@seita.nl>

* docs: shorten the changelog reference

The fixed alignment bugs only affect flex-model shapes introduced during
v1 development (entries without a top-level sensor were rejected outright
in v0.33.1), so no released behavior regressed: reference the PR from the
multiple-feeders feature entry instead of a separate bugfix entry.

Signed-off-by: F.N. Claessen <felix@seita.nl>

* fix: fail fast on conflicting stock parameters (review)

Address review comments on PR #2321:

- Raise a ValidationError when more than one flex-model entry defines
  state-of-charge parameters for the same stock, instead of silently
  letting the last entry win (stricter than the historical behavior).
- Reflow attribute comments to natural clause breaks, clarify that the
  flex_model None check is a precaution for inflexible devices, and note
  why inflexible devices always have a power sensor.
- Word choice: scheduling features (not code sites) re-derived entry
  kinds; pass the sensor name as a keyword argument in the test helper.

Signed-off-by: F.N. Claessen <felix@seita.nl>

---------

Signed-off-by: F.N. Claessen <felix@seita.nl>
@Flix6x

Flix6x commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

Subsumed by #2321.

@Flix6x Flix6x closed this Jul 15, 2026
@Flix6x Flix6x self-assigned this Jul 15, 2026
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