Skip to content

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): 4 Recorded, 3 DerivedFromRecorded, 2 Interpolated and Synthetic, 1 CuratedPlaceholder. 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 against AvoidedCost.VollBaseProvenance) and the harmonic export headline use it.
  • Weakest(tags, what): the lowest rank wins; two different tags at the lowest rank (Interpolated and Synthetic) give CuratedPlaceholder.

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, and RequireRecordedReplay (called by replay-event) refuses an event whose effective tag is not Recorded or DerivedFromRecorded, naming its weakest part (exit 1). The built-in recorded events (gb-2019-08-09, gb-2023-12-22) are effectively DerivedFromRecorded; the screening event gb-largest-loss-1320 is effectively CuratedPlaceholder and 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.
  • Counterfactual is 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.Replay returns Counterfactual when storage is attached), the CLI exports (H0StudyExport, the storage CSV header comment "never an input to another replay").
  • Interpolated and Synthetic data may improve fidelity or test the engine; they never define that an event happened.
  • CuratedPlaceholder data lets a pipeline run end to end; the results it touches are not quotable until a cited figure replaces it.
  • UnvalidatedAtScale results 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-1320 is scaffolding. The event, its inertia, demand and outcome are CuratedPlaceholder, 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 tagged Recorded with its citation while the rest stays placeholder.
  • gb-2019-08-09 has retired its markers by sourcing. Its losses cite the Ofgem investigation report paragraph by paragraph and are tagged Recorded. 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 tagged DerivedFromRecorded.
  • gb-2023-12-22 is a second sourced event, measured end to end. The event and its outcome are Recorded. Its two loss steps (IFA and Cottam, from settlement metering) and its recovery actions are DerivedFromRecorded.

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:

  1. 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.
  2. 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_bit bit-compares the default sparse engine with the dense engine, for ScreenOutage and ScreenBranchesParallel, on five case300 outages.
  • ChainAndScenarioTests.ScreenBranchesParallel_matches_sequential compares outage, convergence and security verdict on case118.
  • ContingencyParallelBitIdentityTests compares 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:

  • ClassicalInit throws InvalidOperationException("Classical init needs a converged power flow.").
  • FdiaScreen and HarmonicIndices check Converged themselves 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 SteadyStateSolver OLTC loop never steps a tap on a non-converged solve and reports TapsSettled.
  • A Q-limit loop that reaches MaxQLimitPasses immediately after a type switch solves once more and reports not converged.
  • DC-OPF is feasible only on an Optimal security 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 Recorded tag 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, DeltaLossMw within 1e-6 MW.
  • GPU arithmetic is outside the bit-identity contract, and no test counts solves on the CPU path.
  • serve --utc-now lets 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