Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .vscode/spellright.dict
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,4 @@ embeddable
kpis
async
href
setpoints
5 changes: 5 additions & 0 deletions flexmeasures/api/v3_0/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,7 @@ def trigger_schedule(
asset: GenericAsset,
start_of_schedule: datetime,
duration: timedelta,
resolution: timedelta | None = None,
belief_time: datetime | None = None,
flex_model: dict | None = None,
flex_context: dict | None = None,
Expand Down Expand Up @@ -1232,6 +1233,9 @@ def trigger_schedule(
Finally, the schedule length is limited by [a config setting](https://flexmeasures.readthedocs.io/stable/configuration.html#flexmeasures-max-planning-horizon), which defaults to 2520 steps of each sensor's resolution.
Targets that exceed the max planning horizon are not accepted.

The 'resolution' field governs how often setpoints are allowed to change.
Note that the resulting schedule is still saved in the resolution of each individual sensor.

The appropriate algorithm is chosen by FlexMeasures (based on asset type).
It's also possible to use custom schedulers and custom flexibility models, [see plugin_customization](https://flexmeasures.readthedocs.io/stable/plugin/customisation.html#plugin-customization).

Expand Down Expand Up @@ -1361,6 +1365,7 @@ def trigger_schedule(
start=start_of_schedule,
end=end_of_schedule,
belief_time=belief_time, # server time if no prior time was sent
resolution=resolution,
flex_model=flex_model,
flex_context=flex_context,
)
Expand Down
30 changes: 28 additions & 2 deletions flexmeasures/api/v3_0/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@
SensorDataFileSchema,
SensorDataFileDescriptionSchema,
)
from flexmeasures.data.schemas.times import AwareDateTimeField, PlanningDurationField
from flexmeasures.data.schemas.times import (
AwareDateTimeField,
DurationField,
PlanningDurationField,
)
from flexmeasures.data.schemas import AssetIdField
from flexmeasures.api.common.schemas.search import SearchFilterField
from flexmeasures.api.common.schemas.sensors import UnitField
Expand Down Expand Up @@ -108,6 +112,14 @@ class TriggerScheduleKwargsSchema(Schema):
example="PT24H",
),
)
resolution = DurationField(
metadata=dict(
description="The resolution of the requested schedule in ISO 8601 duration format. "
"This governs how often setpoints are allowed to change. "
"Note that the resulting schedule is still saved in the sensor resolution.",
example="PT2H",
)
)
flex_model = fields.Dict(
data_key="flex-model",
metadata=dict(
Expand Down Expand Up @@ -598,6 +610,7 @@ def trigger_schedule(
sensor: Sensor,
start_of_schedule: datetime,
duration: timedelta,
resolution: timedelta | None = None,
belief_time: datetime | None = None,
flex_model: dict | None = None,
flex_context: dict | None = None,
Expand Down Expand Up @@ -633,6 +646,9 @@ def trigger_schedule(
- If the flex-model contains targets that lie beyond the planning horizon, the length of the schedule is extended to accommodate them.
- Finally, the schedule length is limited by the config setting `FLEXMEASURES_MAX_PLANNING_HORIZON`, which defaults to 2520 steps of the sensor's resolution. Targets that exceed the max planning horizon are not accepted.

The 'resolution' field governs how often setpoints are allowed to change.
Note that the resulting schedule is still saved in the sensor resolution.

About the scheduling algorithm being used:

- The appropriate algorithm is chosen by FlexMeasures (based on asset type).
Expand Down Expand Up @@ -774,12 +790,22 @@ def trigger_schedule(
tags:
- Sensors
"""

# Check if resolution is a multiple of the sensor resolution
if (
resolution is not None
and resolution % sensor.event_resolution != timedelta(0)
):
raise ValidationError(
f"Resolution of {resolution} is incompatible with the sensor's required resolution of {sensor.event_resolution}."
)

end_of_schedule = start_of_schedule + duration
scheduler_kwargs = dict(
asset_or_sensor=sensor,
start=start_of_schedule,
end=end_of_schedule,
resolution=sensor.event_resolution,
resolution=resolution or sensor.event_resolution,
belief_time=belief_time, # server time if no prior time was sent
flex_model=flex_model,
flex_context=flex_context,
Expand Down
5 changes: 5 additions & 0 deletions flexmeasures/api/v3_0/tests/test_sensor_schedules.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from flask import url_for
import pytest
from isodate import parse_datetime
import pandas as pd

from rq.job import Job
from sqlalchemy import select
Expand Down Expand Up @@ -207,6 +208,7 @@ def test_get_schedule_fallback(
message = {
"start": start,
"duration": "PT24H",
"resolution": "PT15M", # just schedule in the original sensor resolution
"flex-model": {
"soc-at-start": 10,
"soc-min": charging_station.get_attribute("min_soc_in_mwh", 0),
Expand Down Expand Up @@ -257,6 +259,9 @@ def test_get_schedule_fallback(
# Make sure that the db flex_context shows up in the job kwargs
assert "flex-context" not in message and job.kwargs.get("flex_context")

# Make sure the resolution shows up in the job kwargs
assert job.kwargs.get("resolution") == pd.Timedelta(message["resolution"])

# the callback creates the fallback job which is still pending
assert len(app.queues["scheduling"]) == 1
fallback_job_id = Job.fetch(
Expand Down
13 changes: 10 additions & 3 deletions flexmeasures/api/v3_0/tests/test_sensor_schedules_fresh_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,17 @@
@pytest.mark.parametrize(
"message, asset_name",
[
(message_for_trigger_schedule(), "Test battery"),
(message_for_trigger_schedule(with_targets=True), "Test charging station"),
(message_for_trigger_schedule(use_coarser_resolution=True), "Test battery"),
(
message_for_trigger_schedule(with_targets=True, use_time_window=True),
message_for_trigger_schedule(
use_coarser_resolution=True, with_targets=True
),
"Test charging station",
),
(
message_for_trigger_schedule(
use_coarser_resolution=True, with_targets=True, use_time_window=True
),
"Test charging station",
),
],
Expand Down
9 changes: 6 additions & 3 deletions flexmeasures/api/v3_0/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,18 @@ def message_for_trigger_schedule(
too_far_into_the_future_targets: bool = False,
use_time_window: bool = False,
use_perfect_efficiencies: bool = False,
use_coarser_resolution: bool = False,
) -> dict:
message = {
"start": "2015-01-01T00:00:00+01:00",
"duration": "PT24H", # Will be extended in case of targets that would otherwise lie beyond the schedule's end
}
if use_coarser_resolution:
# The sensor resolution is 15 minutes, so we'll schedule more coarsely here
message["resolution"] = "PT1H"
if unknown_prices:
message["start"] = (
"2040-01-01T00:00:00+01:00" # We have no beliefs in our test database about 2040 prices
)
# We have no beliefs in our test database about 2040 prices
message["start"] = "2040-01-01T00:00:00+01:00"

message["flex-model"] = {
"soc-at-start": 12.1, # in kWh, according to soc-unit
Expand Down
6 changes: 6 additions & 0 deletions flexmeasures/data/models/planning/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,12 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901
elif storage_efficiency[d] is not None:
device_constraints[d]["efficiency"] = storage_efficiency[d]

# Convert efficiency from sensor resolution to scheduling resolution
if sensor_d.event_resolution != timedelta(0):
device_constraints[d]["efficiency"] **= (
resolution / sensor_d.event_resolution
)

# check that storage constraints are fulfilled
if not skip_validation:
constraint_violations = validate_storage_constraints(
Expand Down
14 changes: 13 additions & 1 deletion flexmeasures/data/schemas/scheduling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
)
from flexmeasures.data.schemas.scheduling import metadata
from flexmeasures.utils.doc_utils import rst_to_openapi
from flexmeasures.data.schemas.times import AwareDateTimeField, PlanningDurationField
from flexmeasures.data.schemas.times import (
AwareDateTimeField,
DurationField,
PlanningDurationField,
)
from flexmeasures.data.schemas.utils import FMValidationError
from flexmeasures.utils.flexmeasures_inflection import p
from flexmeasures.utils.unit_utils import (
Expand Down Expand Up @@ -925,6 +929,14 @@ class AssetTriggerSchema(Schema):
example="PT24H",
),
)
resolution = DurationField(
metadata=dict(
description="The resolution of the requested schedule in ISO 8601 duration format. "
"This governs how often setpoints are allowed to change. "
"Note that the resulting schedule is still saved in the resolution of each individual sensor.",
example="PT2H",
)
)
flex_model = fields.List(
fields.Nested(MultiSensorFlexModelSchema()),
data_key="flex-model",
Expand Down
11 changes: 10 additions & 1 deletion flexmeasures/data/services/scheduling.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ def create_simultaneous_scheduling_job(
return job


def make_schedule(
def make_schedule( # noqa C901
sensor_id: int | None = None,
start: datetime | None = None,
end: datetime | None = None,
Expand Down Expand Up @@ -634,6 +634,15 @@ def make_schedule(
for dt, value in result["data"].items()
] # For consumption schedules, positive values denote consumption. For the db, consumption is negative
bdf = tb.BeliefsDataFrame(ts_value_schedule)

# Set the correct event resolution
if resolution is not None and bdf.event_resolution != timedelta(0):
bdf.event_resolution = resolution

# Resample from the scheduling resolution to the sensor resolution
# todo: move this into save_to_db
bdf = bdf.resample_events(bdf.sensor.event_resolution)

save_to_db(bdf)

scheduler.persist_flex_model()
Expand Down
Loading