Skip to content
60 changes: 59 additions & 1 deletion flexmeasures/data/models/planning/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,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
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 @@ -935,6 +941,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 @@ -1247,6 +1254,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 @@ -1315,6 +1323,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 @@ -1342,6 +1351,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 @@ -1361,6 +1380,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 Down Expand Up @@ -1397,6 +1428,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 @@ -1405,6 +1443,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 @@ -1421,6 +1464,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
]
commitment_costs = [
{
"name": "commitment_costs",
Expand All @@ -1442,7 +1495,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 @@ -259,6 +259,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 @@ -1188,6 +1189,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 @@ -1370,6 +1372,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