From 7a54d629d3b529888e16154b72f099e98f427e1f Mon Sep 17 00:00:00 2001 From: ekkea Date: Mon, 20 Jul 2026 22:36:11 +0200 Subject: [PATCH 01/11] fixed scaling in result diagram axis for power --- tests/testbench.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/testbench.py b/tests/testbench.py index 4de724a32..e9d57be69 100644 --- a/tests/testbench.py +++ b/tests/testbench.py @@ -127,16 +127,17 @@ dt0 = dt0[:-1] ts_time = np.cumsum(dt0) ts_period = np.array(dt) + ts_Wh_to_kW = np.divide(3.6, ts_input["dt"]) ts_prc_import = np.array(ts_input["p_N"])*1000 ts_prc_export = np.array(ts_input["p_E"])*1000 ts_solar_raw = ts_input["ft"] ts_demand_raw = ts_input["gt"] ts_grid_import_raw = response.json["grid_import"] ts_grid_export_raw = response.json["grid_export"] - ts_solar = np.divide(ts_input["ft"], ts_input["dt"]) - ts_demand = np.negative(np.divide(ts_input["gt"], ts_input["dt"])) - ts_grid = np.divide(np.subtract(response.json["grid_import"], response.json["grid_export"]), ts_input["dt"]) - ts_grid_exp = np.divide(np.subtract(expected_response["grid_import"], expected_response["grid_export"]), ts_input["dt"]) + ts_solar = np.multiply(ts_input["ft"], ts_Wh_to_kW) + ts_demand = np.negative(np.multiply(ts_input["gt"], ts_Wh_to_kW)) + ts_grid = np.multiply(np.subtract(response.json["grid_import"], response.json["grid_export"]), ts_Wh_to_kW) + ts_grid_exp = np.multiply(np.subtract(expected_response["grid_import"], expected_response["grid_export"]), ts_Wh_to_kW) ts_grid_dev = np.divide(np.subtract(ts_grid, ts_grid_exp), ts_grid_exp) # create the table dataframe @@ -182,11 +183,11 @@ bat_capacity = request["batteries"][i]["s_max"] if "s_capacity" in request["batteries"][i]: bat_capacity = request["batteries"][i]["s_capacity"] - df_diagram[f"P_bat{i}"] = np.divide(np.subtract(bat["discharging_power"], bat["charging_power"]), ts_input["dt"]) + df_diagram[f"P_bat{i}"] = np.multiply(np.subtract(bat["discharging_power"], bat["charging_power"]), ts_Wh_to_kW) df_diagram[f"SOC_bat{i}"] = np.divide(bat["state_of_charge"], bat_capacity)*100 - df_diagram[f"P_bat{i}_exp"] = np.divide(np.subtract(expected_response["batteries"][i]["discharging_power"], + df_diagram[f"P_bat{i}_exp"] = np.multiply(np.subtract(expected_response["batteries"][i]["discharging_power"], expected_response["batteries"][i]["charging_power"]), - ts_input["dt"]) + ts_Wh_to_kW) df_diagram[f"P_bat{i}_dev"] = np.divide(np.subtract(df_diagram[f"P_bat{i}"], df_diagram[f"P_bat{i}_exp"]), df_diagram[f"P_bat{i}_exp"]) df_diagram['time'] = pd.to_datetime(df_diagram['time'], unit='s') From a6575317fb18d9567c9b2656231bd60f5bb4b5d5 Mon Sep 17 00:00:00 2001 From: ekkea Date: Mon, 20 Jul 2026 22:37:08 +0200 Subject: [PATCH 02/11] fixed: charging power can undercut c_min if p_demand is set --- src/optimizer/optimizer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/optimizer/optimizer.py b/src/optimizer/optimizer.py index b878ec8e6..5ae681e7f 100644 --- a/src/optimizer/optimizer.py +++ b/src/optimizer/optimizer.py @@ -449,8 +449,9 @@ def _add_battery_constraints(self): for t in self.time_steps: if bat.p_demand[t] > 0: # clip required charge to max charging power if needed - # and leave some air to breathe for the optimizer p_demand = min(bat.c_max * self.time_series.dt[t] / 3600., bat.p_demand[t]) + # clip required charge to min charging power where needed + p_demand = max(bat.c_min * self.time_series.dt[t] / 3600., bat.p_demand[t]) # two alternative constraints, only one is active: # constraint option 1: charge energy tries to reach min charge energy parameter self.problem += (self.variables['c'][i][t] + self.variables['p_demand_pen'][i][t] From 9a4ca3cd9eafc68ffc0ba51eded58753e8e8653b Mon Sep 17 00:00:00 2001 From: ekkea Date: Mon, 20 Jul 2026 22:37:25 +0200 Subject: [PATCH 03/11] added test case --- ...23-c_min-limit-kept-with-p_demand-set.json | 606 ++++++++++++++++++ 1 file changed, 606 insertions(+) create mode 100644 test_cases/023-c_min-limit-kept-with-p_demand-set.json diff --git a/test_cases/023-c_min-limit-kept-with-p_demand-set.json b/test_cases/023-c_min-limit-kept-with-p_demand-set.json new file mode 100644 index 000000000..14fc0818a --- /dev/null +++ b/test_cases/023-c_min-limit-kept-with-p_demand-set.json @@ -0,0 +1,606 @@ +{ + "request": { + "batteries": [ + { + "c_max": 3680, + "c_min": 1380, + "charge_from_grid": true, + "discharge_to_grid": false, + "d_max": 0, + "p_a": 0.0, + "s_initial": 1000, + "s_max": 36000, + "s_min": 0, + "p_demand": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 500, + 600, + 700, + 800, + 900, + 1100, + 1300, + 1400, + 1800, + 2200, + 3000, + 4000, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + }, + { + "c_max": 2000, + "c_min": 0, + "charge_from_grid": true, + "discharge_to_grid": false, + "d_max": 4000, + "p_a": 8e-05, + "s_initial": 2000, + "s_max": 16000, + "s_min": 0 + } + ], + "eta_c": 0.9, + "eta_d": 0.9, + "strategy": { + "charging_strategy": "charge_before_export" + }, + "time_series": { + "dt": [ + 31, + 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": [ + 49.185734, + 5448.66, + 4804.3555, + 3832.6248, + 2577.8723, + 1275.6185, + 318.86014, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1.4505436, + 191.96257, + 775.6313, + 1712.4897, + 3146.5142, + 4718.1426, + 5451.648, + 5263.8657, + 4958.835, + 4296.22, + 3276.9275, + 2289.251, + 1140.1532, + 279.22818, + 0, + 0, + 0, + 0 + ], + "gt": [ + 8.579221, + 491.24472, + 593.9072, + 521.609, + 474.0209, + 431.15894, + 542.4711, + 459.93445, + 385.35126, + 374.0456, + 314.81265, + 276.28183, + 252.70245, + 240.12154, + 245.36407, + 220.05013, + 194.45601, + 253.75883, + 380.507, + 449.2002, + 572.2506, + 587.3133, + 592.0432, + 512.08167, + 514.75323, + 491.24472, + 593.9072, + 521.609, + 474.0209, + 431.15894, + 542.4711, + 459.93445, + 385.35126, + 374.0456, + 314.81265 + ], + "p_E": [ + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05, + 6.43e-05 + ], + "p_N": [ + -8.6e-07, + -9.9e-07, + -1e-08, + 4.099e-05, + 8.76e-05, + 0.00011647, + 0.00015839, + 0.00013954, + 0.00010995, + 9.295e-05, + 8.256e-05, + 8.972e-05, + 8.502e-05, + 8.077e-05, + 8.34e-05, + 8.292e-05, + 8.267e-05, + 8.15e-05, + 5.548e-05, + 0, + -9e-08, + -4e-06, + -2.009e-05, + -4.002e-05, + -5.34e-05, + -4.999e-05, + -2.121e-05, + -2.47e-06, + 3.36e-06, + 9.591e-05, + 0.0001083, + 9.625e-05, + 8.602e-05, + 8.501e-05, + 8.606e-05 + ] + } + }, + "expected_response": { + "status": "Optimal", + "objective_value": 0.5532084584479662, + "limit_violations": { + "grid_import_limit_exceeded": false, + "grid_export_limit_hit": false + }, + "batteries": [ + { + "charging_power": [ + 31.688889, + 3680.0, + 3680.0, + 3680.0, + 1988.3111, + 0.0, + 0.0, + 1380.0, + 1380.0, + 1380.0, + 1380.0, + 1380.0, + 1380.0, + 1380.0, + 1400.0, + 1800.0, + 2200.0, + 3000.0, + 3680.0, + 0.0, + 3680.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 + ], + "state_of_charge": [ + 1028.52, + 4340.52, + 7652.52, + 10964.52, + 12754.0, + 12754.0, + 12754.0, + 13996.0, + 15238.0, + 16480.0, + 17722.0, + 18964.0, + 20206.0, + 21448.0, + 22708.0, + 24328.0, + 26308.0, + 29008.0, + 32320.0, + 32320.0, + 35632.0, + 35632.0, + 35632.0, + 35632.0, + 35632.0, + 35632.0, + 35632.0, + 35632.0, + 35632.0, + 35632.0, + 35632.0, + 35632.0, + 35632.0, + 35632.0, + 35632.0 + ] + }, + { + "charging_power": [ + 17.222222, + 2000.0, + 2000.0, + 2000.0, + 115.54029, + 844.45956, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 2000.0, + 2000.0, + 2000.0, + 0.0, + 0.0, + 1068.7835, + 2000.0, + 2000.0, + 2000.0, + 2000.0, + 2000.0, + 708.99426, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "discharging_power": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 223.61096, + 1839.9345, + 1765.3513, + 1754.0456, + 0.0, + 1656.2818, + 212.32578, + 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, + 263.24292, + 459.93445, + 0.0, + 0.0, + 0.0 + ], + "state_of_charge": [ + 2015.5, + 3815.5, + 5615.5, + 7415.5, + 7519.4863, + 8279.4999, + 8031.0432, + 5986.6716, + 4025.1702, + 2076.2307, + 2076.2307, + 235.91753, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1800.0, + 3600.0, + 5400.0, + 5400.0, + 5400.0, + 6361.9052, + 8161.9052, + 9961.9052, + 11761.905, + 13561.905, + 15361.905, + 16000.0, + 15707.508, + 15196.47, + 15196.47, + 15196.47, + 15196.47 + ] + } + ], + "grid_import": [ + 8.3045981, + 722.58472, + 1469.5517, + 2368.9842, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1694.8127, + 0.0, + 1420.3767, + 1620.1215, + 1645.3641, + 2020.0501, + 2394.456, + 3252.3083, + 5868.5444, + 1673.5689, + 4539.7609, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 184.7699, + 0.0, + 0.0, + 0.0, + 385.35126, + 374.0456, + 314.81265 + ], + "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, + 0.0, + 0.0, + 2559.2009, + 4126.0994, + 3870.7828, + 2749.1125, + 2467.5903, + 1702.3128, + 755.3185, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "flow_direction": [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + "grid_import_overshoot": [], + "grid_export_overshoot": [] + } +} \ No newline at end of file From dfbdd3f4686b931ac49dee9a29417583e1527f0a Mon Sep 17 00:00:00 2001 From: ekkea Date: Mon, 20 Jul 2026 22:39:48 +0200 Subject: [PATCH 04/11] linted and improved doc. --- src/optimizer/optimizer.py | 4 ++-- tests/testbench.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/optimizer/optimizer.py b/src/optimizer/optimizer.py index 5ae681e7f..77dfdc2a3 100644 --- a/src/optimizer/optimizer.py +++ b/src/optimizer/optimizer.py @@ -448,9 +448,9 @@ def _add_battery_constraints(self): if bat.p_demand is not None: for t in self.time_steps: if bat.p_demand[t] > 0: - # clip required charge to max charging power if needed + # clip requested charging power to max charging power if needed p_demand = min(bat.c_max * self.time_series.dt[t] / 3600., bat.p_demand[t]) - # clip required charge to min charging power where needed + # clip requested charging power to min charging power where needed p_demand = max(bat.c_min * self.time_series.dt[t] / 3600., bat.p_demand[t]) # two alternative constraints, only one is active: # constraint option 1: charge energy tries to reach min charge energy parameter diff --git a/tests/testbench.py b/tests/testbench.py index e9d57be69..8307629f2 100644 --- a/tests/testbench.py +++ b/tests/testbench.py @@ -186,8 +186,8 @@ df_diagram[f"P_bat{i}"] = np.multiply(np.subtract(bat["discharging_power"], bat["charging_power"]), ts_Wh_to_kW) df_diagram[f"SOC_bat{i}"] = np.divide(bat["state_of_charge"], bat_capacity)*100 df_diagram[f"P_bat{i}_exp"] = np.multiply(np.subtract(expected_response["batteries"][i]["discharging_power"], - expected_response["batteries"][i]["charging_power"]), - ts_Wh_to_kW) + expected_response["batteries"][i]["charging_power"]), + ts_Wh_to_kW) df_diagram[f"P_bat{i}_dev"] = np.divide(np.subtract(df_diagram[f"P_bat{i}"], df_diagram[f"P_bat{i}_exp"]), df_diagram[f"P_bat{i}_exp"]) df_diagram['time'] = pd.to_datetime(df_diagram['time'], unit='s') From 0bfe8bc194753f447e1a327afaa8a1c94d1de840 Mon Sep 17 00:00:00 2001 From: ekkea Date: Mon, 20 Jul 2026 22:57:23 +0200 Subject: [PATCH 05/11] improved test case, revealing there is a problem left --- ...23-c_min-limit-kept-with-p_demand-set.json | 120 +++++++++--------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/test_cases/023-c_min-limit-kept-with-p_demand-set.json b/test_cases/023-c_min-limit-kept-with-p_demand-set.json index 14fc0818a..946157e3d 100644 --- a/test_cases/023-c_min-limit-kept-with-p_demand-set.json +++ b/test_cases/023-c_min-limit-kept-with-p_demand-set.json @@ -21,8 +21,6 @@ 0, 500, 600, - 700, - 800, 900, 1100, 1300, @@ -31,7 +29,9 @@ 2200, 3000, 4000, - 0, + 4000, + 4000, + 4000, 0, 0, 0, @@ -256,7 +256,7 @@ }, "expected_response": { "status": "Optimal", - "objective_value": 0.5532084584479662, + "objective_value": 0.24398841080996625, "limit_violations": { "grid_import_limit_exceeded": false, "grid_export_limit_hit": false @@ -268,23 +268,23 @@ 3680.0, 3680.0, 3680.0, - 1988.3111, - 0.0, - 0.0, - 1380.0, + 2103.8514, 1380.0, + 0.0, 1380.0, 1380.0, 1380.0, 1380.0, 1380.0, 1400.0, - 1800.0, + 3064.4597, 2200.0, 3000.0, 3680.0, + 1936.8421, + 1019.3906, + 536.52136, 0.0, - 3680.0, 0.0, 0.0, 0.0, @@ -342,37 +342,37 @@ 4340.52, 7652.52, 10964.52, - 12754.0, - 12754.0, - 12754.0, - 13996.0, - 15238.0, - 16480.0, - 17722.0, - 18964.0, - 20206.0, - 21448.0, - 22708.0, + 12857.986, + 14099.986, + 14099.986, + 15341.986, + 16583.986, + 17825.986, + 19067.986, + 20309.986, + 21569.986, 24328.0, 26308.0, 29008.0, 32320.0, - 32320.0, - 35632.0, - 35632.0, - 35632.0, - 35632.0, - 35632.0, - 35632.0, - 35632.0, - 35632.0, - 35632.0, - 35632.0, - 35632.0, - 35632.0, - 35632.0, - 35632.0, - 35632.0 + 34063.158, + 34980.609, + 35463.479, + 35463.479, + 35463.479, + 35463.479, + 35463.479, + 35463.479, + 35463.479, + 35463.479, + 35463.479, + 35463.479, + 35463.479, + 35463.479, + 35463.479, + 35463.479, + 35463.479, + 35463.479 ] }, { @@ -381,8 +381,8 @@ 2000.0, 2000.0, 2000.0, - 115.54029, - 844.45956, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -419,14 +419,14 @@ 0.0, 0.0, 0.0, - 0.0, + 535.54044, 223.61096, 1839.9345, 1765.3513, 1754.0456, 0.0, - 1656.2818, - 212.32578, + 555.46729, + 0.0, 0.0, 0.0, 0.0, @@ -455,14 +455,14 @@ 3815.5, 5615.5, 7415.5, - 7519.4863, - 8279.4999, - 8031.0432, - 5986.6716, - 4025.1702, - 2076.2307, - 2076.2307, - 235.91753, + 7415.5, + 6820.4551, + 6571.9984, + 4527.6268, + 2566.1254, + 617.18588, + 617.18588, + 0.0, 0.0, 0.0, 0.0, @@ -501,16 +501,16 @@ 0.0, 0.0, 1694.8127, - 0.0, - 1420.3767, - 1620.1215, - 1645.3641, - 2020.0501, - 2394.456, - 3252.3083, - 5868.5444, - 1673.5689, - 4539.7609, + 1100.8145, + 1652.7024, + 3304.5813, + 2445.3641, + 3220.0501, + 3874.456, + 2189.1504, + 3207.935, + 2210.0903, + 859.7609, 0.0, 0.0, 0.0, From 7f970f4584e206c9b013f37f5af4f722cb9723c6 Mon Sep 17 00:00:00 2001 From: ekkea Date: Tue, 21 Jul 2026 10:41:08 +0200 Subject: [PATCH 06/11] bug fixed but some existing test cases will now fail ... due to different charging behavior if p_demand is set of a battery. --- src/optimizer/optimizer.py | 37 ++-- ...23-c_min-limit-kept-with-p_demand-set.json | 190 +++++++++--------- 2 files changed, 113 insertions(+), 114 deletions(-) diff --git a/src/optimizer/optimizer.py b/src/optimizer/optimizer.py index 77dfdc2a3..d03380e2b 100644 --- a/src/optimizer/optimizer.py +++ b/src/optimizer/optimizer.py @@ -278,9 +278,10 @@ def _setup_target_function(self): # unmet charging demand due to battery reaching maximum SOC with incentive to do charging early if bat.p_demand is not None: for t in self.time_steps: - objective += - self.prc_p_goal_pen \ - * self.variables['p_demand_pen'][i][t] \ - * (1 + (self.T - t)/self.T) + objective += - self.variables['p_demand_pen'][i][t] * 0.1 * self.time_series.p_N[t] * (1 + (self.T - t)/self.T) + #- self.prc_p_goal_pen \ + # * self.variables['p_demand_pen'][i][t] \ + # * (1 + (self.T - t)/self.T) # penalties for grid power limits that cannot be met. for t in self.time_steps: @@ -450,24 +451,22 @@ def _add_battery_constraints(self): if bat.p_demand[t] > 0: # clip requested charging power to max charging power if needed p_demand = min(bat.c_max * self.time_series.dt[t] / 3600., bat.p_demand[t]) - # clip requested charging power to min charging power where needed - p_demand = max(bat.c_min * self.time_series.dt[t] / 3600., bat.p_demand[t]) - # two alternative constraints, only one is active: - # constraint option 1: charge energy tries to reach min charge energy parameter - self.problem += (self.variables['c'][i][t] + self.variables['p_demand_pen'][i][t] - + self.M * self.variables['z_p_demand'][i][t] >= p_demand) - # constraint option 2: charge energy tries to reach energy to fill the battery to s_max - self.problem += (self.variables['c'][i][t] + self.variables['p_demand_pen'][i][t] - + self.M * (1 - self.variables['z_p_demand'][i][t]) - - (self.batteries[i].s_max - self.variables['s'][i][t]) >= 0.) - elif bat.c_min > 0: - # in time steps without given charging demand, apply normal lower bound: - # Lower bound: either 0 or at least c_min - self.problem += (self.variables['c'][i][t] >= bat.c_min * self.time_series.dt[t] / 3600. - * self.variables['z_c'][i][t]) + + # set a "soft" constraint to reach the requested charging rate if possible + self.problem += (self.variables['c'][i][t] + self.variables['p_demand_pen'][i][t] >= p_demand) + + if bat.c_min > 0: + # introduce z_p_demand to become only 1 if s_max is almost reached and the + # charging with c_min would stop before the end of the time slot + self.problem += ((bat.s_max - self.variables['s'][i][t]) + <= (1 - self.variables['z_p_demand'][i][t]) * self.M + + bat.c_min * self.time_series.dt[t] / 3600.) + # set constraints to exclude charging between 0 and c_min if z_p_demand is not 1. + self.problem += (self.variables['c'][i][t] + self.variables['z_p_demand'][i][t] *self.M + >= bat.c_min * self.time_series.dt[t] / 3600. * self.variables['z_c'][i][t]) self.problem += (self.variables['c'][i][t] <= self.M * self.variables['z_c'][i][t]) - # Constraint (7): Minimum charge power limits if there is not charge demand + # Constraint (7): Minimum charge power limits if there is no charge demand elif bat.c_min > 0: for t in self.time_steps: # Lower bound: either 0 or at least c_min diff --git a/test_cases/023-c_min-limit-kept-with-p_demand-set.json b/test_cases/023-c_min-limit-kept-with-p_demand-set.json index 946157e3d..e8bc2b241 100644 --- a/test_cases/023-c_min-limit-kept-with-p_demand-set.json +++ b/test_cases/023-c_min-limit-kept-with-p_demand-set.json @@ -9,7 +9,7 @@ "d_max": 0, "p_a": 0.0, "s_initial": 1000, - "s_max": 36000, + "s_max": 30000, "s_min": 0, "p_demand": [ 0, @@ -21,13 +21,13 @@ 0, 500, 600, - 900, 1100, 1300, 1400, 1800, 2200, - 3000, + 3800, + 4000, 4000, 4000, 4000, @@ -256,7 +256,7 @@ }, "expected_response": { "status": "Optimal", - "objective_value": 0.24398841080996625, + "objective_value": 0.6920273374810997, "limit_violations": { "grid_import_limit_exceeded": false, "grid_export_limit_hit": false @@ -264,27 +264,27 @@ "batteries": [ { "charging_power": [ - 31.688889, - 3680.0, - 3680.0, - 3680.0, - 2103.8514, - 1380.0, 0.0, - 1380.0, - 1380.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 1380.0, 1380.0, 1380.0, 1400.0, - 3064.4597, + 1800.0, 2200.0, - 3000.0, 3680.0, - 1936.8421, - 1019.3906, - 536.52136, - 0.0, + 3680.0, + 3680.0, + 3680.0, + 3680.0, + 3680.0, + 602.22222, 0.0, 0.0, 0.0, @@ -338,41 +338,41 @@ 0.0 ], "state_of_charge": [ - 1028.52, - 4340.52, - 7652.52, - 10964.52, - 12857.986, - 14099.986, - 14099.986, - 15341.986, - 16583.986, - 17825.986, - 19067.986, - 20309.986, - 21569.986, - 24328.0, - 26308.0, - 29008.0, - 32320.0, - 34063.158, - 34980.609, - 35463.479, - 35463.479, - 35463.479, - 35463.479, - 35463.479, - 35463.479, - 35463.479, - 35463.479, - 35463.479, - 35463.479, - 35463.479, - 35463.479, - 35463.479, - 35463.479, - 35463.479, - 35463.479 + 1000.0, + 1000.0, + 1000.0, + 1000.0, + 1000.0, + 1000.0, + 1000.0, + 1000.0, + 2242.0, + 3484.0, + 4726.0, + 5986.0, + 7606.0, + 9586.0, + 12898.0, + 16210.0, + 19522.0, + 22834.0, + 26146.0, + 29458.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0 ] }, { @@ -381,8 +381,8 @@ 2000.0, 2000.0, 2000.0, - 0.0, - 0.0, + 2000.0, + 844.45956, 0.0, 0.0, 0.0, @@ -419,16 +419,16 @@ 0.0, 0.0, 0.0, - 535.54044, + 0.0, 223.61096, - 1839.9345, + 459.93445, 1765.3513, 1754.0456, 0.0, - 555.46729, - 0.0, - 0.0, + 1676.2818, + 2052.7024, 0.0, + 1046.0357, 0.0, 0.0, 0.0, @@ -455,16 +455,16 @@ 3815.5, 5615.5, 7415.5, - 7415.5, - 6820.4551, - 6571.9984, - 4527.6268, - 2566.1254, - 617.18588, - 617.18588, - 0.0, - 0.0, - 0.0, + 9215.5, + 9975.5136, + 9727.057, + 9216.0187, + 7254.5173, + 5305.5777, + 5305.5777, + 3443.0424, + 1162.2619, + 1162.2619, 0.0, 0.0, 0.0, @@ -490,27 +490,27 @@ } ], "grid_import": [ - 8.3045981, - 722.58472, - 1469.5517, - 2368.9842, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 1694.8127, - 1100.8145, - 1652.7024, - 3304.5813, - 2445.3641, - 3220.0501, + 0.0, + 0.0, + 0.0, + 0.0, + 1694.8126, + 0.0, + 0.0, + 2440.1215, + 2879.3284, + 3900.0501, 3874.456, - 2189.1504, - 3207.935, - 2210.0903, - 859.7609, + 3932.3083, + 5868.5444, + 5353.5689, + 1461.9831, 0.0, 0.0, 0.0, @@ -527,11 +527,11 @@ 314.81265 ], "grid_export": [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 23.384291, + 2957.4153, + 2210.4483, + 1311.0158, + 103.8514, 0.0, 0.0, 0.0, @@ -564,11 +564,11 @@ 0.0 ], "flow_direction": [ - 0, - 0, - 0, - 0, - 0, + 1, + 1, + 1, + 1, + 1, 0, 0, 0, @@ -593,7 +593,7 @@ 1, 1, 0, - 0, + 1, 0, 0, 0, From bd129f5f3d757b718b37103ba5f03955c83752c9 Mon Sep 17 00:00:00 2001 From: ekkea Date: Tue, 21 Jul 2026 10:41:32 +0200 Subject: [PATCH 07/11] linted --- src/optimizer/optimizer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/optimizer/optimizer.py b/src/optimizer/optimizer.py index d03380e2b..2cd832377 100644 --- a/src/optimizer/optimizer.py +++ b/src/optimizer/optimizer.py @@ -279,7 +279,7 @@ def _setup_target_function(self): if bat.p_demand is not None: for t in self.time_steps: objective += - self.variables['p_demand_pen'][i][t] * 0.1 * self.time_series.p_N[t] * (1 + (self.T - t)/self.T) - #- self.prc_p_goal_pen \ + # - self.prc_p_goal_pen \ # * self.variables['p_demand_pen'][i][t] \ # * (1 + (self.T - t)/self.T) @@ -451,19 +451,19 @@ def _add_battery_constraints(self): if bat.p_demand[t] > 0: # clip requested charging power to max charging power if needed p_demand = min(bat.c_max * self.time_series.dt[t] / 3600., bat.p_demand[t]) - + # set a "soft" constraint to reach the requested charging rate if possible self.problem += (self.variables['c'][i][t] + self.variables['p_demand_pen'][i][t] >= p_demand) - + if bat.c_min > 0: # introduce z_p_demand to become only 1 if s_max is almost reached and the # charging with c_min would stop before the end of the time slot - self.problem += ((bat.s_max - self.variables['s'][i][t]) - <= (1 - self.variables['z_p_demand'][i][t]) * self.M + self.problem += ((bat.s_max - self.variables['s'][i][t]) + <= (1 - self.variables['z_p_demand'][i][t]) * self.M + bat.c_min * self.time_series.dt[t] / 3600.) # set constraints to exclude charging between 0 and c_min if z_p_demand is not 1. - self.problem += (self.variables['c'][i][t] + self.variables['z_p_demand'][i][t] *self.M - >= bat.c_min * self.time_series.dt[t] / 3600. * self.variables['z_c'][i][t]) + self.problem += (self.variables['c'][i][t] + self.variables['z_p_demand'][i][t] * self.M + >= bat.c_min * self.time_series.dt[t] / 3600. * self.variables['z_c'][i][t]) self.problem += (self.variables['c'][i][t] <= self.M * self.variables['z_c'][i][t]) # Constraint (7): Minimum charge power limits if there is no charge demand From 2d9d63b8efa8b6433baef2301793d61ac016ee63 Mon Sep 17 00:00:00 2001 From: ekkea Date: Tue, 21 Jul 2026 11:20:29 +0200 Subject: [PATCH 08/11] fixed problem with incentifying unintended charging-discharging swing --- src/optimizer/optimizer.py | 25 +- test_cases/011-infeasible-charge-demand.json | 28 +- test_cases/020-weird-charging-at-night.json | 670 +++++++++--------- ...1-min-pv-use-case-with-weird-behavior.json | 130 ++-- ...23-c_min-limit-kept-with-p_demand-set.json | 152 ++-- 5 files changed, 507 insertions(+), 498 deletions(-) diff --git a/src/optimizer/optimizer.py b/src/optimizer/optimizer.py index 2cd832377..6fe7df241 100644 --- a/src/optimizer/optimizer.py +++ b/src/optimizer/optimizer.py @@ -159,11 +159,14 @@ def _setup_variables(self): self.variables['p_demand_pen'] = [[None for t in self.time_steps] for i in range(len(self.batteries))] # binary variable to allow one out of two alternative constraints self.variables['z_p_demand'] = [[None for t in self.time_steps] for i in range(len(self.batteries))] + # binary variable to capture when s_max is reached + self.variables['z_s_max_reached'] = [[None for t in self.time_steps] for i in range(len(self.batteries))] for i, bat in enumerate(self.batteries): if bat.p_demand is not None: for t in self.time_steps: self.variables['p_demand_pen'][i][t] = pulp.LpVariable(f"p_demand_pen_{i}_{t}", lowBound=0) self.variables['z_p_demand'][i][t] = pulp.LpVariable(f"z_p_demand_{i}_{t}", cat='Binary') + self.variables['z_s_max_reached'][i][t] = pulp.LpVariable(f"z_s_max_reached_{i}_{t}", cat='Binary') # penalty variable for staying above max SOC and below min SOC self.variables['s_max_pen'] = [[pulp.LpVariable(f"s_max_pen_{i}_{t}", lowBound=0) for t in self.time_steps] for i in range(len(self.batteries))] @@ -278,10 +281,9 @@ def _setup_target_function(self): # unmet charging demand due to battery reaching maximum SOC with incentive to do charging early if bat.p_demand is not None: for t in self.time_steps: - objective += - self.variables['p_demand_pen'][i][t] * 0.1 * self.time_series.p_N[t] * (1 + (self.T - t)/self.T) - # - self.prc_p_goal_pen \ - # * self.variables['p_demand_pen'][i][t] \ - # * (1 + (self.T - t)/self.T) + objective += - self.prc_p_goal_pen \ + * self.variables['p_demand_pen'][i][t] \ + * (1 + (self.T - t)/self.T) # penalties for grid power limits that cannot be met. for t in self.time_steps: @@ -452,15 +454,22 @@ def _add_battery_constraints(self): # clip requested charging power to max charging power if needed p_demand = min(bat.c_max * self.time_series.dt[t] / 3600., bat.p_demand[t]) - # set a "soft" constraint to reach the requested charging rate if possible - self.problem += (self.variables['c'][i][t] + self.variables['p_demand_pen'][i][t] >= p_demand) - - if bat.c_min > 0: # introduce z_p_demand to become only 1 if s_max is almost reached and the # charging with c_min would stop before the end of the time slot self.problem += ((bat.s_max - self.variables['s'][i][t]) <= (1 - self.variables['z_p_demand'][i][t]) * self.M + bat.c_min * self.time_series.dt[t] / 3600.) + + # introduce z_s_max_reached to become only 1 if s_max is fully reached + self.problem += ((bat.s_max - self.variables['s'][i][t]) + <= (1 - self.variables['z_s_max_reached'][i][t]) * self.M ) + + # set a "soft" constraint to reach the requested charging rate if possible. + # deactivate it if s_max is already reached + self.problem += (self.variables['c'][i][t] + self.variables['p_demand_pen'][i][t] + >= (1 - self.variables['z_s_max_reached'][i][t]) * p_demand) + + if bat.c_min > 0: # set constraints to exclude charging between 0 and c_min if z_p_demand is not 1. self.problem += (self.variables['c'][i][t] + self.variables['z_p_demand'][i][t] * self.M >= bat.c_min * self.time_series.dt[t] / 3600. * self.variables['z_c'][i][t]) diff --git a/test_cases/011-infeasible-charge-demand.json b/test_cases/011-infeasible-charge-demand.json index de079200b..a49c0c906 100644 --- a/test_cases/011-infeasible-charge-demand.json +++ b/test_cases/011-infeasible-charge-demand.json @@ -336,14 +336,14 @@ { "charging_power": [ 2700.3723, - 2726.2801, + 3775.7398, 4087.0091, 3515.8202, - 2656.4201, + 1606.9604, 1803.9868, 824.5558, - -7.48207e-12, - -2.4940233e-12, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -434,9 +434,9 @@ ], "state_of_charge": [ 3847.3351, - 6300.9872, - 9979.2954, - 13143.534, + 7245.5009, + 10923.809, + 14088.047, 15534.312, 17157.9, 17900.0, @@ -485,10 +485,10 @@ { "charging_power": [ 0.0, - 1049.4597, 0.0, 0.0, 0.0, + 1049.4597, 0.0, 0.0, 0.0, @@ -544,8 +544,8 @@ 552.01445, 689.2983, 326.2485, - 7.1183464e-11, - -7.1183464e-11, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -583,9 +583,9 @@ ], "state_of_charge": [ 1267.2, - 2211.7137, - 2211.7137, - 2211.7137, + 1267.2, + 1267.2, + 1267.2, 2211.7137, 2211.7137, 2211.7137, @@ -732,10 +732,10 @@ ], "flow_direction": [ 0, - 1, 0, 0, 0, + 1, 0, 0, 0, diff --git a/test_cases/020-weird-charging-at-night.json b/test_cases/020-weird-charging-at-night.json index 7b778c744..6d40a9bb4 100644 --- a/test_cases/020-weird-charging-at-night.json +++ b/test_cases/020-weird-charging-at-night.json @@ -1126,7 +1126,7 @@ }, "expected_response": { "status": "Optimal", - "objective_value": -8.8546508913198, + "objective_value": -8.854766700961202, "limit_violations": { "grid_import_limit_exceeded": false, "grid_export_limit_hit": false @@ -1149,12 +1149,12 @@ 808.7126, 777.3878, 635.6457, - -2.2887559e-09, - 8.0093714e-11, - 8.0093714e-11, - 8.0093714e-11, - 8.0093714e-11, - 8.0093714e-11, + 417.0544, + 380.7551, + 442.43226, + 378.34187, + 235.3189, + 123.97605, 0.0, 0.0, 0.0, @@ -1196,18 +1196,12 @@ 2760.0, 2760.0, 2760.0, - 1657.3715, - 872.30079, - 459.10568, - 241.63457, - 127.17609, - 66.934783, - 35.228833, - 18.541491, - 9.7586796, - 5.1361471, - 2.7032353, - 1.4227554, + 1521.0168, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -1284,10 +1278,16 @@ 0.0, 0.0, 0.0, - 0.74881865, - 0.39411508, - 0.20742899, - 0.10917315 + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 ], "discharging_power": [ 0.0, @@ -1461,145 +1461,145 @@ 52707.264, 53406.913, 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 53978.994, - 56462.994, - 58946.994, - 61430.994, - 63914.994, - 66398.994, - 68882.994, - 71366.994, - 73850.994, - 75342.629, - 76127.699, - 76540.894, - 76758.365, - 76872.824, - 76933.065, - 76964.771, - 76981.459, - 76990.241, - 76994.864, - 76997.297, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76998.577, - 76999.251, - 76999.606, - 76999.793, - 76999.891 + 54354.343, + 54697.023, + 55095.212, + 55435.719, + 55647.506, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 55759.085, + 58243.085, + 60727.085, + 63211.085, + 65695.085, + 68179.085, + 70663.085, + 73147.085, + 75631.085, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0, + 77000.0 ] }, { @@ -1619,12 +1619,12 @@ 0.0, 0.0, 0.0, - 417.0544, - 380.7551, - 442.43226, - 378.34187, - 235.3189, - 123.97605, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -1668,16 +1668,16 @@ 2500.0, 2500.0, 2500.0, - 1401.5759, - 737.67155, - 388.24818, - 204.34115, - 107.54797, - 56.604196, - 29.791682, - 15.679833, - 8.2525436, - 4.343444, + 2500.0, + 2436.7611, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -1836,8 +1836,8 @@ 0.0, 985.20835, 1060.0535, - 1191.0201, - 1070.187, + 1191.0202, + 1070.1871, 1184.9706, 1187.7621, 896.34607, @@ -1931,145 +1931,145 @@ 38200.0, 38200.0, 38200.0, - 38575.349, - 38918.029, - 39316.218, - 39656.725, - 39868.512, - 39980.091, - 39848.903, - 39808.272, - 39735.554, - 39644.948, - 39364.979, - 38986.561, - 38416.67, - 37525.684, - 36721.388, - 36021.222, - 35342.526, - 34679.954, - 33971.243, - 33115.01, - 32251.899, - 31381.899, - 30538.697, - 29804.036, - 28966.528, - 28112.578, - 27238.522, - 26497.721, - 25848.848, - 25067.925, - 24119.288, - 23115.718, - 22137.501, - 20960.107, - 19689.375, - 18421.687, - 17172.285, - 16013.342, - 14837.006, - 17087.006, - 19337.006, - 21587.006, - 23837.006, - 26087.006, - 28337.006, - 30587.006, - 32837.006, - 35087.006, - 37337.006, - 38598.424, - 39262.328, - 39611.752, - 39795.659, - 39892.452, - 39943.396, - 39970.208, - 39984.32, - 39991.747, - 39995.657, - 38900.981, - 37723.143, - 36399.788, - 35210.691, - 33894.057, - 32574.321, - 31578.381, - 30916.007, - 30163.49, - 29406.135, - 28836.417, - 28396.706, - 27800.697, - 27417.933, - 27241.078, - 27214.619, - 27396.39, - 27700.862, - 27858.434, - 28013.142, - 28119.806, - 28296.787, - 28731.693, - 29300.597, - 29909.335, - 30449.834, - 31053.02, - 31681.122, - 32349.269, - 33113.957, - 33695.951, - 34196.658, - 34744.819, - 35272.558, - 35856.483, - 36432.411, - 36904.479, - 37207.041, - 37504.158, - 37884.009, - 38226.486, - 38453.629, - 38593.951, - 38514.777, - 38529.819, - 38514.15, - 38478.44, - 38251.214, - 37920.713, - 37391.239, - 36533.169, - 35754.29, - 35073.499, - 34409.594, - 33757.23, - 33054.145, - 32200.933, - 31340.218, - 30471.988, - 29629.932, - 28896.001, - 28059.013, - 27205.375, - 26331.423, - 25590.623, - 24941.749, - 24160.826, - 23212.189, - 22208.62, - 21230.403, - 20053.009, - 18782.277, - 17514.589, - 16265.187, - 15106.244, - 13929.907, - 16179.907, - 18429.907, - 20679.907, - 22929.907 + 38200.0, + 38200.0, + 38200.0, + 38200.0, + 38200.0, + 38200.0, + 38068.812, + 38028.181, + 37955.463, + 37864.857, + 37584.889, + 37206.47, + 36636.58, + 35745.593, + 34941.298, + 34241.132, + 33562.435, + 32899.863, + 32191.152, + 31334.919, + 30471.808, + 29601.808, + 28758.606, + 28023.946, + 27186.437, + 26332.487, + 25458.431, + 24717.63, + 24068.757, + 23287.834, + 22339.197, + 21335.627, + 20357.411, + 19180.016, + 17909.284, + 16641.596, + 15392.195, + 14233.252, + 13056.915, + 15306.915, + 17556.915, + 19806.915, + 22056.915, + 24306.915, + 26556.915, + 28806.915, + 31056.915, + 33306.915, + 35556.915, + 37806.915, + 40000.0, + 40000.0, + 40000.0, + 40000.0, + 40000.0, + 40000.0, + 40000.0, + 40000.0, + 40000.0, + 38905.324, + 37727.487, + 36404.131, + 35215.034, + 33898.4, + 32578.665, + 31582.725, + 30920.35, + 30167.833, + 29410.478, + 28840.76, + 28401.05, + 27805.041, + 27422.276, + 27245.421, + 27218.963, + 27400.733, + 27705.206, + 27862.777, + 28017.486, + 28124.15, + 28301.13, + 28736.037, + 29304.94, + 29913.679, + 30454.177, + 31057.363, + 31685.466, + 32353.612, + 33118.3, + 33700.295, + 34201.002, + 34749.162, + 35276.901, + 35860.827, + 36436.754, + 36908.823, + 37211.384, + 37508.501, + 37888.353, + 38230.829, + 38457.973, + 38598.295, + 38519.121, + 38534.163, + 38518.493, + 38482.783, + 38255.558, + 37925.056, + 37395.582, + 36537.512, + 35758.634, + 35077.843, + 34413.937, + 33761.574, + 33058.488, + 32205.276, + 31344.561, + 30476.332, + 29634.275, + 28900.344, + 28063.356, + 27209.719, + 26335.767, + 25594.966, + 24946.093, + 24165.17, + 23216.533, + 22212.963, + 21234.746, + 20057.352, + 18786.62, + 17518.932, + 16269.53, + 15110.587, + 13934.251, + 16184.251, + 18434.251, + 20684.251, + 22934.251 ] } ], @@ -2136,18 +2136,18 @@ 6673.0345, 6488.2601, 6557.8086, - 5401.1003, - 4585.0378, - 3154.9309, - 2282.5678, - 1764.7798, - 1560.8749, - 1472.1384, - 1358.291, - 1129.5073, - 1096.1547, - 1025.9721, - 1024.0872, + 5264.7456, + 3712.737, + 3794.2493, + 3740.0228, + 1249.3555, + 1289.599, + 1329.3616, + 1283.1453, + 1089.9569, + 1075.3387, + 1015.0164, + 1018.321, 0.0, 0.0, 0.0, @@ -2224,10 +2224,10 @@ 0.0, 0.0, 0.0, - 3546.0578, - 3411.5801, - 3587.7833, - 3856.998 + 3545.309, + 3411.186, + 3587.5759, + 3856.8888 ], "grid_export": [ 0.0, diff --git a/test_cases/021-min-pv-use-case-with-weird-behavior.json b/test_cases/021-min-pv-use-case-with-weird-behavior.json index d64b1881b..417010cca 100644 --- a/test_cases/021-min-pv-use-case-with-weird-behavior.json +++ b/test_cases/021-min-pv-use-case-with-weird-behavior.json @@ -1149,7 +1149,7 @@ }, "expected_response": { "status": "Optimal", - "objective_value": 1.424378272158798, + "objective_value": 1.4243783004750088, "limit_violations": { "grid_import_limit_exceeded": false, "grid_export_limit_hit": false @@ -1157,25 +1157,25 @@ "batteries": [ { "charging_power": [ - 91.52771, - 586.54958, + 59.416667, + 345.0, 682.19391, - 774.469, + 474.469, 829.57685, - 864.68834, + 564.68834, 930.74936, 970.56395, - 719.45303, - 776.11438, - 770.38433, + 1019.453, + 1076.1144, + 987.19421, 812.32132, - 834.99041, + 1134.9904, 1128.2293, 1103.2781, 1103.0993, 1076.299, 1007.6407, - 243.14926, + 0.0, 0.0, 0.0, 0.0, @@ -1529,24 +1529,24 @@ 0.0 ], "state_of_charge": [ - 22307.625, - 22835.52, - 23449.494, - 24146.516, - 24893.135, - 25671.355, - 26509.029, - 27382.537, - 28030.045, - 28728.547, - 29421.893, - 30152.983, - 30904.474, - 31919.88, - 32912.831, - 33905.62, - 34874.289, - 35781.166, + 22278.725, + 22589.225, + 23203.2, + 23630.222, + 24376.841, + 24885.06, + 25722.735, + 26596.242, + 27513.75, + 28482.253, + 29370.728, + 30101.817, + 31123.308, + 32138.715, + 33131.665, + 34124.454, + 35093.123, + 36000.0, 36000.0, 36000.0, 36000.0, @@ -1717,26 +1717,26 @@ }, { "charging_power": [ + 32.111043, + 241.54958, 0.0, + 300.0, 0.0, + 300.0, 0.0, 0.0, 0.0, 0.0, - 0.0, - 0.0, - 300.0, - 300.0, - 300.0, - 300.0, + 83.19012, 300.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 121.33333, 0.0, + 300.0, + 64.48259, 0.0, 0.0, 0.0, @@ -2004,13 +2004,13 @@ 0.0, 0.0, 0.0, - 7.1183464e-11, + 2.5778161e-10, 0.0, 0.0, 0.0, 0.0, 5.0653318, - 7.1183464e-11, + 2.5778161e-10, 0.0, 0.0, 1.5347758, @@ -2089,25 +2089,25 @@ 0.0 ], "state_of_charge": [ - 2304.0, - 2304.0, - 2304.0, - 2304.0, - 2304.0, - 2304.0, - 2304.0, - 2304.0, - 2574.0, - 2844.0, - 3114.0, - 3384.0, - 3654.0, - 3654.0, - 3654.0, - 3654.0, - 3654.0, - 3654.0, - 3763.2, + 2332.8999, + 2550.2946, + 2550.2946, + 2820.2946, + 2820.2946, + 3090.2946, + 3090.2946, + 3090.2946, + 3090.2946, + 3090.2946, + 3165.1657, + 3435.1657, + 3435.1657, + 3435.1657, + 3435.1657, + 3435.1657, + 3435.1657, + 3435.1657, + 3705.1657, 3763.2, 3763.2, 3763.2, @@ -2362,8 +2362,8 @@ 66.15803, 62.891605, 61.67778, - 80.765382, - 97.269257, + 80.765383, + 97.269258, 102.4351, 80.922873, 74.253858, @@ -2481,8 +2481,8 @@ 0.0, 0.0, 0.0, - 591.26062, - 902.58074, + 655.74321, + 838.09815, 803.4667, 749.66251, 662.16709, @@ -2649,22 +2649,22 @@ 0.0 ], "flow_direction": [ + 1, + 1, 0, + 1, 0, + 1, 0, 0, 0, - 0, - 0, - 0, - 1, - 1, 1, 1, 1, 0, 0, 0, + 1, 0, 0, 1, diff --git a/test_cases/023-c_min-limit-kept-with-p_demand-set.json b/test_cases/023-c_min-limit-kept-with-p_demand-set.json index e8bc2b241..7126368ba 100644 --- a/test_cases/023-c_min-limit-kept-with-p_demand-set.json +++ b/test_cases/023-c_min-limit-kept-with-p_demand-set.json @@ -256,7 +256,7 @@ }, "expected_response": { "status": "Optimal", - "objective_value": 0.6920273374810997, + "objective_value": 0.6754182011186658, "limit_violations": { "grid_import_limit_exceeded": false, "grid_export_limit_hit": false @@ -264,27 +264,27 @@ "batteries": [ { "charging_power": [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, + 31.688889, + 3680.0, + 3680.0, + 3680.0, + 2103.8514, + 844.45956, 0.0, 1380.0, 1380.0, 1380.0, + 3680.0, 1400.0, 1800.0, - 2200.0, - 3680.0, - 3680.0, - 3680.0, - 3680.0, 3680.0, - 3680.0, - 602.22222, + 3502.2224, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -338,26 +338,26 @@ 0.0 ], "state_of_charge": [ - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 1000.0, - 2242.0, - 3484.0, - 4726.0, - 5986.0, - 7606.0, - 9586.0, - 12898.0, - 16210.0, - 19522.0, - 22834.0, - 26146.0, - 29458.0, + 1028.52, + 4340.52, + 7652.52, + 10964.52, + 12857.986, + 13618.0, + 13618.0, + 14860.0, + 16102.0, + 17344.0, + 20656.0, + 21916.0, + 23536.0, + 26848.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, + 30000.0, 30000.0, 30000.0, 30000.0, @@ -381,8 +381,8 @@ 2000.0, 2000.0, 2000.0, - 2000.0, - 844.45956, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -421,14 +421,14 @@ 0.0, 0.0, 223.61096, - 459.93445, + 1839.9344, 1765.3513, 1754.0456, 0.0, - 1676.2818, - 2052.7024, + 1091.0077, + 0.0, + 0.0, 0.0, - 1046.0357, 0.0, 0.0, 0.0, @@ -455,16 +455,16 @@ 3815.5, 5615.5, 7415.5, - 9215.5, - 9975.5136, - 9727.057, - 9216.0187, - 7254.5173, - 5305.5777, - 5305.5777, - 3443.0424, - 1162.2619, - 1162.2619, + 7415.5, + 7415.5, + 7167.0434, + 5122.6718, + 3161.1704, + 1212.2308, + 1212.2308, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -490,27 +490,27 @@ } ], "grid_import": [ + 8.3045981, + 722.58472, + 1469.5517, + 2368.9842, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 1694.8126, - 0.0, - 0.0, - 2440.1215, - 2879.3284, - 3900.0501, - 3874.456, - 3932.3083, - 5868.5444, - 5353.5689, - 1461.9831, + 3994.8126, + 585.2741, + 2052.7024, + 3920.1215, + 3747.5864, + 220.05013, + 194.45601, + 252.30829, + 2188.5444, + 1673.5689, + 859.7609, 0.0, 0.0, 0.0, @@ -527,11 +527,11 @@ 314.81265 ], "grid_export": [ - 23.384291, - 2957.4153, - 2210.4483, - 1311.0158, - 103.8514, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, 0.0, 0.0, 0.0, @@ -564,9 +564,10 @@ 0.0 ], "flow_direction": [ - 1, - 1, - 1, + 0, + 0, + 0, + 0, 1, 1, 0, @@ -584,7 +585,6 @@ 0, 0, 0, - 0, 1, 1, 1, @@ -593,7 +593,7 @@ 1, 1, 0, - 1, + 0, 0, 0, 0, From 99caee6fb162d7e76c2a1b1d82d0da1758463e88 Mon Sep 17 00:00:00 2001 From: ekkea Date: Tue, 21 Jul 2026 11:21:12 +0200 Subject: [PATCH 09/11] linted --- src/optimizer/optimizer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/optimizer/optimizer.py b/src/optimizer/optimizer.py index 6fe7df241..c83b9a71a 100644 --- a/src/optimizer/optimizer.py +++ b/src/optimizer/optimizer.py @@ -281,7 +281,7 @@ def _setup_target_function(self): # unmet charging demand due to battery reaching maximum SOC with incentive to do charging early if bat.p_demand is not None: for t in self.time_steps: - objective += - self.prc_p_goal_pen \ + objective += - self.prc_p_goal_pen \ * self.variables['p_demand_pen'][i][t] \ * (1 + (self.T - t)/self.T) @@ -459,14 +459,14 @@ def _add_battery_constraints(self): self.problem += ((bat.s_max - self.variables['s'][i][t]) <= (1 - self.variables['z_p_demand'][i][t]) * self.M + bat.c_min * self.time_series.dt[t] / 3600.) - - # introduce z_s_max_reached to become only 1 if s_max is fully reached + + # introduce z_s_max_reached to become only 1 if s_max is fully reached self.problem += ((bat.s_max - self.variables['s'][i][t]) - <= (1 - self.variables['z_s_max_reached'][i][t]) * self.M ) - + <= (1 - self.variables['z_s_max_reached'][i][t]) * self.M) + # set a "soft" constraint to reach the requested charging rate if possible. # deactivate it if s_max is already reached - self.problem += (self.variables['c'][i][t] + self.variables['p_demand_pen'][i][t] + self.problem += (self.variables['c'][i][t] + self.variables['p_demand_pen'][i][t] >= (1 - self.variables['z_s_max_reached'][i][t]) * p_demand) if bat.c_min > 0: From 690dd814ce0cba1fd3e8058190df7e7197679e27 Mon Sep 17 00:00:00 2001 From: ekkea Date: Sat, 25 Jul 2026 08:41:49 +0200 Subject: [PATCH 10/11] fixed: c_min constraint not effective at times with p_demand = 0 --- src/optimizer/optimizer.py | 3 + ...23-c_min-limit-kept-with-p_demand-set.json | 94 +++++++++---------- 2 files changed, 50 insertions(+), 47 deletions(-) diff --git a/src/optimizer/optimizer.py b/src/optimizer/optimizer.py index c83b9a71a..dc3be7c7b 100644 --- a/src/optimizer/optimizer.py +++ b/src/optimizer/optimizer.py @@ -468,6 +468,9 @@ def _add_battery_constraints(self): # deactivate it if s_max is already reached self.problem += (self.variables['c'][i][t] + self.variables['p_demand_pen'][i][t] >= (1 - self.variables['z_s_max_reached'][i][t]) * p_demand) + else: + # if there is no p_demand set, make sure z_p_demand is 0 to keep the below c_min constraint effective + self.problem += (self.variables['z_p_demand'][i][t] <= 0) if bat.c_min > 0: # set constraints to exclude charging between 0 and c_min if z_p_demand is not 1. diff --git a/test_cases/023-c_min-limit-kept-with-p_demand-set.json b/test_cases/023-c_min-limit-kept-with-p_demand-set.json index 7126368ba..06442a172 100644 --- a/test_cases/023-c_min-limit-kept-with-p_demand-set.json +++ b/test_cases/023-c_min-limit-kept-with-p_demand-set.json @@ -9,7 +9,7 @@ "d_max": 0, "p_a": 0.0, "s_initial": 1000, - "s_max": 30000, + "s_max": 31000, "s_min": 0, "p_demand": [ 0, @@ -256,7 +256,7 @@ }, "expected_response": { "status": "Optimal", - "objective_value": 0.6754182011186658, + "objective_value": 0.5795578573738658, "limit_violations": { "grid_import_limit_exceeded": false, "grid_export_limit_hit": false @@ -269,7 +269,7 @@ 3680.0, 3680.0, 2103.8514, - 844.45956, + 1380.0, 0.0, 1380.0, 1380.0, @@ -278,8 +278,8 @@ 1400.0, 1800.0, 3680.0, - 3502.2224, - 0.0, + 3680.0, + 397.79304, 0.0, 0.0, 0.0, @@ -343,36 +343,36 @@ 7652.52, 10964.52, 12857.986, - 13618.0, - 13618.0, - 14860.0, - 16102.0, - 17344.0, - 20656.0, - 21916.0, - 23536.0, - 26848.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0, - 30000.0 + 14099.986, + 14099.986, + 15341.986, + 16583.986, + 17825.986, + 21137.986, + 22397.986, + 24017.986, + 27329.986, + 30641.986, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0, + 31000.0 ] }, { @@ -419,13 +419,13 @@ 0.0, 0.0, 0.0, - 0.0, + 535.54044, 223.61096, 1839.9344, 1765.3513, 1754.0456, 0.0, - 1091.0077, + 555.46729, 0.0, 0.0, 0.0, @@ -456,12 +456,12 @@ 5615.5, 7415.5, 7415.5, - 7415.5, - 7167.0434, - 5122.6718, - 3161.1704, - 1212.2308, - 1212.2308, + 6820.4551, + 6571.9984, + 4527.6268, + 2566.1254, + 617.18588, + 617.18588, 0.0, 0.0, 0.0, @@ -501,11 +501,11 @@ 0.0, 0.0, 3994.8126, - 585.2741, + 1120.8145, 2052.7024, 3920.1215, - 3747.5864, - 220.05013, + 3925.3641, + 617.84317, 194.45601, 252.30829, 2188.5444, @@ -569,7 +569,7 @@ 0, 0, 1, - 1, + 0, 0, 0, 0, From 8e012d039f7b2bfba22badd98d11335b94a804cd Mon Sep 17 00:00:00 2001 From: ekkea Date: Sat, 25 Jul 2026 08:42:51 +0200 Subject: [PATCH 11/11] linted --- src/optimizer/optimizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/optimizer/optimizer.py b/src/optimizer/optimizer.py index dc3be7c7b..361979c56 100644 --- a/src/optimizer/optimizer.py +++ b/src/optimizer/optimizer.py @@ -468,7 +468,7 @@ def _add_battery_constraints(self): # deactivate it if s_max is already reached self.problem += (self.variables['c'][i][t] + self.variables['p_demand_pen'][i][t] >= (1 - self.variables['z_s_max_reached'][i][t]) * p_demand) - else: + else: # if there is no p_demand set, make sure z_p_demand is 0 to keep the below c_min constraint effective self.problem += (self.variables['z_p_demand'][i][t] <= 0)