Skip to content

Feat: forecast input bounds - #2555

Merged
Flix6x merged 31 commits into
mainfrom
feat/forecast-input-bounds
Sep 17, 2026
Merged

Flix6x merged 31 commits into
mainfrom
feat/forecast-input-bounds

Conversation

@BelhsanHmida

@BelhsanHmida BelhsanHmida commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Description

Stacked on #2542 — base this PR on feat/forecast-target-source-filter, not main. It depends on that PR's change making the forecast target accept a sensor reference.

lower, upper and snap currently shape a forecast on its way out, right before it is stored. This PR lets the same three fields be set on an individual regressor or on the target, where they clean that sensor's readings on the way in, before the model trains on them. It is for a sensor whose recorded data is not trustworthy as it stands — an occasional implausible spike, or an error sentinel such as -9999 — that you would otherwise have to correct upstream or copy to a second sensor.

  • Regressor and target references accept lower, upper and snap alongside the source filters they already take. A bare sensor ID keeps working unchanged.

  • The bounds live on the shared SensorReferenceSchema, so any sensor reference can carry them. Only forecasters apply them in this PR: flex-model and flex-context references refuse them for now, rather than accepting bounds they would ignore. Scheduling applies them in the follow-up, Let the scheduler clean its inputs with bounds on a sensor reference #2561.

  • Bounds that cannot be applied to the referenced sensor (an incompatible unit, a snap target outside its interval, a lower bound above the upper bound) are refused when the reference is loaded, not inside the queued job.

  • Each sensor's bounds are read in that sensor's own unit, not the unit of the sensor being forecast.

  • Bounding runs after gap filling, so a value interpolated across a gap is bounded too.

  • Snapping and clipping reuse the output path's helpers, now in flexmeasures/utils/bound_utils.py, so the snap-then-clip order and the [first, second) interval rule are identical on both sides by construction. A reference parses its bounds once, not once per read.

  • Bounds survive the queued-job payload, and are omitted when unset so a reference without cleaning serialises exactly as before.

  • Also fixes a pre-existing bug found on the way: detect_and_fill_missing_values handed the model N copies of all N regressor columns. That fix is split out into Hand the forecasting model each regressor once, not once per regressor #2560 (with its changelog entry), so it can land and be backported on its own; the same commit stays here, since this PR builds on it.

  • Changelog: one entry under New features.

  • Added changelog item in documentation/changelog.rst

Look & Feel

Regressor bounds go in the forecaster config, next to the regressors:

{
  "past-regressors": [
    2094,
    {"sensor": 2095, "lower": "0 kW", "snap": {"0 kW": ["0 kW", "0.5 kW"]}}
  ]
}

The target is named in the forecast parameters rather than the config, so its bounds go there:

{
  "sensor": {"sensor": 2092, "upper": "20 kW"}
}

Both are passed with flexmeasures add forecasts --config / --parameters, or as the API config / parameters payload. As with the existing output bounds, there are no dedicated CLI flags.

Given a regressor reading [-5, 0.3, 99, <gap>, 4] with lower: 0 kW, upper: 20 kW and snap: {"0 kW": ["0 kW", "0.5 kW"]}:

values reaching the model
without bounds [-5.0, 0.3, 99.0, 51.5, 4.0]
with bounds [0.0, 0.0, 20.0, 20.0, 4.0]

-5 clips to 0, 0.3 snaps to 0, 99 clips to 20, and the gap — which interpolates to 51.5 between 99 and 4 — is clipped to 20 as well, because bounding runs after filling.

How to test

flexmeasures/data/tests/test_forecasting_pipeline.py:

  • test_input_bounds_clean_a_regressor_after_its_gaps_are_filled
  • test_input_bounds_leave_an_unbounded_regressor_alone
  • test_input_bounds_are_read_in_the_regressors_own_unit
  • test_input_bounds_reject_a_unit_the_regressor_cannot_take
  • test_filling_gives_each_regressor_exactly_one_component

