← EnergyViewAdaptive 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
npm run perf:dispatch: three cold, separate Node processes per mode/capacity, median wall time and process peak RSS; raw runs are retained in the JSON output. Modes: pre-correction coarse lattice, exhaustive search on the new fine lattice, production adaptive/accelerated search.DISPATCH_DAYS=365 npm run perf:dispatch: same benchmark over 35,040 quarters.python scripts/perf/dispatch-lp-reference.py: regenerate the independent continuous LP fixture; requires SciPy (development only). The checked-in fixture was generated with SciPy 1.17.0 / HiGHS. No solver dependency is added to the application.node tests/battery-dispatch-adaptive.mjs: capacity monotonicity, LP accuracy, nonconvex fallback, deterministic cold-cache schedules, exhaustive comparison and checkpoint reconstruction.npm run perf:golden: all eight application scenarios against the consciously updated reference, €0.01 tolerance and identical identifiers/rankings.
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.
| Capacity | Continuous LP saving | New saving | Gap |
|---|
| 1 kWh | €1.413538 | €1.396875 | 1.18% |
| 2 kWh | €2.827076 | €2.814915 | 0.43% |
| 5 kWh | €4.607869 | €4.494704 | 2.46% |
| 10 kWh | €5.255533 | €5.045919 | 3.99% |
| 30 kWh | €5.255533 | €5.045919 | 3.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.
| Capacity | Old saving → new saving | Old coarse | Fine exhaustive | New accelerated | Old RSS → new RSS |
|---|
| 5 kWh | €43.90 → €46.64 | 15.97 ms | 85.17 ms | 43.40 ms | 48.77 → 51.40 MiB |
| 10 kWh | €41.60 → €52.45 | 14.91 ms | 161.89 ms | 68.28 ms | 48.38 → 53.38 MiB |
| 30 kWh | €0.00 → €52.45 | 9.74 ms | 464.04 ms | 362.10 ms | 46.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.
| Scenario | Before → after | Explanation |
|---|
| electricity-single | unchanged | No affected battery result |
| electricity-dual | unchanged | No affected battery result |
| pv-single | unchanged | No affected battery result |
| pv-dual | unchanged | No affected battery result |
| gas | unchanged | No affected battery result |
| dynamic-ev-battery | bill €867.58 → €876.53 | +€8.95; same selected contract/battery |
| battery-study | row 0 €667.67 → €664.73; row 2 €912.56 → €911.87 | −€2.94 / −€0.69; ordering/identifiers unchanged |
| purchase-plan | selected 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.
| Capacity | Old saving → new saving | Old coarse | Fine exhaustive | New accelerated | Fine RSS → new RSS |
|---|
| 5 kWh | €517.35 → €549.37 | 111.19 ms | 926.25 ms | 723.68 ms | 88.57 → 70.89 MiB |
| 10 kWh | €490.07 → €617.75 | 82.65 ms | 1789.14 ms | 1350.71 ms | 108.43 → 71.07 MiB |
| 30 kWh | €0.00 → €617.75 | 43.77 ms | 5418.53 ms | 3794.53 ms | 197.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.