Kalibratie van de capturefactor
Waarom de capturefactor een functie van de verhouding tussen opwek en verbruik werd in plaats van één vast getal, en hoe die functie gemeten is.
Dit is het onderzoeksdocument zoals het is vastgelegd, ongewijzigd overgenomen uit docs/calibration/capture-factor-function.md. Het is in het Engels geschreven.
Run: 30 July 2026 · bun scripts/calibration/harness.ts && bun scripts/calibration/capture-fit.ts
Data: the Phase C grid, 80 solvable cells
Status: APPROVED AND IMPLEMENTED — founder, 30 July 2026. See §7 for the decision and its
measured effect.
1. Why this was run
docs/calibration/sensitivity.md established that the capture factor is the single largest
determinant of the site's message: relaxing it alone shortens median payback from 19.5 to 15.2
years, and it is a decisive contributor to the 63.0% battery-favourable case. It is also the only such constant
that is calibrated — derived from our own harness rather than an external source.
A flat 0.70 applied to every household was deciding more about what Batterijscout tells people than any external fact. With the independent review removed from the plan (roadmap A6), leaving that unexamined was not defensible.
2. The Phase C recommendation does not survive testing
The Phase C report recommended replacing the constant with "a function of usable capacity ÷ median daily surplus". Tested against the harness data, that predictor explains almost nothing:
| Predictor | R² |
|---|---|
| Binding ratio — theoretical charge × rte ÷ dischargeable demand | 0.593 |
| Usable capacity ÷ median daily dischargeable demand | 0.327 |
| PV-to-load ratio | 0.165 |
| Usable capacity ÷ median daily surplus (Phase C's proposal) | 0.100 |
Correction to phase-c-report.md: capture does not track battery size against surplus. It
tracks battery size against demand available to discharge into. The distinction matters because
they diverge exactly where the money is — a household with lots of surplus but little evening
demand cannot use a large battery, and the surplus-based predictor cannot see that.
3. What the binding ratio is
r = (theoretical monthly charge × rte) ÷ (dischargeable demand)
How much energy the monthly model thinks the battery could deliver, against how much demand there actually is to deliver it into.
- r well below 1 — demand-limited. Nearly all theoretical charge finds a home, the monthly model is close to right, and little correction is needed.
- r above 1 — surplus-limited. Theoretical charge has nowhere to go and the monthly model overstates what gets shifted.
This is the oversizing effect as a continuous quantity rather than a threshold, and both of its
inputs are already computed per household inside simulate.ts (theoreticalByMonth and
deficitByMonth). No new input is required from the user.
Measured, it is cleanly monotone:
| Binding ratio | n | Median measured capture |
|---|---|---|
| < 0.3 | 32 | 0.853 |
| 0.3 – 0.5 | 15 | 0.835 |
| 0.5 – 0.7 | 12 | 0.677 |
| 0.7 – 0.9 | 13 | 0.552 |
| 0.9 – 1.2 | 4 | 0.569 |
| > 1.2 | 4 | 0.536 |
4. The fitted function
f(r) = 0.85 for r ≤ 0.35
= linear 0.85 → 0.53 for 0.35 < r < 0.85
= 0.53 for r ≥ 0.85
| Model | RMSE | MAE | Bias |
|---|---|---|---|
| Flat 0.70 (shipped base) | 0.1631 | 0.1422 | −0.0433 |
| Fitted f(r) | 0.0907 | 0.0757 | −0.0003 |
Two things this measures.
The fitted function reduces error by 44%. More importantly it removes a systematic bias: the flat 0.70 understates capture by 4.3 percentage points on average across the grid. That is not a judgement about whether the model "feels" harsh — it is a measured, directional error, and it is precisely the kind of finding the removed independent review was meant to produce.
Mean fitted capture across the grid is 0.743 against the shipped 0.700, ranging 0.53–0.85 by household. So this is a modest correction with large dispersion: a well-sized battery in a demand-rich household gets 0.85, an oversized one gets 0.53. The flat constant charged both the same.
5. What the fit still does not explain
R² of 0.59 leaves 41% unexplained, and the residuals are not random — they track orientation:
| Orientation | n | Mean residual (fitted − measured) |
|---|---|---|
| zuid | 26 | −0.079 (under-predicts) |
| oost_west | 27 | +0.009 |
| noord | 27 | +0.066 (over-predicts) |
South-facing households capture more than the binding ratio alone predicts; north-facing less. Intra-day shape is doing work the ratio cannot see. An orientation term would likely absorb much of this, but it should be fitted deliberately rather than bolted on — and every added term needs its own justification, or the function stops being calibration and becomes curve-fitting.
Phase C's own limitations still apply and are not reduced by this work: single weather year (2024), aggregate E1A load chronology rather than individual households, clear-sky intra-day shape.
6. Recommendation, for sign-off
Replace the flat base capture factor with f(r), keeping the scenario triple as a spread around
it rather than as the value itself.
The case for it is accuracy, not outcome: 44% less error and a removed systematic bias, calibrated from the existing harness and fitted on a variable the engine already computes. It will move some households toward WACHT, because the shipped constant is measurably too pessimistic — but that is the consequence, not the reason. If the measurement had shown 0.70 was unbiased, the recommendation would have been to leave it alone.
Open questions for the decision:
- Scenario spread. Simplest defensible option is to keep the approved relative spread
(conservative ≈ base × 0.79, favourable ≈ base × 1.21, from 0.55/0.70/0.85) and apply it around
f(r). This has not been separately calibrated. - Orientation term. Fit it now, or ship
f(r)and disclose the residual? - Re-run the outcome distribution after implementation. The effect is bounded below by the baseline (98.8%) and above by the capture-0.88 sensitivity run (97.5% NIET_KOPEN), but the dispersion means the real figure needs measuring, not interpolating.
Implementation touches assumptions.ts and simulate.ts and re-verifies a gated constant, so it
does not proceed without approval. Recording the decision in
docs/v1-scope-decision-memo.md keeps the register honest.
7. Decision and outcome — 30 July 2026
Approved by: Ev · Date: 2026-07-30
| Question | Decision |
|---|---|
Adopt f(r) as the base capture factor | Yes |
| Scenario spread | Keep the approved relative spread, re-expressed as ratios to the 0.70 base (×0.786 / ×1 / ×1.214) and applied around the curve. Not separately calibrated — disclosed as such. |
| Orientation term | Not added. Ship and disclose the residual. Adding it would need its own calibration rather than being bolted on. |
Refitted on a shippable variable
The first fit used the harness's chronological deficit. simulate.ts has only the monthly
heuristic deficit, and the two differ by roughly 7%. A function fitted on a quantity the engine
cannot compute would have been wrong in production in a way no test would catch, so it was refitted
on the model's own deficit and theoretical charge.
| Predictor | R² |
|---|---|
| Binding ratio, chronological deficit (not shippable) | 0.593 |
| Binding ratio, model deficit (shipped) | 0.578 |
Shipped function: f(r) = 0.85 for r ≤ 0.35, linear to 0.54 at r = 0.80, then 0.54.
RMSE 0.163 → 0.0918 (−44%); bias −0.0433 → +0.0013.
Measured effect on outcomes — the correction makes results slightly WORSE
| Flat 0.70 | Calibrated curve | |
|---|---|---|
| NIET_KOPEN | 98.8% | 98.8% |
| Median payback | 18.5 j | 19.5 j |
| p10 payback | 15.5 j | 15.1 j |
The outcome distribution does not move, and the median household's payback lengthens by a year.
This is the dispersion working as designed. The flat 0.70 charged every household the same penalty; the curve rewards a well-sized battery (p10 improves) and penalises an oversized one (median worsens). Across the four catalogue batteries — several of which are large relative to the modelled households — the net effect on the median is negative.
Worth stating plainly: this change was justified as an accuracy improvement and it made the commercial picture marginally worse. That is the correct direction of travel for a calibration. Had it been fitted toward an outcome it would have gone the other way.
Gap this exposed
All 113 tests passed unchanged after a material change to the capture model. The golden tests
assert outcome labels, not intermediate vectors, so they cannot guard the engine's numerics —
roadmap B3 (numeric freeze of G1–G15 against full intermediate vectors) is not optional
housekeeping, it is the missing regression guard. lib/engine/capture.test.ts now covers the
function's shape as a partial substitute.