Skip to content

Fix scheduling jobs failing on a timed_belief UniqueViolation for devices with nested-output-only power sensors - #2360

Merged
Flix6x merged 3 commits into
mainfrom
fix/storage-scheduler-duplicate-output-sensor
Jul 28, 2026
Merged

Fix scheduling jobs failing on a timed_belief UniqueViolation for devices with nested-output-only power sensors#2360
Flix6x merged 3 commits into
mainfrom
fix/storage-scheduler-duplicate-output-sensor

Conversation

@Flix6x

@Flix6x Flix6x commented Jul 27, 2026

Copy link
Copy Markdown
Member

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:

psycopg2.errors.UniqueViolation: duplicate key value violates unique constraint "timed_belief_pkey"

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 (in flexmeasures/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.compute with return_multiple=True, sensor N then appeared twice in the outputs:

  1. in storage_schedules (keyed by the resolved device power sensor), and
  2. in consumption_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

  1. Targeted: in the return_multiple output assembly, a sensor that is also a declared consumption/production output sensor is no longer emitted as a storage_schedule entry — 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_schedules docstring), and the scheduling service resolves the saving sign via the consumption_is_positive attribute 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.)
  2. Safety net: compute now passes all outputs through _deduplicate_outputs before 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 reach save_to_db, where it is guaranteed to fail the whole job.
  3. The StorageFallbackScheduler's return_multiple output 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 name storage_schedule; it now looks up the (single) consumption_schedule entry, keeping its actual intent (stock coupling) unchanged.

Test coverage

New regression test test_scheduling_device_with_nested_output_sensor_only (in flexmeasures/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:

  • no sensor appears twice across the scheduler's outputs,
  • the nested output sensor's schedule is emitted exactly once, as a consumption_schedule,
  • the scheduling job finishes successfully (it used to fail on the timed_belief UniqueViolation), and
  • the sensor receives exactly one series of timed_belief rows.

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

Flix6x and others added 2 commits July 27, 2026 15:33
… 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>
@read-the-docs-community

read-the-docs-community Bot commented Jul 27, 2026

Copy link
Copy Markdown

Documentation build overview

📚 flexmeasures | 🛠️ Build #33787777 | 📁 Comparing 455b31b against latest (a3fb811)

  🔍 Preview build  

4 files changed
± changelog.html
± genindex.html
± _autosummary/flexmeasures.data.models.planning.storage.html
± api/v3_0.html

@Flix6x
Flix6x marked this pull request as ready for review July 27, 2026 14:58
@Flix6x
Flix6x requested a review from Copilot July 27, 2026 15:04

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 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_schedule output when the same sensor is already present as a declared consumption/production output sensor.
  • Add a defensive _deduplicate_outputs safety 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>
Comment thread documentation/changelog.rst Outdated
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>
@Flix6x Flix6x added this to the 1.0.0 milestone Jul 28, 2026
@Flix6x Flix6x self-assigned this Jul 28, 2026
@Flix6x Flix6x added the bug Something isn't working label Jul 28, 2026
@Flix6x
Flix6x merged commit 45864fd into main Jul 28, 2026
13 checks passed
@Flix6x
Flix6x deleted the fix/storage-scheduler-duplicate-output-sensor branch July 28, 2026 07:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working Scheduling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants