Skip to content
Draft
60 changes: 59 additions & 1 deletion flexmeasures/data/models/planning/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901
if not self.config_deserialized:
self.deserialize_config()

# todo: look for the reason why flex_model has an object(dict) without a sensor, and fix the root cause if possible, instead of filtering it out here
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.

I know why this can happen. It happens when the db flex model of an asset is picked up when its parent asset is being scheduled. That means some devices are included in the optimization with only an asset ID known, and the power sensor ID to save the results is not guaranteed to be known.

Let's discuss what that means for this PR.

if isinstance(self.flex_model, list):
self.flex_model = [
model for model in self.flex_model if model["sensor"] is not None
]

start = self.start
end = self.end
resolution = self.resolution
Expand Down Expand Up @@ -934,6 +940,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901
device_constraints,
ems_constraints,
commitments,
inflexible_device_sensors,
)

def convert_to_commitments(
Expand Down Expand Up @@ -1452,6 +1459,7 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType:
device_constraints,
ems_constraints,
commitments,
inflexible_device_sensors,
) = self._prepare(skip_validation=skip_validation)

# Fallback policy if the problem was unsolvable
Expand Down Expand Up @@ -1578,6 +1586,7 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType:
device_constraints,
ems_constraints,
commitments,
inflexible_device_sensors,
) = self._prepare(skip_validation=skip_validation)

ems_schedule, expected_costs, scheduler_results, model = device_scheduler(
Expand Down Expand Up @@ -1605,6 +1614,16 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType:
elif sensor is not None and sensor in storage_schedule:
storage_schedule[sensor] += ems_schedule[d]

# Obtain the inflexible device schedules
num_flexible_devices = len(sensors)
inflexible_schedules = dict()
for i, inflexible_sensor in enumerate(inflexible_device_sensors):
device_index = num_flexible_devices + i
if inflexible_sensor not in inflexible_schedules:
inflexible_schedules[inflexible_sensor] = ems_schedule[device_index]
else:
inflexible_schedules[inflexible_sensor] += ems_schedule[device_index]

# Obtain the aggregate power schedule, too, if the flex-context states the associated sensor. Fill with the sum of schedules made here.
aggregate_power_sensor = self.flex_context.get("aggregate_power", None)
if isinstance(aggregate_power_sensor, Sensor):
Expand All @@ -1624,6 +1643,18 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType:
if sensor is not None
}

# Convert each inflexible device schedule to the unit of the device's power sensor
inflexible_schedules = {
sensor: convert_units(
inflexible_schedules[sensor],
"MW",
sensor.unit,
event_resolution=sensor.event_resolution,
)
for sensor in inflexible_schedules.keys()
if sensor is not None
}

flex_model = self.flex_model.copy()

if not isinstance(self.flex_model, list):
Expand All @@ -1643,6 +1674,13 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType:
for sensor in storage_schedule.keys()
if sensor is not None
}
inflexible_schedules = {
sensor: inflexible_schedules[sensor]
.resample(sensor.event_resolution)
.mean()
for sensor in inflexible_schedules.keys()
if sensor is not None
}

# Round schedule
if self.round_to_decimals:
Expand All @@ -1651,6 +1689,11 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType:
for sensor in storage_schedule.keys()
if sensor is not None
}
inflexible_schedules = {
sensor: inflexible_schedules[sensor].round(self.round_to_decimals)
for sensor in inflexible_schedules.keys()
if sensor is not None
}
soc_schedule = {
sensor: soc_schedule[sensor].round(self.round_to_decimals)
for sensor in soc_schedule.keys()
Expand All @@ -1667,6 +1710,16 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType:
for sensor in storage_schedule.keys()
if sensor is not None
]
inflexible_device_schedules = [
{
"name": "inflexible_device_schedule",
"sensor": sensor,
"data": inflexible_schedules[sensor],
"unit": sensor.unit,
}
for sensor in inflexible_schedules.keys()
if sensor is not None
]
Comment on lines +1713 to +1722
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.

This also means data/services/scheduling.py ends up saving the inflexible schedules to the sensors where their forecasts were being read from. Does that have added value?

commitment_costs = [
{
"name": "commitment_costs",
Expand All @@ -1688,7 +1741,12 @@ def compute(self, skip_validation: bool = False) -> SchedulerOutputType:
}
for sensor, soc in soc_schedule.items()
]
return storage_schedules + commitment_costs + soc_schedules
return (
storage_schedules
+ inflexible_device_schedules
+ commitment_costs
+ soc_schedules
)
else:
return storage_schedule[sensors[0]]

Expand Down
3 changes: 3 additions & 0 deletions flexmeasures/data/models/planning/tests/test_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ def run_test_charge_discharge_sign(
device_constraints,
ems_constraints,
commitments,
inflexible_device_sensors,
) = scheduler._prepare(skip_validation=True)

planned_power_per_device, planned_costs, results, model = device_scheduler(
Expand Down Expand Up @@ -1192,6 +1193,7 @@ def test_numerical_errors(app_with_each_solver, setup_planning_test_data, db):
device_constraints,
ems_constraints,
commitments,
inflexible_device_sensors,
) = scheduler._prepare(skip_validation=True)

_, _, results, model = device_scheduler(
Expand Down Expand Up @@ -1374,6 +1376,7 @@ def set_if_not_none(dictionary, key, value):
device_constraints,
ems_constraints,
commitments,
inflexible_device_sensors,
) = scheduler._prepare(skip_validation=True)

assert all(device_constraints[0]["derivative min"] == -expected_capacity)
Expand Down
Loading