Fix scheduling jobs failing on a timed_belief UniqueViolation for devices with nested-output-only power sensors - #2360
Merged
Conversation
… output sensor Since PR #2321, a device that references its power sensor only via a nested output reference (e.g. {"consumption": {"sensor": N}}) resolves its power sensor to that output sensor, so it is recognized as a schedulable device. In StorageScheduler.compute with return_multiple, that sensor then appeared both as a "storage_schedule" output and as a "consumption_schedule" output. Both were saved with identical belief coordinates in the same scheduling-job transaction, guaranteeing a timed_belief primary-key violation (UniqueViolation) that deterministically failed the whole job. Now only the consumption/production output entry is emitted for such a sensor - the semantically correct one, whose sign conventions are defined for output sensors (see _build_consumption_production_schedules; the scheduling service resolves the saving sign via the consumption_is_positive attribute). As a safety net, all outputs pass through _deduplicate_outputs before returning, so no sensor can ever appear in more than one output. The fallback scheduler's return_multiple output now also iterates over the deduplicated schedule dict instead of the raw per-device sensor list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WtuVTVfL4fQ9QSqbLXmAGD 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_01WtuVTVfL4fQ9QSqbLXmAGD Signed-off-by: F.N. Claessen <claessen@seita.nl>
Documentation build overview
4 files changed± changelog.html± genindex.html± _autosummary/flexmeasures.data.models.planning.storage.html± api/v3_0.html |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a deterministic timed_belief primary-key (UniqueViolation) failure in asset-level (return_multiple=True) scheduling when a device’s power sensor is only referenced via a nested consumption/production output sensor, which previously caused the same sensor to be emitted (and saved) twice in one scheduling job.
Changes:
- Prevent emitting a
storage_scheduleoutput when the same sensor is already present as a declared consumption/production output sensor. - Add a defensive
_deduplicate_outputssafety net so a sensor cannot appear in more than one returned scheduler output (duplicates are dropped with a warning). - Add/adjust regression tests to ensure nested-output-only devices schedule and persist successfully without duplicated outputs.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| flexmeasures/data/tests/test_scheduling_jobs.py | Adds a regression test reproducing the nested-output-only sensor duplication and asserting successful job completion and single belief series persistence |
| flexmeasures/data/models/planning/tests/test_solver.py | Updates an existing solver regression test to assert the schedule is exposed under consumption_schedule (no longer duplicated under storage_schedule) |
| flexmeasures/data/models/planning/storage.py | Implements targeted suppression of duplicate storage outputs plus a general output de-duplication safeguard; also avoids duplicate emission in fallback scheduler output assembly |
| documentation/changelog.rst | Adds a bugfix changelog entry describing the resolved deterministic scheduling failure |
Flix6x
added a commit
that referenced
this pull request
Jul 27, 2026
…duling
An asset-level scheduling trigger whose flex-model list contains a single
device entry without a top-level "sensor" key (e.g. a battery referencing
its power sensor only via a nested output reference, like
{"consumption": {"sensor": N}, "state-of-charge": {"sensor": M}})
crashed before scheduling with:
AttributeError: 'NoneType' object has no attribute 'generic_asset'
collect_flex_config unwrapped any single-entry flex-model list without a
top-level "sensor" key into a bare dict, which downstream is deserialized
in single-sensor mode - resolving the device against self.sensor, which is
None for asset-level scheduling. The same entry in a multi-entry list
scheduled fine, because multi-device (asset) mode resolves nested output
sensor references (see _resolve_power_sensor).
Only unwrap when scheduling a specific sensor (self.sensor is not None);
for asset-level scheduling, keep the list form (multi-device mode) even
for a single device entry. As a fail-safe, single-sensor (dict) flex-model
deserialization without a sensor now raises a clear ValueError naming the
flex-model entry, so the AttributeError cannot recur through any code path.
Discovered during PR #2360 (whose regression test is multi-device for
exactly this reason).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtuVTVfL4fQ9QSqbLXmAGD
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Flix6x
commented
Jul 27, 2026
The duplicate-output failure is an unreleased regression of the device inventory work (PR #2321), so it does not merit a standalone bugfix entry; append the PR reference there instead, as suggested in review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WtuVTVfL4fQ9QSqbLXmAGD Signed-off-by: F.N. Claessen <claessen@seita.nl>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
A device whose flex-model entry declares its power sensor ONLY via a nested output reference, e.g.
{"consumption": {"sensor": N}, "state-of-charge": {"sensor": M}}fails every scheduling job (with
return_multiple, i.e. the asset-level trigger path) at save time with:The whole scheduling-job transaction rolls back, so the error confusingly claims the key "already exists" while the database afterwards contains none of the rows. Wiping assets does not help — the collision is intra-job. Observed in the field on a fresh partner setup with a battery flex model exactly as above.
Mechanism (origin: PR #2321)
Since PR #2321,
_resolve_power_sensor(inflexmeasures/data/models/planning/devices.py) intentionally resolves such a device's power sensor to its nested consumption/production output sensor, so the device is recognized as schedulable rather than misclassified as a stock-only entry.In
StorageScheduler.computewithreturn_multiple=True, sensor N then appeared twice in the outputs:storage_schedules(keyed by the resolved device power sensor), andconsumption_production_schedules(N is a declared consumption output sensor).Both outputs are saved with identical belief coordinates (
event_start,belief_horizon,cumulative_probability,sensor_id,source_id) in the same scheduling-job transaction → guaranteed primary-key violation → the job fails deterministically.The fix
return_multipleoutput assembly, a sensor that is also a declared consumption/production output sensor is no longer emitted as astorage_scheduleentry — only the consumption/production output entry is kept. That entry is the semantically correct one: the sign conventions for output sensors are defined on that path (see the_build_consumption_production_schedulesdocstring), and the scheduling service resolves the saving sign via theconsumption_is_positiveattribute stamped on output sensors at job creation. (For a consumption-only device the two entries carried identical data anyway — the full power profile in the scheduler's native sign convention — so no information is lost.)computenow passes all outputs through_deduplicate_outputsbefore returning, so no sensor can ever appear in more than one output entry (first entry wins, later ones are dropped with a logged warning). A duplicate must never reachsave_to_db, where it is guaranteed to fail the whole job.StorageFallbackScheduler'sreturn_multipleoutput now iterates over the deduplicated schedule dict rather than the raw per-device sensor list (which can name the same sensor twice for devices sharing a sensor).One existing test (
test_multi_device_battery_couples_stock_from_soc_sensor) asserted the old duplicated behavior by looking up the nested-output device's schedule under the namestorage_schedule; it now looks up the (single)consumption_scheduleentry, keeping its actual intent (stock coupling) unchanged.Test coverage
New regression test
test_scheduling_device_with_nested_output_sensor_only(inflexmeasures/data/tests/test_scheduling_jobs.py): a battery declaring only a nested consumption sensor plus a state-of-charge sensor, scheduled via the asset-level trigger path with a second device (mirroring the field setup). Asserts:consumption_schedule,timed_beliefUniqueViolation), andtimed_beliefrows.Verified to fail on the pre-fix code (the sensor appeared twice) and pass with the fix.
🤖 Generated with Claude Code
https://claude.ai/code/session_01WtuVTVfL4fQ9QSqbLXmAGD