← EnergyView

Adaptive battery lattice and convex transition search

Date: 2026-10-31. Node 24.22.1, Linux, shared managed host (dispatch-only runs did not capture the CPU model). This is an intentional numerical correction, not a result-preserving optimization. Tariffs, taxes, efficiencies, permitted actions and outer invoice/candidate selection rules are unchanged.

Reproduce

Numerical change

Previously resolution was approximately effective capacity / 100. The default now uses the median of positive usage / sqrt(efficiency) and injection × sqrt(efficiency), capped at 0.015 kWh and floored at approximately 0.01 kWh. The discharge-power quantum is divided by a rounded-up integer so the actual step cannot exceed the chosen target. That power alignment can put the actual step just below 0.01 kWh. Capacity is deliberately absent: equal power/efficiency and input produce nested feasible lattices across capacities. A larger battery can retain a smaller battery's entire feasible schedule.

Rolling day-ahead dispatch computes the resolution once over its entire input and explicitly carries it across EV iterations and daily horizons. It never snaps a transferred SOC to a different daily lattice. Initial and terminal SOC validation remains strict.

A 0.02 kWh target initially missed the chosen 5% accuracy bound (5.63% gap at 10/30 kWh). A 0.015 kWh target reduces this fixture's worst gap to 3.99%. The LP fixture contains 288 quarters, five capacities (1/2/5/10/30 kWh), fixed 2.5 kW powers, efficiency 0.9, finite import headroom and zero opening/closing SOC. Its convex piecewise-linear cost is represented by an epigraph LP with continuous SOC, not a finer discrete grid. Maximum balance residual is below 1e-15 kWh. The fixture uses net meter flows, avoiding simultaneous positive usage/injection; such simultaneous flows can themselves make dispatch costs nonconvex.

CapacityContinuous LP savingNew savingGap
1 kWh€1.413538€1.3968751.18%
2 kWh€2.827076€2.8149150.43%
5 kWh€4.607869€4.4947042.46%
10 kWh€5.255533€5.0459193.99%
30 kWh€5.255533€5.0459193.99%

Transition and storage

For each quarter, require sell <= buy and check the discrete cost slopes. Negative prices, conversion losses, simultaneous meter directions and external marginal invoice costs mean sell <= buy alone is insufficient. Arbitrary external callbacks always use exhaustive transitions. A 1e-12 euro per slope tolerance ignores floating-point noise in mathematically linear segments. Genuine detected nonconvexity retains the full band search.

Convex difference costs form a banded Monge matrix, even for arbitrary future-value columns. Divide-and-conquer searches monotone minima in O(states log states) instead of O(states × admissible differences). Unreachable rows have no argmin and cannot narrow neighbouring search bounds.

Both paths now use strict comparison and ascending destination states: the leftmost exact floating-point minimum wins deterministically. This deliberately replaces the old nontransitive cost < best - 1e-10 rule, which cannot safely supply monotone argmin bounds. Equal mathematical costs differing by floating-point noise can produce different schedules from the old code. Cache hits and cold execution must still reproduce every output exactly. Independent exhaustive comparisons assert objective agreement within 1e-8 euro (1e-7 for annual/checkpoint runs), because equal-value schedules need not match between a dense scan and a restricted monotone scan in floating-point arithmetic.

Policies exceeding 4,194,304 state-quarter cells are reconstructed from 96-quarter checkpoints. This avoids an annual quarters × states policy allocation, at the cost of a second transition pass. Short horizons retain the existing bounded policy cache. Uint32 choices are used if absolute state indices exceed Uint16.

Three-run month benchmark

The committed JSON includes every run, median peak RSS and heap use. Fixture: 31 days, 0.05–0.30 kWh household quarters, net midday PV, equal 2.5 kW powers and efficiency 0.9. Figures include result reconstruction; no warm policy-cache hits.

CapacityOld saving → new savingOld coarseFine exhaustiveNew acceleratedOld RSS → new RSS
5 kWh€43.90 → €46.6415.97 ms85.17 ms43.40 ms48.77 → 51.40 MiB
10 kWh€41.60 → €52.4514.91 ms161.89 ms68.28 ms48.38 → 53.38 MiB
30 kWh€0.00 → €52.459.74 ms464.04 ms362.10 ms46.63 → 53.08 MiB

The new solver is approximately 2.0× / 2.4× / 1.3× faster than exhaustive evaluation at the same fine resolution. It is slower than the old incorrect coarse result: approximately 2.7× / 4.6× / 37×. The old 30 kWh case's zero saving was cheap because it rounded all useful small discharges away. This is not presented as an across-the-board performance win. Checkpointing intentionally trades speed for bounded policy storage on larger inputs.

Golden review

