diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index f5cf84200b..38a89c5c59 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -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 @@ -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( @@ -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 @@ -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( @@ -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): @@ -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): @@ -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: @@ -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() @@ -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", @@ -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]] diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 919936d315..ad279a63cf 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -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( @@ -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( @@ -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)