flexmeasures/data/schemas/tests/test_forecasting.py:

  • test_forecaster_config_schema_loads_regressor_cleaning_bounds
  • test_forecaster_config_schema_keeps_an_unbounded_regressor_a_plain_sensor
  • test_forecaster_parameters_schema_loads_target_cleaning_bounds
  • test_forecaster_config_schema_rejects_an_unparseable_regressor_bound
  • test_cleaning_bounds_live_on_the_shared_sensor_reference
  • test_a_reference_without_bounds_serialises_as_it_did_before
  • test_sensor_reference_refuses_bounds_it_cannot_apply_when_loaded
  • test_forecaster_config_schema_refuses_a_regressor_bound_in_an_incompatible_unit

flexmeasures/data/schemas/tests/test_sensor.py:

  • test_scheduling_references_refuse_bounds_they_would_ignore

What was broken to prove these are not vacuous:

  • Making input bounding a no-op reddened the three bounding tests.
  • Restoring the whole-frame copy reddened test_filling_gives_each_regressor_exactly_one_component (four components instead of two).
  • Bounding against the target's unit instead of the regressor's reddened the unit test.
  • Skipping the unit check when a reference is loaded reddened all cases of test_sensor_reference_refuses_bounds_it_cannot_apply_when_loaded and test_forecaster_config_schema_refuses_a_regressor_bound_in_an_incompatible_unit.
  • Removing the refusal from flex-model and flex-context references reddened test_scheduling_references_refuse_bounds_they_would_ignore.

test_input_bounds_leave_an_unbounded_regressor_alone survives all of those breaks. It is a control, not a binding test — it pins the unbounded baseline (51.5) that gives the "after filling" claim in the test above it its meaning, and it would catch a regression that made bounding unconditional. Read it as documentation of the baseline rather than as coverage of the feature.

The existing output post-processing tests were left untouched and still pass, which is what establishes that pulling parse_bounds / apply_bounds_to_values out of apply_forecast_post_processing did not change output behaviour.

Further Improvements

  • Clipping after filling has a sharp edge. An out-of-range reading is not neutralised until after it has been used to interpolate its neighbours. Given 10, -9999, <gap>, 14 with lower: 0, the gap interpolates from -9999 and is then clipped to 0, where filling between 10 and 14 would have given roughly 12. Where readings are wrong rather than merely out of range, the more honest operation may be to treat out-of-bounds values as missing and let interpolation fill across them, instead of clipping them to the bound and injecting a fabricated 0 into the training data. That interacts with the missing_threshold accounting, so it is left as a separate decision.
  • Scheduling applies the bounds in Let the scheduler clean its inputs with bounds on a sensor reference #2561. It lifts the refusal on flex-model and flex-context references.
  • Deleting a sensor does not prune references that carry a default or bounds from stored flex configs: Deleting a sensor leaves flex-config references to it behind when they carry a default (or bounds) #2558.
  • test_forecasting_pipeline.py mixes the db and fresh_db fixtures (2 uses against 49), which .github/instructions/testing.instructions.md explicitly warns will hang CI. Pre-existing and untouched here, but worth its own issue.

Related Items


Sign-off

  • I agree to contribute to the project under Apache 2 License.
  • To the best of my knowledge, the proposed patch is not based on code under GPL or other license that is incompatible with FlexMeasures

BelhsanHmida and others added 10 commits September 16, 2026 00:10
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>
@BelhsanHmida BelhsanHmida self-assigned this Sep 17, 2026
@BelhsanHmida BelhsanHmida changed the title Feat/forecast input bounds Feat: forecast input bounds Sep 17, 2026
…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>
@Flix6x

Flix6x commented Sep 17, 2026

Copy link
Copy Markdown
Member

The bounds live on a forecaster-specific reference schema rather than the shared SensorReferenceSchema, which flex-model and flex-context also use and where these keys would mean nothing. That schema keeps refusing them.

Extending the shared SensorReferenceSchema would actually make this a much stronger feature. It is arguably an even more useful feature for data inputs to the scheduler.