The old golden passed before changes. After the correction it failed, as expected; neither tolerance nor ranking checks were relaxed. The pre-correction golden is recoverable from commit 5dc4e58c3b0fa5d29809b87de3d11e3b250e21be. All changed numerical paths are in dispatch-grid-golden-diff.json (16 fields). No string, identifier or rank-order changes; data/forward-curve hashes unchanged.

ScenarioBefore → afterExplanation
electricity-singleunchangedNo affected battery result
electricity-dualunchangedNo affected battery result
pv-singleunchangedNo affected battery result
pv-dualunchangedNo affected battery result
gasunchangedNo affected battery result
dynamic-ev-batterybill €867.58 → €876.53+€8.95; same selected contract/battery
battery-studyrow 0 €667.67 → €664.73; row 2 €912.56 → €911.87−€2.94 / −€0.69; ordering/identifiers unchanged
purchase-planselected bill €2320.34 → €2318.27−€2.07; dynamic comparison with-battery €2158.95 → €2142.47 (−€16.48)

A finer lattice is nested between capacities of the new solver, not between the old and new solvers. Neither finer discretization nor an optimal subproblem guarantees a lower final annual invoice in the existing rolling-horizon/coordinate search. This work does not establish why every invoice delta has its particular sign, or prove the global EV/battery/peak-fee invoice optimum. The €8.95 regression is disclosed, not described as an improvement.

One existing peak fixture asserted exactly 15 kW, which is not a representable boundary on the new lattice: its exact feasible maximum is now 14.976190476 kW. The test now computes and asserts that exact representable value; the strict <=15 kW physical safety assertion is retained. Idle tests still compare complete schedules with an independent exhaustive fine-grid reference. Policy-cache tests compare complete cached and uncached production outputs; exhaustive optimality is checked separately. The former byte-equality-to-coarse-grid test was intentionally replaced by fine-grid objective equivalence plus exact cold determinism; retaining equality to an erroneous grid would prevent the requested correction.

Limits of evidence

The LP's 5% bound is a regression requirement on these specific fixtures, not a universal approximation guarantee. Monotonicity follows from nested states for the fixed-grid optimization in exact arithmetic; floating-point and the outer annual invoice search do not have a universal monotonicity proof. The 1e-12 slope tolerance has empirical exhaustive checks, not a formal floating-point error proof. Negative/nonconvex inputs may remain expensive because their safe fallback is exhaustive. Browser/mobile timing, UI responsiveness, real-device memory and end-to-end global optimality were not measured or proven in this task. Process RSS includes the Node runtime and output objects; typed-array storage is not fully reflected in heapUsed.

The charging-diagnostics fixture also used identical discharge prices while assuming that the battery would be empty before later demand quarters. That was an unspecified optimal-schedule tie, not a physical guarantee. The fixture now uses strictly decreasing positive discharge prices to force depletion before the later quarters; its original exact 11 kW assertions remain unchanged.

Three-run annual dispatch benchmark

Same fixture repeated over 365 days / 35,040 quarters; three separate processes per cell, median, raw runs in dispatch-grid-annual-benchmark.json.

CapacityOld saving → new savingOld coarseFine exhaustiveNew acceleratedFine RSS → new RSS
5 kWh€517.35 → €549.37111.19 ms926.25 ms723.68 ms88.57 → 70.89 MiB
10 kWh€490.07 → €617.7582.65 ms1789.14 ms1350.71 ms108.43 → 71.07 MiB
30 kWh€0.00 → €617.7543.77 ms5418.53 ms3794.53 ms197.54 → 77.99 MiB

Annual checkpointing roughly doubles transition work, so same-resolution speedups are 1.28× / 1.32× / 1.43×. At 30 kWh peak RSS falls by approximately 61% versus the fine exhaustive reference. Compared with the old incorrect coarse solver, the new annual computation is 6.5× / 16.3× / 86.7× slower. In particular, no claim is made that adaptive accuracy is free or that all original latency targets are met.

The eight-scenario application harness was also measured once before and once after (not three times): dispatch-application-before-after.json. A separate golden recording run confirmed the same outputs. Main figures: dynamic EV/battery 13.20 → 17.58 s, peak RSS 1095 → 1001 MiB; battery study 6.20 → 7.82 s, 854 → 825 MiB; purchase plan 17.03 → 20.54 s, 1184 → 1072 MiB. These include the outer searches and pricing work; they are indicative shared-host observations, not isolated three-run end-to-end medians. The near-1-GiB application memory issue is not fully solved by reducing the dispatch policy allocation.

Validation

Final full test run: 189/189 passed, including deployment-source reproducibility, worker execution and existing energy/invoice conservation checks. Prettier check passed. ESLint/checkJs introduced zero new diagnostic groups (738 / 492 pre-existing diagnostics remain visible). The publishing workflow reruns the eight-case golden comparison after deliberate recording and builds HA, Pages and Worker sequentially. No real browser/device performance assertion is based on these Node checks.