diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index dc0549517..c97c1d1e8 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -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 + 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 @@ -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( @@ -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 @@ -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( @@ -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): @@ -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): @@ -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: @@ -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() @@ -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 + ] commitment_costs = [ { "name": "commitment_costs", @@ -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]] diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index d31aa7c82..dbd18d8a0 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -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( @@ -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( @@ -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)