Three mechanisms now bound values. ensure_positive still clips negatives to zero inside the model, next to the output bounds and now these input bounds. Worth deciding whether it should be deprecated in favour of the explicit bounds.

No rush on formally deprecating this, but worth a comment in the code that it is meant to be deprecated. If in the user documentation we advise using it (perhaps in an example) then let's replace that example with the new way.

Regressor bounds sit in the config while target bounds sit in the parameters, because that is where each sensor is named. Inherited rather than introduced here, but it makes the feature read as two places to configure one idea.

That's completely fine. I actually think all sensor references could use this.

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.

🟡 Changes recommended

Schema/OpenAPI metadata and error-handling/docstring convention issues need to be addressed before this can be safely approved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR extends the forecasting pipeline to support input-side cleaning bounds (lower, upper, snap) on both target and regressor sensor references, so training data can be snapped/clipped before model fitting. It also fixes a regressor gap-filling bug where each per-sensor pass accidentally duplicated all columns into the Darts series, leading to duplicated model components.

Changes:

  • Add a forecaster-specific sensor reference schema (ForecastInputReference*) that supports optional input bounds while keeping the shared SensorReferenceSchema strict.
  • Apply input bounds after missing-value filling in BasePipeline.detect_and_fill_missing_values, and refactor output post-processing to reuse shared bound parsing/application helpers.
  • Add/adjust tests and documentation, plus changelog entries for both the new feature and the regressor-component duplication bugfix.
File summaries
File Description
flexmeasures/data/tests/test_forecasting_pipeline.py Adds pipeline-level tests for input bounds and for the “one component per regressor” regression.
flexmeasures/data/schemas/tests/test_forecasting.py Adds schema tests ensuring bounds load correctly for forecasters and remain rejected on shared sensor references.
flexmeasures/data/schemas/forecasting/references.py Introduces ForecastInputReference, ForecastInputReferenceSchema, and ForecastInputField to support bounds on forecaster inputs.
flexmeasures/data/schemas/forecasting/pipeline.py Switches forecaster regressor/target fields to ForecastInputField and updates schema behavior accordingly.
flexmeasures/data/models/forecasting/utils.py Extracts reusable bound parsing/application (parse_bounds, apply_bounds_to_values) and reuses it for output post-processing.
flexmeasures/data/models/forecasting/pipelines/train_predict.py Ensures queued-job payload round-trips forecasting-specific references (including bounds).
flexmeasures/data/models/forecasting/pipelines/base.py Fixes regressor duplication during filling, and applies input bounds after filling via shared helpers.
documentation/features/forecasting.rst Documents how to configure input-side bounds for regressors and the target.
documentation/changelog.rst Adds changelog entries for the new feature and the regressor duplication bugfix.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 7
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread flexmeasures/data/models/forecasting/utils.py Outdated
Comment thread flexmeasures/data/models/forecasting/utils.py Outdated
Comment thread flexmeasures/data/schemas/forecasting/references.py Outdated
Comment thread flexmeasures/data/models/forecasting/pipelines/train_predict.py Outdated
Comment thread flexmeasures/data/models/forecasting/utils.py Outdated
Comment thread flexmeasures/data/schemas/forecasting/pipeline.py
Comment thread flexmeasures/data/schemas/forecasting/pipeline.py Outdated
…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>
@BelhsanHmida

Copy link
Copy Markdown
Contributor Author

The bounds live on a forecaster-specific reference schema rather than the shared SensorReferenceSchema, which flex-model and flex-context also use and where these keys would mean nothing. That schema keeps refusing them.

Extending the shared SensorReferenceSchema would actually make this a much stronger feature. It is arguably an even more useful feature for data inputs to the scheduler.

Three mechanisms now bound values. ensure_positive still clips negatives to zero inside the model, next to the output bounds and now these input bounds. Worth deciding whether it should be deprecated in favour of the explicit bounds.

No rush on formally deprecating this, but worth a comment in the code that it is meant to be deprecated. If in the user documentation we advise using it (perhaps in an example) then let's replace that example with the new way.

