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
Binding ratio — theoretical charge × rte ÷ dischargeable demand0.593
Usable capacity ÷ median daily dischargeable demand0.327
PV-to-load ratio0.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 rationMedian measured capture
< 0.3320.853
0.3 – 0.5150.835
0.5 – 0.7120.677
0.7 – 0.9130.552
0.9 – 1.240.569
> 1.240.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
ModelRMSEMAEBias
Flat 0.70 (shipped base)0.16310.1422−0.0433
Fitted f(r)0.09070.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:

OrientationnMean residual (fitted − measured)
zuid26−0.079 (under-predicts)
oost_west27+0.009
noord27+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:

  1. 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.
  2. Orientation term. Fit it now, or ship f(r) and disclose the residual?
  3. 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

QuestionDecision
Adopt f(r) as the base capture factorYes
Scenario spreadKeep 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 termNot 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
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.70Calibrated curve
NIET_KOPEN98.8%98.8%
Median payback18.5 j19.5 j
p10 payback15.5 j15.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.