Conversation
The three regressor options accept a source-filtered sensor reference, so a regressor can say which of the sources recording on a sensor it reads. The target sensor took a bare ID only, so a sensor that several sources report on was trained on whichever of them won each event, with no way to say which one holds the truth. The target now accepts the same reference. A bare ID keeps behaving as before: every source is trained on except forecasters, which are left out so that a forecaster does not learn from its own forecasts. A reference replaces that default rather than adding to it, so pass exclude-source-types yourself to keep forecasters out alongside another filter. Forecasts are still recorded on the sensor itself, never on a source-filtered view of it, so the output sensor unwraps a referenced target. The reference survives serialization into a queued job and storage on an automation. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Main gained the scheduling side of automations (#2293, #2294) while this branch was open. Both sides rewrote the sensor-ID collection in get_automation_job_stats: main split it into a scheduling and a forecasting branch, this branch taught it to resolve a target stored as a source-filtered reference. Kept main's split, resolving the reference inside its forecasting branch. Also unwrapped a referenced target in create_automation, whose new check that the sensor to forecast belongs to the automation's asset only recognised a plain Sensor, and so would have passed a filtered target over in silence. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
#2297 moved the code this branch edited into helpers, so this branch's handling of a source-filtered target sensor moves with it: - data_add.py: parse a JSON target sensor reference, then drop unset values the way main now does, empty tuples included. - Job stats: main's shared _relevant_sensor_ids reads each stored parameter through _stored_sensor_id, so a reference counts, where its int() would have skipped it silently. - A forecast automation's preparation, now _prepare_forecast_automation, unwraps a SensorReference before checking which asset the sensor is on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0129WrXeJ5gia2pctFH93BqC Signed-off-by: F.N. Claessen <claessen@seita.nl>
Context: - Clipping and snapping are about to be applied to forecaster inputs as well as to forecast output, so the logic can no longer live inside the output path. Change: - Split parse_bounds and apply_bounds_to_values out of apply_forecast_post_processing, keeping snap-then-clip order and the [first, second) interval semantics. - Let error messages carry a label, so input bounds can name the sensor at fault. - Move _is_parseable_quantity here from the schema, where the input reference schema can reach it too without importing the pipeline schema. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
… per regressor Context: - detect_and_fill_missing_values copied the whole frame on every pass of its per-sensor loop, so each pass built a Darts series holding every sensor's column. Stacking those passes gave N copies of all N columns: two past regressors reached LightGBM as components a, b, a_1, b_1. - The test covering this frame shape monkeypatches the method away, so the real one never ran against more than one regressor. Change: - Narrow the frame to the sensor's own column before converting to a Darts series, keeping the missing-column case on its existing all-NaN path. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
…sor bounds Context: - lower, upper and snap only shaped the forecast on its way out. A sensor with implausible readings could not be cleaned up on its way in, so training on it meant fixing the data upstream or copying it to another sensor. Change: - Let a regressor or target reference carry lower, upper and snap, alongside the source filters it already takes. A bare sensor ID keeps working unchanged. - Hold the bounds on a forecaster-specific reference schema rather than the shared one, which flex-model and flex-context also use, where they mean nothing. - Bound each input series after its gaps are filled, so an interpolated value is bounded too, reusing the snap-then-clip order and interval semantics of the output path. - Carry the bounds through the queued-job payload, omitting them when unset so a reference without cleaning serialises exactly as before. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Context: - Forecaster inputs can now carry cleaning bounds, and filling no longer duplicates regressor columns. Neither was covered. Change: - Test that bounds clip, snap and reach values interpolated across a gap, that they read the regressor's own unit rather than the target's, and that an incompatible unit is refused by name. - Test that two regressors reach the model as two components. - Name the bounded input in quantity-conversion errors too, so a bad bound on a regressor no longer reports itself as forecast post-processing. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Context: - Regressor and target references now take lower, upper and snap, and the shared sensor reference deliberately does not. Change: - Test that a regressor and a target load their bounds, that an unparseable bound is refused at load time, and that a reference asking for neither bounds nor filters still collapses to the plain sensor. - Test that the shared sensor reference, which flex-model and flex-context use, refuses the bound keys that the forecaster's own reference accepts. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Context: - lower, upper and snap can now clean a regressor or the target before training, not only shape the forecast on its way out. Change: - Add a section covering where the bounds go, that each sensor's bounds are read in its own unit, and that input and output bounds are configured separately. - Say plainly that bounding runs after gaps are filled, including what that costs when a bad reading sits next to a gap. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
…or fix Context: - Both are user-visible: a new way to configure a forecaster, and a correction to what a forecaster with several regressors trained on. Change: - Add a New features entry for the cleaning bounds and a Bugfixes entry for the duplicated regressor data, both pointing at PR #2555. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
…tion Context: - Schemas need to tell a usable bound from an unusable one while loading, and the forecaster's copy of that check caught a bare Exception, against the repo's error-handling guideline. - The check has to live somewhere both schemas/sensors.py and the forecasting models can reach, and the latter already imports the former. Change: - Add is_parseable_quantity here, catching QUANTITY_PARSE_ERRORS: pint's own errors plus the tokenizer and arithmetic errors its expression parser surfaces for input like '', '[[[' or '1/0', which PintError alone misses. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Context: - Adding a typing import to this module brought it into mypy's file list, which is built by grepping for "from typing import", and that surfaced a latent error in split_into_magnitude_and_unit. Change: - Return the formatted magnitude through its own local instead of writing it back over the str parameter. Behaviour is unchanged, as its doctests show. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Context: - Review asked for the bounds to sit on the shared sensor reference rather than a forecaster-specific one, since they are at least as useful for the data the scheduler reads. `default` already sits there without applying everywhere. Change: - Move lower, upper and snap onto SensorReferenceSchema and SensorReference, and return a reference whenever either bound keys or source filters are given. - Drop the forecaster-specific reference schema, field and dataclass, which the forecasting pipeline no longer needs. - Say on each field where the bounds are honoured so far, the way `default` documents its own gap: forecaster inputs act on them, flex-model and flex-context accept and ignore them until the scheduler follow-up. - This also corrects the published OpenAPI, which advertised a plain SensorReference for a field that accepted more. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Context: - The bounds moved off the forecaster-specific reference, so the test that pinned them off the shared schema now asserted the opposite of the design. Change: - Point the pipeline and schema tests at SensorReference. - Replace the isolation test with its inverse: the shared reference takes the bounds, and still refuses one that cannot be read as a quantity. - Add a test that a reference setting no bounds serialises as it did before, while a meaningful zero bound survives. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
…nsure-positive Context: - Review found the regressor and target descriptions still describing the dict form as source filtering only, which misleads anyone writing config, and asked for ensure-positive to be marked as intended for deprecation. Change: - Say on the regressor and target fields that a reference can also carry bounds, and spell out on the target that input bounds clean what the model learns from while the forecaster's own bounds shape what it writes back out. - Mark ensure-positive as meant for deprecation, in its description and beside the line that applies it, pointing at the explicit lower bound instead. - Catch pint's parse errors rather than every exception when reading a bound. - Re-wrap the snap docstring to break only after punctuation. - Say in the payload docstring that bounds are serialised alongside filters. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Context: - The bounds moved onto the shared sensor reference, so flex-model and flex-context references accept them, while only forecaster inputs act on them. Change: - Say so, rather than leaving a reader to find out that a bound set on a scheduling reference is quietly ignored. Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
… and generators Bound parsing and application move out of the forecasting utils into their own module, so that sensor references and schedulers can use them without importing forecasting code. The parseability check that the forecaster config and the sensor reference schema both repeated is now one helper. The quantity-parsing helpers in unit_utils now sit below the registry setup instead of splitting it in two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…hen it is loaded The referenced sensor is already loaded when its bounds are validated, so an incompatible unit, a snap target outside its interval and a lower bound above the upper bound are now refused up front, instead of surfacing inside the queued job once its data has been read. The three schema tests that bounded an MWh sensor in kW relied on that gap, and now use the MW dummy sensor. A reference also parses its bounds once, and applies them itself, rather than having each read parse them again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ferences until scheduling applies them The bounds live on the shared sensor reference, but only forecaster inputs act on them for now. A flex-model or flex-context reference used to accept them and silently drop them, without even checking them, since those references are deserialized by their own field rather than by the shared schema. Refusing them keeps a release from shipping bounds that look configured but do nothing. Scheduling will apply them in a follow-up, which lifts this refusal. The shared bound validation also checks the shape of each snap entry itself now, so it reports a malformed interval instead of failing on it, whichever field feeds it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…break a docstring after punctuation Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…rries it on its own Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…eir sensor references Flex-model and flex-context references now keep their lower, upper and snap bounds, instead of refusing them, and the scheduler applies them wherever it reads a referenced sensor: variable quantities, inflexible devices, and the state of charge at the start of a schedule. Readings are cleaned in the sensor's own unit, before they are resampled, converted or defaulted, so a missing reading is still left to the reference's default. Bounds the sensor cannot take are refused when the flex-model or flex-context is loaded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…rence Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
There was a problem hiding this comment.
🟡 Changes recommended
One newly introduced validation path (invalid bounds via InflexibleDeviceSchema) is not covered by tests, leaving a regression risk for flex-config loading.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR extends the “input cleaning bounds” (lower, upper, snap) on SensorReference from forecasting into scheduling, so flex-model and flex-context sensor references can carry bounds and the scheduler applies them when reading referenced sensor data.
Changes:
- Allow flex-model and flex-context sensor references to accept, validate, and serialize
lower/upper/snapbounds. - Apply reference bounds when scheduling reads referenced sensors (variable quantities, inflexible device power, and SoC-at-start).
- Add/adjust tests and documentation (incl. OpenAPI descriptions and changelog) to reflect scheduler support.
File summaries
| File | Description |
|---|---|
| flexmeasures/ui/static/openapi-specs.json | Updates parameter descriptions to reflect that schedulers now apply bounds too |
| flexmeasures/data/schemas/tests/test_sensor.py | Adds coverage for scheduling references carrying bounds; updates validation expectations |
| flexmeasures/data/schemas/tests/test_forecasting.py | Updates wording around which subsystems apply reference bounds |
| flexmeasures/data/schemas/sensors.py | Removes “refuse bounds” logic; validates bounds on load; serializes bounds on references; updates inflexible-device reference handling |
| flexmeasures/data/models/planning/utils.py | Applies reference bounds when reading sensor data for scheduling utilities |
| flexmeasures/data/models/planning/tests/test_utils_fresh_db.py | Adds tests asserting scheduler-side reads apply reference bounds correctly |
| flexmeasures/data/models/planning/storage.py | Applies reference bounds when resolving state-of-charge at schedule start |
| documentation/features/forecasting.rst | Updates documentation to reflect scheduler support for reference bounds |
| documentation/concepts/flexibility-configuration.rst | Documents using bounds on sensor references inside flex configs |
| documentation/changelog.rst | Updates changelog entry to include scheduler support |
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…lly sets, not an explicit null or empty snap Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…tity parse errors Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
… as it does Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ounds Takes #2555's refusal of unset bounds (an explicit null or empty snap) as the rule for when a scheduling reference counts as bounded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…unds they cannot apply Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
There was a problem hiding this comment.
🟡 Changes recommended
There is an input-validation inconsistency in VariableQuantityField for explicit snap: null (vs. schema behavior) that should be resolved to avoid silently accepting malformed configs.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 2
- Review effort level: Lite
Documentation build overview
21 files changed ·
|
…nds a plain sensor An explicit null bound or empty snap no longer turns a regressor or target into a reference, so its queued-job payload stays a bare sensor ID. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…values alone A JSON true or false is a numbers.Real to Python, so it passed as a bound of 1 or 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…wer and upper bounds already are The shared reference schema refused it, while the flex-config field accepted it, so the same reference loaded in one place and failed in another. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ounds Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
…rds, which is kWh when instantaneous Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: F.N. Claessen <claessen@seita.nl>
There was a problem hiding this comment.
🟡 Changes recommended
InflexibleDeviceSchema still accepts default (inherited) but silently drops it on load/dump round-trips, which is surprising user-facing schema behavior and should be refused or explicitly supported.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 1
- Review effort level: Lite
| @post_load | ||
| def to_sensor_or_reference( | ||
| self, data: dict, **kwargs | ||
| ) -> "Sensor | SensorReference": | ||
| if not any( | ||
| reference = SensorReference( |
There was a problem hiding this comment.
Good catch, but this happens on main too: InflexibleDeviceSchema has inherited default and dropped it since before this PR, which neither adds nor changes it. Filed as #2562 so it gets its own decision (support it in get_power_values, or refuse it), rather than widening this PR.
…/forecast-target-source-filter Signed-off-by: F.N. Claessen <claessen@seita.nl> # Conflicts: # flexmeasures/cli/data_add.py
…st-input-bounds Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ounds Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ounds Signed-off-by: F.N. Claessen <claessen@seita.nl>
|
|
||
| This is the same source filtering mechanism described under :ref:`sources`, just scoped to sensor references inside flex-model/flex-context fields rather than GET data endpoints. | ||
|
|
||
| A sensor reference can also clean the readings it points at before the scheduler uses them, with the same ``lower``, ``upper`` and ``snap`` fields a forecaster uses to shape its forecasts. |
There was a problem hiding this comment.
This is still a rather hidden paragraph under the section documenting variable quantities. I think that whole section should get sub-sections.
There's probably a cross-reference are two that would deserve to exist (e.g. between the data cleaning section for forecast input and here).
I'm also entertaining the idea that we should create a dedicated page for the concept of data-generators, including sections on data cleaning and variable quantities. But I suggest we get to that when our work on consolidating data-generators is at an even more advanced point.
| { | ||
| "sensor": {"sensor": 2092, "upper": "20 kW"} | ||
| } |
There was a problem hiding this comment.
The nested "sensor" looks quite bad, actually. What are alternatives? Instinctively, I'd want to flatten this to:
| { | |
| "sensor": 2092, | |
| "upper": "20 kW" | |
| } |
| @@ -122,7 +122,7 @@ | |||
| Three things are worth knowing before relying on this: | |||
|
|
|||
| - Input bounds and output bounds are configured separately and may disagree. Cleaning the target's training data does not bound the forecast that comes out of it, and vice versa. | |||
There was a problem hiding this comment.
I don't see what this accomplishes.
|
|
||
| - Input bounds and output bounds are configured separately and may disagree. Cleaning the target's training data does not bound the forecast that comes out of it, and vice versa. | ||
| - The bounds are part of the general sensor reference, but only forecaster inputs act on them for now. Until scheduling applies them too, a flex-model or flex-context reference refuses them, rather than accepting bounds it would ignore. | ||
| - The bounds are part of the general sensor reference, so a flex-model or flex-context reference takes them too, and the scheduler cleans the readings it takes from that sensor in the same way. |
There was a problem hiding this comment.
This ought to be a cross-reference instead.
| - Input bounds and output bounds are configured separately and may disagree. Cleaning the target's training data does not bound the forecast that comes out of it, and vice versa. | ||
| - The bounds are part of the general sensor reference, but only forecaster inputs act on them for now. Until scheduling applies them too, a flex-model or flex-context reference refuses them, rather than accepting bounds it would ignore. | ||
| - The bounds are part of the general sensor reference, so a flex-model or flex-context reference takes them too, and the scheduler cleans the readings it takes from that sensor in the same way. | ||
| - Bounding runs **after** missing values are filled, so a value interpolated across a gap is bounded too. It also means an out-of-range reading is still used to interpolate its neighbours before it is itself corrected: given readings of ``10``, ``-9999``, a gap, and ``14`` with ``lower: 0``, the gap interpolates from ``-9999`` and is then clipped to ``0``, rather than filling to roughly ``12``. Where readings are wrong rather than merely out of range, correcting them at the source is still the better fix. |
There was a problem hiding this comment.
Consider turning this into a footnote.
| * In the UI, the full record of the data source selected on a sensor page can be inspected, backed by a new API endpoint (``[GET] /sources/(id)``) [see `PR #2290 <https://www.github.com/FlexMeasures/flexmeasures/pull/2290>`_] | ||
| * A forecaster can now be told which data sources hold the truth about the sensor it forecasts, the way its regressors already could, so that a sensor several sources report on is trained on the ones you trust [see `PR #2542 <https://www.github.com/FlexMeasures/flexmeasures/pull/2542>`_] | ||
| * A forecaster can now clean the data it trains on, by giving one of its regressors or its target a ``lower``, ``upper`` or ``snap`` bound, so that a sensor with implausible readings can be forecast from without first correcting it at the source [see `PR #2555 <https://www.github.com/FlexMeasures/flexmeasures/pull/2555>`_] | ||
| * Forecasters and schedulers can now clean the data they read, by giving a sensor reference a ``lower``, ``upper`` or ``snap`` bound (on a forecaster's regressors or target, or in a flex-model or flex-context), so that a sensor with implausible readings can be used without first correcting it at the source [see `PR #2555 <https://www.github.com/FlexMeasures/flexmeasures/pull/2555>`_ and `PR #2561 <https://www.github.com/FlexMeasures/flexmeasures/pull/2561>`_] |
There was a problem hiding this comment.
Let's open two follow-up issues to generalize this feature:
- To make input cleaning a data-generator wide feature: what is needed to enable this for reporter input, too?
- To make output cleaning a data-generator wide feature: what is needed to enable this for reporter and scheduler output, too?
There was a problem hiding this comment.
Please add a deprecation comment in the help string of the CLI option, too, informing the user of the alternative formulation. I'm guessing it's not possible to simply make --ensure-positive a deprecated alias of --lower 0.
Description
Stacked on #2555, which puts
lower,upperandsnapon the shared sensor reference and applies them to forecaster inputs, while flex-model and flex-context references refuse them.This PR lifts that refusal, and lets the scheduler clean the readings it takes from a referenced sensor in the same way.
Flex-model and flex-context references (both variable quantities and inflexible devices) now keep their bounds, and dump them again.
The scheduler applies them at each place it reads a referenced sensor: variable quantities (capacities, prices, SoC constraints, etc.), inflexible devices, and the state of charge at the start of a schedule.
Readings are cleaned in the sensor's own unit, before they are resampled, converted or defaulted, so a missing reading is still left to the reference's
default.Bounds the sensor cannot take (incompatible unit, snap target outside its interval, lower above upper, malformed snap entry) are refused when the flex-model or flex-context is loaded.
Kept out of Feat: forecast input bounds #2555 so the rc does not ship a change that can alter schedules without an error, as argued in the discussion there.
Added changelog item in
documentation/changelog.rstLook & Feel
{ "inflexible-consumption": [{"sensor": 58, "lower": "0 kW", "snap": {"0 kW": ["0 kW", "0.1 kW"]}}], "site-power-capacity": {"sensor": 60, "upper": "1 MW"} }How to test
flexmeasures/data/models/planning/tests/test_utils_fresh_db.py:test_get_series_from_sensor_reference_applies_its_boundstest_get_power_values_applies_reference_boundstest_soc_at_start_applies_reference_boundsflexmeasures/data/schemas/tests/test_sensor.py:test_scheduling_references_carry_their_boundstest_scheduling_references_refuse_bounds_they_cannot_applyMaking
SensorReference.apply_boundsa no-op, and dropping the bounds inVariableQuantityField, reddened the three read tests and the carry test.Further Improvements
inputschema, not the sensor reference, so they do not take bounds yet.defaultor bounds from stored flex configs: Deleting a sensor leaves flex-config references to it behind when they carry a default (or bounds) #2558.Related Items
Sign-off
🤖 Generated with Claude Code