8. Provenance and invariants¶
GridSim is an evidence apparatus as much as a simulator. Two structural mechanisms keep its claims honest. Every datum
the evidence engine emits carries a typed provenance tag (ProvenanceTag) that says where it came from. Every
dated input that reaches one of the dated seams passes the forward-inference guard (ForwardInferenceGuard), which refuses any timestamp newer than
169 hours, so the engine only ever reads the past. A measurement set with no takenUtc is refused (ForwardInferenceGuard.RequireDated), and so is a harmonic source set with no takenUtc that holds a measured (Recorded or DerivedFromRecorded) source; only an all-assumed library source set may stay undated. This chapter specifies both, the [VERIFY] text marker that
accompanies placeholder data, the RestrictionReview gate, and the project-wide invariants that every change must keep:
bit-identical numerics, the fail-loud convergence contract, invariant culture, constitutional tests and the version
contract. For each numerical invariant it states what the code guarantees today and where the code falls short.
The audit log that records the tag of each run, the guard's message and the list of dated seams are specified in ch. 31, Audit log and diagnostics; this chapter links there for them instead of repeating them. The solver chapters are ch. 3, Power flow and ch. 4, Sparse and GPU numerics; the project map is ch. 1, Architecture; the estimator is ch. 6, State estimation; the file formats that carry the tags are in ch. 10, File and wire formats.
Model¶
The ProvenanceTag taxonomy¶
ProvenanceTag (src/GridSim.Core/Diagnostics/Provenance.cs) has seven members. They are declared in the order below,
which gives the numeric values 0 to 6. The numeric value carries no meaning: nothing ranks tags by it, every file
format stores the member name, and the audit log refuses a value outside the enum at write time and at read time
(AuditLogFile, AuditLogWriter: Enum.IsDefined).
| Value | Member | Meaning |
|---|---|---|
| 0 | Recorded |
Directly from an archived record: NESO or GDA data, or a published event log. |
| 1 | DerivedFromRecorded |
Computed from recorded data, for example inertia inferred from a recorded RoCoF and a step change in power. |
| 2 | CuratedPlaceholder |
A value standing in for real data that has not been sourced. Always accompanied by the text [VERIFY]. |
| 3 | Synthetic |
Generated by a model, not from any record, for example a synthetic demand curve for engine testing. |
| 4 | Counterfactual |
The result of a what-if, such as what a device would have done. Never an input to another replay. |
| 5 | Interpolated |
A gap-filled or augmented replay input. It may improve fidelity and is never an event source. |
| 6 | UnvalidatedAtScale |
A distributed-inertia-model result with no GB-scale reference (spatial RoCoF spread, propagation delay). It passes the reduction and invariant checks, but its absolute value at scale awaits PMU or DFR field data and is never presented as validated. |
These record types of the evidence path take a tag as a required constructor argument, so a record cannot be built
without one: PowerStep, RecordedAction, RecordedOutcome and GridEvent (src/GridSim.Core/Events/GridEvent.cs),
EvidenceColumn and EvidenceReport (src/GridSim.Core/Evidence/EvidenceReport.cs), AvoidedCostResult
(src/GridSim.Core/Evidence/AvoidedCost.cs), AssetClassProvenance (src/GridSim.Core/Assets/AssetClassLibrary.cs)
and every audit-log entry. These types default the tag instead: AvoidedCostInputs.VollProvenance, HarmonicSource and
ChemistryParameters (CuratedPlaceholder), AssetEnvelope (UnvalidatedAtScale), FrequencyExport (Synthetic),
H0StudyExport (Counterfactual) and GdaReplayResult (DerivedFromRecorded), each in its own file under
src/GridSim.Core. DivergenceReport sets UnvalidatedAtScale where it builds its result, and HarmonicExport writes a
computed headline tag. Tags serialise as the member name, so a reader of a gridsim-evidence/1 or
gridsim-event-catalog/1 file sees the tag the engine saw.
Trust order and the downgrade-only rule¶
The tags order the data by how much a reader may rely on it. From most to least trusted, as a reader uses the word:
Recorded > DerivedFromRecorded > Interpolated, Synthetic > CuratedPlaceholder
Counterfactual and UnvalidatedAtScale are outputs, labelled and never fed back in
The rule is that trust moves down and never up: a value gets a stronger tag only by being re-sourced from a primary
record, never by passing through a computation. ProvenanceTrust (src/GridSim.Core/Diagnostics/ProvenanceTrust.cs)
puts the mechanical parts of it in code:
Rank(tag): 4Recorded, 3DerivedFromRecorded, 2InterpolatedandSynthetic, 1CuratedPlaceholder. An output tag (Counterfactual,UnvalidatedAtScale;IsOutputOnly) has no rank and is refused.RequireInput(tag, what): an output tag, or an undefined value, used as an input is refused (InvalidDataException). It runs at the replay entry on every tag of an event (ReplayValidator.RequireReplayInputs: the event, its outcome, each loss, each recorded action, each response service and the demand effect) and at the case editor's save gate on every tag written in a case file.RequireNotRaised(inputs, result, what): a computed tag stronger than the weakest of its inputs is refused. The avoided cost (VoLL tag againstAvoidedCost.VollBaseProvenance) and the harmonic export headline use it.Weakest(tags, what): the lowest rank wins; two different tags at the lowest rank (InterpolatedandSynthetic) giveCuratedPlaceholder.
The rules in practice:
- Only an event sourced from a record is replayed as a reproduction of history.
ReplayValidator.EffectiveProvenance(ev)is the weakest tag of all the event's parts, andRequireRecordedReplay(called byreplay-event) refuses an event whose effective tag is notRecordedorDerivedFromRecorded, naming its weakest part (exit 1). The built-in recorded events (gb-2019-08-09,gb-2023-12-22) are effectivelyDerivedFromRecorded; the screening eventgb-largest-loss-1320is effectivelyCuratedPlaceholderand is refused. The study verbs (scenario,fleet,evidence,h0-study) accept a placeholder or synthetic event, a legitimate study input, and their outputs carry the weaker tag. Counterfactualis a conclusion, not a cause. It is labelled at the point it is produced: the evidence column for the device, the result of a storage replay (GdaReplayLoop.ReplayreturnsCounterfactualwhen storage is attached), the CLI exports (H0StudyExport, the storage CSV header comment "never an input to another replay").InterpolatedandSyntheticdata may improve fidelity or test the engine; they never define that an event happened.CuratedPlaceholderdata lets a pipeline run end to end; the results it touches are not quotable until a cited figure replaces it.UnvalidatedAtScaleresults are shown with the tag and never presented as validated.
Where each assignment lives. Most are hard-coded at the site that creates a datum; some compute a tag:
| Site | Tag it assigns |
|---|---|
EvidenceReport.Build (Evidence/EvidenceReport.cs), column "recorded" |
the event outcome's own tag (ev.Outcome.Provenance) |
EvidenceReport.Build, column "simulated (no device)" |
DerivedFromRecorded, fixed |
EvidenceReport.Build, device column |
Counterfactual, fixed |
AvoidedCostInputs (Evidence/AvoidedCost.cs) |
VollProvenance defaults to CuratedPlaceholder |
--voll-source flag of the evidence verb (Program.cs) |
checked, not raised: the flag must name a VollSources reference (S322, S323, S324) and --voll must equal a figure it states (else exit 2); the citation is recorded as VollSourceId and VollProvenance stays CuratedPlaceholder; the cost takes the weaker of the VoLL and duration tags |
HarmonicExport.ToJson (IO/HarmonicExport.cs) |
computed (HarmonicExport.HeadlineProvenance): the weakest of the sources' tags when the source set is non-empty and every source is Recorded or DerivedFromRecorded, else CuratedPlaceholder; checked by RequireNotRaised |
EvidenceReport.Build on a study event |
every column takes at most the event's effective provenance (ProvenanceTrust.Weakest) |
GridFollowingImpedance.FromAssetClass, GridFormingImpedance.FromAssetClass (Dynamics/Impedance/) |
computed: Recorded when the class's PLL bandwidth (grid following) or virtual reactance (grid forming) carries Recorded or DerivedFromRecorded provenance, and for grid following no CLI override was given; else CuratedPlaceholder |
AssetClass.ProvenanceFor (Assets/AssetClassLibrary.cs) |
a parameter with no entry and no _default gets CuratedPlaceholder with the note [VERIFY] unspecified provenance |
V2CaseLoader override handling (IO/Json/V2CaseLoader.cs) |
an override that gives no provenance becomes CuratedPlaceholder with a [VERIFY] note quoting the override reason |
GdaReplayLoop.Replay (Simulation/GdaReplayLoop.cs) |
DerivedFromRecorded by default; Counterfactual when storage is attached |
FrequencyExport, H0StudyExport (IO/) |
defaults Synthetic and Counterfactual |
CoupledHarmonicPowerFlow (Harmonics/CoupledHarmonicPowerFlow.cs) |
always CuratedPlaceholder |
Program.cs audit tag |
estimate: Synthetic with --synth, else DerivedFromRecorded; crossval: Synthetic; serve: DerivedFromRecorded; every other verb, interpose included: Recorded (ch. 31) |
The two defaults that fire when a source does not state its provenance, AssetClass.ProvenanceFor and the v2 override
handling, both choose CuratedPlaceholder, the weakest data tag, so silence downgrades. A computed site that would
raise a tag above its inputs is refused, so a tag only moves down through a computation.
The [VERIFY] marker¶
[VERIFY] is a literal text token paired with the CuratedPlaceholder tag. The enum types the datum and the token
names the debt in plain text, so one search across code, JSON and generated reports finds every figure that still needs
a primary source. The token survives serialisation verbatim. The intent is a biconditional: a curated placeholder
carries [VERIFY], and [VERIFY] flags a curated placeholder.
The check is implemented for asset classes and for nothing else:
AssetClass.ValidateProvenance():
for each (key, prov) in Provenance:
if prov.Tag == CuratedPlaceholder and not prov.Note.Contains("[VERIFY]"):
report "asset class '<name>' parameter '<key>' is CuratedPlaceholder but its note lacks the literal [VERIFY] token."
AssetClassLibrary.ValidateOrThrow(): InvalidDataException listing every violation
JsonGridLoader.Build calls ValidateOrThrow on every asset-class library it loads, so a placeholder without the token
fails the load. The reverse direction (a [VERIFY] token on a non-placeholder) is not checked, and the event catalogue,
the avoided-cost inputs and the evidence report carry no automatic check: their tokens are authored by hand in the
built-in data.
The built-in event catalogue (Events/EventCatalog.cs:EventCatalog.Builtin) holds three events:
gb-largest-loss-1320is scaffolding. The event, its inertia, demand and outcome areCuratedPlaceholder, and its title, notes and source comments carry[VERIFY]. Its single loss step is the exception: 1320 MW is the NETS SQSS normal infeed loss risk, so that step is taggedRecordedwith its citation while the rest stays placeholder.gb-2019-08-09has retired its markers by sourcing. Its losses cite the Ofgem investigation report paragraph by paragraph and are taggedRecorded. The two figures the report only implies, the 350 to 430 MW distributed-generation RoCoF-trip estimate (entered as 390 MW) and the 550 MW distributed-generation loss at the LFDD instant, are taggedDerivedFromRecorded.gb-2023-12-22is a second sourced event, measured end to end. The event and its outcome areRecorded. Its two loss steps (IFA and Cottam, from settlement metering) and its recovery actions areDerivedFromRecorded.
The captured replays below show the tag in the output line provenance:. The avoided-cost default
(AvoidedCostInputs.VollPerMwh, GBP 6,000 per MWh) is a CuratedPlaceholder policy number that must be replaced with a
cited figure before a result is quoted. A gated dossier names the debt in its own summary: "Reproduce the record first
([VERIFY] the event figures/tolerances)." Deleting a [VERIFY] does not source its figure; it only hides the debt. Retire
a marker by sourcing the figure and changing its tag in the same edit.
Placeholder sites follow the same convention. data/library/placeholders/unmodelled-sites.json holds named sites that no
public dataset covers: Nuclear-Sub, Carrier, PCI-Datacentres, Hyperscale-Datacentre, AI-Training-Campus,
Naval-Base-Shore-Supply, Military-Airfield, SMR (small modular reactor), Private-Wire-Network,
Behind-The-Meter-BESS-Aggregate, Electrolyser-Cluster, Arc-Furnace-Steelworks, Rail-Traction-Supply,
Megawatt-EV-Charging-Hub and Crypto-Mining-Site. Each is a status: placeholder template of kind site whose generation
and demand are deliberately null, tagged CuratedPlaceholder with the note "Deliberately empty: not in any public
dataset [VERIFY]". The values are empty on purpose and the code does not model these sites. The v2 placeholder rule
rejects a placeholder that carries a non-null value, so they cannot be filled in by accident.
The forward-inference guard¶
ForwardInferenceGuard (src/GridSim.Core/Diagnostics/ForwardInferenceGuard.cs) makes GridSim a historical state
estimator. The rule and the whole API are:
HorizonHours = 169.0 (7 days + 1 hour; a const double, the single knob)
Horizon(utcNow) = utcNow - 169 h
RejectFuture(stamp, utcNow?, what?):
stamp' = UtcTime.Normalise(stamp) (Unspecified is read as UTC; Local is converted)
throw ForwardInferenceException if stamp' > Horizon(utcNow) (utcNow defaults to DateTime.UtcNow)
AssertHistorical(stamps, utcNow?, what?): RejectFuture on each stamp; the first violation throws
A stamp exactly on the horizon passes and one tick later throws. A stamp at or after utcNow, and any stamp inside the
horizon, throws. The message, the exit-code mapping (REJECTED (forward inference): ..., exit 2) and the table of the
dated seams that call the guard are in
ch. 31, The forward-inference guard. No configuration key, role or overload
changes the horizon or skips the check.
Rationale for 169 hours. The guard is a defence, and the width serves two purposes that the class remarks record:
- Defence in depth against forward inference. A state at least a week old cannot be mistaken for, or repurposed as, a nowcast under any publication-lag or clock-skew argument. The owner's decision of 2026-07-14 records that every source the project consumes has fully published well inside a week, so the margin no longer compares with the lag of the slowest feed. This chapter measures no feed lag.
- A 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 whole output space sits outside the envelope an operational or market-sensitive reading could attach to.
The extra hour beyond seven days keeps a full settlement week strictly behind the boundary across a daylight-saving change.
The live paths. The guard judges a stamp against a clock, and the clock is a parameter, but no live path can move it
forward. gridsim serve takes its effective now from the real clock, or from --utc-now <iso> (parsed strictly as ISO
8601, UtcTime.TryParseIso8601), and ForwardInferenceGuard.RequireNotAfterRealClock refuses a --utc-now later than
the real clock (REJECTED (forward inference), exit 2, before any window is read), so the flag can only move the
horizon back. It loads each window with MeasurementSet.Load(file, now), so the load-time seam is on: a window newer
than the horizon is counted as rejected-fresh and skipped, never estimated, and an undated window stops the run with
REJECTED, exit 2. The real-time estimator engines (RealtimeEstimatorEngine, RealtimeDseEngine) apply the horizon
again against opt.UtcNow, which they also refuse when it is later than the real clock. gridsim interpose judges each
window against the real horizon when it is flushed: a window newer than the horizon is held, never written as an
estimator input (ch. 20). The constant is fixed in source, and
RestrictionInvariantTests.Inv1_The_horizon_is_a_single_knob fails if it changes; no flag, role or configuration key
moves the horizon forward.
The GDA side. The GDA data layer holds the same boundary in its own repository, in v1/Scripts/horizon.py, which the
GDA measurement builders, scenario_replay.py and the bridge server import. HORIZON_DAYS = 7.0 + 1.0/24.0 is the
inference horizon and equals ForwardInferenceGuard.HorizonHours; cutoff() returns now minus that.
INGEST_HORIZON_DAYS is the acquisition currency of the raw ingest layer and is 0.0 there: fetching and storing public
source data is not looking at it, so acquisition can be live while everything that reconstructs state stays behind
cutoff(). The measurement builders (build_estimator_measurements.py, build_gsp_measurements.py,
build_smartmeter_fast.py) and scenario_replay.py call horizon.assert_historical. The bridge server
(v1/Applications/GridSim-Bridge/bridge_server.py) imports only cutoff(), carries a fallback copy of the 7 d 1 h
constant for when the import fails, rejects a replay start past the cutoff and masks every emitted tick to
t <= cutoff() itself. Most paths are therefore guarded twice, once where data is authored (GDA) and once where it is
consumed (GridSim), and the GridSim bridge client refuses a state tick inside the horizon before it builds a frame
(ch. 31).
RestrictionReview¶
RestrictionReview (src/GridSim.Core/Diagnostics/RestrictionReview.cs) is the review gate for the one place an
estimator could quietly become a predictor: fusing a dynamic process model into the estimator. It has two checks.
AssertPermitted(method, usesDynamicsModel):
if not usesDynamicsModel -> return
entry = Reviewed[method]
throw RestrictionViolationException if entry is missing, or not entry.UsesDynamicsModel, or not entry.OutputPresentPastOnly
AssertOutputHistorical(outputStamp, utcNow?, what?):
throw RestrictionViolationException if UtcTime.Normalise(outputStamp) > Horizon(utcNow)
Reviewed is a code-resident table with four entries: Ekf, Ukf, EnKf and FrequencyRocofKf. Each filter calls
AssertPermitted in its constructor, so an unreviewed dynamics-fused method fails before it runs. Both real-time engines
also call AssertOutputHistorical on each estimate before it is emitted, with the same 169 h horizon. It is a second check
at the emit point, on the same stamp and clock that the engine's RejectFuture call has already passed. The table, the
entries' fields and the call sites are specified in
ch. 31, RestrictionReview.
Parameters and defaults¶
| Name | Value | Unit | Meaning | Source symbol |
|---|---|---|---|---|
| Forward-inference horizon | 169.0 | h | minimum age of any processed datum | ForwardInferenceGuard.HorizonHours (src/GridSim.Core/Diagnostics/ForwardInferenceGuard.cs) |
| Real-time clock | DateTime.UtcNow, or an earlier --utc-now |
instant | clock the horizon is judged against in serve (a later one is refused, exit 2) |
RealtimeEstimatorOptions.UtcNow, ForwardInferenceGuard.RequireNotAfterRealClock |
| Avoided-cost VoLL | 6000.0 | GBP per MWh | placeholder policy number | AvoidedCostInputs.VollPerMwh (src/GridSim.Core/Evidence/AvoidedCost.cs) |
| Avoided-cost LFDD duration | 0.5 | h | placeholder outage duration | AvoidedCostInputs.LfddDurationHours |
| Placeholder marker | [VERIFY] |
text | literal token paired with CuratedPlaceholder |
AssetClass.ValidateProvenance (src/GridSim.Core/Assets/AssetClassLibrary.cs) |
| Power-flow dense/sparse routing | 0 buses (always sparse) | buses | CPU Newton power flow routes to the sparse engine | SolverSelector.DefaultSparseThreshold (src/GridSim.Core/PowerFlow/SolverSelector.cs) |
| Estimator dense/sparse crossover | 200 | buses | the WLS observability report is the dense rank proof at or below it; the CPU estimate itself is the sparse step at every size | SolverSelector.DenseSparseCrossoverBuses, EstimationOptions.SparseBusThreshold |
| Non-convergence exit code | 3 | code | a study that did not converge, or a refused non-converged state | Program.cs (Environment.ExitCode = 3) |
| Forward-inference exit code | 2 | code | REJECTED (forward inference) |
Program.cs |
| Tier gate exit code | 3 | code | simdcheck --expect <tier> when the active tier is narrower |
Program.cs (RunSimdCheck) |
Determinism and bit-identity as implemented¶
GridSim's acceptance criterion for every change is bit-identical results: across runs, across parallel paths, across CPU
instruction-set tiers (AVX-512, AVX2, non-FMA) and between the dense and sparse solvers. A change that only reorganises
orchestration leaves every number unchanged to the last bit, proved with exact Assert.Equal. The only accepted
exception is a mathematical proof that a reference implementation elsewhere is wrong. This section states what the code
delivers on each axis today, with the exact places where it does not.
Across runs, on one machine¶
Newton-Raphson solves are bit-identical from run to run: SolverPropertyTests.Solving_twice_is_bit_for_bit_identical
compares iterations, mismatch, losses and every Vm, Va, P and Q exactly on the eight IEEE and benchmark
built-ins. The captured example below solves case118 twice and hashes the result file: the two hashes are equal.
Parallel paths and sequential paths¶
The parallel and sequential contingency screens call the same per-outage function (ContingencyAnalysis.ScreenOutageCore
for contingency screens, LossContingency.ScreenOne for the loss sweep). A parallel sweep gives each worker thread its own
solver and writes each result into a fixed slot of the result array, so the thread schedule cannot change which result
lands where. Ranked lists sort by a pure score with a stable sort. The tests pin this much:
- Two parallel exhaustive sweeps agree exactly on outage, convergence, maximum branch loading and minimum and maximum
voltage (
ContingencyTests.N2_enumeration_is_deterministic_despite_the_parallel_solve,N1_1_is_deterministic_despite_the_parallel_solve). DefaultEngineRoutingTests.Contingency_screens_default_engine_matches_the_dense_engine_bit_for_bitbit-compares the default sparse engine with the dense engine, forScreenOutageandScreenBranchesParallel, on five case300 outages.ChainAndScenarioTests.ScreenBranchesParallel_matches_sequentialcompares outage, convergence and security verdict on case118.ContingencyParallelBitIdentityTestscompares the parallel and sequential N-1 screens bit for bit on case30 at one, two and four workers, and two parallel runs on case57.
The live analysis cycle (a stated exception)¶
The cycle verb and the live web cycle schedule their Low-priority analysis jobs (the rotating N-1 shards, the RoCoF
screen, the physics audit) against a wall-clock budget of workers x window-ms. Which jobs are admitted, completed or
cancelled in a cycle therefore depends on the machine's speed, its load and its core count, so two runs of the same
command can report different job sets per cycle. Each job that does run gives the deterministic result of its own study;
what varies is which jobs ran. Pin a study by running it directly (nminus1, screen, physics-check), not through
the cycle.
- LossAllocationTests.Parallel_agrees_with_sequential compares outage and convergence, and DeltaLossMw within 1e-6 MW.
No test compares a parallel screen with a sequential one at the bit level, for the contingency screens or the loss sweep.
Dense and sparse Newton-Raphson¶
Both Newton engines solve every step through the shared NewtonShell step: one CPU direct sparse LU and one damped
update (ch. 3). DenseSparseParityTests bit-compares voltages, angles, injections, flows,
losses, mismatch, iterations and status on the IEEE built-ins, the listed bundled MATPOWER files and JSON cases, with Q
limits on and off, flat start and Q-limit revert, and a missing data file fails the test. The MATPOWER lists omit
case9241pegase and the two GB files, and gb-full runs one option set
(ch. 4). The CLI and analysis entry points route through SolverSelector
(SolverRoutingTests, including an anti-drift scan of the CLI, and DefaultEngineRoutingTests). The sparse Y-bus builder
sums duplicate cells in insertion order, which is the dense build order. The mechanism is the (column << 32) | index
sort key in ComplexCsr.Builder, and DenseSparseParityTests is the end-to-end pin. ComplexCsrBuilderPinTests compares
with a stable reference on short rows and cannot tell a stable order from an unstable one (ch. 4).
Across CPU tiers¶
SimdCapabilities.Tier selects Avx512, Avx256, Sse128 or Scalar kernels once per process
(ch. 4). The bit-exact kernels, SubtractScaledRow and AddScaledRow with
fastFma: false, do a separate multiply then add per lane and equal the scalar loop to the bit at every width
(SimdTierSeamTests.SubtractScaledRow_is_bit_exact_vs_scalar_at_every_tier,
AddScaledRow_without_fma_is_bit_exact_vs_scalar_at_every_tier). Newton-Raphson uses only the CPU sparse LU, which calls no
SimdKernels and no explicit FMA, so its results do not depend on the tier.
Every numerical caller of AddScaledRow passes fastFma: false, the dense WLS gain (DenseWlsStep.FormNormalEquations,
called by DenseWlsStep, ConstrainedDenseWlsStep and QrWlsStep) included. SimdKernels.Dot, which BadDataAnalysis
and ClassicalMultiMachine call, sums in one defined order (four partial sums, then the tail, never fused) that every
tier reproduces, and the batch SimdKernels.SinCos that ClassicalMultiMachine calls is the deterministic fdlibm
function (DetMath) per element. D1CrossTierTests pins each of these at every tier
(Dense_WLS_gain_is_bit_identical_at_every_tier_and_has_no_fused_multiply_add,
Dot_is_bit_identical_at_every_tier_and_equals_the_defined_order,
Batch_SinCos_is_bit_identical_at_every_tier_and_to_DetMath_for_every_element,
Classical_swing_trajectory_is_bit_identical_at_every_tier), and DeterministicMathGuardTests fails the test run if a
numerical method calls a fused multiply-add, a horizontal vector reduction or a platform Math transcendental. The CI
workflow runs the whole suite, with its exact-value pins, in the normal, AVX2-off, hardware-intrinsics-off, forced
AVX-512 and FMA-off modes.
One pair remains outside bit-identity on the CPU: the dense estimator step and the sparse step factorise differently.
A plain WLS or robust estimate on the CPU always takes the sparse step (CanonicalWlsStep), so it does not depend on
the bus count (CanonicalWlsParityTests); the dense step solves an estimate only with a GPU linear solver, and the
constrained, feasible and QR methods are dense only.
Sparse construction by CsrMatrix.Builder¶
CsrMatrix.Builder.Build (src/GridSim.Core/Numerics/Sparse.cs) sorts each row by the unique 64-bit key
(column << 32) | insertionIndex and sums entries that share a column in that order, which is insertion order. The keys
are unique, so the result does not depend on the sort algorithm at any row length, as for the complex builder
(ComplexCsr.Builder); Doc6CsrStableOrderTests pins insertion-order sums in long rows and in a three-entry row. The
builder serves the EMT engines (EmtEngine, ThreePhaseEmt), DistributionFactors and PowerFlowTracing, among others.
GPU is outside the contract¶
GPU arithmetic is outside the bit-identity contract. A GPU sparse solve that gives up is retried on the exact CPU LU and
equals the CPU path to the bit (GpuFallbackTests.A_backend_that_always_gives_up_falls_back_to_the_exact_cpu_lu_bit_for_bit).
GpuFallbackTests.The_cpu_path_is_unchanged_and_never_double_solves pins that the sparse and dense engines agree on
case118 in iterations and Vm, to the bit; no test counts solves.
Linux and Windows¶
The transcendental functions come from DetMath, not the operating system's C library, so the operating system does
not enter a result. The CI workflow runs the same exact-match pins on Linux, Windows and macOS (Apple silicon), and
every leg is a hard gate; it runs when the release workflow calls it on a release tag, or by hand, not on every push.
Fail loud: the convergence contract¶
A non-converged or invalid state is never reported as success. The mechanism is
ConvergenceContract.RequireConverged (src/GridSim.Core/PowerFlow/ConvergenceContract.cs):
RequireConverged(state, study):
if not state.Converged: throw NonConvergedStateException
"<study>: the power-flow state it starts from did not converge (status S, max mismatch M p.u. after N iterations). ..."
Where it is called, as the first check on the converged state and before any solving: loss contingency (LossContingency.Sweep and
SweepParallel), RMS transient stability (two entry points), subtransient initialisation, small-signal assembly (two
entry points and the synchronising coupling), fault analysis (two entry points), protection assessment (three entry
points) and the protection cascade. Since D-DOC-087 (October 2026) also loss allocation (LossAllocation.Allocate),
power-flow tracing (PowerFlowTracing.Trace, BranchContributions, BoundaryTransfers), carbon tracing
(CarbonTracing.Trace) and boundary analysis (BoundaryAnalysis.Evaluate, after its branch-name check). The live views
(web, desktop, the CLI dashboard and case report) check Converged first and show no boundary rows or trace for a frame
that did not converge (Doc13ConvergenceContractTests).
Where it is not called. Consumers that take a solved state and apply their own gate or none:
ClassicalInitthrowsInvalidOperationException("Classical init needs a converged power flow.").FdiaScreenandHarmonicIndicescheckConvergedthemselves and refuse.BoundaryAnalysis.EvaluateAfterOutage(internal, the contingency screen's post-outage reading) has no check of its own: the screen classifies a non-converged outage before it reads boundaries.
Other mechanisms in the same contract:
- The
SteadyStateSolverOLTC loop never steps a tap on a non-converged solve and reportsTapsSettled. - A Q-limit loop that reaches
MaxQLimitPassesimmediately after a type switch solves once more and reports not converged. - DC-OPF is feasible only on an
Optimalsecurity LP (DcOpfResult.SecurityLpStatus). - Live and GDA replay warm-start only from converged states.
- Contingency screens record the actual solver error and let cancellation through.
Exit 3. The CLI maps a non-converged result to exit code 3, the failure code for a validation or gate failure
(ch. 19 lists the full exit-code table, including the checks that exit 1). A refused
non-converged state surfaces as the NonConvergedStateException message and a non-zero exit.
simdcheck --expect <tier> also exits 3 when the active tier is narrower than the expected one.
Invariant culture¶
Every number parsed from a flag and every number written to a file is locale-independent in the CLI, the web host and the
schema editor. Each sets CultureInfo.DefaultThreadCurrentCulture and CultureInfo.CurrentCulture to InvariantCulture
before any other work (src/GridSim.Cli/Program.cs, src/GridSim.Web/Program.cs, src/GridSim.SchemaEditor/EntryPoint.cs),
so a comma-decimal machine parses --dt-ms 0.5 as 0.5 and never writes 1,2340 into a CSV. The WPF app
(src/GridSim.Wpf) does not pin the culture.
Every date flag takes ISO 8601. --as-of accepts ISO 8601 forms only (DateTime.TryParseExact), and
serve --utc-now and estimate --phaseangle-at parse with UtcTime.TryParseIso8601 (ISO 8601 and round-trip
shapes only, invariant culture; a stamp with no zone is UTC), so an ambiguous date such as 01/02/2100 is refused
with exit 2.
The writers pin the culture explicitly as well:
CliResultWriterTests.Files_are_invariant_culture_whatever_the_thread_culture sets the thread to de-DE and requires
0.5 in both the CSV and the JSON. No test covers flag parsing or the process-level pin. Numbers in result files are
written round-trip, so a parsed value equals the one printed.
The constitutional-test rule and the version contract¶
The tests are constitutional. No test is edited, loosened, re-baselined or deleted to make a change pass: a change that breaks a test is wrong. A test changes only with a written mathematical justification, agreed with the owner first and recorded in the commit and the changelog. Adding tests is always permitted. This is a working rule for contributors; no test enforces it.
The version is one number, <Version>1.95.NNN</Version>, in Directory.Build.props. By rule every commit bumps NNN and
adds the matching ## [1.95.NNN] heading to CHANGELOG.md; no project file sets its own version. VersionContractTests
pins that no project file sets a version and that the current version has a heading, but not the per-commit bump:
No_project_file_sets_its_own_version, Changelog_has_a_heading_for_the_current_version,
Core_build_info_reports_the_props_version and Every_loadable_gridsim_assembly_carries_the_same_stamp. The build is
warnings-as-errors, so a Release build has 0 warnings.
Invariants¶
Every project-wide invariant with the test that pins it. "None" marks an invariant that no test pins.
| Invariant | Pinned by |
|---|---|
The horizon constant is 169.0 h and Horizon(now) subtracts it |
RestrictionInvariantTests.Inv1_The_horizon_is_a_single_knob |
RejectFuture rejects a stamp inside the horizon and a future one, and passes one beyond it; MeasurementSet.Load rejects a fresh TakenUtc and accepts a historical one |
RestrictionInvariantTests.Inv1_A_dated_ingestion_seam_rejects_a_fresh_stamp, Inv1_A_dated_ingestion_seam_accepts_a_historical_stamp; ForwardInferenceGuardTests.A_recent_stamp_inside_the_horizon_is_rejected, A_future_stamp_is_rejected, A_stamp_beyond_the_horizon_passes |
| The horizon instant passes and one tick inside throws; one minute either side of it behaves the same way | ForwardInferenceGuardTests.The_exact_horizon_instant_passes_and_one_tick_inside_throws (exact), The_horizon_boundary_is_exactly_HorizonHours (one minute either side) |
AssertHistorical throws on the first violation and passes a clean series |
ForwardInferenceGuardTests.AssertHistorical_throws_on_the_first_future_row, AssertHistorical_passes_an_all_historical_series |
An Unspecified stamp is UTC and a Local stamp is compared by its true instant |
ForwardInferenceGuardTests.Unspecified_kind_is_treated_as_utc, A_local_kind_stamp_is_compared_by_true_instant_not_naive_ticks |
| A future-dated measurement set is refused at load, and a past one round-trips | RestrictionInvariantTests.Inv2_The_state_time_channel_is_guarded_end_to_end |
RejectFuture refuses a stamp inside the horizon for a real-time window (the guard call only) |
RestrictionInvariantTests.Inv4_A_realtime_window_stamped_fresh_is_refused |
| A real-time engine skips a window newer than the horizon and counts it as rejected-fresh | RealtimeEstimatorEngineTests.A_window_newer_than_the_horizon_is_skipped_never_estimated, RealtimeDseEngineTests.A_fresh_window_in_the_stream_is_skipped |
| A dynamics-fused estimator needs a review entry that certifies present-or-past output | RestrictionInvariantTests.Inv5_A_dynamics_fused_method_without_review_is_rejected, Inv5_A_static_measurement_only_method_is_permitted_without_review, Inv5_Every_reviewed_entry_certifies_present_past_output_and_carries_a_note |
RestrictionReview.AssertOutputHistorical rejects a stamp inside the horizon, with the same boundary as the input horizon |
RestrictionInvariantTests.Inv5_A_future_dated_output_is_rejected_mechanically, Inv5_A_present_past_stamped_output_is_permitted, Inv5_The_output_guard_boundary_matches_the_input_horizon |
| The tag names for recorded, synthetic and counterfactual are distinct | RestrictionInvariantTests.Inv3_Model_output_provenance_labels_are_distinct_from_recorded (checks the enum members differ; it does not check any flow) |
| A built-in event's tag survives a save and load of the catalogue (the member-name text is not checked) | EventCatalogTests.Builtins_round_trip_through_json |
The EvidenceReport.Build baseline column is DerivedFromRecorded and the device column is the only Counterfactual column |
EvidenceReportTests.Columns_are_provenance_labelled_and_never_merge_inertia_with_ffr |
The default avoided cost carries CuratedPlaceholder |
EvidenceReportTests.Avoided_cost_arithmetic_is_exact_and_grounded_in_the_recorded_shed (one assertion) |
An asset-class library holding a CuratedPlaceholder parameter without [VERIFY] fails ValidateOrThrow (no test drives it through JsonGridLoader) |
AssetProvenanceTests.Curated_placeholder_without_verify_is_a_violation, Curated_placeholder_with_verify_is_clean, Recorded_and_derived_tags_do_not_require_verify |
A parameter with no entry of its own takes the _default provenance entry (the last fallback, CuratedPlaceholder, has no test) |
AssetProvenanceTests.Provenance_for_a_parameter_falls_back_to_default_then_synthetic |
| A placeholder site cannot carry a value | V2PlaceholderRuleTests.A_placeholder_with_a_non_null_value_fails, An_active_template_with_a_null_value_fails |
AuditLogWriter.Append refuses a tag cast from 999 and writes nothing (the read side has no test) |
AuditLogFileTests.An_undefined_provenance_is_refused_before_anything_is_written |
| A trust level is only ever lowered (downgrade-only); the trust order ranks input tags and refuses output tags | Cf5SaveGateTrustTests.Trust_only_moves_down, The_trust_order_ranks_input_tags_and_refuses_to_rank_output_tags; Cf6TrustRulesTests.A_raised_voll_tag_is_refused_with_or_without_a_citation, The_harmonic_headline_is_the_weakest_measured_tag_never_raised |
An event's effective provenance is the weakest of its parts; replay-event refuses an event that is not recorded, naming its weakest part; the study verbs run it with downgraded tags |
Cf6TrustRulesTests.The_effective_provenance_is_the_weakest_of_the_parts, A_replay_refuses_an_event_that_is_not_recorded_by_name, A_recorded_event_with_a_placeholder_loss_is_refused_naming_the_loss, Evidence_on_a_placeholder_event_builds_and_every_tag_is_downgraded, Cli_replay_event_refuses_the_screening_event_but_fleet_studies_it_with_the_downgraded_tag |
A [VERIFY] token marks a placeholder (reverse direction), for events and avoided cost |
None |
A Counterfactual datum is never an input to another replay, and an output tag in a case file refuses the save |
Cf5SaveGateTrustTests.A_counterfactual_is_never_a_replay_input, Every_built_in_event_passes_the_replay_input_rule, An_output_tag_written_in_a_case_file_refuses_the_save |
RequireConverged throws for a non-converged state and names the study; fault analysis, protection assessment and cascade, and loss contingency refuse a non-converged state |
ConvergenceContractTests.RequireConverged_names_the_study_and_the_state, Fault_analysis_refuses_a_non_converged_prefault, Protection_cascade_and_assessment_refuse_a_non_converged_state, Loss_contingency_refuses_a_non_converged_base_case |
| Related fail-loud cases: a diverged subtransient run, an AC-OPF that cannot settle, a continuation, a bolted fault at the source and a chi-square test that did not run | ConvergenceContractStudyTests |
The OLTC loop never steps on a non-converged solve and reports TapsSettled; a Q-limit pass cap is reported not converged |
ConvergenceContractTests.Oltc_loop_never_steps_a_tap_on_a_non_converged_state, Oltc_loop_that_hits_its_pass_cap_reports_unsettled, Q_limit_loop_that_hits_its_pass_cap_with_a_switch_pending_is_not_converged |
SecurityLpStatus is null when no security LP ran (the link from an Optimal LP to feasibility has no test) |
ConvergenceContractTests.Dc_opf_reports_its_security_lp_outcome |
The simdcheck --expect gate exists in Program.cs (a source-text check; the exit code is shown in the capture, not tested) |
SimdTierSeamTests.Simdcheck_has_an_expect_gate_that_exits_3 |
| Run to run: Newton-Raphson is bit-identical on the eight built-in cases | SolverPropertyTests.Solving_twice_is_bit_for_bit_identical |
| Run to run: two parallel exhaustive sweeps agree exactly on outage, convergence, maximum branch loading and minimum and maximum voltage | ContingencyTests.N2_enumeration_is_deterministic_despite_the_parallel_solve, N1_1_is_deterministic_despite_the_parallel_solve |
The default sparse contingency screen equals the dense-engine screen bit for bit, for ScreenOutage and ScreenBranchesParallel on five case300 outages |
DefaultEngineRoutingTests.Contingency_screens_default_engine_matches_the_dense_engine_bit_for_bit |
Parallel and sequential ScreenBranches agree on outage, convergence and security verdict on case118 (bit-level equality is unpinned) |
ChainAndScenarioTests.ScreenBranchesParallel_matches_sequential |
Parallel and sequential loss sweeps agree on outage and convergence, and on DeltaLossMw within 1e-6 MW |
LossAllocationTests.Parallel_agrees_with_sequential |
Dense and sparse Newton-Raphson agree to the bit on the cases the tests list (the MATPOWER lists omit case9241pegase and the two GB files; gb-full runs one option set) |
DenseSparseParityTests.Built_in_case_is_bit_identical_dense_vs_sparse, Matpower_file_is_bit_identical_dense_vs_sparse, Json_case_is_bit_identical_dense_vs_sparse, Repeated_solves_on_one_sparse_instance_stay_bit_identical; SolverRoutingTests; DefaultEngineRoutingTests |
| An always-giving-up device backend falls back to the CPU LU and equals the CPU sparse path to the bit on case300 | GpuFallbackTests.A_backend_that_always_gives_up_falls_back_to_the_exact_cpu_lu_bit_for_bit |
The sparse and dense engines agree on case118 in iterations and Vm, to the bit (no test counts solves) |
GpuFallbackTests.The_cpu_path_is_unchanged_and_never_double_solves |
| The bit-exact SIMD kernels equal the scalar loop at every tier | SimdTierSeamTests.SubtractScaledRow_is_bit_exact_vs_scalar_at_every_tier, AddScaledRow_without_fma_is_bit_exact_vs_scalar_at_every_tier; SimdKernelsTests.SubtractScaledRow_is_bit_exact_vs_scalar_at_all_lengths |
ComplexCsr.Builder sums duplicates in the (column, insertion index) order, which matches the dense build |
DenseSparseParityTests (end to end); ComplexCsrBuilderPinTests.Build_is_byte_identical_to_a_reference_for_shuffled_and_duplicate_entries compares with a stable reference on short rows and cannot tell a stable order from an unstable one (ch. 4) |
| Newton-Raphson is identical across CPU tiers | None (observed by a manual probe) |
DenseWlsStep, ConstrainedDenseWlsStep, QrWlsStep, BadDataAnalysis and ClassicalMultiMachine are identical across CPU tiers |
None (they are not identical today); SimdKernelsTests.Dot_agrees_with_scalar_within_tolerance and Vectorised_SinCos_matches_Math_SinCos_to_1e12 pin tolerances only |
| Dense WLS equals sparse WLS to the bit | None (they are not identical today); SparseEstimatorTests.Sparse_and_dense_backends_agree_on_a_noisy_set pins a tolerance only |
CsrMatrix.Builder sums duplicates in insertion order |
None (it does not today) |
Result files are written in invariant culture when the thread culture is de-DE (CSV and JSON); flag parsing and the process-level pin have no test |
CliResultWriterTests.Files_are_invariant_culture_whatever_the_thread_culture |
One <Version>, set by no project file, and a changelog heading for the current version (the per-commit bump is by rule) |
VersionContractTests.No_project_file_sets_its_own_version, Changelog_has_a_heading_for_the_current_version, Core_build_info_reports_the_props_version, Every_loadable_gridsim_assembly_carries_the_same_stamp |
| No test is edited to fit a change | None (a contributor rule, enforced by review) |
Worked example¶
Three runs on the reference machine show the guard, the tags and the determinism contract. Timings and clock stamps in the captures differ by machine and run.
A horizon refusal¶
docs/_captures/estimation/case9-telemetry-future.json carries takenUtc 2099-01-01T12:00:00Z. gridsim estimate
loads it through MeasurementSet.Load, which calls RejectFuture, and the verb prints the guard's message with the
horizon it computed from the clock and exits 2.
$ gridsim estimate case9 --measurements docs/_captures/estimation/case9-telemetry-future.json
REJECTED (forward inference): measurement set timestamp 2099-01-01T12:00:00.0000000Z is newer than the historical horizon 2026-10-13T04:02:06.6214291Z (data must lag real time by >=169 h). GridSim replays history only - it never processes recent or future data, so it cannot forward-infer.
[exit 2]
The printed horizon is the clock the run read minus 169 h (7 days and 1 hour). 2099 is newer than any horizon, so the set is refused before any measurement is read.
The same window on the live path, with and without --utc-now¶
serve loads each window with its effective now. The first capture ran without the window folder
results/prov-future-dir in place, so it stops at the load with exit 1; with the folder in place the 2099 window counts
as rejected-fresh and the run fails. The second run is refused before any window is read: --utc-now
2100-01-01T00:00:00Z is later than the real clock, so the run prints REJECTED (forward inference): ... and exits 2.
$ gridsim serve case9 --measurements-dir results/prov-future-dir
error reading 'results/prov-future-dir': Could not find a part of the path 'results\prov-future-dir'.
[exit 1]
$ gridsim serve case9 --measurements-dir results/prov-future-dir --utc-now 2100-01-01T00:00:00Z
REJECTED (forward inference): --utc-now 2100-01-01T00:00:00.0000000Z is later than the real clock 2026-10-20T05:02:08.8394001Z. It may only move the horizon back (a stricter, reproducible check), never forward: a later 'now' would let data newer than 169 h through.
[exit 2]
The refusal names the real clock the run read, 2026-10-20T05:02:08Z: --utc-now may only move the horizon back from it.
A provenance tag in a replay¶
gridsim replay-event prints the event's effective tag and replays only an event sourced from a record. The screening
event is a curated placeholder and is refused, by name, with exit 1 (ReplayValidator.RequireRecordedReplay); the 2019
disturbance is replayed, and its line shows the effective tag, DerivedFromRecorded (event tagged Recorded; the weakest
of its parts):
$ gridsim replay-event gb-largest-loss-1320 --out results/prov-replay-placeholder
error: event 'gb-largest-loss-1320' (GB normal infeed loss risk screening (SQSS): single 1320 MW infeed trip [VERIFY]) is effectively CuratedPlaceholder (its weakest part: the event itself (CuratedPlaceholder)): a replay presents its output as a reproduction of a recorded event, so only an event sourced from a record (Recorded or DerivedFromRecorded throughout) is replayed (technical ch. 8, trust order). Study it with scenario, fleet, evidence or h0-study instead (their outputs carry the weaker tag), or source its figures and re-tag it.
[exit 1]
$ gridsim replay-event gb-2019-08-09 --out results/prov-replay-recorded
=== Replay: GB 9 Aug 2019 disturbance (Hornsea + Little Barford) ===
when (UTC): 2019-08-09T15:52:33.0000000Z provenance: DerivedFromRecorded (event tagged Recorded; the weakest of its parts)
pre-event inertia: 215.0 GVA.s demand: 21.1 GW total loss: 2768 MW
...
VERDICT: REPRODUCED - baseline reproduces the record; counterfactuals unlocked.
-> results\prov-replay-recorded
[exit 0]
A placeholder event's RoCoF is the arithmetic of its placeholder inertia (-1320 x 50 / (2 x 200000) = -0.165 Hz/s), so reproducing it would not make the figures sourced; that is why the replay refuses it and the study verbs carry its weaker tag instead.
Determinism: two runs, one hash¶
The same power flow is run twice with the same result file, and the result file is hashed after each run. The file contains every bus quantity at full round-trip precision, and the two hashes are equal. The hashed file also embeds the tool version and its own arguments, so compare two runs of the same build and output path, not the printed value.
$ gridsim pf case118 --json results/prov-pf.json
Power flow - IEEE 118-bus (118 buses)
converged in 6 iteration(s), mismatch 2.85E-014 pu losses 132.481 MW / -644.082 MVAr
voltage range 0.9430 - 1.0500 pu
...
[pf] wrote results\prov-pf.json
[exit 0]
$ sha256sum results/prov-pf.json
486001715c607a8fccc4e426e1765a8f40fbf8cf2906ee57dc75803f3470d1d3 results/prov-pf.json
[exit 0]
$ sha256sum results/prov-pf.json
486001715c607a8fccc4e426e1765a8f40fbf8cf2906ee57dc75803f3470d1d3 results/prov-pf.json
[exit 0]
The CPU tier¶
gridsim simdcheck reports the vector tier the dense kernels dispatch to, and --expect <tier> turns it into a gate:
exit 3 when the active tier is narrower. The reference machine is AVX2 and FMA, so an AVX-512 expectation fails.
$ gridsim simdcheck --expect Avx512
GridSim SIMD preflight
...
SIMD: Avx256 (4 doubles/step) FMA=yes [Avx512F=False Avx2=True Fma=True Sse2=True AdvSimd=False]
Active vector tier: Avx256 (4 doubles per vector step)
...
simdcheck: FAILED - expected at least Avx512, but the active tier is Avx256.
[exit 3]
On this machine the kernels run the 4-wide path. Every kernel a numerical path calls gives the same bits at every tier and with or without FMA, so an AVX-512 host or a non-FMA host produces the same estimates and swing trajectories.
The captures are in docs/_captures/provenance/.
Source map¶
| File | Type | Responsibility |
|---|---|---|
src/GridSim.Core/Diagnostics/Provenance.cs |
ProvenanceTag |
the seven-member provenance enum |
src/GridSim.Core/Diagnostics/ForwardInferenceGuard.cs |
ForwardInferenceGuard, ForwardInferenceException |
the 169 h horizon, Horizon, RejectFuture, AssertHistorical |
src/GridSim.Core/Diagnostics/UtcTime.cs |
UtcTime |
the Unspecified is UTC rule shared by the guard and the review gate |
src/GridSim.Core/Diagnostics/RestrictionReview.cs |
RestrictionReview, RestrictionReviewEntry, RestrictionViolationException |
the dynamics-fusion review table and output guard |
src/GridSim.Core/Assets/AssetClassLibrary.cs |
AssetClass.ValidateProvenance, AssetClassLibrary.ValidateOrThrow, AssetClassProvenance |
the [VERIFY] check and the provenance fallback |
src/GridSim.Core/IO/Json/V2CaseLoader.cs |
override handling | an override with no provenance becomes CuratedPlaceholder with a [VERIFY] note |
src/GridSim.Core/Events/EventCatalog.cs, GridEvent.cs |
EventCatalog.Builtin, Validate, record types |
tagged events and the horizon check on WhenUtc |
src/GridSim.Core/Evidence/EvidenceReport.cs, AvoidedCost.cs |
EvidenceReport.Build, AvoidedCostInputs |
the tagged evidence columns and the placeholder VoLL |
src/GridSim.Core/IO/HarmonicExport.cs, Dynamics/Impedance/GridFollowingImpedance.cs, GridFormingImpedance.cs |
HarmonicExport.ToJson, FromAssetClass |
tags computed from input tags |
src/GridSim.Core/Simulation/GdaReplayLoop.cs |
GdaReplayLoop.Replay |
re-asserts the horizon and labels a storage replay Counterfactual |
src/GridSim.Core/Simulation/RealtimeEstimatorEngine.cs, RealtimeDseEngine.cs |
Run |
the horizon on live windows and the output guard |
src/GridSim.Core/PowerFlow/ConvergenceContract.cs |
ConvergenceContract, NonConvergedStateException |
RequireConverged |
src/GridSim.Core/PowerFlow/SolverSelector.cs |
SolverSelector |
the dense/sparse routing constants |
src/GridSim.Core/Numerics/SimdKernels.cs |
SimdKernels |
the tiered kernels, Dot, SinCos |
src/GridSim.Core/Estimation/WlsStep.cs |
DenseWlsStep, ConstrainedDenseWlsStep, QrWlsStep, SparseWlsStep, CanonicalWlsStep |
the dense gain formation, its two other callers, the sparse step and the canonical CPU step |
src/GridSim.Core/Estimation/BadDataAnalysis.cs |
BadDataAnalysis |
the Dot users in normalised residuals |
src/GridSim.Core/Dynamics/Distributed/ClassicalMultiMachine.cs |
ClassicalMultiMachine |
the SinCos and Dot user |
src/GridSim.Core/Numerics/Sparse.cs |
CsrMatrix.Builder, ComplexCsr.Builder |
sparse construction and its duplicate-sum order |
src/GridSim.Cli/Program.cs |
entry point | invariant culture, serve --utc-now, exit codes, simdcheck |
data/library/placeholders/unmodelled-sites.json |
library file | the named placeholder sites with deliberately empty values |
Directory.Build.props, CHANGELOG.md |
version | the single <Version> and its changelog heading |
Limits¶
- A hand-written or loaded file can carry any input tag; the code refuses output tags as inputs and raised computed
tags, but it cannot check that a
Recordedtag in a file is backed by a record. - The
[VERIFY]check runs for asset classes only; an event, the avoided-cost inputs and the evidence report are not checked, and a[VERIFY]token on a non-placeholder is not checked anywhere. - The dense WLS and sparse WLS steps are not bit-identical to each other; on the CPU a WLS or robust estimate always takes the sparse step, so this affects only a GPU estimate.
- Newton-Raphson equality across CPU tiers holds by construction (no tier-dispatched kernel, no fused multiply-add) and is checked by running the exact-value pins in every SIMD mode; no single test compares two tiers' Newton results.
- No test pins bit-level equality between a parallel and a sequential contingency screen or loss sweep. The tests pin
verdicts and, for the loss sweep,
DeltaLossMwwithin 1e-6 MW. - GPU arithmetic is outside the bit-identity contract, and no test counts solves on the CPU path.
serve --utc-nowlets a caller move the clock the horizon is judged against back, never forward past the real clock.- The GDA side of the horizon lives in a separate repository and is not tested from this one.
See also¶
- Architecture - the projects the invariants cross
- Power flow - the shared Newton step and the routing rule behind dense/sparse parity
- Sparse and GPU numerics - the SIMD tiers,
Dot,SinCosand the GPU fallback - State estimation - measurement loading and the WLS steps
- File and wire formats - how the tags and
[VERIFY]markers serialise - Audit log and diagnostics - the guard's seam table, the message,
RestrictionReviewand the audit tag - Frequency dynamics - the NESO trace path the guard protects;
UnvalidatedAtScaleoutputs - 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)