Sustain in-progress forced export across plan cycles (anti-flapping on flat peaks)#4118
Open
tieskuh wants to merge 1 commit into
Open
Sustain in-progress forced export across plan cycles (anti-flapping on flat peaks)#4118tieskuh wants to merge 1 commit into
tieskuh wants to merge 1 commit into
Conversation
optimise_export already nudges the metric to "keep existing windows" when an export is running, but the binding selection gate is the raw cost check ((cost + min_improvement_scaled) <= off_cost). On a near-flat multi-slot price peak with live PV, exporting the current slot versus holding it and exporting the adjacent equal-priced slot is a cost coin-toss, so the per-recompute decision oscillated and the forced export "flapped" (drive/stop/drive) instead of draining continuously through the peak. When that same hysteresis condition fires (already exporting within this window) also relax the cost gate by the commitment amount, so an in-progress forced export is sustained across recomputes. This only sustains an export that is already running and never opens a new one, so it cannot reintroduce the metric_keep gaming guarded by issue springfall2008#2984. Adds tests/test_export_commitment.py (registered as export_commitment), which fails without the patch and passes with it.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates Predbat’s export-window optimisation to better sustain an already-running forced export across 5‑minute re-plan cycles, reducing “flapping” during near-flat high export-price peaks, and adds a targeted regression test to validate the commitment behavior.
Changes:
- Add a “keep exporting” commitment that relaxes the cost gate (not only the metric) when a forced export is already in progress.
- Add a new regression test module covering fresh-plan gating, in-progress retention, and a stale-flag guard scenario.
- Register the new test in the unit test runner.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| apps/predbat/plan.py | Relaxes the optimise_export cost gate when keeping an in-progress export to prevent oscillation across plan recomputes. |
| apps/predbat/tests/test_export_commitment.py | Adds regression tests for the forced-export commitment / anti-flapping behavior. |
| apps/predbat/unit_test.py | Registers the new export_commitment test in the test runner. |
Comment on lines
1719
to
+1724
| if window_n < 2 and this_export_limit < 99.0 and self.export_window and self.isExporting: | ||
| pwindow = export_window[window_n] | ||
| dwindow = self.export_window[0] | ||
| if self.minutes_now >= pwindow["start"] and self.minutes_now < pwindow["end"] and ((self.minutes_now >= dwindow["start"] and self.minutes_now < dwindow["end"]) or (dwindow["end"] == pwindow["start"])): | ||
| metric -= max(0.5, self.metric_min_improvement_export) | ||
| keep_export = True |
Comment on lines
+124
to
+128
| if failed: | ||
| print("**** Export commitment tests FAILED ****") | ||
| else: | ||
| print("**** Export commitment tests passed ****") | ||
| return failed |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On an evening with a high export price that stays nearly flat across several half-hour slots while PV is still producing, forced export does not run continuously through the peak. Predbat re-plans every ~5 minutes and the per-slot forced-export decision oscillates: the battery exports for a few minutes, the slot then flips to "hold", PV charges the battery instead of exporting, then it flips back. Net effect is that energy is shifted out of the most expensive slot into later, cheaper ones.
The offline plan is clean and continuous — the oscillation only shows up across live recomputes.
Root cause
optimise_exportalready has a hysteresis to keep an existing export window:But the binding selection gate is on the raw
cost, not the metric:On a near-flat multi-slot peak, exporting the current slot versus holding it and exporting the adjacent equal-priced slot is a cost coin-toss (tipped by the intraday load/PV adjustment each cycle). Because the hysteresis only lowers
metricand never touches the cost gate, it cannot hold the export and the decision flip-flops between recomputes.Fix
When that same hysteresis condition fires (we are already exporting within this window) also relax the cost gate by the commitment amount, so an in-progress forced export is sustained across recomputes:
This only sustains an export that is already running (
self.isExporting,window_n < 2, current time inside the window). It never opens a new export window, so it cannot reintroduce themetric_keepgaming guarded by #2984. The relaxation is bounded (max(0.5, metric_min_improvement_export)), so if holding becomes clearly better the export is still released.Tests
Adds
apps/predbat/tests/test_export_commitment.py(registered asexport_commitment):The test fails without the patch (the in-progress export is dropped to 100%) and passes with it.
optimise_levels,optimise_windows,compute_metric,executeandmodelgroups still pass; black / ruff (F401) / interrogate / cspell are clean.Live validation
Validated on a real SolarEdge hybrid install across two consecutive evenings with similar near-flat two-hour export peaks — the two peak half-hours within ~2c/kWh of each other, which is the condition that triggers the oscillation (the absolute height is not what drives it):
Discharge to Maximize Export→Maximize Self Consumption→Discharge to Maximize Exportwithin the first 15 minutes — and during the "hold" the remaining PV charged the battery instead of exporting, shifting energy out of the dearest slot into cheaper later ones.Discharge to Maximize Exportat the start of the peak, held continuously for the full two hours, returning to demand only when the price dropped. Zero mode-flips; the battery held a steady ~10 kW discharge with no charging dips; SoC fell monotonically (100% → ~23%) with no saw-tooth.A single evening is not conclusive on its own, but the oscillation condition was present and the before/after contrast on the same install is clear.