Normalization: apportion a long unread gap across the months it covers
A counter delta is booked at the reading that closes it. That is right at the reporting cadence -- a monthly series books December against the 1 January reading, which is what the reference sheet does -- and wrong after an outage. Observed on Solar 1: 78 days of generation arrived as one July row, leaving June looking like the array was switched off. An interval containing two or more complete calendar months is now divided across them in proportion to elapsed time. The meter recorded a total, not a shape, so every row a split produces is marked Estimated. The sum is exact: the final segment absorbs the rounding remainder, so a split never creates or destroys energy. Counting whole months *contained* rather than boundaries *crossed* is what makes the rule safe. A monthly series contains exactly one whole month per interval and is untouched, so the golden fixtures keep measuring the normalizer rather than the splitter; and a reading landing hours late cannot tip the rule and hand the new month a sliver. GapSplittingIsInertOnFixturesTests asserts the rule declines to fire on every reference interval, so this cannot drift into the oracle unnoticed. Not apportioned: swap and reset amounts (explicit corrections booked at their event -- apportioning one would rewrite a number the operator supplied), a rejected decrease, and a zero delta, which would otherwise fan out into rows that say nothing. Segments are stamped at their end, keeping the existing convention that a row records the period ending at its timestamp -- so nothing shifts relative to how unsplit intervals are already labelled. Claude-Session: https://claude.ai/code/session_01V6joyergfvVLFEizH1hJLd
This commit is contained in:
@@ -429,6 +429,10 @@ The key ring must be persisted outside the app directory (`MeterVault__DataProte
|
||||
### 7.1 Register → consumption
|
||||
For `cumulative_counter`/`generation_counter`: for each new reading, `amount = value − previous_value`. Persist to `consumption`. Cross a `meter_swap` as `(old_final − prev) + (curr − new_initial)`; a `counter_reset` starts a fresh baseline. Ignore/annotate negative deltas that lack an explaining event (flag as anomaly).
|
||||
|
||||
**Gap attribution.** A delta is booked at the reading that closes it — correct at the reporting cadence, and what the reference sheets do. After a long unread stretch it misleads: 78 days of PV output arriving as one July row makes June look idle. So an interval containing **two or more complete calendar months** is apportioned across the months it covers, in proportion to elapsed time, and every row it yields is marked `quality = estimated` — the meter recorded a total, not a shape.
|
||||
|
||||
The threshold is deliberately conservative. A monthly series contains exactly one whole month per interval and is never touched, which is what keeps the golden-fixture reconciliation (§13) measuring the normalizer rather than the splitter. Counting whole months *contained* rather than boundaries *crossed* keeps the rule stable when a reading lands hours late. Swap and reset amounts are never apportioned: they are explicit corrections booked at the event. Split points are UTC, so one can sit an hour or two from a displayed month edge (§10) — immaterial when dividing a multi-month gap, and the alternative is threading a timezone through an otherwise timezone-free engine.
|
||||
|
||||
### 7.2 Runtime → consumption (burner)
|
||||
For `runtime_counter`: `amount = Δhours × rate`. `rate` comes from the linked `tank`: `fixed` (nozzle spec, L/h) or `empirical` (`Δlevel ÷ Δhours` measured between deliveries/level reads — reproduce the spreadsheet's 1.87/1.94/2.92 … behaviour). Expose both; default empirical when level data exists, else fixed.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user