Regressor bounds sit in the config while target bounds sit in the parameters, because that is where each sensor is named. Inherited rather than introduced here, but it makes the feature read as two places to configure one idea.

That's completely fine. I actually think all sensor references could use this.

All addressed, plus the seven Copilot comments

Shared schema: you're right, moved. lower/upper/snap now sit on SensorReferenceSchema, so any reference can carry them; the forecaster-specific schema is gone. That also fixes the OpenAPI Copilot flagged, by construction. Only forecaster inputs act on them so far — the fields say so, the way default documents its own gap.

Scheduling: follow-up PR, landing before v1.1. Small change, but it sits behind ~50 scheduling call sites, and a wrong bound there produces a plausible schedule rather than an error — not what I want in an rc. Changelog stays one entry, both PR links.

ensure_positive: flagged as deprecated in the description and at the line that applies it. Nothing in documentation/ recommends it, so no example to migrate.

Flix6x and others added 3 commits September 17, 2026 17:22
… 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>
…rries it on its own

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

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.

🟡 Changes recommended

There are a few correctness/documentation inconsistencies in the changed code paths (notably bounds refusal logic for scheduling references and ensure_positive default documentation) that should be addressed before approval.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 14/14 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment thread flexmeasures/data/schemas/sensors.py Outdated
Comment thread flexmeasures/utils/unit_utils.py Outdated
Comment thread flexmeasures/data/schemas/forecasting/pipeline.py Outdated
Comment thread flexmeasures/ui/static/openapi-specs.json Outdated
Flix6x and others added 3 commits September 17, 2026 17:47
…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>
Flix6x added a commit that referenced this pull request Sep 17, 2026
…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>
@Flix6x
Flix6x requested a lite review from Copilot September 17, 2026 15:50

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.

🟡 Changes recommended

The current SensorIdOrReferenceField treats no-op bound keys as “cleaning requested” (changing backward-compatible deserialization/serialization), and boolean values are unintentionally accepted as numeric bounds.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

Previously missed (3) — in code that hasn't changed since the last review.

flexmeasures/utils/bound_utils.py:35

  • _quantity_to_sensor_value accepts bool as a numeric bound because bool is a subclass of int (numbers.Real). That can silently turn true/false into 1.0/0.0 bounds.

It’s safer to explicitly exclude booleans from the numeric fast-path.
flexmeasures/utils/unit_utils.py:63

  • is_parseable_quantity currently treats bool values as parseable quantities because bool is a subclass of int (numbers.Real). That means schema validation would accept true/false as bounds and later interpret them as 1.0/0.0, which is very likely unintended input.
    flexmeasures/utils/bound_utils.py:218
  • apply_bounds_to_values says “Values that are not a number are left alone”, but the implementation coerces values to dtype=float, which will raise on non-numeric inputs (it only preserves NaNs). To avoid misleading readers, consider tightening the wording to describe NaN handling explicitly.
  • Files reviewed: 14/14 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread flexmeasures/data/schemas/sensors.py
Flix6x and others added 3 commits September 17, 2026 17:59
…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>

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.

🔵 Needs a closer look

It changes core forecasting schema and pipeline behavior (input data cleaning and series construction), which merits final human review despite strong test coverage.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

flexmeasures/data/schemas/sensors.py:386

  • The _sets_bounds docstring summary does not end with a period, which is inconsistent with the repo’s docstring convention and can make the docs/readability uneven.
  • Files reviewed: 14/14 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@BelhsanHmida
BelhsanHmida requested a review from Flix6x September 17, 2026 17:17
Base automatically changed from feat/forecast-target-source-filter to main September 17, 2026 18:04
@Flix6x Flix6x added this to the 1.1.0 milestone Sep 17, 2026
…/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>

@Flix6x Flix6x left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

@Flix6x
Flix6x merged commit e77febb into main Sep 17, 2026
13 checks passed
@Flix6x
Flix6x deleted the feat/forecast-input-bounds branch September 17, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Apply clipping and snapping to forecast inputs (regressors and target), not just outputs

3 participants