feat(prediction): model PV recapture beyond the export limit during Freeze Export - #4582
Open
chalfontchubby wants to merge 2 commits into
Open
feat(prediction): model PV recapture beyond the export limit during Freeze Export#4582chalfontchubby wants to merge 2 commits into
chalfontchubby wants to merge 2 commits into
Conversation
…reeze Export, enable it for FoxESS/FoxCloud (#4207) Freeze Export previously forced charge_rate_now to 0 unconditionally and fell through into the general export-limit clip, so any PV genuinely exceeding load + the hardware export limit was modelled as clipped/lost - regardless of whether the real inverter actually routes it into the battery instead (confirmed on FoxESS's "Feed-in First" mode and independently on Sigenergy under active Force Export). inverter_can_charge_during_export already modelled this exact recapture for active Force Export windows; it was just never wired into the freeze path. Freeze Export now gets its own branch in prediction.py (and its C++ kernel mirror): the battery is never actively discharged, but once PV alone exceeds what load + the export limit can absorb, the genuine spillover charges the battery (capped at the overflow amount and the battery's own charge rate, not the full rate) rather than being clipped, when inverter_can_charge_during_export allows it. SoC still holds flat whenever the export limit alone can cover the surplus - only genuine overflow moves it. FoxESS and FoxCloud's support_discharge_freeze reverts to True (both are the same hardware via different connection methods) now that this is correctly modelled instead of disabled outright - supersedes the earlier fix that just turned Freeze Export off for them. Kernel parity revision bumped (6 -> 7) and all six platform binaries cross-rebuilt via zig. coverage/cases/random_results.json and predbat_debug_pre_saving1.yaml.expected.json regenerated - Freeze Export now recaptures more energy than before in scenarios where PV exceeds the export limit, so some plans shift to use it more. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
chalfontchubby
marked this pull request as draft
August 19, 2026 07:34
chalfontchubby
marked this pull request as ready for review
August 19, 2026 07:59
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.
Summary
Follow-up to #4538 (merged) - reworks the fix properly instead of disabling Freeze Export for FoxESS/FoxCloud outright.
Root cause, not FoxESS-specific: Predbat's Freeze Export model forced
charge_rate_now = 0unconditionally and fell through to the general export-limit clip, so any inverter with PV genuinely exceeding load + the hardware export limit had that surplus modelled as clipped/lost - regardless of whether the real hardware actually routes it into the battery instead.inverter_can_charge_during_exportalready modelled exactly this recapture behaviour, but only for active Force Export windows (export_limit_now < 99.0) - it was never wired into the freeze path (export_limit_now == 99.0).Confirmed on real hardware two ways:
Fix: Freeze Export now gets its own branch in
prediction.py(mirrored inprediction_kernel.cpp) - the battery is never actively discharged (still a genuine freeze), but once PV alone exceeds what load + the export limit can absorb, the genuine spillover charges the battery (capped at the overflow amount and the battery's own charge rate) instead of being clipped, wheninverter_can_charge_during_exportallows it. SoC still holds flat whenever the export limit alone can cover the surplus.FoxESS/FoxCloud'ssupport_discharge_freezereverts toTrue(same hardware via two connection methods) now that this is correctly modelled instead of disabled - Freeze Export is offered again and correctly predicted for both.Kernel
KERNEL_PARITY_REVISION/PK_PARITY_REVISIONbumped 6 → 7.zig(build_kernel_cross.sh)../run_all --test kernel_paritypasses - the random/clipping sweeps randomiseset_export_freezeand include a dedicatedexport_freezescenario, so the new branch is genuinely exercised, bit-identical between Python and C++.Baselines regenerated
Freeze Export now recaptures more energy than before in scenarios where PV exceeds the export limit, so some plans shift to use it more (strictly an improvement in the underlying model's accuracy, never a regression in capability - freeze could previously never do worse than clip everything, now it can do better).
coverage/cases/random_results.jsonregenerated via./run_random- self-consistent (0 diff on re-compare).coverage/cases/predbat_debug_pre_saving1.yaml.expected.jsonregenerated (one of the two debug-case regression fixtures picked a different, cheaper plan).Docs
inverter_can_charge_during_export's apps-yaml.md entry updated to cover Freeze Export, not just Force Export. Also added it toAPPS_SCHEMA(config.py) - it was previously read viaget_argwith no schema entry at all.Test plan
test_freeze_export_recapture_beyond_limitintest_optimise_solar.py- PV well above load+export limit charges the battery wheninverter_can_charge_during_export=True, stays flat (old behaviour) whenFalse. Directly exercisesPrediction.run_prediction(), isolated from the optimiser's window search.test_foxess_support_discharge_freeze_matches_foxcloud(test_inverter.py) updated: both nowTrue../run_all --test kernel_paritypasses../run_all --quickpasses (full suite, including regenerated baselines)../run_pre_commitpasses.🤖 Generated with Claude Code