From a1fc6cc7e725ca2a08bb46467eec693a4b63f370 Mon Sep 17 00:00:00 2001 From: Sankalp Thakur Date: Sat, 25 Jul 2026 15:16:52 +0530 Subject: [PATCH 1/2] Fix chart interpolation flipping to linear for rows mixing real sensors with flex-config constants Signed-off-by: Sankalp Thakur --- documentation/changelog.rst | 1 + .../data/models/charts/belief_charts.py | 7 +- flexmeasures/data/tests/test_belief_charts.py | 112 ++++++++++++++++++ 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 21087adce7..6cc9a43dfc 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -163,6 +163,7 @@ Bugfixes * Show icons for more asset types in the UI's asset structure view, which previously fell back to a question mark: the ``wind``, ``process`` and ``heat-storage`` types that FlexMeasures seeds by default, and EV infrastructure under its various names (such as ``one-way_evse``, ``two-way_evse``, ``evse``, ``charging_station`` and ``charging_hub``) and building services equipment (``hvac``, ``ahu``, ``dhw``, ``heatpump``, ``chiller``, ``lighting`` and ``other-loads``). Asset type names are now matched ignoring case and separators, so an asset type named ``charge-point`` gets the same icon as ``chargepoint`` [see `PR #2391 `_] * Replaying a chart for a past window no longer shows annotations that were only recorded later; annotation searches and the ``chart_annotations`` endpoints can now be scoped by recording (belief) time [see `PR #2367 `_] * Continuing the query-parameter cleanup started in PR #2352: the chart-related endpoints now use ``prior``, ``start``, ``end`` and hyphenated field names, with a new ``duration`` field to derive a missing ``start``/``end``; old spellings keep working as legacy aliases [see `PR #2367 `_] +* Fix chart interpolation flipping to linear for a row that mixes a real sensor with fixed-value flex-model/flex-context sensors, even when the real sensor itself is not instantaneous [see `PR #2356 `_ and `issue #2253 `_] * Scheduling jobs no longer print ``Job ... made schedule.`` before ``scheduler.compute()`` runs (only after a successful schedule) [see `PR #2342 `_] * ``flexmeasures add user --roles`` now correctly accepts a comma-separated list of roles (and repeated ``--roles`` options) instead of creating one role whose name contains commas [see `PR #2339 `_] * A ``production-capacity`` or ``consumption-capacity`` that is zero throughout the scheduling window is now enforced strictly, even where device capacity relaxation is in effect, so one-way devices such as heat pumps and one-way chargers can no longer be scheduled in the forbidden direction. A zero covering only part of the window still expresses a preference and remains breachable, so keeping a device idle during part of a schedule (an EV charger during a calendar car reservation, say) works as before [see `PR #2345 `_] diff --git a/flexmeasures/data/models/charts/belief_charts.py b/flexmeasures/data/models/charts/belief_charts.py index fb1cff0449..73d92ff8fc 100644 --- a/flexmeasures/data/models/charts/belief_charts.py +++ b/flexmeasures/data/models/charts/belief_charts.py @@ -951,8 +951,11 @@ def create_line_layer( if shared_unit != "a.u.": scale_values = True - # Use linear interpolation if any of the sensors shown within one row is instantaneous; otherwise, use step-after - if any(sensor.event_resolution == timedelta(0) for sensor in sensors): + # Use linear interpolation if any of the sensors shown within one row is instantaneous; otherwise, use step-after. + # Only real sensors determine interpolation; fixed-value/constant sensors (negative id) + # are always event_resolution=0 by construction and must not force a mixed row to linear (#2253). + real_sensors = [s for s in sensors if getattr(s, "id", None) is None or s.id >= 0] + if any(sensor.event_resolution == timedelta(0) for sensor in real_sensors): interpolate = "linear" else: interpolate = "step-after" diff --git a/flexmeasures/data/tests/test_belief_charts.py b/flexmeasures/data/tests/test_belief_charts.py index 2a134438fc..b2bdee10ef 100644 --- a/flexmeasures/data/tests/test_belief_charts.py +++ b/flexmeasures/data/tests/test_belief_charts.py @@ -19,6 +19,7 @@ import pytz from flexmeasures import Sensor +from flexmeasures.data.models.charts.belief_charts import create_line_layer from flexmeasures.data.models.charts.utils import source_legend_label_transformation from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType from flexmeasures.data.models.data_sources import DataSource @@ -942,3 +943,114 @@ def test_validate_sensors_to_show_omits_y_axis_by_default( rows = battery.validate_sensors_to_show() assert len(rows) == 1 assert "y-axis" not in rows[0] + + +# --------------------------------------------------------------------------- +# Tests for create_line_layer interpolation (#2253) +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="function") +def battery_with_interval_sensor_and_soc_min(app, fresh_db): + """Battery asset (public, no owner) with: + + * A real, non-instantaneous sensor (unit ``kWh``, 15-min resolution). + * ``soc-min: "20 kWh"`` in the flex_model, i.e. a fixed-value/constant + sensor, which always has ``event_resolution == timedelta(0)`` by + construction (see ``GenericAsset._create_fixed_value_sensors``). + * ``sensors_to_show`` configured so both appear together in **one row** + (same Vega-Lite layer), reproducing the row shape that triggers #2253. + """ + battery_type = GenericAssetType(name="test_battery_type_for_charts_interval") + fresh_db.session.add(battery_type) + fresh_db.session.flush() + + battery = GenericAsset( + name="Test Battery (interval sensor chart tests)", + generic_asset_type=battery_type, + flex_model={ + "soc-min": "20 kWh", + }, + ) + fresh_db.session.add(battery) + fresh_db.session.flush() + + interval_sensor = Sensor( + name="energy per interval", + unit="kWh", + event_resolution=timedelta(minutes=15), # real, non-instantaneous sensor + generic_asset=battery, + ) + fresh_db.session.add(interval_sensor) + fresh_db.session.flush() + + battery.sensors_to_show = [ + { + "title": None, + "plots": [ + {"sensor": interval_sensor.id}, + {"asset": battery.id, "flex-model": "soc-min"}, + ], + } + ] + fresh_db.session.flush() + + return battery, interval_sensor + + +def test_mixed_row_with_fixed_value_sensor_keeps_step_after( + battery_with_interval_sensor_and_soc_min, +): + """A row mixing a real, non-instantaneous sensor with a fixed-value/constant + sensor must keep step-after interpolation. + + Regression test for #2253: fixed-value sensors (always + ``event_resolution == timedelta(0)``) used to force the whole row to + linear interpolation, even when the real sensor in that row was not + instantaneous. + """ + battery, interval_sensor = battery_with_interval_sensor_and_soc_min + + rows = battery.validate_sensors_to_show() + all_sensors = rows[0]["plots"][0]["sensors"] + + # Sanity-check the fixture: one real (non-instantaneous) sensor plus one + # fixed-value (instantaneous-by-construction) sensor, in the same row. + assert interval_sensor.event_resolution == timedelta(minutes=15) + fixed_value_sensors = [s for s in all_sensors if s.id < 0] + assert len(fixed_value_sensors) == 1 + assert fixed_value_sensors[0].event_resolution == timedelta(0) + + layer = create_line_layer( + all_sensors, + event_start_field_definition={}, + event_value_field_definition={}, + sensor_field_definition={}, + combine_legend=True, + ) + + assert layer["mark"]["interpolate"] == "step-after" + + +def test_real_instantaneous_sensor_still_uses_linear(battery_with_soc_flex_model): + """Guard case: a row containing only a real, instantaneous sensor must + still use linear interpolation (proves the #2253 fix didn't change + behavior for the original, non-mixed case).""" + battery, soc_sensor = battery_with_soc_flex_model + + rows = battery.validate_sensors_to_show() + all_sensors = rows[0]["plots"][0]["sensors"] + real_sensors = [s for s in all_sensors if s.id >= 0] + + assert real_sensors == [soc_sensor] + assert soc_sensor.event_resolution == timedelta(0) + + layer = create_line_layer( + real_sensors, + event_start_field_definition={}, + event_value_field_definition={}, + sensor_field_definition={}, + combine_legend=True, + ) + + assert layer["mark"]["interpolate"] == "linear" From fddcae1cfd5a5a0cdf801b9ea84d101c3657af48 Mon Sep 17 00:00:00 2001 From: Sankalp Thakur Date: Sat, 29 Aug 2026 12:42:36 +0530 Subject: [PATCH 2/2] Move chart interpolation changelog entry to unreleased v1.1.0 Signed-off-by: Sankalp Thakur --- documentation/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 6cc9a43dfc..d44adbeb96 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -42,6 +42,7 @@ Bugfixes * Avoid crashing on startup when the database is stamped with an Alembic revision unknown to this FlexMeasures checkout [see `PR #2465 `_] * The "module not installed" error for an unresolved ``FLEXMEASURES_PLUGINS`` entry now hints at the expected comma-separated format, which helps people who accidentally use an incorrect format like a JSON-array [see `PR #2473 `_] * ``flexmeasures jobs run-job`` ran each job twice, and always as if it were a scheduling job, which lost the queue-specific reporting of why a job failed [see `PR #2480 `_] +* Fix chart interpolation flipping to linear for a row that mixes a real sensor with fixed-value flex-model/flex-context sensors, even when the real sensor itself is not instantaneous [see `PR #2356 `_ and `issue #2253 `_] @@ -163,7 +164,6 @@ Bugfixes * Show icons for more asset types in the UI's asset structure view, which previously fell back to a question mark: the ``wind``, ``process`` and ``heat-storage`` types that FlexMeasures seeds by default, and EV infrastructure under its various names (such as ``one-way_evse``, ``two-way_evse``, ``evse``, ``charging_station`` and ``charging_hub``) and building services equipment (``hvac``, ``ahu``, ``dhw``, ``heatpump``, ``chiller``, ``lighting`` and ``other-loads``). Asset type names are now matched ignoring case and separators, so an asset type named ``charge-point`` gets the same icon as ``chargepoint`` [see `PR #2391 `_] * Replaying a chart for a past window no longer shows annotations that were only recorded later; annotation searches and the ``chart_annotations`` endpoints can now be scoped by recording (belief) time [see `PR #2367 `_] * Continuing the query-parameter cleanup started in PR #2352: the chart-related endpoints now use ``prior``, ``start``, ``end`` and hyphenated field names, with a new ``duration`` field to derive a missing ``start``/``end``; old spellings keep working as legacy aliases [see `PR #2367 `_] -* Fix chart interpolation flipping to linear for a row that mixes a real sensor with fixed-value flex-model/flex-context sensors, even when the real sensor itself is not instantaneous [see `PR #2356 `_ and `issue #2253 `_] * Scheduling jobs no longer print ``Job ... made schedule.`` before ``scheduler.compute()`` runs (only after a successful schedule) [see `PR #2342 `_] * ``flexmeasures add user --roles`` now correctly accepts a comma-separated list of roles (and repeated ``--roles`` options) instead of creating one role whose name contains commas [see `PR #2339 `_] * A ``production-capacity`` or ``consumption-capacity`` that is zero throughout the scheduling window is now enforced strictly, even where device capacity relaxation is in effect, so one-way devices such as heat pumps and one-way chargers can no longer be scheduled in the forbidden direction. A zero covering only part of the window still expresses a preference and remains breachable, so keeping a device idle during part of a schedule (an EV charger during a calendar car reservation, say) works as before [see `PR #2345 `_]