Skip to content

Commit 5012fcd

Browse files
committed
chore: apply Freeze Export accounting review fix
1 parent 03a49f1 commit 5012fcd

1 file changed

Lines changed: 198 additions & 0 deletions

File tree

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
name: Apply Freeze Export accounting review fix
2+
3+
on:
4+
push:
5+
branches:
6+
- feature/freeze-export-loss
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
patch-test-build:
13+
if: github.actor != 'github-actions[bot]'
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
ref: feature/freeze-export-loss
19+
fetch-depth: 0
20+
21+
- uses: actions/setup-python@v4
22+
with:
23+
python-version: '3.x'
24+
25+
- uses: mlugg/setup-zig@v2
26+
with:
27+
version: 0.16.0
28+
29+
- name: Apply review fix
30+
shell: python
31+
run: |
32+
from pathlib import Path
33+
34+
def replace_once(path, old, new):
35+
p = Path(path)
36+
text = p.read_text()
37+
count = text.count(old)
38+
if count != 1:
39+
raise SystemExit(f"Expected exactly one match in {path}, found {count}")
40+
p.write_text(text.replace(old, new, 1))
41+
42+
# Python predictor: treat the configured value as real battery-side flow.
43+
path = "apps/predbat/prediction.py"
44+
old = ''' # Export limit, clip PV output
45+
diff = get_diff(battery_draw, pv_dc, pv_ac, load_yesterday, inverter_loss, inverter_loss_recp)
46+
'''
47+
new = ''' # Some inverters continue to supply house load from the battery during Freeze Export.
48+
# The configured rate is battery-side power, so feed it through the normal battery/inverter
49+
# energy path rather than subtracting unexplained energy directly from SoC.
50+
freeze_export_discharge_step = 0.0
51+
if freeze_export_active and inverter_freeze_export_discharge_rate > 0:
52+
freeze_export_discharge_step = min(
53+
inverter_freeze_export_discharge_rate * step / 60000.0,
54+
max((soc - reserve_expected) * battery_loss_discharge, 0),
55+
)
56+
battery_draw += freeze_export_discharge_step
57+
58+
# Export limit, clip PV output
59+
diff = get_diff(battery_draw, pv_dc, pv_ac, load_yesterday, inverter_loss, inverter_loss_recp)
60+
'''
61+
replace_once(path, old, new)
62+
63+
old = ''' # Some inverters continue to discharge the battery internally while Freeze Export
64+
# blocks normal external battery flow. Model that measured battery-side discharge
65+
# directly in SoC without creating fictitious house load or grid energy.
66+
freeze_export_discharge_step = 0.0
67+
if freeze_export_active and inverter_freeze_export_discharge_rate > 0:
68+
freeze_export_discharge_step = min(inverter_freeze_export_discharge_rate * step / 60000.0, max(soc - reserve_expected, 0))
69+
soc -= freeze_export_discharge_step
70+
71+
'''
72+
replace_once(path, old, '')
73+
replace_once(
74+
path,
75+
' # Count battery cycles, including internal Freeze Export battery discharge\n battery_cycle = battery_cycle + abs(battery_draw) + freeze_export_discharge_step\n',
76+
' # Count battery cycles\n battery_cycle = battery_cycle + abs(battery_draw)\n',
77+
)
78+
79+
# C++ predictor: mirror Python exactly and bump parity so stale kernels are rejected.
80+
path = "apps/predbat/prediction_kernel.cpp"
81+
replace_once(path, '#define PK_PARITY_REVISION 8\n', '#define PK_PARITY_REVISION 9\n')
82+
replace_once(
83+
path,
84+
' double inverter_freeze_export_discharge_rate; // W, internal battery discharge while Freeze Export is active\n',
85+
' double inverter_freeze_export_discharge_rate; // W, battery-side discharge while Freeze Export is active\n',
86+
)
87+
old = ''' // Export limit, clip PV output - prediction.py:1051-1058
88+
double diff = get_diff(battery_draw, pv_dc, pv_ac, load_yesterday, inverter_loss, inverter_loss_recp);
89+
'''
90+
new = ''' // Some inverters continue to supply house load from the battery during Freeze Export.
91+
// Treat the configured rate as battery-side flow and feed it through normal energy accounting.
92+
double freeze_export_discharge_step = 0.0;
93+
if (freeze_export_active && inverter_freeze_export_discharge_rate > 0) {
94+
freeze_export_discharge_step = std::min(
95+
inverter_freeze_export_discharge_rate * step / 60000.0,
96+
std::max((soc - reserve_expected) * battery_loss_discharge, 0.0));
97+
battery_draw += freeze_export_discharge_step;
98+
}
99+
100+
// Export limit, clip PV output - prediction.py:1051-1058
101+
double diff = get_diff(battery_draw, pv_dc, pv_ac, load_yesterday, inverter_loss, inverter_loss_recp);
102+
'''
103+
replace_once(path, old, new)
104+
old = ''' // Internal, directional battery discharge while Freeze Export is active.
105+
// This mirrors prediction.py and deliberately does not alter battery_draw/grid energy.
106+
double freeze_export_discharge_step = 0.0;
107+
if (freeze_export_active && inverter_freeze_export_discharge_rate > 0) {
108+
freeze_export_discharge_step = std::min(inverter_freeze_export_discharge_rate * step / 60000.0, std::max(soc - reserve_expected, 0.0));
109+
soc -= freeze_export_discharge_step;
110+
}
111+
112+
'''
113+
replace_once(path, old, '')
114+
replace_once(
115+
path,
116+
' battery_cycle = battery_cycle + std::fabs(battery_draw) + freeze_export_discharge_step;\n',
117+
' battery_cycle = battery_cycle + std::fabs(battery_draw);\n',
118+
)
119+
120+
# Python/kernel parity constant.
121+
replace_once(
122+
"apps/predbat/prediction_kernel.py",
123+
'KERNEL_PARITY_REVISION = 8\n',
124+
'KERNEL_PARITY_REVISION = 9\n',
125+
)
126+
127+
# Regression test: prove the battery energy now offsets house import and uses inverter loss.
128+
path = "apps/predbat/tests/test_model.py"
129+
old = ''' # Freeze Export internal battery discharge regression coverage. 240 W for one hour
130+
# is 0.24 kWh. It applies only in Freeze Export, respects reserve, and counts as cycling.
131+
'''
132+
new = ''' # Freeze Export battery-side discharge regression coverage. The configured flow must
133+
# supply house load through normal inverter accounting, while respecting reserve and cycling.
134+
'''
135+
replace_once(path, old, new)
136+
old = ''' failed |= simple_scenario(
137+
"freeze_export_discharge_rate_240w_one_hour",
138+
my_predbat,
139+
0,
140+
0,
141+
assert_final_metric=0,
142+
assert_final_soc=9.76,
143+
battery_size=10.0,
144+
battery_soc=10.0,
145+
discharge=99,
146+
end_record=60,
147+
inverter_freeze_export_discharge_rate=240.0,
148+
assert_battery_cycle=0.24,
149+
)
150+
'''
151+
new = ''' failed |= simple_scenario(
152+
"freeze_export_discharge_rate_240w_supplies_house",
153+
my_predbat,
154+
1.0,
155+
0,
156+
# 240 W battery-side for 24h = 5.76 kWh. At 80% inverter efficiency this
157+
# supplies 4.608 kWh of the 24 kWh house load, leaving 19.392 kWh imported.
158+
assert_final_metric=193.92,
159+
assert_final_soc=4.24,
160+
battery_size=10.0,
161+
battery_soc=10.0,
162+
inverter_loss=0.8,
163+
discharge=99,
164+
end_record=24 * 60,
165+
inverter_freeze_export_discharge_rate=240.0,
166+
assert_battery_cycle=5.76,
167+
)
168+
'''
169+
replace_once(path, old, new)
170+
171+
# Remove this one-shot workflow in the commit it creates.
172+
Path('.github/workflows/apply-freeze-export-accounting-fix.yml').unlink()
173+
174+
- name: Install dependencies
175+
run: |
176+
python -m pip install --upgrade pip
177+
pip install -r requirements.txt
178+
179+
- name: Build native prediction kernel
180+
run: bash apps/predbat/build_kernel.sh
181+
182+
- name: Run quick unit tests
183+
env:
184+
PREDBAT_KERNEL_REQUIRED: "1"
185+
run: |
186+
cd coverage
187+
python3 ../apps/predbat/unit_test.py --quick
188+
189+
- name: Cross-build checked-in kernel binaries
190+
run: bash apps/predbat/build_kernel_cross.sh
191+
192+
- name: Commit review fix
193+
run: |
194+
git config user.name "github-actions[bot]"
195+
git config user.email "github-actions[bot]@users.noreply.github.com"
196+
git add -A
197+
git commit -m "fix(prediction): account Freeze Export discharge as house supply"
198+
git push origin HEAD:feature/freeze-export-loss

0 commit comments

Comments
 (0)