From f8f44d560eea24eafdd12abeddf88106bcf68b57 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 27 Dec 2024 14:20:18 +0100 Subject: [PATCH 01/19] refactor: separate classes for flow and stock commitments Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/__init__.py | 8 ++++ flexmeasures/data/models/planning/storage.py | 39 +++++++++++-------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/flexmeasures/data/models/planning/__init__.py b/flexmeasures/data/models/planning/__init__.py index fa2233860..002ae2a44 100644 --- a/flexmeasures/data/models/planning/__init__.py +++ b/flexmeasures/data/models/planning/__init__.py @@ -306,6 +306,14 @@ def to_frame(self) -> pd.DataFrame: ) +class FlowCommitment(Commitment): + pass + + +class StockCommitment(Commitment): + pass + + """ Deprecations """ diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 0561a54f7..8bb43c39b 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -11,7 +11,11 @@ from flexmeasures import Sensor -from flexmeasures.data.models.planning import Commitment, Scheduler, SchedulerOutputType +from flexmeasures.data.models.planning import ( + FlowCommitment, + Scheduler, + SchedulerOutputType, +) from flexmeasures.data.models.planning.linear_optimization import device_scheduler from flexmeasures.data.models.planning.utils import ( get_prices, @@ -231,7 +235,8 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) # Set up commitments to optimise for - commitments = [] + flow_commitments = [] + stock_commitments = [] index = initialize_index(start, end, self.resolution) commitment_quantities = initialize_series(0, start, end, self.resolution) @@ -249,14 +254,14 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) # Set up commitments DataFrame - commitment = Commitment( + flow_commitment = FlowCommitment( name="energy", quantity=commitment_quantities, upwards_deviation_price=commitment_upwards_deviation_price, downwards_deviation_price=commitment_downwards_deviation_price, index=index, ) - commitments.append(commitment) + flow_commitments.append(flow_commitment) # Set up peak commitments if self.flex_context.get("ems_peak_consumption_price", None) is not None: @@ -293,7 +298,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) # Set up commitments DataFrame - commitment = Commitment( + flow_commitment = FlowCommitment( name="consumption peak", quantity=ems_peak_consumption, # positive price because breaching in the upwards (consumption) direction is penalized @@ -301,7 +306,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 _type="any", index=index, ) - commitments.append(commitment) + flow_commitments.append(flow_commitment) if self.flex_context.get("ems_peak_production_price", None) is not None: ems_peak_production = get_continuous_series_sensor_or_quantity( variable_quantity=self.flex_context.get("ems_peak_production_in_mw"), @@ -336,7 +341,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) # Set up commitments DataFrame - commitment = Commitment( + flow_commitment = FlowCommitment( name="production peak", quantity=-ems_peak_production, # production is negative quantity # negative price because peaking in the downwards (production) direction is penalized @@ -344,7 +349,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 _type="any", index=index, ) - commitments.append(commitment) + flow_commitments.append(flow_commitment) # Set up capacity breach commitments and EMS capacity constraints ems_consumption_breach_price = self.flex_context.get( @@ -381,7 +386,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) # Set up commitments DataFrame to penalize any breach - commitment = Commitment( + flow_commitment = FlowCommitment( name="any consumption breach", quantity=ems_consumption_capacity, # positive price because breaching in the upwards (consumption) direction is penalized @@ -389,17 +394,17 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 _type="any", index=index, ) - commitments.append(commitment) + flow_commitments.append(flow_commitment) # Set up commitments DataFrame to penalize each breach - commitment = Commitment( + flow_commitment = FlowCommitment( name="all consumption breaches", quantity=ems_consumption_capacity, # positive price because breaching in the upwards (consumption) direction is penalized upwards_deviation_price=ems_consumption_breach_price, index=index, ) - commitments.append(commitment) + flow_commitments.append(flow_commitment) # Take the physical capacity as a hard constraint ems_constraints["derivative max"] = ems_power_capacity_in_mw @@ -430,7 +435,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) # Set up commitments DataFrame to penalize any breach - commitment = Commitment( + flow_commitment = FlowCommitment( name="any production breach", quantity=ems_production_capacity, # positive price because breaching in the upwards (consumption) direction is penalized @@ -438,17 +443,17 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 _type="any", index=index, ) - commitments.append(commitment) + flow_commitments.append(flow_commitment) # Set up commitments DataFrame to penalize each breach - commitment = Commitment( + flow_commitment = FlowCommitment( name="all production breaches", quantity=ems_production_capacity, # positive price because breaching in the upwards (consumption) direction is penalized downwards_deviation_price=-ems_production_breach_price, index=index, ) - commitments.append(commitment) + flow_commitments.append(flow_commitment) # Take the physical capacity as a hard constraint ems_constraints["derivative min"] = -ems_power_capacity_in_mw @@ -663,7 +668,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 soc_at_start, device_constraints, ems_constraints, - commitments, + flow_commitments, ) def persist_flex_model(self): From 96f040cd9e048eedb71639e3ad9e2e5cf4520456 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 27 Dec 2024 14:22:59 +0100 Subject: [PATCH 02/19] feat: set up stock commitment for breaching soc-minima and soc-maxima Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 66 ++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 8bb43c39b..0a2ecdff8 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -15,6 +15,7 @@ FlowCommitment, Scheduler, SchedulerOutputType, + StockCommitment, ) from flexmeasures.data.models.planning.linear_optimization import device_scheduler from flexmeasures.data.models.planning.utils import ( @@ -486,6 +487,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 as_instantaneous_events=True, boundary_policy="first", ) + # todo: check flex-model for soc_minima_breach_price and soc_maxima_breach_price fields; if these are defined, create a StockCommitment using both prices (if only 1 price is given, still create the commitment, but only penalize one direction) if isinstance(soc_minima, Sensor): soc_minima = get_continuous_series_sensor_or_quantity( variable_quantity=soc_minima, @@ -497,6 +499,38 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 as_instantaneous_events=True, boundary_policy="max", ) + if self.flex_model.get("soc_minima_breach_price", None) is not None: + soc_minima_breach_price = self.flex_context.get( + "soc_minima_breach_price" + ) + soc_minima_breach_price = get_continuous_series_sensor_or_quantity( + variable_quantity=soc_minima_breach_price, + actuator=sensor, + unit=( + soc_minima_breach_price.unit + if isinstance(soc_minima_breach_price, Sensor) + else ( + soc_minima_breach_price[0]["value"].units + if isinstance(soc_minima_breach_price, list) + else str(soc_minima_breach_price.units) + ) + ), + query_window=(start, end), + resolution=resolution, + beliefs_before=belief_time, + fallback_attribute="soc-minima-breach-price", + fill_sides=True, + ) + # Set up commitments DataFrame + stock_commitment = StockCommitment( + name="soc minima", + quantity=soc_minima, + # negative price because breaching in the downwards (shortage) direction is penalized + downwards_deviation_price=-soc_minima_breach_price, + _type="any", + index=index, + ) + stock_commitments.append(stock_commitment) if isinstance(soc_maxima, Sensor): soc_maxima = get_continuous_series_sensor_or_quantity( variable_quantity=soc_maxima, @@ -508,6 +542,38 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 as_instantaneous_events=True, boundary_policy="min", ) + if self.flex_model.get("soc_maxima_breach_price", None) is not None: + soc_maxima_breach_price = self.flex_context.get( + "soc_maxima_breach_price" + ) + soc_maxima_breach_price = get_continuous_series_sensor_or_quantity( + variable_quantity=soc_maxima_breach_price, + actuator=sensor, + unit=( + soc_maxima_breach_price.unit + if isinstance(soc_maxima_breach_price, Sensor) + else ( + soc_maxima_breach_price[0]["value"].units + if isinstance(soc_maxima_breach_price, list) + else str(soc_maxima_breach_price.units) + ) + ), + query_window=(start, end), + resolution=resolution, + beliefs_before=belief_time, + fallback_attribute="soc-maxima-breach-price", + fill_sides=True, + ) + # Set up commitments DataFrame + stock_commitment = StockCommitment( + name="soc maxima", + quantity=soc_maxima, + # positive price because breaching in the upwards (surplus) direction is penalized + upwards_deviation_price=soc_maxima_breach_price, + _type="any", + index=index, + ) + stock_commitments.append(stock_commitment) device_constraints[0] = add_storage_constraints( start, From e893cfe8522a501ff039f70e42fc7a8484e6a9d0 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 27 Dec 2024 14:32:38 +0100 Subject: [PATCH 03/19] fix: remove hard constraints when moving to soft constraint Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 0a2ecdff8..ffcf57c46 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -531,6 +531,10 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 index=index, ) stock_commitments.append(stock_commitment) + + # soc-minima will become a soft constraint (modelled as stock commitments), so remove hard constraint + soc_minima = None + if isinstance(soc_maxima, Sensor): soc_maxima = get_continuous_series_sensor_or_quantity( variable_quantity=soc_maxima, @@ -575,6 +579,9 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) stock_commitments.append(stock_commitment) + # soc-maxima will become a soft constraint (modelled as stock commitments), so remove hard constraint + soc_maxima = None + device_constraints[0] = add_storage_constraints( start, end, From 67921ddead3efb80e8c8d9e192cb8bcf2b775449 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Sat, 28 Dec 2024 23:37:32 +0100 Subject: [PATCH 04/19] feat: take into account StockCommitment Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/__init__.py | 1 + .../models/planning/linear_optimization.py | 60 ++++++++++++++++++- flexmeasures/data/models/planning/storage.py | 41 +++++++------ 3 files changed, 80 insertions(+), 22 deletions(-) diff --git a/flexmeasures/data/models/planning/__init__.py b/flexmeasures/data/models/planning/__init__.py index 002ae2a44..d2d4be7e7 100644 --- a/flexmeasures/data/models/planning/__init__.py +++ b/flexmeasures/data/models/planning/__init__.py @@ -301,6 +301,7 @@ def to_frame(self) -> pd.DataFrame: self.upwards_deviation_price, self.downwards_deviation_price, self.group, + pd.Series(self.__class__, index=self.index, name="class"), ], axis=1, ) diff --git a/flexmeasures/data/models/planning/linear_optimization.py b/flexmeasures/data/models/planning/linear_optimization.py index 450a2eb95..ffaa3327c 100644 --- a/flexmeasures/data/models/planning/linear_optimization.py +++ b/flexmeasures/data/models/planning/linear_optimization.py @@ -59,11 +59,12 @@ def device_scheduler( # noqa C901 :param ems_constraints: EMS constraints are on an EMS level. Handled constraints (listed by column name): derivative max: maximum flow derivative min: minimum flow - :param commitments: Commitments are on an EMS level. Handled parameters (listed by column name): + :param commitments: Commitments are on an EMS level by default. Handled parameters (listed by column name): quantity: for example, 5.5 downwards deviation price: 10.1 upwards deviation price: 10.2 group: 1 (defaults to the enumerate time step j) + device: 0 (corresponds to device d; if not set, commitment is on an EMS level) :param initial_stock: initial stock for each device. Use a list with the same number of devices as device_constraints, or use a single value to set the initial stock to be the same for all devices. @@ -443,12 +444,66 @@ def device_down_derivative_sign(m, d, j): def ems_derivative_bounds(m, j): return m.ems_derivative_min[j], sum(m.ems_power[:, j]), m.ems_derivative_max[j] + def device_stock_commitment_equalities(m, c, j, d): + """Couple device stocks to each commitment.""" + if "d" not in commitments[c] or commitments[c]["d"] != d: + # Commitment c does not concern device d + return + if commitments[c]["class"] == "FlowCommitment": + raise NotImplementedError( + "FlowCommitment on a device level has not been implemented. Please file a GitHub ticket explaining your use case." + ) + if isinstance(initial_stock, list): + initial_stock_d = initial_stock[d] + else: + initial_stock_d = initial_stock + + stock_changes = [ + ( + m.device_power_down[d, k] / m.device_derivative_down_efficiency[d, k] + + m.device_power_up[d, k] * m.device_derivative_up_efficiency[d, k] + + m.stock_delta[d, k] + ) + for k in range(0, j + 1) + ] + efficiencies = [m.device_efficiency[d, k] for k in range(0, j + 1)] + return ( + ( + 0 + if len(commitments[c]) == 1 + or "upwards deviation price" in commitments[c].columns + else None + ), + # 0 if "upwards deviation price" in commitments[c].columns else None, # todo: possible simplification + m.commitment_quantity[c] + - [ + stock - initial_stock_d + for stock in apply_stock_changes_and_losses( + initial_stock_d, stock_changes, efficiencies + ) + ][-1], + ( + 0 + if len(commitments[c]) == 1 + or "downwards deviation price" in commitments[c].columns + else None + ), + # 0 if "downwards deviation price" in commitments[c].columns else None, # todo: possible simplification + ) + def ems_flow_commitment_equalities(m, c, j): """Couple EMS flows (sum over devices) to each commitment. - Creates an inequality for one-sided commitments. - Creates an equality for two-sided commitments and for groups of size 1. """ + if "d" in commitments[c]: + # Commitment c does not concern EMS + return + if commitments[c]["class"] == "StockCommitment": + raise NotImplementedError( + "StockCommitment on an EMS level has not been implemented. Please file a GitHub ticket explaining your use case." + ) return ( ( 0 @@ -498,6 +553,9 @@ def device_derivative_equalities(m, d, j): model.ems_power_commitment_equalities = Constraint( model.cj, rule=ems_flow_commitment_equalities ) + model.device_energy_commitment_equalities = Constraint( + model.cj, model.d, rule=device_stock_commitment_equalities + ) model.device_power_equalities = Constraint( model.d, model.j, rule=device_derivative_equalities ) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index ffcf57c46..129d1d3dd 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -236,8 +236,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) # Set up commitments to optimise for - flow_commitments = [] - stock_commitments = [] + commitments = [] index = initialize_index(start, end, self.resolution) commitment_quantities = initialize_series(0, start, end, self.resolution) @@ -255,14 +254,14 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) # Set up commitments DataFrame - flow_commitment = FlowCommitment( + commitment = FlowCommitment( name="energy", quantity=commitment_quantities, upwards_deviation_price=commitment_upwards_deviation_price, downwards_deviation_price=commitment_downwards_deviation_price, index=index, ) - flow_commitments.append(flow_commitment) + commitments.append(commitment) # Set up peak commitments if self.flex_context.get("ems_peak_consumption_price", None) is not None: @@ -299,7 +298,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) # Set up commitments DataFrame - flow_commitment = FlowCommitment( + commitment = FlowCommitment( name="consumption peak", quantity=ems_peak_consumption, # positive price because breaching in the upwards (consumption) direction is penalized @@ -307,7 +306,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 _type="any", index=index, ) - flow_commitments.append(flow_commitment) + commitments.append(commitment) if self.flex_context.get("ems_peak_production_price", None) is not None: ems_peak_production = get_continuous_series_sensor_or_quantity( variable_quantity=self.flex_context.get("ems_peak_production_in_mw"), @@ -342,7 +341,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) # Set up commitments DataFrame - flow_commitment = FlowCommitment( + commitment = FlowCommitment( name="production peak", quantity=-ems_peak_production, # production is negative quantity # negative price because peaking in the downwards (production) direction is penalized @@ -350,7 +349,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 _type="any", index=index, ) - flow_commitments.append(flow_commitment) + commitments.append(commitment) # Set up capacity breach commitments and EMS capacity constraints ems_consumption_breach_price = self.flex_context.get( @@ -387,7 +386,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) # Set up commitments DataFrame to penalize any breach - flow_commitment = FlowCommitment( + commitment = FlowCommitment( name="any consumption breach", quantity=ems_consumption_capacity, # positive price because breaching in the upwards (consumption) direction is penalized @@ -395,17 +394,17 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 _type="any", index=index, ) - flow_commitments.append(flow_commitment) + commitments.append(commitment) # Set up commitments DataFrame to penalize each breach - flow_commitment = FlowCommitment( + commitment = FlowCommitment( name="all consumption breaches", quantity=ems_consumption_capacity, # positive price because breaching in the upwards (consumption) direction is penalized upwards_deviation_price=ems_consumption_breach_price, index=index, ) - flow_commitments.append(flow_commitment) + commitments.append(commitment) # Take the physical capacity as a hard constraint ems_constraints["derivative max"] = ems_power_capacity_in_mw @@ -436,7 +435,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 ) # Set up commitments DataFrame to penalize any breach - flow_commitment = FlowCommitment( + commitment = FlowCommitment( name="any production breach", quantity=ems_production_capacity, # positive price because breaching in the upwards (consumption) direction is penalized @@ -444,17 +443,17 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 _type="any", index=index, ) - flow_commitments.append(flow_commitment) + commitments.append(commitment) # Set up commitments DataFrame to penalize each breach - flow_commitment = FlowCommitment( + commitment = FlowCommitment( name="all production breaches", quantity=ems_production_capacity, # positive price because breaching in the upwards (consumption) direction is penalized downwards_deviation_price=-ems_production_breach_price, index=index, ) - flow_commitments.append(flow_commitment) + commitments.append(commitment) # Take the physical capacity as a hard constraint ems_constraints["derivative min"] = -ems_power_capacity_in_mw @@ -522,7 +521,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 fill_sides=True, ) # Set up commitments DataFrame - stock_commitment = StockCommitment( + commitment = StockCommitment( name="soc minima", quantity=soc_minima, # negative price because breaching in the downwards (shortage) direction is penalized @@ -530,7 +529,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 _type="any", index=index, ) - stock_commitments.append(stock_commitment) + commitments.append(commitment) # soc-minima will become a soft constraint (modelled as stock commitments), so remove hard constraint soc_minima = None @@ -569,7 +568,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 fill_sides=True, ) # Set up commitments DataFrame - stock_commitment = StockCommitment( + commitment = StockCommitment( name="soc maxima", quantity=soc_maxima, # positive price because breaching in the upwards (surplus) direction is penalized @@ -577,7 +576,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 _type="any", index=index, ) - stock_commitments.append(stock_commitment) + commitments.append(commitment) # soc-maxima will become a soft constraint (modelled as stock commitments), so remove hard constraint soc_maxima = None @@ -741,7 +740,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 soc_at_start, device_constraints, ems_constraints, - flow_commitments, + commitments, ) def persist_flex_model(self): From aae0bbf49d07ec7aa58d8a327e44bfe4bff4b8a4 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Sat, 28 Dec 2024 23:41:56 +0100 Subject: [PATCH 05/19] fix: apply StockCommitment to device 0 Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/__init__.py | 6 ++++++ flexmeasures/data/models/planning/storage.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/flexmeasures/data/models/planning/__init__.py b/flexmeasures/data/models/planning/__init__.py index d2d4be7e7..a9b2f8daa 100644 --- a/flexmeasures/data/models/planning/__init__.py +++ b/flexmeasures/data/models/planning/__init__.py @@ -214,6 +214,8 @@ class Commitment: ---------- name: Name of the commitment. + device: + Device to which the commitment pertains. If None, the commitment pertains to the EMS. index: Pandas DatetimeIndex defining the time slots to which the commitment applies. The index is shared by the group, quantity. upwards_deviation_price and downwards_deviation_price Pandas Series. @@ -234,6 +236,7 @@ class Commitment: """ name: str + device: pd.Series = None index: pd.DatetimeIndex = field(repr=False, default=None) _type: str = field(repr=False, default="each") group: pd.Series = field(init=False) @@ -262,6 +265,8 @@ def __post_init__(self): ) # Force type conversion of repr fields to pd.Series + if not isinstance(self.device, pd.Series): + self.device = pd.Series(self.device, index=self.index) if not isinstance(self.quantity, pd.Series): self.quantity = pd.Series(self.quantity, index=self.index) if not isinstance(self.upwards_deviation_price, pd.Series): @@ -284,6 +289,7 @@ def __post_init__(self): raise ValueError('Commitment `_type` must be "any" or "each".') # Name the Series as expected by our device scheduler + self.device = self.device.rename("device") self.quantity = self.quantity.rename("quantity") self.upwards_deviation_price = self.upwards_deviation_price.rename( "upwards deviation price" diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 129d1d3dd..a82d1ce02 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -528,6 +528,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 downwards_deviation_price=-soc_minima_breach_price, _type="any", index=index, + device=0, ) commitments.append(commitment) @@ -575,6 +576,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 upwards_deviation_price=soc_maxima_breach_price, _type="any", index=index, + device=0, ) commitments.append(commitment) From 9eb4a36f67d8f2915b03253318ba3a1c80512af3 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Sat, 28 Dec 2024 23:44:10 +0100 Subject: [PATCH 06/19] fix: comparison and skipping of constraint Signed-off-by: F.N. Claessen --- .../data/models/planning/linear_optimization.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/flexmeasures/data/models/planning/linear_optimization.py b/flexmeasures/data/models/planning/linear_optimization.py index ffaa3327c..e53088c3b 100644 --- a/flexmeasures/data/models/planning/linear_optimization.py +++ b/flexmeasures/data/models/planning/linear_optimization.py @@ -448,8 +448,8 @@ def device_stock_commitment_equalities(m, c, j, d): """Couple device stocks to each commitment.""" if "d" not in commitments[c] or commitments[c]["d"] != d: # Commitment c does not concern device d - return - if commitments[c]["class"] == "FlowCommitment": + return Constraint.Skip + if not all(cl.__name__ == "StockCommitment" for cl in commitments[c]["class"]): raise NotImplementedError( "FlowCommitment on a device level has not been implemented. Please file a GitHub ticket explaining your use case." ) @@ -499,8 +499,10 @@ def ems_flow_commitment_equalities(m, c, j): """ if "d" in commitments[c]: # Commitment c does not concern EMS - return - if commitments[c]["class"] == "StockCommitment": + return Constraint.Skip + if "class" in commitments[c].columns and not all( + cl.__name__ == "FlowCommitment" for cl in commitments[c]["class"] + ): raise NotImplementedError( "StockCommitment on an EMS level has not been implemented. Please file a GitHub ticket explaining your use case." ) From 0957f1670ddc109bdcf4df29b7c413675795e143 Mon Sep 17 00:00:00 2001 From: Tammevesky Date: Tue, 31 Dec 2024 10:29:37 +0100 Subject: [PATCH 07/19] assign deviations from stock position to variable --- flexmeasures/data/models/planning/linear_optimization.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flexmeasures/data/models/planning/linear_optimization.py b/flexmeasures/data/models/planning/linear_optimization.py index e53088c3b..136cbb3ae 100644 --- a/flexmeasures/data/models/planning/linear_optimization.py +++ b/flexmeasures/data/models/planning/linear_optimization.py @@ -476,6 +476,8 @@ def device_stock_commitment_equalities(m, c, j, d): ), # 0 if "upwards deviation price" in commitments[c].columns else None, # todo: possible simplification m.commitment_quantity[c] + + m.commitment_downwards_deviation[c] + + m.commitment_upwards_deviation[c] - [ stock - initial_stock_d for stock in apply_stock_changes_and_losses( From 19d6406326de25e05b7605fb45f5d3b8c97b022e Mon Sep 17 00:00:00 2001 From: Tammevesky Date: Fri, 3 Jan 2025 14:13:38 +0100 Subject: [PATCH 08/19] fix: minor fixes to StockCommitment handling --- flexmeasures/data/models/planning/linear_optimization.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flexmeasures/data/models/planning/linear_optimization.py b/flexmeasures/data/models/planning/linear_optimization.py index 136cbb3ae..f2fe95fba 100644 --- a/flexmeasures/data/models/planning/linear_optimization.py +++ b/flexmeasures/data/models/planning/linear_optimization.py @@ -446,7 +446,10 @@ def ems_derivative_bounds(m, j): def device_stock_commitment_equalities(m, c, j, d): """Couple device stocks to each commitment.""" - if "d" not in commitments[c] or commitments[c]["d"] != d: + if ( + "device" not in commitments[c].columns + or (commitments[c]["device"] != d).all() + ): # Commitment c does not concern device d return Constraint.Skip if not all(cl.__name__ == "StockCommitment" for cl in commitments[c]["class"]): @@ -499,7 +502,7 @@ def ems_flow_commitment_equalities(m, c, j): - Creates an inequality for one-sided commitments. - Creates an equality for two-sided commitments and for groups of size 1. """ - if "d" in commitments[c]: + if "device" in commitments[c].columns: # Commitment c does not concern EMS return Constraint.Skip if "class" in commitments[c].columns and not all( From 6edaac57bdee76262d002cf36563bb7c7b66c06d Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 13 Jan 2025 16:01:19 +0100 Subject: [PATCH 09/19] feat: expose new field flex-model fields Signed-off-by: F.N. Claessen --- flexmeasures/data/schemas/scheduling/storage.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index b8775c6a0..5563d9fe5 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -150,6 +150,21 @@ class StorageFlexModelSchema(Schema): validate=validate.Length(min=1), ) + soc_minima_breach_price = VariableQuantityField( + "/MWh", + data_key="soc-minima-breach-price", + required=False, + value_validator=validate.Range(min=0), + default=None, + ) + soc_maxima_breach_price = VariableQuantityField( + "/MWh", + data_key="soc-maxima-breach-price", + required=False, + value_validator=validate.Range(min=0), + default=None, + ) + def __init__( self, start: datetime, From 1be3431d866f221a1c50383da0857b220ca4924e Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 13 Jan 2025 16:08:51 +0100 Subject: [PATCH 10/19] feat: move price fields to flex-context Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 4 ++-- flexmeasures/data/schemas/scheduling/__init__.py | 16 ++++++++++++++++ flexmeasures/data/schemas/scheduling/storage.py | 15 --------------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 8d148591a..348099444 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -502,7 +502,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 as_instantaneous_events=True, resolve_overlaps="max", ) - if self.flex_model.get("soc_minima_breach_price", None) is not None: + if self.flex_context.get("soc_minima_breach_price", None) is not None: soc_minima_breach_price = self.flex_context.get( "soc_minima_breach_price" ) @@ -550,7 +550,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 as_instantaneous_events=True, resolve_overlaps="min", ) - if self.flex_model.get("soc_maxima_breach_price", None) is not None: + if self.flex_context.get("soc_maxima_breach_price", None) is not None: soc_maxima_breach_price = self.flex_context.get( "soc_maxima_breach_price" ) diff --git a/flexmeasures/data/schemas/scheduling/__init__.py b/flexmeasures/data/schemas/scheduling/__init__.py index 942c0571a..560737c3f 100644 --- a/flexmeasures/data/schemas/scheduling/__init__.py +++ b/flexmeasures/data/schemas/scheduling/__init__.py @@ -15,6 +15,22 @@ class FlexContextSchema(Schema): This schema lists fields that can be used to describe sensors in the optimised portfolio """ + # Device commitments + soc_minima_breach_price = VariableQuantityField( + "/MWh", + data_key="soc-minima-breach-price", + required=False, + value_validator=validate.Range(min=0), + default=None, + ) + soc_maxima_breach_price = VariableQuantityField( + "/MWh", + data_key="soc-maxima-breach-price", + required=False, + value_validator=validate.Range(min=0), + default=None, + ) + # Energy commitments ems_power_capacity_in_mw = VariableQuantityField( "MW", diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index 5563d9fe5..b8775c6a0 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -150,21 +150,6 @@ class StorageFlexModelSchema(Schema): validate=validate.Length(min=1), ) - soc_minima_breach_price = VariableQuantityField( - "/MWh", - data_key="soc-minima-breach-price", - required=False, - value_validator=validate.Range(min=0), - default=None, - ) - soc_maxima_breach_price = VariableQuantityField( - "/MWh", - data_key="soc-maxima-breach-price", - required=False, - value_validator=validate.Range(min=0), - default=None, - ) - def __init__( self, start: datetime, From e87a1dc4dfa7b11abe246668712108046c982be5 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 13 Jan 2025 16:10:15 +0100 Subject: [PATCH 11/19] feat: check compatibility of price units Signed-off-by: F.N. Claessen --- flexmeasures/data/schemas/scheduling/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flexmeasures/data/schemas/scheduling/__init__.py b/flexmeasures/data/schemas/scheduling/__init__.py index 560737c3f..56af3c0b4 100644 --- a/flexmeasures/data/schemas/scheduling/__init__.py +++ b/flexmeasures/data/schemas/scheduling/__init__.py @@ -144,6 +144,8 @@ def check_prices(self, data: dict, **kwargs): if any( field_map[field] in data for field in ( + "soc-minima-breach-price", + "soc-maxima-breach-price", "site-consumption-breach-price", "site-production-breach-price", "site-peak-consumption-price", From 25d21e0e97c749eed808315e835ee51da06002bf Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 13 Jan 2025 16:17:07 +0100 Subject: [PATCH 12/19] fix: param explanation 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 348099444..8e98e173f 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -1115,7 +1115,7 @@ def add_storage_constraints( :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 resolution: Timedelta used to resample the constraints 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. From b17ca057e62e4f1971c1f8662c2e8d11536e9095 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 13 Jan 2025 16:24:48 +0100 Subject: [PATCH 13/19] docs: add note regarding how the SoC breach price depends on the sensor resolution Signed-off-by: F.N. Claessen --- documentation/features/scheduling.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/features/scheduling.rst b/documentation/features/scheduling.rst index d849134ef..e43f8b587 100644 --- a/documentation/features/scheduling.rst +++ b/documentation/features/scheduling.rst @@ -117,6 +117,8 @@ For more details on the possible formats for field values, see :ref:`variable_qu .. [#production] Example: with a connection capacity (``site-power-capacity``) of 1 MVA (apparent power) and a production capacity (``site-production-capacity``) of 400 kW (active power), the scheduler will make sure that the grid inflow doesn't exceed 400 kW. +.. [#soc_breach_prices] The SoC breach prices (e.g. 5 EUR/kWh) to use for the schedule is applied over each time step equal to the sensor resolution. For example, a SoC breach price of 5 EUR/kWh per hour, for scheduling a 5-minute resolution sensor, should be passed as a SoC breach price of :math:`5*60/5 = 12` EUR/kWh. + .. note:: If no (symmetric, consumption and production) site capacity is defined (also not as defaults), the scheduler will not enforce any bound on the site power. The flexible device can still have its own power limit defined in its flex-model. From e619a9627498c5cdebca6decad8547b3d9164ef1 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 13 Jan 2025 16:54:04 +0100 Subject: [PATCH 14/19] docs: add documentation for new fields Signed-off-by: F.N. Claessen --- documentation/features/scheduling.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/documentation/features/scheduling.rst b/documentation/features/scheduling.rst index e43f8b587..c69defc79 100644 --- a/documentation/features/scheduling.rst +++ b/documentation/features/scheduling.rst @@ -104,6 +104,12 @@ For more details on the possible formats for field values, see :ref:`variable_qu * - ``site-peak-production-price`` - ``"260 EUR/MWh"`` - Production peaks above the ``site-peak-production`` are penalized against this per-kW price. [#penalty_field]_ + * - ``soc-minima-breach-price`` + - ``"5 EUR/kWh"`` + - Penalty for not meeting ``soc-minima`` defined in the flex-model. [#penalty_field]_ [#soc_breach_prices]_ + * - ``soc-maxima-breach-price`` + - ``"5 EUR/kWh"`` + - Penalty for not meeting ``soc-maxima`` defined in the flex-model. [#penalty_field]_ [#soc_breach_prices]_ .. [#old_sensor_field] The old field only accepted an integer (sensor ID). @@ -117,7 +123,7 @@ For more details on the possible formats for field values, see :ref:`variable_qu .. [#production] Example: with a connection capacity (``site-power-capacity``) of 1 MVA (apparent power) and a production capacity (``site-production-capacity``) of 400 kW (active power), the scheduler will make sure that the grid inflow doesn't exceed 400 kW. -.. [#soc_breach_prices] The SoC breach prices (e.g. 5 EUR/kWh) to use for the schedule is applied over each time step equal to the sensor resolution. For example, a SoC breach price of 5 EUR/kWh per hour, for scheduling a 5-minute resolution sensor, should be passed as a SoC breach price of :math:`5*60/5 = 12` EUR/kWh. +.. [#soc_breach_prices] The SoC breach prices (e.g. 5 EUR/kWh) to use for the schedule are applied over each time step equal to the sensor resolution. For example, a SoC breach price of 5 EUR/kWh per hour, for scheduling a 5-minute resolution sensor, should be passed as a SoC breach price of :math:`5*60/5 = 12` EUR/kWh. .. note:: If no (symmetric, consumption and production) site capacity is defined (also not as defaults), the scheduler will not enforce any bound on the site power. The flexible device can still have its own power limit defined in its flex-model. From 569e2e540d4f5517432997e693ae929434dcca3d Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 14 Jan 2025 18:29:11 +0100 Subject: [PATCH 15/19] fix: example calculation Signed-off-by: F.N. Claessen --- documentation/features/scheduling.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/features/scheduling.rst b/documentation/features/scheduling.rst index c69defc79..61bf6baa5 100644 --- a/documentation/features/scheduling.rst +++ b/documentation/features/scheduling.rst @@ -105,10 +105,10 @@ For more details on the possible formats for field values, see :ref:`variable_qu - ``"260 EUR/MWh"`` - Production peaks above the ``site-peak-production`` are penalized against this per-kW price. [#penalty_field]_ * - ``soc-minima-breach-price`` - - ``"5 EUR/kWh"`` + - ``"6 EUR/kWh"`` - Penalty for not meeting ``soc-minima`` defined in the flex-model. [#penalty_field]_ [#soc_breach_prices]_ * - ``soc-maxima-breach-price`` - - ``"5 EUR/kWh"`` + - ``"6 EUR/kWh"`` - Penalty for not meeting ``soc-maxima`` defined in the flex-model. [#penalty_field]_ [#soc_breach_prices]_ .. [#old_sensor_field] The old field only accepted an integer (sensor ID). @@ -123,7 +123,7 @@ For more details on the possible formats for field values, see :ref:`variable_qu .. [#production] Example: with a connection capacity (``site-power-capacity``) of 1 MVA (apparent power) and a production capacity (``site-production-capacity``) of 400 kW (active power), the scheduler will make sure that the grid inflow doesn't exceed 400 kW. -.. [#soc_breach_prices] The SoC breach prices (e.g. 5 EUR/kWh) to use for the schedule are applied over each time step equal to the sensor resolution. For example, a SoC breach price of 5 EUR/kWh per hour, for scheduling a 5-minute resolution sensor, should be passed as a SoC breach price of :math:`5*60/5 = 12` EUR/kWh. +.. [#soc_breach_prices] The SoC breach prices (e.g. 6 EUR/kWh) to use for the schedule are applied over each time step equal to the sensor resolution. For example, a SoC breach price of 6 EUR/kWh per hour, for scheduling a 5-minute resolution sensor, should be passed as a SoC breach price of :math:`6*5/60 = 0.50` EUR/kWh. .. note:: If no (symmetric, consumption and production) site capacity is defined (also not as defaults), the scheduler will not enforce any bound on the site power. The flexible device can still have its own power limit defined in its flex-model. From 92318a1a8a8eb317fe0875ead2d3a97772588ab1 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Sat, 18 Jan 2025 13:53:05 +0100 Subject: [PATCH 16/19] fix: wrong indentation Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/storage.py | 136 +++++++++---------- 1 file changed, 66 insertions(+), 70 deletions(-) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 8e98e173f..b67c13b5e 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -502,42 +502,40 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 as_instantaneous_events=True, resolve_overlaps="max", ) - if self.flex_context.get("soc_minima_breach_price", None) is not None: - soc_minima_breach_price = self.flex_context.get( - "soc_minima_breach_price" - ) - soc_minima_breach_price = get_continuous_series_sensor_or_quantity( - variable_quantity=soc_minima_breach_price, - actuator=sensor, - unit=( - soc_minima_breach_price.unit - if isinstance(soc_minima_breach_price, Sensor) - else ( - soc_minima_breach_price[0]["value"].units - if isinstance(soc_minima_breach_price, list) - else str(soc_minima_breach_price.units) - ) - ), - query_window=(start, end), - resolution=resolution, - beliefs_before=belief_time, - fallback_attribute="soc-minima-breach-price", - fill_sides=True, - ) - # Set up commitments DataFrame - commitment = StockCommitment( - name="soc minima", - quantity=soc_minima, - # negative price because breaching in the downwards (shortage) direction is penalized - downwards_deviation_price=-soc_minima_breach_price, - _type="any", - index=index, - device=0, - ) - commitments.append(commitment) + if self.flex_context.get("soc_minima_breach_price", None) is not None: + soc_minima_breach_price = self.flex_context.get("soc_minima_breach_price") + soc_minima_breach_price = get_continuous_series_sensor_or_quantity( + variable_quantity=soc_minima_breach_price, + actuator=sensor, + unit=( + soc_minima_breach_price.unit + if isinstance(soc_minima_breach_price, Sensor) + else ( + soc_minima_breach_price[0]["value"].units + if isinstance(soc_minima_breach_price, list) + else str(soc_minima_breach_price.units) + ) + ), + query_window=(start, end), + resolution=resolution, + beliefs_before=belief_time, + fallback_attribute="soc-minima-breach-price", + fill_sides=True, + ) + # Set up commitments DataFrame + commitment = StockCommitment( + name="soc minima", + quantity=soc_minima, + # negative price because breaching in the downwards (shortage) direction is penalized + downwards_deviation_price=-soc_minima_breach_price, + _type="any", + index=index, + device=0, + ) + commitments.append(commitment) - # soc-minima will become a soft constraint (modelled as stock commitments), so remove hard constraint - soc_minima = None + # soc-minima will become a soft constraint (modelled as stock commitments), so remove hard constraint + soc_minima = None if isinstance(soc_maxima, Sensor): soc_maxima = get_continuous_series_sensor_or_quantity( @@ -550,42 +548,40 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 as_instantaneous_events=True, resolve_overlaps="min", ) - if self.flex_context.get("soc_maxima_breach_price", None) is not None: - soc_maxima_breach_price = self.flex_context.get( - "soc_maxima_breach_price" - ) - soc_maxima_breach_price = get_continuous_series_sensor_or_quantity( - variable_quantity=soc_maxima_breach_price, - actuator=sensor, - unit=( - soc_maxima_breach_price.unit - if isinstance(soc_maxima_breach_price, Sensor) - else ( - soc_maxima_breach_price[0]["value"].units - if isinstance(soc_maxima_breach_price, list) - else str(soc_maxima_breach_price.units) - ) - ), - query_window=(start, end), - resolution=resolution, - beliefs_before=belief_time, - fallback_attribute="soc-maxima-breach-price", - fill_sides=True, - ) - # Set up commitments DataFrame - commitment = StockCommitment( - name="soc maxima", - quantity=soc_maxima, - # positive price because breaching in the upwards (surplus) direction is penalized - upwards_deviation_price=soc_maxima_breach_price, - _type="any", - index=index, - device=0, - ) - commitments.append(commitment) + if self.flex_context.get("soc_maxima_breach_price", None) is not None: + soc_maxima_breach_price = self.flex_context.get("soc_maxima_breach_price") + soc_maxima_breach_price = get_continuous_series_sensor_or_quantity( + variable_quantity=soc_maxima_breach_price, + actuator=sensor, + unit=( + soc_maxima_breach_price.unit + if isinstance(soc_maxima_breach_price, Sensor) + else ( + soc_maxima_breach_price[0]["value"].units + if isinstance(soc_maxima_breach_price, list) + else str(soc_maxima_breach_price.units) + ) + ), + query_window=(start, end), + resolution=resolution, + beliefs_before=belief_time, + fallback_attribute="soc-maxima-breach-price", + fill_sides=True, + ) + # Set up commitments DataFrame + commitment = StockCommitment( + name="soc maxima", + quantity=soc_maxima, + # positive price because breaching in the upwards (surplus) direction is penalized + upwards_deviation_price=soc_maxima_breach_price, + _type="any", + index=index, + device=0, + ) + commitments.append(commitment) - # soc-maxima will become a soft constraint (modelled as stock commitments), so remove hard constraint - soc_maxima = None + # soc-maxima will become a soft constraint (modelled as stock commitments), so remove hard constraint + soc_maxima = None device_constraints[0] = add_storage_constraints( start, From c191a775004d4676b9c69ae135b0222d4d91f88e Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 21 Jan 2025 11:59:26 +0100 Subject: [PATCH 17/19] fix: return NaN quantity in the correct units in case of a missing fallback attribute Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/flexmeasures/data/models/planning/utils.py b/flexmeasures/data/models/planning/utils.py index c14da577f..fdb375dcf 100644 --- a/flexmeasures/data/models/planning/utils.py +++ b/flexmeasures/data/models/planning/utils.py @@ -332,7 +332,7 @@ def idle_after_reaching_target( def get_quantity_from_attribute( entity: Asset | Sensor, - attribute: str, + attribute: str | None, unit: str | ur.Quantity, ) -> ur.Quantity: """Get the value (in the given unit) of a quantity stored as an entity attribute. @@ -342,6 +342,9 @@ def get_quantity_from_attribute( :param unit: The unit in which the value should be returned. :return: The retrieved quantity or the provided default. """ + if attribute is None: + return np.nan * ur.Quantity(unit) # at least return result in the desired unit + # Get the default value from the entity attribute value: str | float | int = entity.get_attribute(attribute, np.nan) From c60b808482a08cee515fdf2363dd9bc9dc72c128 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 25 Feb 2025 17:57:56 +0100 Subject: [PATCH 18/19] dev: test StockCommitment in simultaneous scheduling Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/tests/test_solver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index fe02eefbd..5026b3b92 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -25,6 +25,7 @@ get_sensors_from_db, ) from flexmeasures.data.models.planning.utils import initialize_series, initialize_df +from flexmeasures.data.models.planning import StockCommitment from flexmeasures.data.schemas.sensors import TimedEventSchema from flexmeasures.utils.calculations import ( apply_stock_changes_and_losses, @@ -2633,9 +2634,8 @@ def initialize_combined_commitments(num_devices: int): stock_commitment["downwards deviation price"] = -soc_target_penalty stock_commitment["upwards deviation price"] = soc_target_penalty stock_commitment["group"] = list(range(len(stock_commitment))) - # todo: amend test for https://github.com/FlexMeasures/flexmeasures/pull/1300 - # stock_commitment["device"] = 0 - # stock_commitment["class"] = StockCommitment + stock_commitment["device"] = 0 + stock_commitment["class"] = StockCommitment commitments.append(stock_commitment) return commitments From ec63bb6143ea532543e7e259de01b232e7a9545d Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 6 Mar 2025 10:01:09 +0100 Subject: [PATCH 19/19] fix: merge conflicts relating to soc_maxima/minima for multiple devices 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 5ef2976ef..67a316979 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -553,7 +553,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 # Set up commitments DataFrame commitment = StockCommitment( name="soc minima", - quantity=soc_minima, + quantity=soc_minima[d], # negative price because breaching in the downwards (shortage) direction is penalized downwards_deviation_price=-soc_minima_breach_price, _type="any", @@ -563,7 +563,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 commitments.append(commitment) # soc-minima will become a soft constraint (modelled as stock commitments), so remove hard constraint - soc_minima = None + soc_minima[d] = None if isinstance(soc_maxima[d], Sensor): soc_maxima[d] = get_continuous_series_sensor_or_quantity( @@ -601,7 +601,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 # Set up commitments DataFrame commitment = StockCommitment( name="soc maxima", - quantity=soc_maxima, + quantity=soc_maxima[d], # positive price because breaching in the upwards (surplus) direction is penalized upwards_deviation_price=soc_maxima_breach_price, _type="any", @@ -611,7 +611,7 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901 commitments.append(commitment) # soc-maxima will become a soft constraint (modelled as stock commitments), so remove hard constraint - soc_maxima = None + soc_maxima[d] = None device_constraints[d] = add_storage_constraints( start,