Skip to content

Commit cac38ab

Browse files
committed
feat(prediction): model residual battery discharge during Freeze Export
1 parent bb39a50 commit cac38ab

13 files changed

Lines changed: 171 additions & 5 deletions

apps/predbat/predbat.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,9 @@ def reset(self):
388388
self.battery_loss = 1.0
389389
self.battery_loss_discharge = 1.0
390390
self.inverter_loss = 1.0
391+
# Battery-side discharge (W) that may continue to supply house load during Freeze Export.
392+
self.inverter_freeze_export_discharge_rate = max(float(self.args.get("inverter_freeze_export_discharge_rate", 0)), 0.0)
393+
self.log("Freeze Export discharge rate configured: {:.0f} W".format(self.inverter_freeze_export_discharge_rate))
391394
self.inverter_hybrid = True
392395
self.pv_ac_limit = 0
393396
self.inverter_soc_reset = False

apps/predbat/prediction.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ def __init__(
128128
self.iboost_rate_threshold_export = base.iboost_rate_threshold_export
129129
self.rate_gas = base.rate_gas
130130
self.inverter_loss = base.inverter_loss
131+
self.inverter_freeze_export_discharge_rate = base.inverter_freeze_export_discharge_rate
131132
self.inverter_hybrid = base.inverter_hybrid
132133
self.inverter_limit = base.inverter_limit
133134
self.export_limit = base.export_limit
@@ -634,6 +635,7 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi
634635
battery_rate_max_discharge = self.battery_rate_max_discharge
635636
battery_rate_max_export = self.battery_rate_max_export
636637
battery_rate_min = self.battery_rate_min
638+
inverter_freeze_export_discharge_rate = self.inverter_freeze_export_discharge_rate
637639
carbon_intensity = self.carbon_intensity
638640
set_discharge_during_charge = self.set_discharge_during_charge
639641
battery_charge_power_curve_tuple = charge_curve_to_tuple(self.battery_charge_power_curve)
@@ -1089,7 +1091,25 @@ def run_prediction(self, charge_limit, charge_window, export_window, export_limi
10891091
pv_dc = min(abs(battery_draw), pv_now)
10901092
pv_ac = (pv_now - pv_dc) * inverter_loss_ac
10911093

1092-
battery_state = "fz+" if battery_draw < 0 else "fz~"
1094+
# Some inverters (observed on AlphaESS) continue a small residual battery
1095+
# discharge during Freeze Export. Treat the configured value as battery-side
1096+
# power and feed it into the normal AC balance: house load consumes it first and
1097+
# any surplus may reach the grid. Respect the battery reserve and physical export
1098+
# limit rather than capping the discharge at house demand.
1099+
if inverter_freeze_export_discharge_rate > 0 and battery_draw >= 0:
1100+
freeze_soc_available = min(inverter_freeze_export_discharge_rate * step / 60000.0, max(soc - reserve_expected, 0))
1101+
freeze_draw = freeze_soc_available * battery_loss_discharge
1102+
freeze_diff = get_diff(freeze_draw, pv_dc, pv_ac, load_yesterday, inverter_loss, inverter_loss_recp)
1103+
if freeze_diff < 0 and abs(freeze_diff) > export_limit:
1104+
freeze_draw = max(freeze_draw - (abs(freeze_diff) - export_limit) * inverter_loss_recp, 0)
1105+
battery_draw = freeze_draw
1106+
1107+
if battery_draw < 0:
1108+
battery_state = "fz+"
1109+
elif battery_draw > 0:
1110+
battery_state = "fz-"
1111+
else:
1112+
battery_state = "fz~"
10931113
else:
10941114
# ECO Mode
10951115
pv_ac = pv_now * inverter_loss_ac

apps/predbat/prediction_kernel.cpp

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
// unconditionally, so loading one against this Python segfaults on the first prediction rather than
4343
// falling back. Bumping makes the loader reject it and use the Python engine, which is the whole
4444
// point of the check.
45-
#define PK_ABI_VERSION 4
46-
#define PK_PARITY_REVISION 7
45+
#define PK_ABI_VERSION 5
46+
#define PK_PARITY_REVISION 9
4747
#define PK_MAX_CARS 8
4848
#define PK_RUN_EVERY 5 // const.py RUN_EVERY
4949

@@ -169,6 +169,7 @@ struct PkContext {
169169
double battery_loss;
170170
double battery_loss_discharge;
171171
double inverter_loss;
172+
double inverter_freeze_export_discharge_rate; // W, residual battery-side discharge entering the AC balance during Freeze Export
172173
double inverter_limit; // per-minute rate (multiplied by step in the kernel)
173174
double export_limit; // per-minute rate
174175
double pv_ac_limit; // per-minute rate
@@ -714,6 +715,7 @@ static int32_t pk_run_one(const ContextStore *store, const PkScenario *s, PkResu
714715
const double battery_rate_max_discharge = c->battery_rate_max_discharge;
715716
const double battery_rate_max_export = c->battery_rate_max_export;
716717
const double battery_rate_min = c->battery_rate_min;
718+
const double inverter_freeze_export_discharge_rate = c->inverter_freeze_export_discharge_rate;
717719
// PV10 de-rating of the charge rate - prediction.py:587-592. PV90 is the upside case, no de-rate.
718720
const double battery_rate_max_scaling = is_pv10 ? c->battery_rate_max_scaling10 : c->battery_rate_max_scaling;
719721
const double battery_rate_max_scaling_discharge = c->battery_rate_max_scaling_discharge;
@@ -1077,6 +1079,20 @@ static int32_t pk_run_one(const ContextStore *store, const PkScenario *s, PkResu
10771079
pv_ac = (pv_now - pv_dc) * inverter_loss_ac;
10781080
}
10791081
}
1082+
1083+
// Some inverters (observed on AlphaESS) continue a small residual battery
1084+
// discharge during Freeze Export. Feed the battery-side rate into the normal AC
1085+
// balance so load consumes it first and any surplus may export, while respecting
1086+
// the reserve and the physical grid export limit.
1087+
if (inverter_freeze_export_discharge_rate > 0 && battery_draw >= 0) {
1088+
const double freeze_soc_available = std::min(inverter_freeze_export_discharge_rate * step / 60000.0, std::max(soc - reserve_expected, 0.0));
1089+
double freeze_draw = freeze_soc_available * battery_loss_discharge;
1090+
const double freeze_diff = get_diff(freeze_draw, pv_dc, pv_ac, load_yesterday, inverter_loss, inverter_loss_recp);
1091+
if (freeze_diff < 0 && std::abs(freeze_diff) > export_limit) {
1092+
freeze_draw = std::max(freeze_draw - (std::abs(freeze_diff) - export_limit) * inverter_loss_recp, 0.0);
1093+
}
1094+
battery_draw = freeze_draw;
1095+
}
10801096
} else {
10811097
// ECO Mode - prediction.py:951-997
10821098
pv_ac = pv_now * inverter_loss_ac;

apps/predbat/prediction_kernel.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
from utils import get_curve_value, find_battery_temperature_cap, in_car_slot, in_iboost_slot
3232

3333
# Expected ABI/parity revisions of the shared library (see prediction_kernel.cpp)
34-
KERNEL_ABI_VERSION = 4
35-
KERNEL_PARITY_REVISION = 7
34+
KERNEL_ABI_VERSION = 5
35+
KERNEL_PARITY_REVISION = 9
3636

3737
# Maximum number of cars supported by the kernel (PK_MAX_CARS in prediction_kernel.cpp)
3838
KERNEL_MAX_CARS = PREDBAT_MAX_CARS
@@ -78,6 +78,7 @@ class PkContext(ctypes.Structure):
7878
("battery_loss", ctypes.c_double),
7979
("battery_loss_discharge", ctypes.c_double),
8080
("inverter_loss", ctypes.c_double),
81+
("inverter_freeze_export_discharge_rate", ctypes.c_double),
8182
("inverter_limit", ctypes.c_double),
8283
("export_limit", ctypes.c_double),
8384
("pv_ac_limit", ctypes.c_double),
@@ -658,6 +659,7 @@ def create_kernel_context(pred, static_cache=None):
658659
ctx.battery_loss = pred.battery_loss
659660
ctx.battery_loss_discharge = pred.battery_loss_discharge
660661
ctx.inverter_loss = pred.inverter_loss
662+
ctx.inverter_freeze_export_discharge_rate = pred.inverter_freeze_export_discharge_rate
661663
ctx.inverter_limit = pred.inverter_limit
662664
ctx.export_limit = pred.export_limit
663665
ctx.pv_ac_limit = pred.pv_ac_limit
80 Bytes
Binary file not shown.
128 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
368 Bytes
Binary file not shown.
320 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)