From 8280ee847201454bf1ac1a92d83209d140043619 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 10:32:05 +0200 Subject: [PATCH 01/13] style: sphinx docstrings Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 47 ++++++++++---------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 7f8e6a5d0..752670bfa 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -41,10 +41,8 @@ def compute(self, skip_validation: bool = False) -> pd.Series | None: """Schedule a battery or Charge Point based directly on the latest beliefs regarding market prices within the specified time window. For the resulting consumption schedule, consumption is defined as positive values. - Args: - skip_validation: bool, default=False - whether to skip validation of constraints from data - + :param skip_validation: if True, skip validation of constraints specified in the data + :returns: the computed schedule """ if not self.config_deserialized: self.deserialize_config() @@ -440,22 +438,21 @@ def add_storage_constraints( soc_max: float, soc_min: float, ) -> pd.DataFrame: - """_summary_ - - Args: - storage_device_constraints (pd.DataFrame): _description_ - start (datetime): start of the schedule. - end (datetime): end of the schedule. - resolution (timedelta): timedelta used to resample the forecasts to the resolution of the schedule. - soc_at_start (float): state of charge at the start time. - soc_targets (List[Dict[str, datetime | float]] | pd.Series | None): list of (time : value) pairs - soc_maxima (List[Dict[str, datetime | float]] | pd.Series | None): _description_ - soc_minima (List[Dict[str, datetime | float]] | pd.Series | None): _description_ - soc_max (float): _description_ - soc_min (float): _description_ - - Returns: - pd.DataFrame: _description_ + """Collect all constraints for a given storage device in a DataFrame that the device_scheduler can interpret. + + :param storage_device_constraints: Empty frame without constraints (columns) for a storage device, + but already defining each time step (index). + :param start: Start of the schedule. + :param end: End of the schedule. + :param resolution: Timedelta used to resample the forecasts to the resolution of the schedule. + :param soc_at_start: State of charge at the start time. + :param soc_targets: Exact targets for the state of charge at each time. + :param soc_maxima: Maximum state of charge at each time. + :param soc_minima: Minimum state of charge at each time. + :param soc_max: Maximum state of charge at all times. + :param soc_min: Minimum state of charge at all times. + :returns: Constraints (columns) for a storage device, at each time step (index). + See device_scheduler for possible column names. """ if soc_targets is not None: @@ -529,10 +526,12 @@ def validate_storage_constraints( C.5) condition equals(t) - max(t-1) <= `derivative max`(t) C.6) `derivative min`(t) <= equals(t) - min(t-1) - Args: - storage_constraints: pd.DataFrame - dataframe containing the constraints of a storage device - + :param storage_constraints: dataframe containing the constraints of a storage device + :param soc_at_start: State of charge at the start time. + :param min_soc: Minimum state of charge at all times. + :param max_soc: Maximum state of charge at all times. + :param resolution: Constant duration between the start of each time step. + :returns: List of constraint violations, specifying their time, constraint and violation. """ constraint_violations = [] From af836466075e70fff336f898bd95efa1bec00b4f Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 10:32:58 +0200 Subject: [PATCH 02/13] style: more detailed type annotation Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 752670bfa..d45605fe5 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -508,7 +508,7 @@ def validate_storage_constraints( min_soc: float, max_soc: float, resolution: timedelta, -) -> list: +) -> list[dict]: """Check that the storage constraints are fulfilled, e.g min <= equals <= max. A. Global validation From 8548777ea297bcc6e5bca9435239ed1b84dd4e88 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 10:35:09 +0200 Subject: [PATCH 03/13] style: consistency Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index d45605fe5..9d2182a03 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -41,8 +41,8 @@ def compute(self, skip_validation: bool = False) -> pd.Series | None: """Schedule a battery or Charge Point based directly on the latest beliefs regarding market prices within the specified time window. For the resulting consumption schedule, consumption is defined as positive values. - :param skip_validation: if True, skip validation of constraints specified in the data - :returns: the computed schedule + :param skip_validation: If True, skip validation of constraints specified in the data. + :returns: The computed schedule. """ if not self.config_deserialized: self.deserialize_config() From 8394901419823940bc8eb75cae3d21c73bde9426 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 10:36:03 +0200 Subject: [PATCH 04/13] fix: incomplete type annotation Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 9d2182a03..a0d06b91a 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -326,7 +326,7 @@ def get_min_max_targets( def get_min_max_soc_on_sensor( self, adjust_unit: bool = False, deserialized_names: bool = True - ) -> tuple[float | None]: + ) -> tuple[float | None, float | None]: soc_min_sensor = self.sensor.get_attribute("min_soc_in_mwh", None) soc_max_sensor = self.sensor.get_attribute("max_soc_in_mwh", None) soc_unit_label = "soc_unit" if deserialized_names else "soc-unit" From 59ec68a55e6e00fa7adf4b8080d53da0b6ff9e28 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 10:39:20 +0200 Subject: [PATCH 05/13] fix: validation A.1) Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 8 ++++---- flexmeasures/data/models/planning/tests/test_solver.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index a0d06b91a..fe8af018b 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -512,7 +512,7 @@ def validate_storage_constraints( """Check that the storage constraints are fulfilled, e.g min <= equals <= max. A. Global validation - A.1) min <= min_soc + A.1) min >= min_soc A.2) max <= max_soc B. Validation in the same time frame B.1) min <= max @@ -541,7 +541,7 @@ def validate_storage_constraints( ######################## min_soc = (min_soc - soc_at_start) * timedelta(hours=1) / resolution - # 1) min <= min_soc + # 1) min >= min_soc mask = ~(storage_constraints["min"] >= min_soc) time_condition_fails = storage_constraints.index[mask] @@ -551,8 +551,8 @@ def validate_storage_constraints( constraint_violations.append( dict( dt=dt.to_pydatetime(), - condition="min <= min_soc", - violation=f"min [{value_min}] <= min_soc [{min_soc}]", + condition="min >= min_soc", + violation=f"min [{value_min}] >= min_soc [{min_soc}]", ) ) diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 8b436acae..08a1cb7bd 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -607,7 +607,7 @@ def test_add_storage_constraints( "value_min1, value_equals1, value_max1, value_min2, value_equals2, value_max2, expected_constraint_type_violations", [ (1, np.nan, 9, 2, np.nan, 20, ["max <= max_soc"]), - (-1, np.nan, 9, 1, np.nan, 9, ["min <= min_soc"]), + (-1, np.nan, 9, 1, np.nan, 9, ["min >= min_soc"]), (1, 10, 9, 1, np.nan, 9, ["equals <= max"]), (1, 0, 9, 1, np.nan, 9, ["min <= equals"]), ( From cc532db2867661d1cca882cc7e49aba5b9a81b0c Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 10:53:04 +0200 Subject: [PATCH 06/13] fix: Validation B.2) Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 4 ++-- flexmeasures/data/models/planning/tests/test_solver.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index fe8af018b..d5c747ba2 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -593,7 +593,7 @@ def validate_storage_constraints( ) ) - # 2) min <= equals + # 2) equals >= min mask = ~(storage_constraints["equals"] >= storage_constraints["min"]) mask = mask & ~storage_constraints["equals"].isna() time_condition_fails = storage_constraints.index[mask] @@ -605,7 +605,7 @@ def validate_storage_constraints( constraint_violations.append( dict( dt=dt.to_pydatetime(), - condition="min <= equals", + condition="equals >= min", violation=f"equal [{value_equals}] >= min [{value_min}]", ) ) diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 08a1cb7bd..5fa9dade1 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -609,7 +609,7 @@ def test_add_storage_constraints( (1, np.nan, 9, 2, np.nan, 20, ["max <= max_soc"]), (-1, np.nan, 9, 1, np.nan, 9, ["min >= min_soc"]), (1, 10, 9, 1, np.nan, 9, ["equals <= max"]), - (1, 0, 9, 1, np.nan, 9, ["min <= equals"]), + (1, 0, 9, 1, np.nan, 9, ["equals >= min"]), ( 1, np.nan, @@ -619,7 +619,7 @@ def test_add_storage_constraints( 1, ["min <= max"], ), - (9, 5, 1, 1, np.nan, 9, ["min <= equals", "equals <= max", "min <= max"]), + (9, 5, 1, 1, np.nan, 9, ["equals >= min", "equals <= max", "min <= max"]), (1, np.nan, 9, 1, np.nan, 9, []), # same interval, should not fail (1, np.nan, 9, 3, np.nan, 7, []), # should not fail, containing interval (1, np.nan, 3, 3, np.nan, 5, []), # difference = 0 < 1, should not fail From 8d7d1d0bc8ca2cd4c0f09cc301e9d561536baba4 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 10:57:08 +0200 Subject: [PATCH 07/13] fix: Validation C.1) Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index d5c747ba2..629e0a394 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -655,7 +655,7 @@ def validate_storage_constraints( dict( dt=dt.to_pydatetime(), condition="equals(t) - equals(t-1) <= `derivative max`(t)", - violation=f"equals(t) [{value_equals}] - equals(t-1) [{value_equals_previous}] <= max [{value_derivative_max}]", + violation=f"equals(t) [{value_equals}] - equals(t-1) [{value_equals_previous}] <= `derivative max`(t) [{value_derivative_max}]", ) ) From d5dc5810f211daeef31d1e217a98acddf42c63e8 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 10:59:32 +0200 Subject: [PATCH 08/13] fix: Validation C.3) Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 629e0a394..88c095bf5 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -714,7 +714,7 @@ def validate_storage_constraints( dict( dt=dt.to_pydatetime(), condition="min(t) - max(t-1) <= `derivative max`(t)", - violation=f"min(t) [{value_min}] <= max(t-1) [{value_max_previous}] + `derivative max` [{value_derivative_max}]", + violation=f"min(t) [{value_min}] - max(t-1) [{value_max_previous}] <= `derivative max`(t) [{value_derivative_max}]", ) ) From 97da1c97cc5c0de141c4a814c77383019fa250b1 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 11:00:41 +0200 Subject: [PATCH 09/13] fix: Validation C.4) Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 88c095bf5..221c3283e 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -732,7 +732,7 @@ def validate_storage_constraints( dict( dt=dt.to_pydatetime(), condition="max(t) - min(t-1) >= `derivative min`", - violation=f"max(t) [{value_max}] - min(t-1) [{value_min_previous}]<= `derivative min` [{value_derivative_min}]", + violation=f"max(t) [{value_max}] - min(t-1) [{value_min_previous}] >= `derivative min`(t) [{value_derivative_min}]", ) ) From fa49442cfb1a6b5032058311a573b9d78d063c0b Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 11:05:17 +0200 Subject: [PATCH 10/13] refactor: move statements to where they matter Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 221c3283e..7a704b7c5 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -694,13 +694,10 @@ def validate_storage_constraints( min_extended[storage_constraints.index[0] - resolution] = min_soc min_extended = min_extended.sort_index() + # 3) min(t) - max(t-1) <= `derivative max`(t) delta_min_max = min_extended - max_extended.shift(1) delta_min_max = delta_min_max[1:] - delta_max_min = max_extended - min_extended.shift(1) - delta_max_min = delta_max_min[1:] - - # 3) min(t) - max(t-1) <= `derivative max`(t) condition3 = delta_min_max <= storage_constraints["derivative max"] * factor_w_wh mask = ~condition3 time_condition_fails = storage_constraints.index[mask] @@ -719,6 +716,9 @@ def validate_storage_constraints( ) # 4) max(t) - min(t-1) >= `derivative min`(t) + delta_max_min = max_extended - min_extended.shift(1) + delta_max_min = delta_max_min[1:] + condition4 = delta_max_min >= storage_constraints["derivative min"] * factor_w_wh mask = ~condition4 time_condition_fails = storage_constraints.index[mask] From 47b067b3950531a70e8b72bf36ca72b11ba0720f Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 11:21:33 +0200 Subject: [PATCH 11/13] style: consistency (also: https://books.google.com/ngrams/graph?content=infeasible%2Cunfeasible&year_start=1800&year_end=2019&corpus=en-2019&smoothing=0&case_insensitive=true ) Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 7a704b7c5..b7e4491aa 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -179,7 +179,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 - raise ValueError("The input data yields an unfeasible problem.") + raise ValueError("The input data yields an infeasible problem.") # Set up EMS constraints columns = ["derivative max", "derivative min"] From 63ed923bc3422a93bf80d6082653c208e977d8bd Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 11:24:33 +0200 Subject: [PATCH 12/13] style: Validation B.2) Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 8 ++++---- flexmeasures/data/models/planning/tests/test_solver.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index b7e4491aa..7b7286efa 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -593,8 +593,8 @@ def validate_storage_constraints( ) ) - # 2) equals >= min - mask = ~(storage_constraints["equals"] >= storage_constraints["min"]) + # 2) min <= equals + mask = ~(storage_constraints["min"] <= storage_constraints["equals"]) mask = mask & ~storage_constraints["equals"].isna() time_condition_fails = storage_constraints.index[mask] @@ -605,8 +605,8 @@ def validate_storage_constraints( constraint_violations.append( dict( dt=dt.to_pydatetime(), - condition="equals >= min", - violation=f"equal [{value_equals}] >= min [{value_min}]", + condition="min <= equals", + violation=f"min [{value_min}] <= equal [{value_equals}]", ) ) diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 5fa9dade1..08a1cb7bd 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -609,7 +609,7 @@ def test_add_storage_constraints( (1, np.nan, 9, 2, np.nan, 20, ["max <= max_soc"]), (-1, np.nan, 9, 1, np.nan, 9, ["min >= min_soc"]), (1, 10, 9, 1, np.nan, 9, ["equals <= max"]), - (1, 0, 9, 1, np.nan, 9, ["equals >= min"]), + (1, 0, 9, 1, np.nan, 9, ["min <= equals"]), ( 1, np.nan, @@ -619,7 +619,7 @@ def test_add_storage_constraints( 1, ["min <= max"], ), - (9, 5, 1, 1, np.nan, 9, ["equals >= min", "equals <= max", "min <= max"]), + (9, 5, 1, 1, np.nan, 9, ["min <= equals", "equals <= max", "min <= max"]), (1, np.nan, 9, 1, np.nan, 9, []), # same interval, should not fail (1, np.nan, 9, 3, np.nan, 7, []), # should not fail, containing interval (1, np.nan, 3, 3, np.nan, 5, []), # difference = 0 < 1, should not fail From f0072043ba2a0cf6f7dbf500347a2b953fa8f677 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 25 May 2023 12:43:58 +0200 Subject: [PATCH 13/13] refactor: Constraint validation for cases A and B Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 215 ++++++++---------- .../data/models/planning/tests/test_solver.py | 2 +- 2 files changed, 101 insertions(+), 116 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 7b7286efa..7c194b336 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -167,10 +167,10 @@ def compute(self, skip_validation: bool = False) -> pd.Series | None: ) device_constraints[0]["derivative up efficiency"] = roundtrip_efficiency**0.5 - # check that storage constraints are fullfiled + # check that storage constraints are fulfilled if not skip_validation: constraint_violations = validate_storage_constraints( - storage_constraints=device_constraints[0], + constraints=device_constraints[0], soc_at_start=soc_at_start, min_soc=soc_min, max_soc=soc_max, @@ -503,7 +503,7 @@ def add_storage_constraints( def validate_storage_constraints( - storage_constraints: pd.DataFrame, + constraints: pd.DataFrame, soc_at_start: float, min_soc: float, max_soc: float, @@ -526,7 +526,7 @@ def validate_storage_constraints( C.5) condition equals(t) - max(t-1) <= `derivative max`(t) C.6) `derivative min`(t) <= equals(t) - min(t-1) - :param storage_constraints: dataframe containing the constraints of a storage device + :param constraints: dataframe containing the constraints of a storage device :param soc_at_start: State of charge at the start time. :param min_soc: Minimum state of charge at all times. :param max_soc: Maximum state of charge at all times. @@ -539,94 +539,39 @@ def validate_storage_constraints( ######################## # A. Global validation # ######################## - min_soc = (min_soc - soc_at_start) * timedelta(hours=1) / resolution # 1) min >= min_soc - mask = ~(storage_constraints["min"] >= min_soc) - time_condition_fails = storage_constraints.index[mask] - - for dt in time_condition_fails: - value_min = storage_constraints.loc[dt, "min"] - - constraint_violations.append( - dict( - dt=dt.to_pydatetime(), - condition="min >= min_soc", - violation=f"min [{value_min}] >= min_soc [{min_soc}]", - ) - ) + min_soc = (min_soc - soc_at_start) * timedelta(hours=1) / resolution + constraint_violations += validate_constraint( + constraints, + "min", + ">=", + "min_soc", + right_value=min_soc, + ) # 2) max <= max_soc max_soc = (max_soc - soc_at_start) * timedelta(hours=1) / resolution - - mask = ~(storage_constraints["max"] <= max_soc) - time_condition_fails = storage_constraints.index[mask] - - for dt in time_condition_fails: - value_max = storage_constraints.loc[dt, "max"] - - constraint_violations.append( - dict( - dt=dt.to_pydatetime(), - condition="max <= max_soc", - violation=f"max [{value_max}] <= max_soc [{max_soc}]", - ) - ) + constraint_violations += validate_constraint( + constraints, + "max", + "<=", + "max_soc", + right_value=max_soc, + ) ######################################## # B. Validation in the same time frame # ######################################## # 1) min <= max - mask = ~(storage_constraints["min"] <= storage_constraints["max"]) - time_condition_fails = storage_constraints.index[mask] - - for dt in time_condition_fails: - value_min = storage_constraints.loc[dt, "min"] - value_max = storage_constraints.loc[dt, "max"] - - constraint_violations.append( - dict( - dt=dt.to_pydatetime(), - condition="min <= max", - violation=f"min [{value_min}] <= max [{value_max}]", - ) - ) + constraint_violations += validate_constraint(constraints, "min", "<=", "max") # 2) min <= equals - mask = ~(storage_constraints["min"] <= storage_constraints["equals"]) - mask = mask & ~storage_constraints["equals"].isna() - time_condition_fails = storage_constraints.index[mask] - - for dt in time_condition_fails: - value_equals = storage_constraints.loc[dt, "equals"] - value_min = storage_constraints.loc[dt, "min"] - - constraint_violations.append( - dict( - dt=dt.to_pydatetime(), - condition="min <= equals", - violation=f"min [{value_min}] <= equal [{value_equals}]", - ) - ) + constraint_violations += validate_constraint(constraints, "min", "<=", "equals") # 3) equals <= max - mask = ~(storage_constraints["equals"] <= storage_constraints["max"]) - mask = mask & ~storage_constraints["equals"].isna() - - time_condition_fails = storage_constraints.index[mask] - - for dt in time_condition_fails: - value_equals = storage_constraints.loc[dt, "equals"] - value_max = storage_constraints.loc[dt, "max"] - - constraint_violations.append( - dict( - dt=dt.to_pydatetime(), - condition="equals <= max", - violation=f"equals [{value_equals}] <= max [{value_max}]", - ) - ) + constraint_violations += validate_constraint(constraints, "equals", "<=", "max") ########################################## # C. Validation in different time frames # @@ -635,21 +580,21 @@ def validate_storage_constraints( factor_w_wh = resolution / timedelta(hours=1) # 1) equals(t) - equals(t-1) <= `derivative max`(t) - equals_extended = storage_constraints["equals"].copy() - equals_extended[storage_constraints.index[0] - resolution] = soc_at_start + equals_extended = constraints["equals"].copy() + equals_extended[constraints.index[0] - resolution] = soc_at_start equals_extended = equals_extended.sort_index() diff_equals = equals_extended.diff()[1:] mask = ( - ~(diff_equals <= storage_constraints["derivative max"] * factor_w_wh) + ~(diff_equals <= constraints["derivative max"] * factor_w_wh) & ~diff_equals.isna() ) - time_condition_fails = storage_constraints.index[mask] + time_condition_fails = constraints.index[mask] for dt in time_condition_fails: - value_equals = storage_constraints.loc[dt, "equals"] - value_equals_previous = storage_constraints.loc[dt - resolution, "equals"] - value_derivative_max = storage_constraints.loc[dt, "derivative max"] + value_equals = constraints.loc[dt, "equals"] + value_equals_previous = constraints.loc[dt - resolution, "equals"] + value_derivative_max = constraints.loc[dt, "derivative max"] constraint_violations.append( dict( @@ -660,21 +605,21 @@ def validate_storage_constraints( ) # 2) `derivative min`(t) <= equals(t) - equals(t-1) - equals_extended = storage_constraints["equals"].copy() - equals_extended[storage_constraints.index[0] - resolution] = soc_at_start + equals_extended = constraints["equals"].copy() + equals_extended[constraints.index[0] - resolution] = soc_at_start equals_extended = equals_extended.sort_index() diff_equals = equals_extended.diff()[1:] mask = ( - ~((storage_constraints["derivative min"] * factor_w_wh) <= diff_equals) + ~((constraints["derivative min"] * factor_w_wh) <= diff_equals) & ~diff_equals.isna() ) - time_condition_fails = storage_constraints.index[mask] + time_condition_fails = constraints.index[mask] for dt in time_condition_fails: - value_equals = storage_constraints.loc[dt, "equals"] - value_equals_previous = storage_constraints.loc[dt - resolution, "equals"] - value_derivative_min = storage_constraints.loc[dt, "derivative min"] + value_equals = constraints.loc[dt, "equals"] + value_equals_previous = constraints.loc[dt - resolution, "equals"] + value_derivative_min = constraints.loc[dt, "derivative min"] constraint_violations.append( dict( @@ -685,27 +630,27 @@ def validate_storage_constraints( ) # extend max - max_extended = storage_constraints["max"].copy() - max_extended[storage_constraints.index[0] - resolution] = max_soc + max_extended = constraints["max"].copy() + max_extended[constraints.index[0] - resolution] = max_soc max_extended = max_extended.sort_index() # extend min - min_extended = storage_constraints["min"].copy() - min_extended[storage_constraints.index[0] - resolution] = min_soc + min_extended = constraints["min"].copy() + min_extended[constraints.index[0] - resolution] = min_soc min_extended = min_extended.sort_index() # 3) min(t) - max(t-1) <= `derivative max`(t) delta_min_max = min_extended - max_extended.shift(1) delta_min_max = delta_min_max[1:] - condition3 = delta_min_max <= storage_constraints["derivative max"] * factor_w_wh + condition3 = delta_min_max <= constraints["derivative max"] * factor_w_wh mask = ~condition3 - time_condition_fails = storage_constraints.index[mask] + time_condition_fails = constraints.index[mask] for dt in time_condition_fails: - value_min = storage_constraints.loc[dt, "min"] + value_min = constraints.loc[dt, "min"] value_max_previous = max_extended.loc[dt - resolution] - value_derivative_max = storage_constraints.loc[dt, "derivative max"] + value_derivative_max = constraints.loc[dt, "derivative max"] constraint_violations.append( dict( @@ -719,14 +664,14 @@ def validate_storage_constraints( delta_max_min = max_extended - min_extended.shift(1) delta_max_min = delta_max_min[1:] - condition4 = delta_max_min >= storage_constraints["derivative min"] * factor_w_wh + condition4 = delta_max_min >= constraints["derivative min"] * factor_w_wh mask = ~condition4 - time_condition_fails = storage_constraints.index[mask] + time_condition_fails = constraints.index[mask] for dt in time_condition_fails: - value_max = storage_constraints.loc[dt, "max"] + value_max = constraints.loc[dt, "max"] value_min_previous = min_extended.loc[dt - resolution] - value_derivative_min = storage_constraints.loc[dt, "derivative min"] + value_derivative_min = constraints.loc[dt, "derivative min"] constraint_violations.append( dict( @@ -737,17 +682,17 @@ def validate_storage_constraints( ) # 5) equals(t) - max(t-1) <= `derivative max`(t) - delta_equals_max = storage_constraints["equals"] - max_extended.shift(1) + delta_equals_max = constraints["equals"] - max_extended.shift(1) delta_equals_max = delta_equals_max[1:] - condition5 = delta_equals_max <= storage_constraints["derivative max"] * factor_w_wh - mask = ~condition5 & ~storage_constraints["equals"].isna() - time_condition_fails = storage_constraints.index[mask] + condition5 = delta_equals_max <= constraints["derivative max"] * factor_w_wh + mask = ~condition5 & ~constraints["equals"].isna() + time_condition_fails = constraints.index[mask] for dt in time_condition_fails: - value_equals = storage_constraints.loc[dt, "equals"] + value_equals = constraints.loc[dt, "equals"] value_max_previous = max_extended.loc[dt - resolution] - value_derivative_max = storage_constraints.loc[dt, "derivative max"] + value_derivative_max = constraints.loc[dt, "derivative max"] constraint_violations.append( dict( @@ -758,17 +703,17 @@ def validate_storage_constraints( ) # 6) `derivative min`(t) <= equals(t) - min(t-1) - delta_equals_min = storage_constraints["equals"] - min_extended.shift(1) + delta_equals_min = constraints["equals"] - min_extended.shift(1) delta_equals_min = delta_equals_min[1:] - condition5 = delta_equals_min >= storage_constraints["derivative min"] * factor_w_wh - mask = ~condition5 & ~storage_constraints["equals"].isna() - time_condition_fails = storage_constraints.index[mask] + condition5 = delta_equals_min >= constraints["derivative min"] * factor_w_wh + mask = ~condition5 & ~constraints["equals"].isna() + time_condition_fails = constraints.index[mask] for dt in time_condition_fails: - value_equals = storage_constraints.loc[dt, "equals"] + value_equals = constraints.loc[dt, "equals"] value_min_previous = min_extended.loc[dt - resolution] - value_derivative_min = storage_constraints.loc[dt, "derivative min"] + value_derivative_min = constraints.loc[dt, "derivative min"] constraint_violations.append( dict( @@ -781,6 +726,46 @@ def validate_storage_constraints( return constraint_violations +def validate_constraint( + constraints, + left_constraint_name, + inequality, + right_constraint_name, + left_value: float | None = None, + right_value: float | None = None, +) -> list[dict]: + """Validate the feasibility of a given set of constraints. + + :returns: List of constraint violations, specifying their time, constraint and violation. + """ + mask = True + if left_value is None: + left_value = constraints[left_constraint_name] + mask = mask & ~constraints[left_constraint_name].isna() + if right_value is None: + right_value = constraints[right_constraint_name] + mask = mask & ~constraints[right_constraint_name].isna() + if inequality == "<=": + mask = mask & ~(left_value <= right_value) + elif inequality == ">=": + mask = mask & ~(left_value >= right_value) + else: + raise NotImplementedError(f"Inequality '{inequality}' not supported.") + time_condition_fails = constraints.index[mask] + constraint_violations = [] + for dt in time_condition_fails: + lv = left_value[dt] if isinstance(left_value, pd.Series) else left_value + rv = right_value[dt] if isinstance(right_value, pd.Series) else right_value + constraint_violations.append( + dict( + dt=dt.to_pydatetime(), + condition=f"{left_constraint_name} {inequality} {right_constraint_name}", + violation=f"{left_constraint_name} [{lv}] {inequality} {right_constraint_name} [{rv}]", + ) + ) + return constraint_violations + + ##################### # TO BE DEPRECATED # #################### diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 08a1cb7bd..1581175fb 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -717,7 +717,7 @@ def test_validate_constraints( ] = value_equals2 constraint_violations = validate_storage_constraints( - storage_constraints=storage_device_constraints, + constraints=storage_device_constraints, soc_at_start=0.0, min_soc=0, max_soc=10,