Provenance and invariants
GridSim is an evidence apparatus as much as a simulator, and it rests on two
structural guarantees: every datum the evidence engine emits carries a typed
provenance tag saying where it came from, and every dated input passes a
forward-inference guard that makes the engine structurally incapable of
nowcasting. This chapter documents both, the [VERIFY] text convention that
accompanies placeholders, and where each invariant is enforced in code.
The ProvenanceTag taxonomy
src/GridSim.Core/Diagnostics/Provenance.cs:ProvenanceTag has seven members.
Every record type in the evidence path carries one - a PowerStep, a
RecordedAction, a RecordedOutcome, a whole GridEvent, every
EvidenceColumn, the avoided-cost inputs - so the tag cannot be forgotten
(the constructor demands it) and it survives serialisation, so a reviewer
reading a gridsim-evidence/1 file sees the same provenance the engine saw.
| Tag | Meaning |
|---|---|
Recorded |
directly from an archived record (NESO/GDA/published event log) |
DerivedFromRecorded |
computed from recorded data (e.g. inertia inferred from a recorded RoCoF + deltaP) |
CuratedPlaceholder |
a placeholder standing in for real data not yet sourced - always accompanied by [VERIFY] |
Synthetic |
model-generated, not from any record (e.g. a synthetic demand curve for engine testing) |
Counterfactual |
a counterfactual result (what a device would have done) - never an input to another replay |
Interpolated |
gap-filled / augmented replay input (e.g. ML/MoE) - usable to improve fidelity, never as an event source |
UnvalidatedAtScale |
a distributed-inertia-model result with no GB-scale reference (spatial RoCoF spread, propagation delay) - self-consistent, passes the reduction/invariant checks, but its absolute value at scale is not validated (awaits PMU/DFR field data) and must never be presented as validated |
One-way flow rules
The tags are permissions, not just labels, and the flow is one-directional: data may only ever move down the trust ladder, never up.
- Only
Recorded(and its computed consequence,DerivedFromRecorded) may seed the forward integrator as a real event. Replaying a record is replay; anything else fed in as if recorded would be prediction dressed as history. Counterfactualis a conclusion, never a cause: it is never an input to another replay. Feeding a counterfactual back in is exactly the circular reasoning the apparatus exists to prevent.InterpolatedandSyntheticdata may improve replay fidelity (smoothing, resolution, engine testing) but must never define that an event happened or masquerade as a recorded input.CuratedPlaceholderfigures let the pipeline run end-to-end before every primary source is in hand, but the results they touch are not quotable until the placeholder is replaced with a cited figure.UnvalidatedAtScaleresults may be shown (they are self-consistent physics) but always carry the tag; nothing downstream may launder them into a validated claim.- Nothing ever promotes itself: a value acquires a stronger tag only by being
re-sourced, not by passing through a computation. The dossier illustrates
the direction - the
recordedcolumn carries the event's own tag, the no-device baseline is hard-taggedDerivedFromRecorded, and the device column is hard-taggedCounterfactual(Evidence/EvidenceReport.cs:EvidenceReport.Build).
The [VERIFY] convention
[VERIFY] is a text convention paired with the CuratedPlaceholder tag, and
the pairing is a biconditional: a curated placeholder always carries a
[VERIFY] marker, and a [VERIFY] marker always flags a curated
placeholder. The enum types the datum; the greppable literal names the debt
in plain text - a reviewer can run one search across code, JSON and generated
reports and see every figure still awaiting a primary source. The markers
survive serialisation verbatim.
The built-in event catalogue shows both sides
(Events/EventCatalog.cs:EventCatalog.Builtin):
gb-largest-loss-1320is scaffolding: every figure taggedCuratedPlaceholder, every text field flagged[VERIFY]("single largest secured infeed [VERIFY]").gb-2019-08-09shows markers being retired the only way the convention allows - by sourcing. Its losses cite the Ofgem investigation report paragraph by paragraph and are taggedRecorded; the two figures the record only implies (the 350-430 MW DG RoCoF-trip midpoint, the ~550 MW DG loss at the LFDD instant) are taggedDerivedFromRecorded, not dressed up asRecorded.
Other standing [VERIFY] figures: the avoided-cost VoLL default (£6,000/MWh,
Evidence/AvoidedCost.cs:AvoidedCostInputs - a policy number that must be
replaced with a cited figure before the result is quoted) and the GB inertia
trajectory constants (Scenarios/ScenarioSpec.cs:SweepAxes.GbInertiaTrajectoryGvaS).
A gated dossier's summary even names the debt: "Reproduce the record first
([VERIFY] the event figures/tolerances)."
The markers are an integrity ledger, not a defect: deleting a [VERIFY]
would not make its figure sourced, only make it look sourced. Do not strip
them; retire them by sourcing.
The forward-inference guard
src/GridSim.Core/Diagnostics/ForwardInferenceGuard.cs enforces the hard
invariant: GridSim is a historical state estimator and may only process
data at least HorizonHours old. Anything newer - recent or future - is
rejected at the ingestion boundary with a ForwardInferenceException, so the
engine cannot nowcast or predict by construction, not by policy. It can only
ever lag reality.
public const double HorizonHours = 169.0; // 7 days + 1 hour
Horizon(utcNow)- the newest instant the engine may process (utcNow - 169 h).RejectFuture(stampUtc, utcNow?, what?)- throws when the stamp is newer than the horizon. AnUnspecified-kind stamp is assumed UTC; an offset-carrying one is converted.AssertHistorical(stamps, ...)- a whole dated series, first violation throws.
Why 169 hours
The horizon was widened from 25 h to 7 days + 1 hour (2026-07-14, owner decision), and the rationale is documented on the guard itself in two parts:
- Forward-inference defence in depth. A state at least a week old cannot be mistaken for - or repurposed as - a nowcast under any publication-lag or clock-skew argument: every data source the project consumes has fully published well inside a week, so the guard margin is no longer comparable to the slowest feed's lag.
- Reduced operational constraint space. A week-lagged state carries no operationally actionable information - unit commitment, outage windows, market positions and constraint patterns have all rolled over - so the system's entire output space sits outside the envelope an operational or market-sensitive reading could attach to.
The extra hour keeps a full settlement week strictly behind the boundary across DST transitions.
Every dated ingestion point
The guard is called at every boundary where a dated datum enters the engine. If you add a new dated input, call it there too - that is the convention that makes the invariant structural.
| Ingestion point | Call | What is checked |
|---|---|---|
| Event catalogue load | Events/EventCatalog.cs:EventCatalog.Validate (via Load, Get, Builtin) |
every event's WhenUtc - a catalogue cannot smuggle a future "event" |
| Measurement set load | Estimation/MeasurementSet.cs:MeasurementSet.Load |
the set's TakenUtc (when present) - the estimator only reconstructs the past |
| NESO frequency CSV | Frequency/NesoFrequencyCsv.cs:NesoFrequencyCsv.Read |
every row's timestamp as it streams - the reader can only read the past |
| GDA time-series load | Gda/GdaTimeSeries.cs:GdaTimeSeries.Load |
every step's TimeUtc |
| Historical replay loop | Simulation/GdaReplayLoop.cs:GdaReplayLoop.Replay |
re-asserts the whole series at the ingestion boundary, even though Load already checked it |
| Bridge state ticks | Net/GridBridgeClient.cs:GridBridgeClient.HandleState |
each streamed state frame's timestamp, before any frame is built - a future tick never reaches a view; a 15-minute ClockSkewToleranceMinutes allowance absorbs modest client/server skew, beyond which the tick is a protocol violation and refused |
| Replay RMSE path | Replay/ReplayValidator.cs:ReplayValidator |
inherits the per-row guard by reading the attached trace through NesoFrequencyCsv |
The GDA-side horizon split
The GDA data layer enforces the same boundary from its side, in
D:\Work\GDA\v1\Scripts\horizon.py - the single module every replay-facing
GDA component imports. It deliberately distinguishes two horizons:
| Horizon | Value | Applies to |
|---|---|---|
cutoff() - the inference horizon |
now - (7 d + 1 h), HORIZON_DAYS = 7 + 1/24 |
everything replay/reconstruction-facing: builders, the bridge server, replay tools |
ingest_cutoff() - the acquisition currency |
now - 1.05 d, INGEST_HORIZON_DAYS = 1.05 |
the raw ingest layer only |
The split encodes a principle stated in the module: raw acquisition is not
inference - fetching and storing public source data is not looking at it.
The lake may pull sources up to ~25 h behind real time to stay current, but
nothing on the inference side (measurement builders, state reconstruction,
the bridge) may read past cutoff(). The inference value matches GridSim's
ForwardInferenceGuard.HorizonHours = 169.0 exactly, and the module carries
the same two-part widening rationale.
GDA-side enforcement points include horizon.assert_historical in the
measurement builders (NetworkModel/build_gsp_measurements.py,
build_estimator_measurements.py) and the GridSim bridge server
(Applications/GridSim-Bridge/bridge_server.py), which rejects any replay
start past the cutoff outright and masks every emitted tick to t <= cutoff()
even when stored frames extend further.
No forward inference as a code invariant
Putting the two sides together: the rule "GridSim never nowcasts" is not a policy statement, it is the composition of guard calls at every boundary a dated datum can cross -
- catalogue load - a future event cannot enter the evidence pipeline
(
EventCatalog.Validate); - bridge state ticks - a too-recent streamed frame is refused before a
SimFrameis even built (GridBridgeClient.HandleState), and the GDA server masks its output to the same cutoff, so the wire is guarded at both ends; - measurement load - telemetry newer than the horizon cannot be estimated
(
MeasurementSet.Load), and the GDA builders refuse to author such a set in the first place; - replay - the historical replay loop re-asserts the horizon on the whole
series at ingestion (
GdaReplayLoop.Replay), and the NESO trace reader guards every sample it yields.
Most paths are therefore guarded twice - once where the data is authored (GDA side) and once where it is consumed (GridSim side) - so a single missed check never opens the boundary. The provenance tags complete the picture: the guard keeps the future out, and the tags keep non-recorded data from masquerading as the recorded past. Together they are what lets an evidence dossier claim, structurally rather than rhetorically, that everything in it is a replay of settled history.
See also
- Frequency dynamics - the NESO trace path the guard protects;
UnvalidatedAtScaleoutputs - State estimation - measurement loading and the GDA-side builders
- The evidence engine - the tags at work in events, replays and dossiers
- GDA integration - the bridge protocol whose ticks are horizon-guarded
- User manual: Replaying estimated states, Troubleshooting (horizon rejections)