diff --git a/client/client.gen.go b/client/client.gen.go index cf50a8c0f..b5debd028 100644 --- a/client/client.gen.go +++ b/client/client.gen.go @@ -74,6 +74,16 @@ type BatteryConfig struct { // PDemand Minimum charge demand per time step (Wh) PDemand []float32 `json:"p_demand,omitempty"` + // PrcDplSocHigh Price in €/h for higher battery life depletion when sitting at high SOC. + // A linear cost model is applied, starting at 80% SOC, applying full price at 100% SOC + // The battery capacity is assumed to be s_capacity or s_max if s_capacity is not specified + PrcDplSocHigh float32 `json:"prc_dpl_soc_high,omitempty"` + + // PrcDplSocLow Price in €/h for higher battery life depletion when sitting at low SOC. + // A linear cost model is applied, starting at 20% SOC, applying full price at 0% SOC + // The battery capacity is assumed to be s_capacity or s_max if s_capacity is not specified + PrcDplSocLow float32 `json:"prc_dpl_soc_low,omitempty"` + // SCapacity The capacity at 100% SOC in Wh. If not specified s_capacity will be set to s_max. // s_initial must be less or equal s_capacity, otherwise the optimization will return an error. SCapacity float32 `json:"s_capacity,omitempty"` diff --git a/openapi.yaml b/openapi.yaml index 7447c542e..27d4f5555 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -269,7 +269,20 @@ components: maximum: 2 default: 0 description: Charging and discharging priority 0..2 compared to other batteries. 2 = highest priority. - + prc_dpl_soc_high: + type: number + minimum: 0 + description: | + Price in €/h for higher battery life depletion when sitting at high SOC. + A linear cost model is applied, starting at 80% SOC, applying full price at 100% SOC + The battery capacity is assumed to be s_capacity or s_max if s_capacity is not specified + prc_dpl_soc_low: + type: number + minimum: 0 + description: | + Price in €/h for higher battery life depletion when sitting at low SOC. + A linear cost model is applied, starting at 20% SOC, applying full price at 0% SOC + The battery capacity is assumed to be s_capacity or s_max if s_capacity is not specified TimeSeries: type: object required: diff --git a/src/optimizer/app.py b/src/optimizer/app.py index 860d1b406..a799e8aa1 100644 --- a/src/optimizer/app.py +++ b/src/optimizer/app.py @@ -77,7 +77,9 @@ def handle_validation_error(error): 'c_max': fields.Float(required=True, description='Maximum charge power (W)'), 'd_max': fields.Float(required=True, description='Maximum discharge power (W)'), 'p_a': fields.Float(required=True, description='Monetary value per Wh at end of the optimization horizon'), - 'c_priority': fields.Integer(required=False, description='Charging and discharging priority compared to other batteries. 2 = highest priority.') + 'c_priority': fields.Integer(required=False, description='Charging and discharging priority compared to other batteries. 2 = highest priority.'), + 'prc_dpl_soc_high': fields.Float(required=False, description='Price in €/h for higher battery life depletion when sitting at high SOC.'), + 'prc_dpl_soc_low': fields.Float(required=False, description='Price in €/h for higher battery life depletion when sitting at low SOC.') }) time_series_model = api.model('TimeSeries', { @@ -168,6 +170,8 @@ def post(self): d_max=bat_data['d_max'], p_a=bat_data['p_a'], c_priority=bat_data.get('c_priority', 0), + prc_dpl_soc_high=bat_data.get('prc_dpl_soc_high', 0.0), + prc_dpl_soc_low=bat_data.get('prc_dpl_soc_low', 0.0) )) # Parse time series data diff --git a/src/optimizer/optimizer.py b/src/optimizer/optimizer.py index d03026e12..441b48e5c 100644 --- a/src/optimizer/optimizer.py +++ b/src/optimizer/optimizer.py @@ -36,6 +36,8 @@ class BatteryConfig: p_demand: Optional[List[float]] = None # Minimum charge demand (Wh) s_goal: Optional[List[float]] = None # Goal state of charge (Wh) c_priority: int = 0 + prc_dpl_soc_high: float = 0.0 + prc_dpl_soc_low: float = 0.0 @dataclass @@ -213,6 +215,14 @@ def _setup_variables(self): for t in self.time_steps ] + # Cost variable to charge for battery depletion at very high and very low battery SOC state + self.variables['cst_bat_dpl'] = {} + for i, bat in enumerate(self.batteries): + self.variables['cst_bat_dpl'][i] = [ + pulp.LpVariable(f"cst_bat_dpl_{i}_{t}", lowBound=0) + for t in self.time_steps + ] + def _setup_target_function(self): """ Gather all target function contributions and instantiate the objective @@ -254,6 +264,11 @@ def _setup_target_function(self): if self.is_grid_demand_rate_active: objective += - self.grid.prc_p_exc_imp * self.variables['p_max_imp_exc'] + # cost for depleting battery life by sitting at very high or very low SOC + for i, bat in enumerate(self.batteries): + for t in self.time_steps: + objective += - self.variables['cst_bat_dpl'][i][t] + ############################################################################ # Penalties for exceeding battery SOC limits at start for i, bat in enumerate(self.batteries): @@ -480,6 +495,25 @@ def _add_battery_constraints(self): # Charge constraint self.problem += self.variables['c'][i][t] <= self.M * (1 - self.variables['z_cd'][i][t]) + # cost for depleting battery life by sitting at very high or very low SOC + # figure out battery capacity, avoid div 0 in case s_amx is set to 0 + bat_capacity = self.batteries[i].s_max + if self.batteries[i].s_capacity is not None and self.batteries[i].s_capacity > 0.0: + bat_capacity = self.batteries[i].s_capacity + bat_capacity = max(bat_capacity, 1.0) + for t in self.time_steps: + # scale the prices to the time step width + prc_dpl_soc_high = self.batteries[i].prc_dpl_soc_high / 3600.0 * self.time_series.dt[t] + prc_dpl_soc_low = self.batteries[i].prc_dpl_soc_low / 3600.0 * self.time_series.dt[t] + # for all SOCs + self.problem += self.variables['cst_bat_dpl'][i][t] >= 0 + # for high SOCs, linear cost ramp starting at 80% SOC, full cost at 100% + self.problem += self.variables['cst_bat_dpl'][i][t] \ + - ((prc_dpl_soc_high / (0.2 * bat_capacity)) * (self.variables['s'][i][t] - 0.8 * bat_capacity)) >= 0 + # for low SOCs, linear cost ramp starting at 20% SOC, full cost at 0% + self.problem += self.variables['cst_bat_dpl'][i][t] \ + - ((prc_dpl_soc_low) * (1.0 - (self.variables['s'][i][t] / (0.2 * bat_capacity)))) >= 0 + def solve(self) -> Dict: """ Creates the MILP model if none exists and solves the optimization problem. diff --git a/test_cases/022-cost-for-bat-life-depletion-at-extreme-SOC.json b/test_cases/022-cost-for-bat-life-depletion-at-extreme-SOC.json new file mode 100644 index 000000000..fcb58eb04 --- /dev/null +++ b/test_cases/022-cost-for-bat-life-depletion-at-extreme-SOC.json @@ -0,0 +1,751 @@ +{ + "request": { + "batteries": [ + { + "c_max": 11040, + "c_min": 1380, + "charge_from_grid": true, + "d_max": 0, + "p_a": 0.0002896641, + "s_initial": 1577.9, + "s_max": 17900, + "s_min": 0 + }, + { + "c_max": 1200, + "c_min": 0, + "d_max": 800, + "p_a": 0.0002896641, + "s_initial": 3187.2, + "s_max": 3763.2, + "s_min": 384, + "prc_dpl_soc_high": 0.02, + "prc_dpl_soc_low": 0.02 + } + ], + "eta_c": 0.9, + "eta_d": 0.9, + "strategy": { + "charging_strategy": "charge_before_export", + "discharging_strategy": "discharge_before_import" + }, + "time_series": { + "dt": [ + 1634, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600, + 3600 + ], + "ft": [ + 3.775227, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 11.1, + 259.15, + 1105.675, + 2319.25, + 3386.375, + 4134.9, + 4625.8, + 4869.575, + 4702.05, + 4097.275, + 3185.025, + 1976.225, + 722.525, + 82.175, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 8.075, + 188.325, + 960.95, + 2198.2, + 3392.05, + 4356.125, + 4965.6, + 5130.85, + 4858.975, + 4158.075, + 3037.05, + 1755.425, + 0 + ], + "gt": [ + 205.80344, + 303.26315, + 229.11836, + 203.44693, + 197.63982, + 201.09181, + 197.18556, + 193.33128, + 201.50006, + 216.00891, + 331.9448, + 364.72153, + 436.75186, + 487.87656, + 622.73224, + 525.58954, + 517.96594, + 506.94574, + 475.90045, + 486.6551, + 415.6253, + 460.8452, + 497.76898, + 437.7715, + 441.00735, + 303.26315, + 229.11836, + 203.44693, + 197.63982, + 201.09181, + 197.18556, + 193.33128, + 201.50006, + 216.00891, + 331.9448, + 364.72153, + 436.75186, + 487.87656, + 622.73224, + 525.58954, + 517.96594, + 506.94574, + 475.90045, + 486.6551, + 415.6253, + 460.8452, + 497.76898, + 437.7715 + ], + "p_E": [ + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012, + 0.00012 + ], + "p_N": [ + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251, + 0.0003251 + ] + } + }, + "expected_response": { + "status": "Optimal", + "objective_value": 8.660289109878299, + "limit_violations": { + "grid_import_limit_exceeded": false, + "grid_export_limit_hit": false + }, + "batteries": [ + { + "charging_power": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1696.5178, + 2860.7855, + 2516.3325, + 4118.8543, + 4393.6746, + 2549.5021, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "discharging_power": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "state_of_charge": [ + 1577.9, + 1577.9, + 1577.9, + 1577.9, + 1577.9, + 1577.9, + 1577.9, + 1577.9, + 1577.9, + 1577.9, + 1577.9, + 1577.9, + 1577.9, + 1577.9, + 3104.766, + 5679.4729, + 7944.1721, + 11651.141, + 15605.448, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0, + 17900.0 + ] + }, + { + "charging_power": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 617.79844, + 0.0, + 0.0, + 1100.6016, + 0.0, + 0.0, + 1200.0, + 0.0, + 0.0, + 551.51317, + 284.7535, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 473.07344, + 1200.0, + 1200.0, + 45.32656, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 836.26667, + 0.0 + ], + "discharging_power": [ + 202.02821, + 303.26315, + 229.11836, + 203.44693, + 197.63982, + 201.09181, + 197.18556, + 193.33128, + 201.50006, + 216.00891, + 46.489907, + 154.17414, + 177.60186, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 358.83235, + 303.26315, + 229.11836, + 203.44693, + 197.63982, + 201.09181, + 197.18556, + 193.33128, + 201.50006, + 216.00891, + 331.9448, + 159.49011, + 248.42686, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 437.7715 + ], + "state_of_charge": [ + 2962.7242, + 2625.7652, + 2371.1892, + 2145.1371, + 1925.5373, + 1702.1019, + 1483.0068, + 1268.1943, + 1044.3054, + 804.29545, + 752.64, + 581.3354, + 384.0, + 940.0186, + 940.0186, + 940.0186, + 1930.56, + 1930.56, + 1930.56, + 3010.56, + 3010.56, + 3010.56, + 3506.9219, + 3763.2, + 3364.4974, + 3027.5383, + 2772.9624, + 2546.9102, + 2327.3104, + 2103.8751, + 1884.78, + 1669.9675, + 1446.0785, + 1206.0686, + 837.24108, + 660.02984, + 384.0, + 809.7661, + 1889.7661, + 2969.7661, + 3010.56, + 3010.56, + 3010.56, + 3010.56, + 3010.56, + 3010.56, + 3763.2, + 3276.7872 + ] + } + ], + "grid_import": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 285.45489, + 199.44739, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 197.15642, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "grid_export": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 465.89276, + 3681.6497, + 2724.1798, + 926.94285, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 375.46776, + 1666.4605, + 3792.8325, + 4458.6543, + 4654.9495, + 4372.3199, + 3742.4497, + 2576.2048, + 421.38935, + 0.0 + ], + "flow_direction": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0 + ], + "grid_import_overshoot": [], + "grid_export_overshoot": [] + } +} \ No newline at end of file