Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
8edbf8a
feat: start test cases for deserializing time series data to BeliefsD…
Flix6x Nov 21, 2025
3b2770f
fix: update type annotation
Flix6x Nov 21, 2025
34a2dc5
feat: handle resampling after converting to a BeliefsDataFrame
Flix6x Nov 21, 2025
8d2d2d3
fix: catch NotImplementedError to fix failing test
Flix6x Nov 21, 2025
86ad774
feat: two more test cases
Flix6x Nov 21, 2025
30fb2ad
fix: update test expectations
Flix6x Nov 21, 2025
7f0caa1
Merge branch 'main' into mohamed/dev/floor-event-start
BelhsanHmida May 1, 2026
6c08284
fix: floor direct POST sensor data starts
BelhsanHmida May 2, 2026
f590e58
fix: floor uploaded sensor data datetimes
BelhsanHmida May 2, 2026
0566ceb
fix: floor flex-model scheduling datetimes
BelhsanHmida May 2, 2026
4afd828
Merge remote-tracking branch 'origin/main' into mohamed/dev/floor-eve…
BelhsanHmida May 5, 2026
a857806
fix: preserve 422 validation for invalid flex-model datetimes
BelhsanHmida May 9, 2026
f30429c
style: apply pre-commit
BelhsanHmida May 9, 2026
3a56e0c
fix: return 422 for invalid flex-model timed-event datetimes
Copilot May 9, 2026
dd2fcc4
test: refine invalid flex-model datetime regression assertion
Copilot May 9, 2026
aa93fe1
test: make flex-model datetime error assertion less brittle
Copilot May 9, 2026
e0f88a8
test: assert error payload shape for invalid flex-model datetime
Copilot May 9, 2026
aaebfaa
refactor: simplify test, incl. by not mixing timezone offsets
Flix6x May 12, 2026
43fe0b5
refactor: rename variable to match variable name in test_trigger_sche…
Flix6x May 12, 2026
9624f09
refactor: use search method over writing custom query
Flix6x May 12, 2026
860c700
feat: test resampling from 5 to 15 minutes
Flix6x May 12, 2026
2228410
docs: document flex-model datetime flooring helper
BelhsanHmida May 15, 2026
0d92e52
test: clarify sensor_index assumptions in upload tests
BelhsanHmida May 15, 2026
b7d3cf0
feat: handle floored downsampling posts
BelhsanHmida May 17, 2026
531d2db
test: cover datetime flooring opt-out
BelhsanHmida May 17, 2026
067bd92
fix: reject flooring event collisions
BelhsanHmida May 17, 2026
fa31e0e
docs: document datetime flooring attribute
BelhsanHmida May 17, 2026
cd4cfd2
docs: add changelog entry
BelhsanHmida May 17, 2026
d4e56bc
Merge branch 'main' into mohamed/dev/floor-event-start
BelhsanHmida May 17, 2026
c66b334
Merge remote-tracking branch 'origin/main' into mohamed/dev/floor-eve…
BelhsanHmida May 21, 2026
d4ee945
docs: align sensor data resampling docs
BelhsanHmida May 21, 2026
61583e3
fix: preserve submitted flex-model in schedule jobs
BelhsanHmida May 22, 2026
2eceadf
feat: keep storage soc event times in schema
BelhsanHmida May 22, 2026
1d37e4f
feat: project off-tick soc constraints to grid
BelhsanHmida May 22, 2026
a7b01a6
test: cover off-tick soc target projection
BelhsanHmida May 22, 2026
7dea2a7
fix: relax off-tick soc constraints by default
BelhsanHmida May 22, 2026
e7b9ee1
test: cover off-tick soc relaxation
BelhsanHmida May 22, 2026
03cf1e2
fix: preserve non-event soc constraints
BelhsanHmida May 22, 2026
9e26e03
fix: normalize off-tick soc on schedule grid
BelhsanHmida May 22, 2026
9e4e709
test: cover off-tick soc bounds
BelhsanHmida May 22, 2026
32a3d2b
test: tolerate upload conversion precision
BelhsanHmida May 22, 2026
10fc632
test: compare quantities by value
BelhsanHmida May 22, 2026
56ad4eb
test: avoid exact quantity string comparison
BelhsanHmida May 22, 2026
9268de8
fix: skip flooring for instantaneous sensors
BelhsanHmida May 26, 2026
7c0c9b4
test: fetch upload sensors by name
BelhsanHmida May 26, 2026
baf5543
Update flexmeasures/api/v3_0/tests/test_sensor_data.py
BelhsanHmida May 26, 2026
8b2bb7e
Update flexmeasures/api/v3_0/tests/test_sensor_data.py
BelhsanHmida May 27, 2026
6fd5cec
Merge remote-tracking branch 'origin/mohamed/dev/floor-event-start' i…
BelhsanHmida May 28, 2026
f1a852e
Merge remote-tracking branch 'origin/main' into feat/offtick-soc-norm…
BelhsanHmida Jun 4, 2026
26a8e22
docs: update changelog for soc projection
BelhsanHmida Jun 5, 2026
95e5922
docs: clarify soc projection terminology
BelhsanHmida Jun 5, 2026
8b07cfa
refactor: rename soc normalization to projection
BelhsanHmida Jun 5, 2026
6568284
test: explain soc projection assertions
BelhsanHmida Jun 5, 2026
2a3309b
Update flexmeasures/data/models/planning/storage.py
BelhsanHmida Jun 5, 2026
0ab3ccd
docs: explain soc constraint projection policy
BelhsanHmida Jun 5, 2026
26c85a6
fix: support missing soc bounds in projection
BelhsanHmida Jun 5, 2026
dc2bf9a
refactor: move off-tick soc detection to scheduling schema
BelhsanHmida Jun 5, 2026
b3b2d2b
refactor: make soc projection policy explicit
BelhsanHmida Jun 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: handle floored downsampling posts
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
  • Loading branch information
