diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 88c29ee2f..63722c737 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -3,6 +3,15 @@ FlexMeasures Changelog ********************** +v0.14.2 | July 21, 2023 +============================ + +Bugfixes +----------- + +* The error handling for infeasible constraints in storage.py was given too many arguments. This caused the response from the API to be unhelpful when a schedule was requested with infeasible constraints. [see `PR #758 `_] + + v0.14.1 | June 26, 2023 ============================ diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index a46f5a88b..d3bb52fb5 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -186,9 +186,7 @@ def compute(self, skip_validation: bool = False) -> pd.Series | None: if len(constraint_violations) > 0: # TODO: include hints from constraint_violations into the error message - message = create_constraint_violations_message( - constraint_violations, self.round_to_decimals - ) + message = create_constraint_violations_message(constraint_violations) raise ValueError( "The input data yields an infeasible problem. Constraint validation has found the following issues:\n" + message diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 0f7fba07e..ec16608c5 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -57,9 +57,7 @@ def test_storage_loss_function( def test_battery_solver_day_1( add_battery_assets, add_inflexible_device_forecasts, use_inflexible_device ): - epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none() - battery = Sensor.query.filter(Sensor.name == "Test battery").one_or_none() - assert battery.get_attribute("market_id") == epex_da.id + epex_da, battery = get_sensors_from_db() tz = pytz.timezone("Europe/Amsterdam") start = tz.localize(datetime(2015, 1, 1)) end = tz.localize(datetime(2015, 1, 2)) @@ -118,9 +116,7 @@ def test_battery_solver_day_2( and so we expect the scheduler to only: - completely discharge within the last 8 hours """ - epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none() - battery = Sensor.query.filter(Sensor.name == "Test battery").one_or_none() - assert battery.get_attribute("market_id") == epex_da.id + _epex_da, battery = get_sensors_from_db() tz = pytz.timezone("Europe/Amsterdam") start = tz.localize(datetime(2015, 1, 2)) end = tz.localize(datetime(2015, 1, 3)) @@ -490,9 +486,7 @@ def test_soc_bounds_timeseries(add_battery_assets): """ # get the sensors from the database - epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none() - battery = Sensor.query.filter(Sensor.name == "Test battery").one_or_none() - assert battery.get_attribute("market_id") == epex_da.id + epex_da, battery = get_sensors_from_db() # time parameters tz = pytz.timezone("Europe/Amsterdam") @@ -792,3 +786,61 @@ def test_validate_constraints( ) assert set(expected_constraint_type_violations) == constraint_type_violations_output + + +def test_infeasible_problem_error(add_battery_assets): + """Try to create a schedule with infeasible constraints. soc-max is 4.5 and soc-target is 8.0""" + + # get the sensors from the database + _epex_da, battery = get_sensors_from_db() + + # time parameters + tz = pytz.timezone("Europe/Amsterdam") + start = tz.localize(datetime(2015, 1, 2)) + end = tz.localize(datetime(2015, 1, 3)) + resolution = timedelta(hours=1) + + def compute_schedule(flex_model): + scheduler = StorageScheduler( + battery, + start, + end, + resolution, + flex_model=flex_model, + ) + schedule = scheduler.compute() + + soc_schedule = integrate_time_series( + schedule, + soc_at_start, + decimal_precision=1, + ) + + return soc_schedule + + # soc parameters + soc_at_start = battery.get_attribute("soc_in_mwh") + infeasible_max_soc_targets = [ + {"datetime": "2015-01-02T16:00:00+01:00", "value": 8.0} + ] + + flex_model = { + "soc-at-start": soc_at_start, + "soc-min": 0.5, + "soc-max": 4.5, + "soc-targets": infeasible_max_soc_targets, + } + + with pytest.raises( + ValueError, match="The input data yields an infeasible problem." + ): + compute_schedule(flex_model) + + +def get_sensors_from_db(): + # get the sensors from the database + epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none() + battery = Sensor.query.filter(Sensor.name == "Test battery").one_or_none() + assert battery.get_attribute("market_id") == epex_da.id + + return epex_da, battery