Skip to content

feat: support a default fallback on sensor references - #2405

Merged
BelhsanHmida merged 39 commits into
mainfrom
feat/sensor-reference-defaults
Aug 7, 2026
Merged

feat: support a default fallback on sensor references#2405
BelhsanHmida merged 39 commits into
mainfrom
feat/sensor-reference-defaults

Conversation

@BelhsanHmida

@BelhsanHmida BelhsanHmida commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Description

Split out of #2267 on @Flix6x's suggestion, so the uncontroversial half can land while the SoC constraint hardness question is settled across the whole flex-model (see #2395).

  • A sensor reference on a variable-quantity flex-model or flex-context field (soc-minima, soc-maxima, the capacity fields, the price fields) may now carry a default fallback quantity, e.g. {"sensor": 50, "default": "0 kWh"}.
  • Settable from the flex-model UI.
  • Documented that a default fills every empty slot, so a sensor recording only occasional setpoints becomes densely constrained.
  • Omit default from serialized sensor references when unset, instead of emitting default: null (which is not valid input on the way back in, and broke the forecaster config round-trips).
  • Changelog + API changelog + OpenAPI specs updated.

Not included, and still parked on feat/dynamic-soc-bounds-defaults (#2267) pending #2395: the soc-minsoc_minima folding, the scalar softening (_relax_scalar_soc_minimum / _relax_scalar_soc_maximum), the soc-minima / soc-maxima deprecation labelling, and the soc_min / soc_max field-type change.

Look & Feel

The flex-model sensor picker gains an optional "Default fallback" input.

How to test

pytest flexmeasures/data/schemas/tests/test_sensor.py \
  flexmeasures/data/schemas/tests/test_scheduling.py \
  flexmeasures/data/schemas/tests/test_forecasting.py \
  flexmeasures/data/tests/test_forecasting_pipeline.py \
  flexmeasures/data/models/planning/tests/test_utils_fresh_db.py

Also ran the full CI-equivalent suite (--doctest-modules, same ignores as the workflow): 1923 passed, with only failures that reproduce identically on main (test_closest_sensor[1]/[3], and test_numerical_errors[cbc] where the CBC solver binary is absent locally).

Manual check — a sensor reference with a fallback in a storage flex-model:

{
  "flex-model": {
    "soc-at-start": "12 kWh",
    "soc-minima": {"sensor": 300, "default": "10 kWh"},
    "power-capacity": "5 kW"
  }
}

Slots where sensor 300 has no value are filled with 10 kWh.

Related Items

Sign-off

  • I confirm that my contribution follows the contribution guidelines.
  • I certify that I wrote the contribution or otherwise have the right to submit it under the project license.

BelhsanHmida and others added 30 commits June 25, 2026 22:09
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
…scheduler

Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>

# Conflicts:
#	documentation/changelog.rst
#	documentation/features/scheduling.rst
#	flexmeasures/api/v3_0/tests/test_sensors_api_freshdb.py
#	flexmeasures/data/schemas/scheduling/__init__.py
#	flexmeasures/data/schemas/tests/test_sensor.py
#	flexmeasures/ui/static/openapi-specs.json
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Both relax-soc-constraints and relax-constraints default to True, so the
former's default swallowed an explicit opt-out through the umbrella flag.
Now an explicit relax-soc-constraints wins and otherwise the umbrella
relax-constraints decides, per the semantics discussed in PR #2267:
setting either flag to False keeps SoC minima/maxima hard.

Also document why DBFlexContextSchema turns the relaxation defaults off.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pt-out

The capacity bounds (power-capacity and site-power-capacity) still remain
hard constraints after relaxation; mention them again alongside soc-min,
soc-max and soc-targets, and explain that setting either
relax-soc-constraints or relax-constraints to false keeps SoC
minima/maxima hard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Correct RELAX_CONSTRAINTS metadata text: it now defaults to True, not False.
- Give DBFlexContextSchema's relax flags their own description, since a
  stored flex-context defaults to False, unlike the True scheduling-time
  default applied after merging with the request.
- Align the hard-constraints list in scheduling.rst with introduction.rst
  (both now mention power-capacity / site-power-capacity).
- Add the missing v3.0-32 API changelog entry for the relax-soc-constraints
  default flip and the retired fallback scheduler's effect on GET schedule.
- Drop now-tautological assertions checking for "StorageFallbackScheduler"
  in belief sources, since that class no longer exists in the codebase.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
BelhsanHmida and others added 3 commits July 22, 2026 13:26
# Conflicts:
#	documentation/api/change_log.rst
Conflicts were in four files:

- storage.py: keep the removal of StorageFallbackScheduler (main had since
  changed its return_multiple block to dedupe shared sensors).
- test_sensor_schedules.py: keep both main's new
  test_get_schedule_unfinished_job_returns_202_when_sunset_active and this
  branch's renamed test_get_schedule_infeasible_storage_job_without_fallback;
  drop the two retired fallback tests.
- test_scheduling_sequential.py: keep both this branch's no-fallback
  assertions and main's new test_create_sequential_jobs_with_sign_explicit_context.
- api/change_log.rst: keep both sides' entries.

Also clean up the deferred subjobs in the no-fallback sequential test. With
the fallback retired nothing unblocks them, so they used to leak into the
next test and break main's new sign-explicit-context test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A sensor reference on any flex-model or flex-context field may now carry a
`default` quantity, e.g. {"sensor": 50, "default": "0 kWh"}. It fills the
time slots for which the referenced sensor holds no value, and is settable
from the flex-model UI.

This is the uncontroversial half of #2267, split out on Felix's suggestion
so it can land while the SoC constraint hardness question is settled across
the whole flex-model (see #2395). The canonical soc-min/soc-max work stays
on feat/dynamic-soc-bounds-defaults.

Note that a default fills *every* slot the sensor leaves empty, so a sensor
recording only occasional setpoints becomes densely constrained; the field
documentation says so.

Also omit `default` from serialized sensor references when it is unset,
rather than emitting `default: None`, which is not valid input on the way
back in and broke the forecaster config round-trips.

Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
@BelhsanHmida
BelhsanHmida requested a review from Flix6x August 6, 2026 23:15
@BelhsanHmida BelhsanHmida self-assigned this Aug 6, 2026
@read-the-docs-community

read-the-docs-community Bot commented Aug 6, 2026

Copy link
Copy Markdown

`default` is declared on the shared SensorReferenceSchema, so it is accepted
on every sensor reference in the API — but it is only applied in
get_series_from_quantity_or_sensor. Sensor references resolved by
get_power_values (inflexible devices) and by the forecasting pipelines
(regressors) ignore it, silently.

Rather than claim more than the code does, say where the field takes effect.
Applying it in those two paths is worth a follow-up.

Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
@BelhsanHmida

Copy link
Copy Markdown
Contributor Author

Manual end-to-end check. I drove the real StorageScheduler with a soc-minima sensor holding a value for exactly one slot of the horizon (40 kWh at 6am), with prices set expensive for the first 4 hours and cheap afterwards — so the scheduler has every incentive to stay empty early, and any early charging must come from a constraint rather than arbitrage.

flex-model SoC over the first 2 hours
"soc-minima": {"sensor": X} 0.0 MWh throughout
"soc-minima": {"sensor": X, "default": "20 kWh"} 0.02 MWh throughout

So the fallback fills the slots the sensor leaves silent, and the solver honours it as a floor, paying the high price to hold it. Also verified that a reference without a default no longer serializes default: null (the bug that was failing the forecaster config round-trips).

Known gap — follow-up needed

default is declared on the shared SensorReferenceSchema, so the API accepts it on every sensor reference, but it is only applied in get_series_from_quantity_or_sensor. Two other consumers unpack a SensorReference, read its source filters, and ignore default entirely:

  • get_power_values (flexmeasures/data/models/planning/utils.py) — inflexible devices
  • _regressor_sensor_and_source_filters (flexmeasures/data/models/forecasting/pipelines/base.py) — forecaster regressors

This is visible in the generated spec: the InflexibleDevice component now advertises default, where setting it does nothing and raises no error.

@Flix6x I've scoped the changelog and the field description to say where the field actually takes effect, rather than implement the missing paths in this PR. Opening a follow-up issue to apply default in those two paths happy to take it, or to fold it in here if you'd rather it land together.

@BelhsanHmida
BelhsanHmida changed the base branch from feat/retire-fallback-scheduler to main August 7, 2026 08:23
This branch was carved out of #2267, which was stacked on
feat/retire-fallback-scheduler, so #2252's commits travelled along with it.
Now that this PR targets main, drop that content here: the tree of this
commit equals main plus the sensor-reference `default` work alone.

No behaviour of this PR changes; #2252 lands on its own.

Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
@BelhsanHmida

Copy link
Copy Markdown
Contributor Author

@Flix6x i rebased the pr to main

@BelhsanHmida BelhsanHmida linked an issue Aug 7, 2026 that may be closed by this pull request
Signed-off-by: Mohamed Belhsan Hmida <149331360+BelhsanHmida@users.noreply.github.com>
Since #2345, a consumption-capacity or production-capacity that is zero
throughout the scheduling window is read as a physical statement about the
device and enforced strictly. A default fills every slot the sensor leaves
empty, so a fallback of 0 on one of those fields turns a silent sensor into a
hard bound, which is not what "use this value when the sensor has nothing to
say" sounds like it does.

Regenerating the specs here also restores their version to 1.0.0. The generator
takes it from the installed FlexMeasures, so the 0.33.2 in the previous revision
records a stale environment rather than an intended change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: F.N. Claessen <claessen@seita.nl>
@Flix6x

Flix6x commented Aug 7, 2026

Copy link
Copy Markdown
Member

Reviewed and approved — thanks for splitting this out, it lands cleanly. I pushed one commit rather than leaving review notes, since both parts are small and mechanical.

1. Restored the specs version to 1.0.0. The previous revision had "version": "0.33.2" in openapi-specs.json. That is not something you chose: generate_open_api_specs reads it from the installed FlexMeasures and truncates to major.minor.patch (flexmeasures/api/v3_0/__init__.py), so it records whatever version the generating environment had installed — a stale editable install in this case. Regenerating from an up-to-date environment puts it back. Worth a uv sync before regenerating the specs in future, or the version will keep drifting back.

2. Documented one interaction with #2345, which landed after this branch was opened.

Since #2345, a consumption-capacity or production-capacity that is zero throughout the scheduling window is read as a statement about what the device physically cannot do, and is enforced strictly — it is no longer breachable at a price. A default fills every slot the referenced sensor leaves empty, as you already document. Put together:

"production-capacity": {"sensor": 42, "default": "0 kW"}

against a sensor that is silent for the whole window yields an all-zero capacity, which becomes a hard bound. That is a reasonable thing for someone to write meaning "assume no export capability when I have no data", and they would not expect it to become uncrossable. So the caveat is now in three places: the default field description (and therefore OpenAPI), the API changelog entry, and the changelog entry.

Nothing to change in your implementation — this is a documentation consequence of two features meeting, and the note belongs with default because that is where a user meets it.

For context on the wider question of why a directional capacity carries two meanings at all, and what would let us stop inferring physics from the value 0, see #2395. Your parked work on #2267 is waiting on the same decision.

@BelhsanHmida
BelhsanHmida merged commit 8c4e7d3 into main Aug 7, 2026
12 of 13 checks passed
@BelhsanHmida
BelhsanHmida deleted the feat/sensor-reference-defaults branch August 7, 2026 19:55
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.

Make soc-minima and soc-maxima the default, simplifying usage

2 participants