BelhsanHmida committed May 17, 2026
commit b7d3cf0251851202eef73baa01a309f7c157cf3f
27 changes: 23 additions & 4 deletions flexmeasures/api/common/schemas/sensor_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,8 @@ def check_resolution_compatibility_of_sensor_data(self, data, **kwargs):

For a sensor recording instantaneous values, any event frequency is compatible.
For a sensor recording non-instantaneous values, the event frequency must fit the sensor's event resolution.
Currently, only upsampling is supported (e.g. converting hourly events to 15-minute events).
Upsampling and downsampling are supported when the inferred resolution and
the sensor resolution are multiples of each other.
"""
required_resolution = data["sensor"].event_resolution

Expand All @@ -380,7 +381,9 @@ def check_resolution_compatibility_of_sensor_data(self, data, **kwargs):
# The event frequency is inferred by assuming sequential, equidistant values within a time interval.
# The event resolution is assumed to be equal to the event frequency.
inferred_resolution = data["duration"] / len(data["values"])
if inferred_resolution % required_resolution != timedelta(hours=0):
if inferred_resolution % required_resolution != timedelta(
hours=0
) and required_resolution % inferred_resolution != timedelta(hours=0):
raise ValidationError(
f"Resolution of {inferred_resolution} is incompatible with the sensor's required resolution of {required_resolution}."
)
Expand All @@ -407,6 +410,7 @@ def post_load_sequence(self, data: dict, **kwargs) -> dict[str, BeliefsDataFrame
data = self.possibly_upsample_values(data)
data = self.possibly_convert_units(data)
bdf = self.load_bdf(data)
bdf = self.possibly_downsample_bdf(bdf, data["sensor"].event_resolution)

# Post-load validation against message type
_type = data.get("type", None)
Expand All @@ -429,7 +433,7 @@ def possibly_convert_units(data):
data["values"],
from_unit=data["unit"],
to_unit=data["sensor"].unit,
event_resolution=data["sensor"].event_resolution,
event_resolution=data["duration"] / len(data["values"]),
)
return data

Expand Down Expand Up @@ -457,6 +461,21 @@ def possibly_upsample_values(data):
)
return data

@staticmethod
def possibly_downsample_bdf(
bdf: BeliefsDataFrame, required_resolution: timedelta
) -> BeliefsDataFrame:
"""
Downsample the data if needed, to fit the sensor's resolution.
Marshmallow runs this after validation.
"""
if required_resolution == timedelta(hours=0):
return bdf

if bdf.event_resolution < required_resolution:
bdf = bdf.resample_events(required_resolution)
return bdf

@staticmethod
def load_bdf(sensor_data: dict) -> BeliefsDataFrame:
"""
Expand All @@ -469,7 +488,7 @@ def load_bdf(sensor_data: dict) -> BeliefsDataFrame:
sensor = sensor_data["sensor"]

