diff --git a/apps/predbat/plan.py b/apps/predbat/plan.py index 2a1e25a38..0bcd49205 100644 --- a/apps/predbat/plan.py +++ b/apps/predbat/plan.py @@ -4113,10 +4113,12 @@ def optimise_swap_export(self, record_charge_windows, record_export_windows, dro set_window_start(self.export_window_best[window_n_target], self.export_window_best[window_n_target]["end"] - (window_length + window_length_target)) is_combined = True elif export_mode_of(export_limit_target) == EXPORT_MODE_TARGET and window_length_target < orig_length_target: - # Partial combine + # Partial combine - move export time from this window into the target, so + # this window shrinks by what the target gains. Growing it instead pushed + # its start back into the slot before, through any manual override there (#5423) amount_to_move = min(orig_length_target - window_length_target, window_length) window_length_target_new = amount_to_move + window_length_target - window_length_new = amount_to_move + window_length + window_length_new = window_length - amount_to_move self.export_limits_best[window_n] = min(export_limit, export_limit_target, key=export_limit_sort_key) set_window_start(self.export_window_best[window_n], self.export_window_best[window_n]["end"] - window_length_new) set_window_start(self.export_window_best[window_n_target], self.export_window_best[window_n_target]["end"] - window_length_target_new) diff --git a/apps/predbat/tests/test_optimise_swap_export.py b/apps/predbat/tests/test_optimise_swap_export.py index b419b82a7..af917c0db 100644 --- a/apps/predbat/tests/test_optimise_swap_export.py +++ b/apps/predbat/tests/test_optimise_swap_export.py @@ -195,6 +195,29 @@ def run_optimise_swap_export_tests(my_predbat): failed = True my_predbat.calculate_best_export = True + # --------------------------------------------------------------------------------------------- + # Partial combine regression (#5423): moving export time from an earlier window into a later, + # trimmed one must shrink the earlier window, never grow it backwards into the slot before it. + # Here the slot before is a manual demand override, so growing into it exports through it. + # --------------------------------------------------------------------------------------------- + now = my_predbat.minutes_now + export_window_best = [ + {"start": now, "end": now + 30, "average": 50.0}, + {"start": now + 30, "end": now + 60, "average": 50.0}, + {"start": now + 80, "end": now + 90, "average": 50.0, "start_orig": now + 60}, + ] + setup_swap_export(my_predbat, export_window_best, export_limits_best=[100.0, 0.0, 0.0], rate_import=30.0, rate_export=50.0) + saved_manual_all_times = my_predbat.manual_all_times + my_predbat.manual_all_times = {now} + try: + my_predbat.optimise_swap_export(0, len(export_window_best)) + finally: + my_predbat.manual_all_times = saved_manual_all_times + middle = my_predbat.export_window_best[1] + if middle["start"] < now + 30: + print("ERROR: partial combine grew window 1 back to {} into the manual slot ending {}, windows {}".format(middle["start"], now + 30, my_predbat.export_window_best)) + failed = True + # --------------------------------------------------------------------------------------------- # Ordering regression (#4478): the export swap must run after every pass that can enable an # export, otherwise the exports the plan pass / optimise_solar add are never diff --git a/coverage/cases/random_results.json b/coverage/cases/random_results.json index c8944e63e..15b0282e0 100644 --- a/coverage/cases/random_results.json +++ b/coverage/cases/random_results.json @@ -27,17 +27,17 @@ { "id": 1, "seed": 1, - "metric": 1164.5356, - "cost": 1154.7627, + "metric": 1164.4733, + "cost": 1154.5992, "cost_pv10": 1429.766, - "cost_pv90": 1017.5324, + "cost_pv90": 1018.3924, "import_kwh_battery": 8.2145, - "import_kwh_house": 20.3602, - "export_kwh": 10.5863, + "import_kwh_house": 20.3497, + "export_kwh": 10.5909, "soc_min": 0.38, - "soc_final": 4.762, - "battery_cycles": 16.5804, - "carbon_g": 20357.15, + "soc_final": 4.7532, + "battery_cycles": 16.4913, + "carbon_g": 20352.73, "runtime_s": 1.161, "failed": false, "error": null, @@ -65,17 +65,17 @@ { "id": 3, "seed": 3, - "metric": 628.401, - "cost": 646.8817, - "cost_pv10": 624.2009, - "cost_pv90": 579.9167, + "metric": 628.3622, + "cost": 649.2458, + "cost_pv10": 623.6014, + "cost_pv90": 582.2808, "import_kwh_battery": 0.0792, "import_kwh_house": 0.0, - "export_kwh": 8.3425, + "export_kwh": 8.1858, "soc_min": 0.3907, - "soc_final": 1.2819, - "battery_cycles": 11.1137, - "carbon_g": 9719.92, + "soc_final": 1.4502, + "battery_cycles": 10.9505, + "carbon_g": 9769.74, "runtime_s": 0.343, "failed": false, "error": null, @@ -179,9 +179,9 @@ { "id": 9, "seed": 9, - "metric": 477.0656, + "metric": 477.3462, "cost": 581.2203, - "cost_pv10": 805.6605, + "cost_pv10": 807.2194, "cost_pv90": 254.3911, "import_kwh_battery": 16.7377, "import_kwh_house": 5.1054,