if sensor.event_resolution != timedelta(0) and sensor.get_attribute(
"round_datetimes_on_ingestion", True
"floor_datetimes_to_resolution", True
):
start = pd.Timestamp(start).floor(sensor.event_resolution)
elif frequency := sensor.get_attribute("frequency"):
Expand Down
55 changes: 41 additions & 14 deletions flexmeasures/api/v3_0/tests/test_sensor_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,10 @@ def test_post_sensor_data_bad_auth(
("start", "2021-06-07T00:00:00", "start", "Not a valid aware datetime"),
(
"duration",
"PT30M",
"PT25M",
"_schema",
"Resolution of 0:05:00 is incompatible",
), # downsampling not supported
"Resolution of 0:04:10 is incompatible",
),
("unit", "m", "_schema", "Required unit"),
("type", "GetSensorDataRequest", "type", "Must be one of"),
],
Expand Down Expand Up @@ -243,14 +243,40 @@ def test_post_invalid_sensor_data(
@pytest.mark.parametrize(
"requesting_user", ["test_supplier_user_4@seita.nl"], indirect=True
)
@pytest.mark.parametrize(
"offclock_start, precise_start, precise_end, values, expected_values",
[
(
"2021-06-08T00:00:40+02:00",
"2021-06-08T00:00:00+02:00",
"2021-06-08T01:00:00+02:00",
[-11.28] * 6,
[-11.28] * 6,
),
(
"2021-06-09T00:00:40+02:00",
"2021-06-09T00:00:00+02:00",
"2021-06-09T01:00:00+02:00",
[100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200],
[150, 350, 550, 750, 950, 1150],
),
],
)
def test_post_non_instantaneous_sensor_data_floor(
client, setup_api_test_data, requesting_user
client,
setup_api_test_data,
requesting_user,
offclock_start,
precise_start,
precise_end,
values,
expected_values,
):
offclock_start = "2021-06-08T00:00:40+02:00"
precise_start = "2021-06-08T00:00:00+02:00"
precise_end = "2021-06-08T01:00:00+02:00"
post_data = make_sensor_data_request_for_gas_sensor(unit="m³/h")
post_data = make_sensor_data_request_for_gas_sensor(
num_values=len(values), unit="m³/h"
)
post_data["start"] = offclock_start
post_data["values"] = values
sensor = setup_api_test_data["some gas sensor"]

assert (
Expand All @@ -267,13 +293,14 @@ def test_post_non_instantaneous_sensor_data_floor(
new_data = sensor.search_beliefs(precise_start, precise_end).reset_index()
assert len(new_data) == 6
assert list(new_data["event_start"]) == [
pd.Timestamp("2021-06-08 00:00:00+02"),
pd.Timestamp("2021-06-08 00:10:00+02"),
pd.Timestamp("2021-06-08 00:20:00+02"),
pd.Timestamp("2021-06-08 00:30:00+02"),
pd.Timestamp("2021-06-08 00:40:00+02"),
pd.Timestamp("2021-06-08 00:50:00+02"),
pd.Timestamp(precise_start),
pd.Timestamp(precise_start) + pd.Timedelta(minutes=10),
pd.Timestamp(precise_start) + pd.Timedelta(minutes=20),
pd.Timestamp(precise_start) + pd.Timedelta(minutes=30),
pd.Timestamp(precise_start) + pd.Timedelta(minutes=40),
pd.Timestamp(precise_start) + pd.Timedelta(minutes=50),
]
assert new_data["event_value"].to_list() == expected_values


@pytest.mark.parametrize(
Expand Down