1. Architecture¶
This chapter maps the codebase for an engineer who modifies or audits it: the projects in GridSim.slnx and the
dependency rules between them, the namespaces inside GridSim.Core, the seams (the interfaces and registries where an
alternative implementation plugs in), the end-to-end data flows, the cross-cutting rules that hold everywhere, and the
tests that pin them. It names types and files and leaves the mathematics to the subsystem chapters, which it links.
The shape is simple. GridSim.Core holds every capability: the model, the solvers, the estimator, the dynamics, the
protection layer, the importers and exporters, the evidence engine and the wire codecs. The CLI (GridSim.Cli) is the
only place that selects a compute backend and reads argv; the exporters in Core write the files and the CLI chooses the
paths. The two dashboards (GridSim.Web,
GridSim.Wpf) and the case editor (GridSim.SchemaEditor) are presentation only. GridSim.Gpu and GridSim.Gda are
leaves that hold the two things Core is forbidden to hold: device drivers and a Parquet reader.
The contract behind the shape is the one in the repository README: power-flow results are bit-identical across runs, across parallel paths, across CPU instruction-set tiers and between the dense and sparse solvers, and a result that is not converged or not valid is never reported as success. Each cross-cutting rule below exists to keep that contract.
Projects¶
GridSim.slnx lists seven projects under src/ and one under tests/. The dependency column is read from the
ProjectReference and PackageReference items in each .csproj.
| Project | Kind and target | Responsibility | Project references | Packages |
|---|---|---|---|---|
src/GridSim.Core |
class library, net10.0 |
The physics and every capability: model, power flow, sparse and complex numerics, estimation, dynamics, harmonics, protection, assets, storage, scenarios and evidence, IO, interposer and synchrophasor codecs, the bridge client, the cycle scheduler, diagnostics, licensing. | none | none |
src/GridSim.Cli |
console exe gridsim, net10.0 |
Argv parsing, the unknown-flag gate, every verb, the licence choke point for model loads, backend selection, run metadata, result files, the TCP transports of the interposer. Server GC, concurrent GC and tiered PGO are on for the compute loops. | Core, Gda, Gpu | none |
src/GridSim.Gda |
class library, net10.0 |
Maps the GB data lake (NESO and the DNO LTDS tables) onto a GridModel: Ltds/, Neso/, Npg/, Ukpn/, Csv/, Geo/, the case writer and the interposer's quarantine store. Embeds no data. Not packable: it ships inside the CLI. |
Core | Parquet.Net |
src/GridSim.Gpu |
class library, net10.0 |
The GPU linear-solver backends behind the Core seams: ILGPU (CUDA and OpenCL) and native ROCm/HIP. | Core | ILGPU, ILGPU.Algorithms |
src/GridSim.Web |
web exe (Microsoft.NET.Sdk.Web), net10.0 |
Blazor Server dashboard, the REST API under /api, the /ws/sim WebSocket, sessions, the resident real-time estimator host, estate SSO consumer, deployment settings. |
Core | none |
src/GridSim.Wpf |
WinExe, net10.0-windows |
The desktop dashboard: view-models, views, and the schematic renderer. Runs on Windows only; EnableWindowsTargeting lets it compile on Linux. |
Core | none |
src/GridSim.SchemaEditor |
console exe, net10.0 |
The full-screen text-mode case editor: key map, renderer, views. The format engine it drives is in Core under IO/Json/Lossless and IO/Json/CaseEditing. Not packable. |
Core | none |
tests/GridSim.Tests |
xUnit, net10.0 |
The test suite, including source scans of Cli and Wpf and four linked Cli source files. | Core, Gda, Gpu, Web, SchemaEditor, and Cli with ReferenceOutputAssembly=false |
xunit, xunit.runner.visualstudio, Microsoft.NET.Test.Sdk, Microsoft.AspNetCore.Mvc.Testing |
Three things sit outside the solution:
tools/holds the Python cross-validation sidecars (tools/crossval/, which thecrossvalverb runs against pandapower and scipy), the documentation tooling (tools/docs/:lint.py,capture.ps1), the benchmark and regime shell scripts (tools/gpu-bench.sh,tools/run-gpu.sh,tools/run-regime.sh,tools/transient-suite.sh,tools/fetch-cases.sh) andtools/gb-reference/. None of it is built by the solution.tools/AssetSmokeis an off-screen WPF smoke harness (Windows only to run) that instantiates every asset page and the map, generation, machine and SCADA views on a solved frame. It is inGridSim.slnx, so every build compiles it; it used to sit outside the solution and stopped building unnoticed (D-DOC-201). Nothing runs it automatically.- The GDA repository, a separate repository, holds the Python builders and the bridge server that sit on the far side of the wire (ch. 9). Nothing from the lake is committed here.
Dependency rules¶
GridSim.Core
(Abstractions, Model, PowerFlow, Numerics, Estimation, Dynamics, ...)
/ | | | \
GridSim.Gpu GridSim.Gda GridSim.Web GridSim.Wpf GridSim.SchemaEditor
\ /
GridSim.Cli (also references Core)
tests/GridSim.Tests references Core, Gda, Gpu, Web and SchemaEditor, and builds Cli without referencing it.
- Core is the root and holds no packages.
GridSim.Core.csprojhas noPackageReferenceand noProjectReference. Its only non-source inputs are embedded resources: the MATPOWER library cases asGridSim.Core.cases.<name>.mand the v1 and v2 schema files asGridSim.Core.schemas.v1.<file>andGridSim.Core.schemas.v2.<file>. Core opens WebSockets withSystem.Net.WebSocketsdirectly (Net/GridBridgeClient.cs). Anything that needs a NuGet package or a device driver goes in a leaf project behind a Core seam. - Only the CLI and the tests reach
GridSim.Gpu. GPU backends meet the solvers only throughILinearSolver,ISparseLinearSolverandIBatchedLinearSolver.GridSim.WebandGridSim.Wpfdo not reference Gpu, so the dashboards run the CPU engines only. - Only the CLI and the tests reach
GridSim.Gda. Thegda,gda-replayandinterposeverbs use it; the dashboards read the lake only through the bridge. - Presentation projects reference Core and nothing else. Web, Wpf and SchemaEditor run no solver of their own: Web
re-projects each
SimFrameinto DTOs on the server (Web/Projection/FrameProjector.cs) and Wpf binds views to the frame. Formatting rules, thresholds and fuel and carbon tables that both dashboards use live inCore/Presentation/. The rule is not yet complete. Some display arithmetic still lives in the shells, written twice (Wpf view-model and Web service) and off any validated path: THE MACHINE's narrative read-outs (the droop-implied surplus, commanded and delivered torque, the Hz.s and regeneration accumulators and the illustrative BESS state of charge, inWpf/Views/MachineVm.csandWeb/Sessions/MachineService.cs), the per-unit reactive estimate on the generator pages (bus injection shared by output), the per-end circuit currentsS / (sqrt(3) x V), and the intrinsic-inertia sum on NESO RP1. The trip studies (FrequencyDynamics.Simulate,TsgbSizer,InertiaEstimator) are Core calls that the shells invoke. Until that arithmetic moves into Core presenters, a desktop or web number of this kind is a display estimate, not a validated Core result. - Capability lives in Core and Cli. A verb's arithmetic is a Core call;
Program.csparses, calls, formats and sets the exit code. - Internals are opened by name. Core grants
InternalsVisibleTotoGridSim.TestsandGridSim.SchemaEditor; Web and SchemaEditor grant it toGridSim.Tests. The case-editing engine reaches the editor this way, and the tests reach internal numerics such asIWlsStepthis way, without widening the public API. - One version. No project sets
<Version>;Directory.Build.propsowns it (see Cross-cutting rules).
The tests do not reference GridSim.Wpf; tests/GridSim.Tests/WpfPresentationSourceContractTests.cs and
WpfDeadCodeAndCycleTests.cs read its source instead. The test project references Cli only to build it
(ReferenceOutputAssembly=false), and the CLI contract tests read Program.cs as text. Four Cli files are the
exception: tests/GridSim.Tests/GridSim.Tests.csproj compiles InterposeTransport.cs, VerbHelp.cs,
CliResultWriter.cs and VerbFlags.cs into the test assembly (Link="Linked\..."), so InterposeTransportTests,
VerbHelpTests, CliResultWriterTests and VerbFlagsTests run the shipped code, not a source scan.
Core namespaces¶
Every namespace below is under src/GridSim.Core/<Folder>/ and GridSim.Core.<Namespace>. The key types are the ones
a reader meets first; each subsystem chapter lists the rest.
| Namespace | Responsibility and key types |
|---|---|
Abstractions |
The seam interfaces and nothing else: IPowerFlowSolver, ILinearSolver, ISparseLinearSolver, IBatchedLinearSolver, IComplexLinearSolver with IComplexLinearFactorization, IInertiaModel. |
Model |
The immutable network records and GridModel: Bus, BusType, Generator, Branch, Oltc, Boundary, ZipLoad, ConverterPlant, GridDiagnostic, plus FuelTypes. GridModel.CopyWith is the one copy path that carries every property (ch. 2). |
PowerFlow |
The steady-state engines and the loops around them: NewtonRaphsonPowerFlow, SparseNewtonRaphsonPowerFlow, BackwardForwardSweep, NewtonShell (the one shared Newton step and update), SteadyStateSolver (the OLTC loop), SolverSelector, YBus, DenseLuSolver, CpuSparseLuSolver, CpuBatchedLuSolver, PowerFlowOptions, PowerFlowResult, ConvergenceContract, TopologyOps, IslandPartition, KronReduction (ch. 3). |
Numerics |
Sparse and dense kernels with no network knowledge: CsrMatrix, ComplexCsr, SparseLu, Ilu0, BiCgStab, HouseholderQr, SimdKernels, SimdCapabilities, NumericTolerances (the one tolerance table), the complex solvers, eigen solvers, BoundedSimplex (ch. 4). |
Estimation |
State estimation: WlsStateEstimator, the internal IWlsStep backends, MeasurementSet, MeasurementModel, ObservabilityAnalysis, BadDataAnalysis, ChiSquare, HypothesisTestingBadData, NoiseFloorAnalysis, SyntheticMeasurements, EstimationResult, Dse/ (Kalman, unscented and ensemble trackers), Security/FdiaScreen (ch. 6). |
Dynamics |
Frequency and stability: FrequencyDynamics, FrequencyIntegrator, AssetAwareFrequencyIntegrator, LumpedScalarInertiaModel, InertiaModelSelector, RocofScreening, TsgbSizer, FrequencyLimits; Distributed/ (multi-domain swing), Forming/ (grid-forming formation), Models/ (machines, exciters, governors, RMS transient stability), SmallSignal/, Impedance/, Emt/ (ch. 5, ch. 24, ch. 30, ch. 21). |
Assets |
The per-asset dynamics framework on the asset seam: IAssetDynamics, IActiveAssetDynamics, IPassiveAssetDynamics, AssetClassLibrary, AssetBindingSet, AssetControllerFactory, AssetDynamicsFleet, PassivePlantFleet, GridFollowingInverter, GridFormingInverter, and Assets/Protection/ (ProtectionController, ProtectionEvaluator, ProtectionModel) (ch. 13, ch. 15). |
Protection |
The passive relay overlay and fault studies: IProtectionRelay, ProtectionRegistry, ProtectionAssessment, ProtectionCascade, FaultAnalysis, SequenceOfEvents, Coordination/OvercurrentCoordination, ShortCircuit/Iec60909 (ch. 17, ch. 25, ch. 26). |
Analysis |
Network studies over a solved state: ContingencyAnalysis, DcPowerFlow, DcOptimalPowerFlow and its variants, AcOptimalPowerFlow, ContinuationPowerFlow, DistributionFactors, LossAllocation, TapOptimizer, RestorationSequencer, WatchList, BoundaryAnalysis, VoltageLimits (ch. 23, ch. 29). |
Harmonics |
Y(h), frequency scan, penetration, coupled harmonic power flow, harmonic state estimation, compliance: FrequencyDependentYBus, HarmonicPenetration, HarmonicStateEstimator, ConverterHarmonicSources (ch. 19). |
Distribution |
The unbalanced three-phase feeder model and its sweep, fault study and estimator: MultiPhaseModel, ThreePhaseSweep, ThreePhaseWlsEstimator, BranchCurrentDsse, SequenceFaultStudy (ch. 18). |
Physics |
The physical-law audit and its registry: PhysicsLaws, PhysicsFeasibility, PhysicsCheckRegistry, and the opt-in Experimental/ models (ch. 16). |
Simulation |
The loops that drive solves over time: LiveSim and SimFrame, LiveSolveSettings, GdaReplayLoop, PresolvedReplay, RealtimeEstimatorEngine, RealtimeDseEngine, GridScenario, LoadProfile, GcScope; HybridMultiRateScheduler is internal. |
Scheduling |
The cycle engine: one base solve, then a prioritised parallel fan-out. CycleRunner, CycleScheduler, AnalysisJob, AnalysisJobLibrary, JobCostModel, CycleBudget, CycleResult, ResidentCycleService. |
Dispatch |
Dispatcher: default capacity factors by fuel and the slack balance. |
Storage |
The chemistry-first battery model and its dispatch policies: CellModel, PackModel, AgeingModel, StorageFleet, IStorageDispatchPolicy (ch. 27). |
Events, Replay, Scenarios, Evidence, Fleet |
The evidence engine: EventCatalog, GridEvent, ReplayValidator (the credibility gate), ScenarioEngine, H0StudyEngine, FleetDynamics, EvidenceReport, AvoidedCost (ch. 7). |
Frequency |
NESO frequency traces and their replay: NesoFrequencyCsv, RocofUpsampler, ReplayController. |
Gda |
GdaTimeSeries: the dated demand series that GdaReplayLoop replays. (The lake reader is the separate GridSim.Gda project.) |
IO |
Everything that crosses a file or wire boundary: IeeeCases, Matpower/, Json/ (v1 loader, V2CaseLoader, Schema/, Lossless/, CaseEditing/), Cim/, PyPsa/, Geo/, Telemetry/ frame codecs, BridgeProtocol, SolveExport, SolveImport, EstimateExport, FrequencyExport, HarmonicExport, SmallSignalExport, ImpedanceStabilityExport, H0StudyExport, NeverOverwrite, DirectoryScan (ch. 10, ch. 28). |
Interpose |
The RTU/SCADA interposer pipeline: InterposePipeline, FrameCorrection, IRtuProtocol, RtuProtocolRegistry, QualityFold, RtuPointMap, RtuMeasurementSetMapper, Protocols/ (ch. 20). |
Synchrophasor |
IEEE C37.118 decode and concentration: C37118Decoder, PhasorDataConcentrator, PmuMeasurementSetMapper. |
Net |
GridBridgeClient, the WebSocket client of the GDA bridge. |
Diagnostics |
Cross-cutting guarantees: ForwardInferenceGuard, ProvenanceTag, SolveTrace, AuditLog and its writer and recorder, RestrictionReview, CrashLog, UtcTime (ch. 31, ch. 8). |
Licensing |
LicenceKey, LicenceGate, Entitlement, BundledNetworks (ch. 22). |
Presentation, Layout, Viz |
Shared, UI-free presentation logic: thresholds, status-lamp rules, carbon factors, fuel taxonomy (Presentation); schematic layout (Layout/GraphLayout, TopologyTiers); the visualisation JSON (Viz/VizExport). |
Synthetic |
SyntheticGb: a procedurally generated GB-shaped network at GDA scale, under no licence. |
| root | BuildInfo: the release number and commit read from the assembly's informational version. |
Seams¶
A seam is an interface or registry placed where an alternative implementation is expected. Each entry gives the contract, the implementations, who owns the call site, and the file.
Power-flow solver¶
IPowerFlowSolver.Solve(GridModel, PowerFlowOptions?) returns a PowerFlowResult
(src/GridSim.Core/Abstractions/IPowerFlowSolver.cs).
| Implementation | File | Chosen when |
|---|---|---|
SparseNewtonRaphsonPowerFlow |
src/GridSim.Core/PowerFlow/SparseNewtonRaphsonPowerFlow.cs |
Every meshed network on the CPU: SolverSelector.DefaultSparseThreshold is 0, so any model with at least one bus routes here. |
NewtonRaphsonPowerFlow |
src/GridSim.Core/PowerFlow/NewtonRaphsonPowerFlow.cs |
By explicit choice (SolverKind.DenseNewton, gda --solve --dense, bench), on a GPU backend up to 200 buses, and with a caller threshold above the model's bus count. |
BackwardForwardSweep |
src/GridSim.Core/PowerFlow/BackwardForwardSweep.cs |
A strictly radial network: exactly one slack, in-service branches equal to buses minus one, and every bus reachable (SolverSelector.LooksRadial). |
SolverSelector (src/GridSim.Core/PowerFlow/SolverSelector.cs) is the single source of truth for the choice. On the
CPU the two Newton engines are bit-identical, so the threshold is a speed and memory choice and never changes a number.
The dense engine solves every step with the shared CPU sparse LU unless it is handed a GPU ILinearSolver
(NewtonRaphsonPowerFlow builds a CpuSparseLuSolver for a null or DenseLuSolver argument). Three thresholds exist,
each owned by one caller:
SolverSelector.DefaultSparseThreshold(0): the power-flow rule used bySolverBackendon the CPU, by the analyses (contingency, AC-OPF, CPF, tap optimiser, restoration), the protection cascade andSteadyStateSolverwith no engine.SolverSelector.DenseSparseCrossoverBuses(200): the GPU routing inSolverBackend.SparseThreshold, the traced solve export (SolverBackend.ExportSolver, because only the dense engine records the Jacobian) andEstimationOptions.SparseBusThreshold.LiveSolveSettings.SparseThresholdBuses(1500): the per-tick loopsLiveSimandGdaReplayLoop, which callSolverSelector.Create(kind, model, sparseThreshold: 1500). When the resolved engine is sparse Newton (above1500buses onAuto, or by explicitSolverKind.SparseNewton) they run it withMaxIterations = 40and no Q-limit switching, and skip the OLTC loop (LiveSim.cs,GdaReplayLoop.cs).
SteadyStateSolver (src/GridSim.Core/PowerFlow/SteadyStateSolver.cs) wraps any IPowerFlowSolver in the on-load
tap-changer loop and exposes RegulatedModel, TapsSettled, TapsAtLimit and OltcPasses. It runs on internal clones,
so a consumer that needs post-solve taps reads RegulatedModel, never the input model. An instance keeps per-solve state
and rejects concurrent Solve calls with InvalidOperationException; parallel code builds one instance per thread.
PowerFlowOptions.Cancellation lets a deferrable solve abort at a cycle deadline. NewtonRaphsonPowerFlow,
SparseNewtonRaphsonPowerFlow and BackwardForwardSweep check it at the top of every iteration or sweep, and the default
token never throws, so a solve with no token is bit-for-bit the solve with default. ContingencyAnalysis.ScreenBranchesParallel
takes a CancellationToken parameter and aborts between outages.
Linear solvers¶
src/GridSim.Core/Abstractions/ defines four contracts and the backends sit on both sides of the Core boundary.
| Contract | Signature | Consumers |
|---|---|---|
ILinearSolver |
void SolveInPlace(double[,] a, double[] b): dense Ax = b; the solution overwrites b and a may be overwritten, so the Newton loop reuses one buffer set. The extension LinearSolverExtensions.Solve(a, b) (src/GridSim.Core/Abstractions/ILinearSolver.cs) clones both inputs first and returns the solution without mutating the caller's arrays. |
The dense estimator steps (DenseWlsStep, ConstrainedDenseWlsStep, FeasibleWlsStep) and NewtonRaphsonPowerFlow on a GPU backend. |
ISparseLinearSolver |
double[]? Solve(CsrMatrix a, double[] b): the solution, or null when it cannot solve. |
Both Newton engines, through NewtonShell.SolveStep. |
IBatchedLinearSolver |
bool[] SolveBatch(int count, int n, double[] aFlat, double[] bFlat): count independent n x n systems in one dispatch; a singular system is filled with NaN and reported false, and never fails the batch. |
The batchsolve verb. |
IComplexLinearSolver |
Complex[]? Solve(ComplexCsr a, Complex[] b) and Factor for repeated right-hand sides. |
Harmonics, the impedance-stability scan, the coupled harmonic power flow. |
| Implementation | File | Notes |
|---|---|---|
DenseLuSolver |
src/GridSim.Core/PowerFlow/DenseLuSolver.cs |
Partial-pivot Gaussian elimination over a flat row-major copy with SIMD row updates (DenseLuKernels.SubtractScaledRow): a separate multiply then subtract, never FMA, so it equals the scalar textbook order bit for bit. Throws InvalidOperationException on a pivot at or below NumericTolerances.RelativePivotFloor (1e-12) times max abs of A. Name is dense-lu (cpu). |
CpuSparseLuSolver |
src/GridSim.Core/PowerFlow/CpuSparseLuSolver.cs |
The canonical Newton step solver on the CPU. Gilbert-Peierls LU over a reverse Cuthill-McKee ordering, caching the ordering while the sparsity pattern is unchanged (a deterministic function of the pattern, so reuse is bit-identical). Name is sparse-lu (cpu). |
CpuBatchedLuSolver |
src/GridSim.Core/PowerFlow/CpuBatchedLuSolver.cs |
Loops DenseLuSolver.FactorSolveInPlace across the batch in parallel, each system on its own segment. The reference the GPU batched kernel is validated against. Name is batched-dense-lu (cpu). |
DenseComplexLuSolver, RealExpandedComplexSolver, SparseComplexBiCgStabSolver |
src/GridSim.Core/Numerics/ComplexLinearSolvers.cs |
The complex backends that cross-validate. |
GpuDenseLinearSolver |
src/GridSim.Gpu/GpuDenseLinearSolver.cs |
ILGPU elimination over an augmented [A|b], selectable by AcceleratorType (CUDA or OpenCL, same kernels); defers to DenseLuSolver when no such device exists. EliminateKernel runs one GPU thread per row; the pivot search (PivotKernel) is a one-block, 256-thread shared-memory argmax; a device flag records the first column whose pivot is at or below NumericTolerances.PivotFloor(aNorm) (1e-12 times the matrix scale aNorm); the host synchronises exactly twice, once to read that flag and once to read the solution. |
GpuSparseLinearSolver |
src/GridSim.Gpu/GpuSparseLinearSolver.cs |
ILGPU Jacobi-preconditioned BiCGSTAB with the 4n + 200 iteration cap; returns null on breakdown, stagnation, non-convergence or a non-finite result; defers to CpuSparseLuSolver when no device exists or a diagonal is missing. |
GpuBatchedDenseSolver |
src/GridSim.Gpu/GpuBatchedDenseSolver.cs |
The dense elimination with a batch index threaded through every kernel: one flat augmented buffer factored under one GpuContext.Lock acquisition and one pair of host syncs. Defers to CpuBatchedLuSolver with no device. |
RocmDenseLinearSolver, RocmSparseLinearSolver |
src/GridSim.Gpu/RocmDenseLinearSolver.cs, RocmSparseLinearSolver.cs |
Native hipSOLVER Dgetrf/Dgetrs, and a device-resident BiCGSTAB (rocsparse_dcsrmv, rocBLAS level 1, rocblas_ddgmm Jacobi apply), through P/Invoke in RocmInterop.cs. Both defer to the CPU solver with no HIP device. ROCm has no batched entry point, so its batched solver is the CPU reference. |
A null from a sparse backend is not a failed solve. NewtonShell.SolveStep
(src/GridSim.Core/PowerFlow/NewtonShell.cs) retries a null from a GPU backend on the CPU direct LU
(directFallback), and only a null from the CPU LU goes to ILU(0)-preconditioned BiCGSTAB with the cap
NumericTolerances.DefaultKrylovMaxIterations (4m + 200). A step that is not finite is never applied: the step is
rejected and the engine reports non-convergence. The mechanics are in ch. 4.
SolverBackend (src/GridSim.Cli/SolverBackend.cs) is the one place a backend is chosen. --cpu is the default;
--gpu probes AMD-first (ROCm, then OpenCL, then CUDA, then CPU); --gpu=rocm|opencl|cuda is explicit; any other value
is refused with exit 2 (SolverBackend.IsKnownSpec). An unavailable accelerator prints a warning on stderr and falls
back to the CPU, and the availability probes (GpuContext.IsAvailable, GpuContext.IsOpenClAvailable,
RocmContext.IsAvailable) never throw. The choice is recorded in RunInfo.Backend so reports are comparable. Engine
choice per model goes through Solver(m) (radial check, then the shared rule), NewtonSolver(m) (the rule only) and
Pick(m) (the rule, wrapped in a SteadyStateSolver); DenseBatch() vends the batched solver. GPU arithmetic is outside
the bit-identity contract, and the CPU backends are the production path.
GpuContext (src/GridSim.Gpu/GpuContext.cs) owns one ILGPU Context and one Accelerator per accelerator type.
Because the CLI fans solves out with Parallel.ForEach over a single device, every device touch is serialised through
GpuContext.Lock. An OpenCL device is accepted only if an on-device kernel computes (1 + 2^-40) - 1 in double precision from operands sent by the host and returns exactly 2^-40 (single precision returns 0), so a device without usable FP64 is rejected rather than selected and faulted. The device description flags data-centre parts
(A100, H100, V100, Tesla, GH200 on CUDA; the Instinct MI series on OpenCL) as fast FP64 and everything else as rate-limited.
Power flow is double end to end, so FP64 throughput decides GPU value, which is why auto-detection is AMD-first. The
measurement behind that order: an NVIDIA RTX A6000 (FP64 at 1:64 of its FP32 rate) took over 5.5 minutes on the dense
case300 solve against 0.6 s on the CPU (ch. 28). CUDA stays
supported through ILGPU and is probed last. Batching a full AC N-1 across divergent Newton iteration counts, and
memory-chunking very large batches, is not implemented (see Limits).
Estimator step backend¶
IWlsStep (src/GridSim.Core/Estimation/WlsStep.cs, internal) separates the estimator loop from the representation
of the normal equations. WlsStateEstimator owns the loop: start point, trust-capped damping, convergence, chi-square,
bad-data removal and the IRLS reweighting outer loop. A step owns Begin (bind a model, measurement set and weights, do
structural work once), RebindWeights (IRLS passes), GateObservability, TrySolve (one undamped Gauss-Newton
correction; false means a singular gain) and NormalizedResiduals.
| Implementation | Chosen by | Notes |
|---|---|---|
CanonicalWlsStep |
EstimationMethod.Wls, Huber, LavIrls, SchweppeGm on the CPU (the default DenseLuSolver), at every size |
Solves with the SparseWlsStep arithmetic at every size, so the estimate does not depend on the bus count; at or below EstimationOptions.SparseBusThreshold (200) its observability report is the dense gain-rank proof of DenseWlsStep, above it the sparse diagnosis. |
DenseWlsStep |
the same methods with a non-CPU ILinearSolver (a GPU dense backend) |
Dense normal equations G = H^T W H on ILinearSolver; proves observability by gain-matrix rank. |
SparseWlsStep |
inside CanonicalWlsStep |
CSR gain and Gilbert-Peierls LU; builds the gain pattern, slot maps and RCM ordering once per Begin, then refills values. |
ConstrainedDenseWlsStep |
EstimationMethod.Constrained |
Rows tagged virtual-zero-injection become hard equalities in a KKT system. Dense only. |
FeasibleWlsStep |
EstimationMethod.Feasible |
The constrained KKT plus an active set (generator Q capability on by default, angle difference, branch thermal rating opt-in) and an interior guard that keeps Vm above FeasibilityConstraints.VmFloor (1e-6 pu). The voltage band is audited after convergence by PhysicsFeasibility.Check, not enforced during iteration. Dense only. |
QrWlsStep |
EstimationMethod.Qr |
Householder QR on the weighted Jacobian, without forming G. Dense only. |
The dense-only methods throw NotSupportedException above DenseEstimationLimits.DenseMethodMaxBuses (3000) rather
than run unconstrained or exhaust memory; the CLI turns that into exit 2. The CLI estimate --method takes wls,
huber, lav, lav-irls, schweppe-gm (alias gm), qr, constrained and feasible (alias constrained-physics).
Huber, LAV-IRLS and Schweppe-GM are not separate steps: the estimator reweights and re-runs the same step backend. The
exact LAV (lav) solves a linear program at each iterate (LavLinearProgram).
IJacobianSink (src/GridSim.Core/Estimation/JacobianSinks.cs, internal) gives the dense and sparse backends one
Jacobian evaluation path: MeasurementModel.EvaluateJacobian computes each partial once and pushes it into a
DenseJacobianSink or SparseJacobianRows, so the two cannot drift. Injection rows use the private
MeasurementModel.ComputeInjectionsSparse, which sums only the electrical neighbourhood and is pinned bit-equal to
NewtonRaphsonPowerFlow.ComputeInjections; flow rows use the same pi-model stamps as NewtonShell.ComputeLineFlows. Details:
ch. 6.
Inertia model¶
IInertiaModel (src/GridSim.Core/Abstractions/IInertiaModel.cs) turns a model, a demand baseline and power-step events
into a FrequencyResponse, with a CoiInertiaGvaS aggregate for scalar readouts and a LastDistributedResult for
spatial detail. LumpedScalarInertiaModel (Dynamics/LumpedScalarInertiaModel.cs) is the default;
DistributedInertiaModel (Dynamics/Distributed/DistributedInertiaModel.cs) is the multi-domain model and returns the
COI-collapsed response so scalar consumers keep working. InertiaModelSelector (Dynamics/InertiaModelSelector.cs)
builds either; there is no shape-based auto-selection, because the choice is explicit (--inertia-model=lumped|distributed).
IFrequencyIntegrator (Dynamics/IFrequencyIntegrator.cs) is the stateful swing-integrator contract that LiveSim
drives, with FrequencyIntegrator as the baseline and AssetAwareFrequencyIntegrator when an authored fleet is bound.
Asset dynamics¶
The asset seam attaches authored, stateful devices to a GridModel without touching the steady-state solve.
- Data. A case folder may carry
asset-classes.json(the class library),asset-bindings.json(bulk binding rules) andprotections.json(placed protective devices);JsonGridLoaderturns them intoGridModel.AssetDynamics(AssetDynamicsModel(Library, Bindings)) andGridModel.Protections(ProtectionModel). Absent files leave bothnull, and a case with nothing bound takes exactly the path of the feature off. - Contracts (
src/GridSim.Core/Assets/IAssetDynamics.cs).IAssetDynamicsis the base.IActiveAssetDynamics.Update(in AssetStepContext)returns anAssetActuationand contributes a swing-power term on the fast sub-step clock.IPassiveAssetDynamics.Advance(in AssetStepContext)returns aPlantStateUpdate(tap ratio, rating derate, thermal state) on the steady-state tick and is never a swing term. - Frequency seen by an asset. The active fleet has one step,
AssetDynamicsFleet.StepCoi(hz, rocof, phase, dt, clock, voltageOf, loadingOf): every asset sees the same lumped (centre-of-inertia) frequency, RoCoF and phase sample, with the bus voltage and branch loading from the network when the caller supplies them. Without them each asset's context carriesNaNvoltage and loading (AssetStepContext.NoNetworkVoltagePu,NoNetworkLoadingFraction), on which no voltage or loading stage operates; frequency, RoCoF and phase stages still act. There is no per-domain frequency sampling for assets. - Construction.
AssetControllerFactory(Assets/AssetBinding.cs) buildsGridFollowingInverterandGridFormingInverteras active assets, andTransformerDynamicsandLineThermalDynamicsas passive assets.AssetDynamicsFleet(Assets/AssetDynamicsFleet.cs) steps the active fleet andPassivePlantFleet(Assets/PassivePlantFleet.cs) the passive one; both are empty, and contribute exactly zero, when nothing is bound. - Consumers.
LiveSimbuilds the fleet whenDynamicsConfig.EnableAssetDynamicsis set and the model carries a bundle;GdaReplayLoopadvances the passive plant when one is supplied. The CLI switches are--asset-dynamicsand--passive-plant.
Protections¶
There are two protection layers, deliberately separate.
- The passive relay overlay.
IProtectionRelay.Evaluate(ProtectionContext)(src/GridSim.Core/Protection/ProtectionRelays.cs) scans a solved state and yields every operating location plus the closest non-operating one.ProtectionRegistry.Alllists the relays in registry order: undervoltage (27), overvoltage (59), directional power (32), undercurrent (37), thermal overload (49), instantaneous (50) and time (51) overcurrent, distance (21), under- and over-frequency (81U, 81O), RoCoF (81R), vector shift (78), differential (87) and ground fault (50N/51N/21N/46).ProtectionAssessment.Assess(model, result)runs them and never touches the caller's solve;ProtectionCascade.Simulatechains the branch-tripping elements through topology on copies of the model. The default solve report callsAssesson a converged state, and theprotection-checkverb prints the full list. The study entry points refuse a non-converged state throughConvergenceContract.RequireConverged(ch. 17). - The active layer.
ProtectionController : IActiveAssetDynamics(src/GridSim.Core/Assets/Protection/ProtectionController.cs) is an asset on the asset seam:AssetDynamicsFleet.Buildcreates one perProtectionInstance, it watches frequency, voltage or loading at its monitored bus, and its trips and recloser state feed back intoLiveSimas a deficit and an open-branch set (ch. 15).
Importers and exporters¶
Every format has one reader and one writer in src/GridSim.Core/IO/, and one owner chapter (ch. 10).
| Direction | Format | Type and file |
|---|---|---|
| In | Built-in cases (embedded MATPOWER) | IeeeCases (IO/IeeeCases.cs); Case9() lays the published WSCC machine data over case9.m |
| In | MATPOWER .m |
MatpowerParser (IO/Matpower/MatpowerParser.cs), with MatpowerUnitNormalizer for raw ohm and kW feeders |
| In | JSON v1 case folder | JsonGridLoader.LoadDirectory (IO/Json/JsonGridLoader.cs): system.json, buses.json and generation.json required; demand, transmission, transformer, onloadtapchanger, boundaries, asset-classes, asset-bindings and protections optional |
| In | JSON v2 manifest (gridsim-case/2) |
V2CaseLoader (IO/Json/V2CaseLoader.cs) with IO/Json/Schema/ validators |
| In | CIM / CGMES | CimImporter, CimGridModelBridge (IO/Cim/) |
| In | PyPSA-GB | PypsaGbReduced, PypsaGbZonal, PypsaCsv (IO/PyPsa/) |
| In | GDA lake | GdaModelBuilder in GridSim.Gda |
| In | Measurements (gridsim-measurements/1) |
MeasurementSet.Load (Estimation/MeasurementSet.cs) |
| In | Solve export (gridsim-solve-export/1) |
SolveImport (IO/SolveImport.cs) |
| Out | MATPOWER, JSON v1, CIM | MatpowerWriter, JsonCaseWriter, CimExporter (the convert verb) |
| Out | Solve and estimate exports | SolveExport, EstimateExport |
| Out | Study exports | FrequencyExport, HarmonicExport, SmallSignalExport, ImpedanceStabilityExport, H0StudyExport, EvidenceReport.ToJson, VizExport |
| Out | CLI result files | CliResultWriter (gridsim-cli-result/1) |
| Out | GDA case folder | GdaModelWriter in GridSim.Gda |
Each case source has rules worth knowing before extending it:
- MATPOWER (
MatpowerParser.Parse,ParseFile,ParseWithDiagnostics):%comments,...continuations andInfand-Infare preprocessed; bus type 3 becomesSlack, 2 becomesPvand anything elsePq; type-4 (isolated) buses are dropped with their incident elements and the drop is a parse note; a file with no type-3 bus is refused; a generator'sMBasefalls back to the casebaseMVAwhen it is not positive and itsHis0, because MATPOWER carries no inertia. The CLI prints every parse note on stderr asnote: <file>: <text>. - JSON v1 folder (
JsonGridLoader): bus type is resolved from the generator roles (aslackunit makes its busSlack, apqunit leaves the bus type alone, any other in-service unit makes its busPv), and a case with no slack is refused; a generator role that is notslack,pvorpqis refused; demandQcomes from the power factor whenqMvaris absent; parallelCircuitsdivide the series impedance and multiply the charging and rating; a unit's output isOutputMw, else capacity timesCapacityFactor, else capacity timesDispatcher.DefaultCapacityFactor; withdispatch: true(the default) the model is balanced byDispatcher.Balancebefore it is returned;baseKvis required on every bus; every boundary branch name must resolve to exactly one branch. The DTOs inIO/Json/JsonDtos.csread case insensitively, with comments and trailing commas allowed. - JSON v2 (
V2CaseLoader.Load) builds through the sameJsonGridLoader.Buildas v1, so a v2 case whose elements equal a v1 case gives the same model bit for bit. - Synthetic GB (
SyntheticGb.Build(Options?)) generates a GB-shaped network fromOptions.Scale(a finite number above 0;1.0gives a network of more than two thousand buses) with no data under licence; thesynthverb builds it, solves it on the sparse engine and materialises it as a JSON case folder. - Built-in names are resolved by
LoadNamedinProgram.csfor every verb that takes a case argument:case9(aliasieee9),case14,case30,case57,case118,case300,case145,case24(aliasrts),cigre,uk(aliasesgb,gb-reduced),gb-full,gb-synthetic(aliassynthetic),gb-reduced-29,gb-network-2224,pypsa-gb-reduced, a.mfile, a.jsonv2 manifest, or a folder;pypsa-gb-zonalis refused as not solvable by AC power flow. An unknown name throwsunknown case '<name>'with the list of built-in names, and each verb reports it in its own way:estimateprintserror loading case '<name>': ...and exits 1;nminus1printserror: nminus1: unknown case ...and exits 2;stabilityprintsstability: unknown case ...and exits 2. The bare-case path (gridsim <case> --once) has its own shorter list, described under Data flows.
Wire formats and the boundaries they cross¶
"Boundary" names where the bytes change hands. The field reference for each is ch. 10.
| Format | Written by | Read by | Boundary |
|---|---|---|---|
gridsim-solve-export/1 |
SolveExport (--enableExport, transient, gda-replay, estimate --export-solve); the GDA Python exporter |
SolveImport, PresolvedReplay, web CaseCatalog |
disk: CLI run directories to desktop and web replay; Python to .NET |
Bridge messages (topology, state, result, error, and the server's info and done) |
the GDA bridge server | BridgeProtocol through GridBridgeClient, which acts on topology, state, result and error and ignores every other type |
one WebSocket, Python server to .NET client (desktop and web); the topology model block and the result payload reuse the solve-export shapes |
gridsim-measurements/1 |
the GDA measurement pipeline; SyntheticMeasurements; the interposer (MeasurementSet.SaveWithoutOverwrite) |
MeasurementSet.Load (estimate --measurements, serve) |
disk: lake or telemetry to estimator |
gridsim-estimate-export/1 |
EstimateExport |
the GDA build_estimated_state pipeline; validation tooling |
disk: estimator to lake |
gridsim-frequency-export/1 |
FrequencyExport |
analysis tooling | disk |
gridsim-evidence/1 |
EvidenceReport.ToJson |
consumers of the dossier | disk |
gridsim-event-catalog/1 |
curated files under data/events/ |
EventCatalog.Load |
disk: recorded-event evidence to the replay gate |
gridsim-rtu-pointmap/1 |
the engineer | RtuPointMap.Load |
disk: crosswalk to interposer |
gridsim-audit/1 |
AuditLogWriter |
AuditLogFile and the audit verb |
disk: a run to its tamper-evident log |
gridsim-cli-result/1 |
CliResultWriter |
scripts | disk |
MATPOWER .m |
external tools, MatpowerWriter |
MatpowerParser |
disk: the MATPOWER ecosystem to GridSim |
| JSON case folder | synth, GDA materialisation, convert, the case editor, hand editing |
JsonGridLoader.LoadDirectory |
disk: case library to any entry point |
REST /api/* and WebSocket /ws/sim |
GridSim.Web/Api/RestApi.cs, SimWebSocket.cs |
external consumers (the Blazor UI drives its SimSession in process) |
HTTP and WebSocket out of GridSim.Web |
All inbound JSON is case-insensitive and accepts the named floats Infinity, -Infinity and NaN (open generator
limits): SolveImport, BridgeProtocol.JsonOptions and MeasurementSet pin the same options. Two writers refuse to overwrite:
NeverOverwrite is used by MeasurementSet.SaveWithoutOverwrite (the interposer's measurement files) and by
QuarantineLiveStore (GridSim.Gda), which write a -dupN name instead of replacing a file. Every other result file
overwrites: Program.cs writes directly with File.WriteAllText, StreamWriter and File.Create at 37 sites (for
example residuals.csv and measurements.json of estimate), the Core exporters (SolveExport, EstimateExport,
FrequencyExport, HarmonicExport, MeasurementSet.Save, AuditLogWriter, CrashLog) write their own files, the case
editor saves through CaseCommitter, and the desktop app writes its settings file (GridSim.Wpf/AppSettings.cs). The case editor reads and writes JSON through the lossless text layer
(IO/Json/Lossless/), edits it through CaseSession and CaseOps, and saves only through SaveGate
(schemas, loader, licence) and CaseCommitter.
Telemetry protocols¶
IRtuProtocol (src/GridSim.Core/Interpose/IRtuProtocol.cs) has Name, DisplayName, Transport, DefaultPort and
Decode(ReadOnlySpan<byte> frame, QualityFold quality), which returns IReadOnlyList<RtuPoint>. RtuProtocolRegistry registers
Iec104Protocol (IEC 60870-5-104) and ModbusProtocol (Modbus TCP) and nothing else. DNP3 and IEC 61850 GOOSE exist only
as link-framing scaffolding in IO/Telemetry/ (Dnp3Frame, Iec61850GooseFrame) and are not selectable.
FrameCorrection.ForProtocol supplies the per-protocol framing gate. The transports are in the CLI
(InterposeTransport.cs: IRtuTransport, TcpDelimitedFrameTransport, TcpMbapFrameTransport), so Core opens no
RTU socket (Core's only socket is the bridge WebSocket). IEEE C37.118 is a separate path: C37118Decoder and
PhasorDataConcentrator in Synchrophasor/ are driven by the pmu-ingest verb, which decodes and aligns frames and
prints them; it builds no MeasurementSet. PmuMeasurementSetMapper (which would build a horizon-guarded
MeasurementSet from a superframe) is library code that no verb calls. The interposer does build MeasurementSets, with
the horizon guard deferred (see the interposer flow) (ch. 20,
ch. 18).
Bridge¶
The bridge is a wire-level seam, not a C# interface. The server is in the GDA repository; the client is
GridBridgeClient (src/GridSim.Core/Net/GridBridgeClient.cs) and the schema is BridgeProtocol
(src/GridSim.Core/IO/BridgeProtocol.cs). One WebSocket carries topology, state, result and error
messages, which the client acts on; any other well-formed type, such as the server's info, is a forward-compatible
no-op in GridBridgeClient.Dispatch. Every stream carries a source: live, presolved, estimated, solves or raw; GridBridgeClient.StartAsync
defaults to presolved. The WPF source picker offers presolved, estimated and live
(GridSim.Wpf/MainWindow.xaml.cs, --bridge-source); the web case id bridge:<case>@<source> accepts all five
(GridSim.Web/Sessions/SessionManager.cs), and the landing id of a GridSim:ReplayOnly instance is bridge:gb-full@raw.
The server echoes the choice in StateMessage.Source, and an estimated frame carries a per-frame EstimateBlock
(objective, chi-square verdict, redundancy, removals). The protocol is specified in ch. 9.
Cycle jobs¶
AnalysisJob (src/GridSim.Core/Scheduling/AnalysisJob.cs) is a unit of post-solve work with a stable Id, a
JobPriority, a SeedCoreMs cost seed and a Run(CycleContext, CancellationToken) delegate. AnalysisJobLibrary.Build
wraps the existing analyses and re-implements none of them: watch (WatchList.Assess, High), n-1:hot (High) over
ContingencyAnalysis.ScreenBranchesParallel, n-1:lp:<shard> (Low; each shard is a resumable loop over
ContingencyAnalysis.ScreenOutage that keeps its progress across deadline cancellations and keys each branch by identity
(BranchKey: from bus, to bus, occurrence), so a de-energised island is skipped), harmonics (HarmonicPenetration.Solve, High,
skipped above CycleOptions.HarmonicsBusLimit), rocof (RocofScreening.Screen, Low) and physics (PhysicsLaws.Audit,
Low). A new job is a new AnalysisJob; the scheduler does not change.
Dispatch and scenario helpers¶
These are the pure functions every loop builds on.
Dispatcher(src/GridSim.Core/Dispatch/Dispatcher.cs).DefaultCapacityFactor(type)maps a generator type to a capacity factor: nuclear0.90, biomass0.70, hydro0.40, pumped storage, storage and battery0.20, offshore wind0.45, onshore wind0.30, solar0.15, interconnector0.60, CCGT and gas0.50, OCGT and peaker0.05, coal0.0, anything else0.40.Balance(model, lossAllowance = 0.02)sets the first generator on the first slack bus tomax(0, totalDemand * (1 + lossAllowance) - otherGeneration)and returns a copy;BalanceDetailedalso returns a note when that target lies outside the marginal plant'sPminandPmax, and never clamps it, because the power flow decides what is feasible.Dispatcher.Clonegoes throughGridModel.CopyWith, which carries every model property, including the out-of-service units.GridScenario(Simulation/GridScenario.cs).AtLoad(base, factor)scales every busPdandQdand every generator not on the slack bus byfactor, then re-balances withDispatcher.Balance.WithSolvedVoltages(base, result)returns a copy whose busVmandVaDegare the solved values keyed by bus id: a warm start.LoadProfile.DailyFactor(hour)(Simulation/LoadProfile.cs):
x = ((hour - 4 + 24) mod 24) / 24
factor = 0.60 + 0.40 * (0.5 - 0.5 * cos(2 * pi * x)) trough 0.60 at 04:00, peak 1.00 at 16:00
GcScope(Simulation/GcScope.cs).LowLatency()selectsSustainedLowLatencyfor the live and replay loops, andNoGc(budgetBytes = 256 MiB)opens a no-GC region; each returns anIDisposable.LowLatencyrestores the previous latency mode;NoGcends its region (GC.EndNoGCRegion) and restores nothing. Both degrade to a no-op if the runtime cannot honour them.- Contingency screening (
Analysis/ContingencyAnalysis.cs).ScreenBranchesParallelbuilds one solver per worker from asolverFactorythroughThreadLocal, fills a pre-allocated result array in branch order, and defaults the degree of parallelism toEnvironment.ProcessorCount; it is thread-safe becauseGridModelis immutable and an engine holds no state shared across instances.ContingencyResult.SecureisConverged && Violations.Count == 0.ScreenBranchesis the sequential sweep beside it. PowerFlowOptions.FlatStart(defaultfalse) starts every bus at flat voltage instead of the model values;PowerFlowOptions.EnforceQLimits(defaulttrue) runs the reactive-limit loop, whose violation tolerance isNewtonShell.QLimitTolMva(1e-3MVA).Oltc.DefaultStepPuis0.0125andOltc.DeadbandPudefaults to0.01; the OLTC loop is described in ch. 3.FrequencyLimits(Dynamics/FrequencyLimits.cs) holds the GB limits: operational band49.8to50.2Hz, statutory49.5to50.5Hz, first low-frequency demand disconnection stage48.8Hz and the legacy RoCoF limit0.125Hz/s.InertiaEstimator.EstimateInertiaMwSinverts the swing equation,E = f * |deltaP| / (2 * |RoCoF|)(ch. 5).
Other registries and policies¶
IStorageDispatchPolicy(Storage/StorageDispatch.cs):IdlePolicy,PeakShavePolicy,PriceThresholdPolicy,ResponseHoldingPolicy,SchedulePolicy.IMultiPhaseSolver(Distribution/ThreePhaseSweep.cs),IHarmonicCouplingModel(Harmonics/CoupledHarmonicPowerFlow.cs),IConverterImpedanceModel(Dynamics/Impedance/IConverterImpedanceModel.cs),IProcessModel(Estimation/Dse/IProcessModel.cs) andIRmsNetwork(Dynamics/Models/RmsNetwork.cs) are the extension points of their subsystems.IRealtimeWindowFeed(GridSim.Web/Realtime/RealtimeEstimatorHostedService.cs) feeds the resident estimator host its dated measurement windows.PhysicsCheckRegistry(Physics/PhysicsCheckRegistry.cs) lists the physical-law checks.
Adding a solver backend¶
A new backend is a drop-in with no change to the physics. For an ILGPU-supported accelerator no new solver is needed,
because the ILGPU solvers take an AcceleratorType. For a native vendor stack:
- Implement the interface. A dense backend implements
ILinearSolver.SolveInPlace; a sparse backend implementsISparseLinearSolver.Solveand returnsnullwhen it cannot solve. Mirror the ILGPU backends: probe device availability once and never throw from the probe, defer to the CPU implementation when the device is absent, serialise access to a shared device asGpuContext.Lockdoes, and take the singular-pivot decision fromNumericTolerances.PivotFloorso every backend declares singularity by the same rule. - Wire it into the factory. Extend
SolverBackend: theVendorenum,Select,IsKnownSpec,DenseLinear,SparseLinearand, if batched,DenseBatch. Record a description inRunInfo.Backend. The--gpu=<vendor>value is validated beforeSelectruns, so a typo fails with exit 2. - Feed it through.
NewtonRaphsonPowerFlow(ILinearSolver)andSparseNewtonRaphsonPowerFlow(ISparseLinearSolver)take the backend by constructor, andSteadyStateSolveraccepts either anILinearSolveror a wholeIPowerFlowSolver.YBus, the mismatch and Jacobian assembly, the Q-limit loop and the OLTC loop do not change. Anullfrom the new sparse solver is retried on the CPU direct LU automatically. - Keep engine choice in one place.
CliEngineChoiceScanTestsfails if any CLI file other thanSolverBackend.csbuilds an engine directly or compares a bus count in a line that names an engine, unless the line carries a reasoned// routing-exempt:marker. - Validate. The CPU solvers are the reference: compare the new backend against
CpuSparseLuSolverandDenseLuSolverin the pattern ofGpuSolverSeamTests, then rungridsim gpucheckandgridsim bench. A GPU backend agrees to solver tolerance and sits outside bit-identity.
Data flows¶
Each flow lists the types in the order they run.
CLI solve: gridsim <case> --once¶
src/GridSim.Cli/Program.csruns top-level statements in a fixed order. It pinsInvariantCulture; answers--versionand the per-verb help (VerbHelp.Render, which prints that verb's section of the embedded CLI reference) before anything can refuse them; registers the process-wide unhandled-exception net (a missing path or aCaseLoadExceptionbecomeserror: ...and exit 1, anything else is a defect, written byCrashReport.Writeand left to crash); resolvesLicenceGate.Resolve()and callsGated.Init; checks every--flagagainst theknownFlagsset and the verb's own flags (exit 2); opens the--audit-logwhen the verb isestimate,crossvalorinterpose; builds the backend withSolverBackend.Select(gpuSpec); and parses the global switches.- The model is loaded through the licence choke point.
Gated.Dir(JSON case folder) andGated.Matpower(a.mfile, afterMatpowerUnitNormalizer.Normalize) enforce the tier's bus limit unless the path is a bundled network; the bare-case switch accepts ten names:case9(ieee9),case14,case30,case57,case118,case300,case145,case24(rts) andcigrecallIeeeCases, andukgoes throughGated.Dirondata/uk. The longerLoadNamedlist applies only to verbs that take a case argument, sogridsim gb-full --onceis an unknown token. An unknown first token isUnknownVerb(error: unknown command or case '<name>'), exit 2, and never starts the live loop. - The engine is chosen.
PickSolverreturns aSteadyStateSolveroverBackwardForwardSweepfor a radial network (or with--bfs), elseSolverBackend.Pick(model), which resolves the Newton engine bySolverSelector.ResolveNewtonatSolverBackend.SparseThreshold(sparse for every model on the CPU). SteadyStateSolver.Solve(model)runs the engine inside the OLTC loop. The engine builds the Y-bus (ComplexCsrfor the sparse engine), builds the bus specification (NewtonShell.BuildBusSpec), runs Newton passes (NewtonShell.SolveStepthenNewtonShell.ApplyStep, which damps the largest correction to0.4and floorsVmat0.2pu), runs the reactive-limit loop (NewtonShell.ApplyQLimits), and assembles aPowerFlowResultwithNewtonShell.AssembleResultandComputeLineFlows. The OLTC loop steps taps and re-solves on internal clones until the taps sit inside their deadbands orDefaultMaxOltcPasses(25) passes are used.PrintDetailedreports the result:Totals, the bus table,BoundaryAnalysis.Evaluate, the most-loaded branches, the voltage range againstVoltageLimits.OperationalLowPuandOperationalHighPu, and, only on a converged state,ProtectionAssessment.Assess. A non-converged result printsDID NOT CONVERGEand setsEnvironment.ExitCode = 3. A converged result whose OLTC loop did not settle prints one extra line and keeps exit 0.
case9 and ieee9 are the exception: they call PrintAndExitCase9, which prints the report above and exits whether or
not --once is given. Without --once, every other bare case runs RunLive: each tick scales the base model with GridScenario.AtLoad at
LoadProfile.DailyFactor(hour) under GcScope.LowLatency(), solves it with the same SteadyStateSolver and renders it
to the console. RunLive does not use LiveSim.
Live view frame: web and desktop¶
- A host creates the feed. The web
SessionManager.Create(caseId, solver)builds aSimSessionthroughSimSession.CreateLive,CreateReplayorCreateBridge; the WPFMainWindowbuilds aLiveSimor loads aPresolvedReplaydirectly. Neither host references Gpu orSolverBackend. LiveSimresolves its engine withSolverSelector.Create(solver, model, sparseThreshold: 1500)and wraps it in aSteadyStateSolver. When the resolved engine is sparse Newton (above 1500 buses onAuto, or by explicit choice) it runs withMaxIterations = 40and no Q-limit switching and skips the OLTC loop; a radial network uses the sweep with no OLTC loop; anything else runs the full regulated solve.- One tick is
LiveSim.Step(tick, stepMinutes): scale demand withGridScenario.AtLoad(or hold the base loads and warm start fromGridScenario.WithSolvedVoltagesin hold-steady mode), advance any storage fleet, apply passive-plant taps, apply the open-branch set throughTopologyOps.Apply(islanding and blackout), solve, takeRegulatedModelso the frame carries the solved tap ratios, advance the swing integrator, and returnSimFrame(Model, Result, Hour, Tick, FrequencyHz, GenerationMw, DemandMw, RocofHzPerS, ...). Only a converged result seeds the next warm start. The ambient load jitter uses a fixed seed (LiveSolveSettings.LiveSimSeed), so a live session is reproducible. - Two other producers build the same
SimFrame:PresolvedReplay(loads a run directory ofexports/*.jsoninto a frame cursor, with telemetry from the sidecar CSV) andGridBridgeClient(rebuilds frames from streamedstatemessages against the cached topology). All three feed the same views, so replay and bridge are branches in the shell and not separate UIs. - The view layer consumes the frame. WPF:
MainWindowrunssim.Stepon a worker task, skips a tick while a solve is in flight, and on the UI thread callsDashboardViewModel.Update(frame, boundaries, solveMs). Web:SimSession.BuildEnvelopehands the frame of a live session toSessionCycleService.Offer, callsFrameProjector.Projectto produce aFrameEnvelopeof per-tab DTOs, and raisesFrameProduced; the BlazorDashboardpage and the/ws/simendpoint (SimWebSocketthrough a boundedSimOutboundqueue that drops the oldest frame) both subscribe to it. The web serialises DTOs and never view models, with non-finite doubles written asnullbyNonFiniteDoubleConverter.
Cycle fan-out¶
The base Newton solve is single-core, and everything a control room needs after it converges reads the same solved frame and parallelises.
CycleRunner.RunOncecallsLiveSim.Stepfor the base solve, thenCycleScheduler.RunCycle(frame, solverFactory, baseSolveMs). The frame is a shared reference, not a deep copy, becauseGridModelandPowerFlowResultare immutable by convention and jobs treat the bare arrays as read-only. The solver factory gives each worker its own engine.- The scheduler runs every High job unconditionally and never cancels it. It reserves their estimated cost, then admits Low jobs from a rotating pool while the running estimate fits the budget, and always admits at least one Low job per cycle even when its estimate does not fit. A Low job that completes or faults rotates to the back of the pool; one that is cancelled or deferred keeps the front, so later cycles cover it (round-robin, no starvation).
- The currency is core-milliseconds:
CycleBudget(workers, windowMs)ownsworkers x windowMs. Low jobs run against a deadline token linked towindowMs, whichAnalysisJobLibraryfolds intoPowerFlowOptions.Cancellation, so a long deferrable solve aborts mid-iteration.JobCostModelkeeps a per-job exponentially weighted average of measured core-ms (CycleOptions.CostEwmaAlpha) that feeds admission. - Workers drain the queue with a
Parallel.Forover an atomic cursor. A cycle is sequential; only the fan-out inside it is parallel. CycleResult(Scheduling/CycleResult.cs) carriesCycleIndex,BaseSolveMs,WallMs,HighTotal,HighCompleted,LowAdmitted,LowCompleted,LowDeferred,LowCancelled,BudgetTotalCoreMs,BudgetSpentCoreMs,BacklogDepth,FromStaleFrameandResults, with the derivedUtilizationandHighTierMet. Shard coverage isResidentCycleService.ShardsCoveredoverShardsTotal; the worst contingency is read from theResultspayloads by the web DTO.ResidentCycleServiceis the host-agnostic background driver both dashboards wrap:Offer(frame, baseMs)runs a cycle off the caller's thread, coalesces to the newest frame, publishesLatestand the rolling shard coverage, and raisesUpdated. The webCycleTab.razorand the WPFCycleViewrender it. The web host gates it withCycle:Enabled(defaulttruefor live sessions) and sizes it withCycle:WindowMs,Cycle:WorkersandCycle:Shards; its default worker count is the library default capped atPresentationLimits.DefaultCycleWorkerCap(4). The CLIcycleverb is the soak driver and exits 1 if High is ever missed.
Estimation run: gridsim estimate¶
RunEstimateloads the case throughGated.DirorLoadNamed.- The measurements arrive from
MeasurementSet.Load, which rejects an unknown schema and runsForwardInferenceGuard.RejectFutureonTakenUtc(a rejection printsREJECTED (forward inference)and exits 2), or fromSyntheticMeasurements.Sampleover a truth solved bybackend.Solver(model)(--synth, seeded).--phaseangleadds angle rows throughPhaseAngleFeed, guarded the same way;--phaseangle-from-gdaonly supplies the common-mode reference frame of those rows and has no effect without--phaseangle. EstimationOptionscarries the method, tolerance (1e-8), iteration cap (30) and bad-data switch. The CLI runsnew WlsStateEstimator().Estimate(model, set, options), orHtiBadDataEstimation.Estimatewith--hti. The CLI estimator uses the defaultDenseLuSolver, not the selected GPU backend.- Inside
Estimate:MeasurementSet.Validate(model), the start state, theIWlsStepchoice (dense methods fail loud above 3000 buses), aMeasurementModelover the active measurements, the observability gate (ObservabilityAnalysis), the Gauss-Newton loop with the trust cap, the optional IRLS reweighting, then the chi-square test and largest-normalised-residual removal inBadDataAnalysis(critical measurements are flagged and never removed), repeated until the set is clean or the removal cap is reached. - The result is an
EstimationResultwith the state, residuals, chi-square verdict, observability report and removals.EstimateExport.WriteJsonwritesestimate-export.json(gridsim-estimate-export/1);--export-solvealso writesestimated-solve-export.jsonfromEstimationResult.ToPowerFlowResultthroughSolveExport.WriteJson, an ordinarygridsim-solve-export/1document, so replay and bridge plumbing consume estimated states unchanged.residuals.csvis written beside them. --export-solvewritesestimated-solve-export.jsononly when the estimate converged. The exit code is0when the estimate converged and the network is observable, else3; with--fdia-screenit is also3for a suspected false-data injection and when the screen has no verdict (not converged, or the chi-square test did not run). With--audit-log,AuditRunRecorderwrites one hash-chained entry whose provenance isDerivedFromRecorded(orSyntheticwith--synth).- The GDA estimated-state pipeline batches such exports into the lake's
Derived/estimated_stateproduct, and the bridge streams them back assource=estimatedframes. The resident path isserve:RealtimeEstimatorEngine.Run(orRealtimeDseEngineforekf,ukfandenkf) estimates a stream of dated windows, guards each window'sTakenUtcand each output stamp, and the web hostRealtimeEstimatorHostedServiceruns the same engine once over anIRealtimeWindowFeedand publishes throughGET /api/realtime.
GDA build and replay¶
Build, gridsim gda:
RunGdaresolves the lake root (--root, elseGDA_DATA_ROOT, elseGDA_ROOT), constructsGdaPaths(root, registerAsOf)(a missing root throwsDirectoryNotFoundException, exit 1) and reads the register vintage with--as-of, which fails loud on an unparseable value.- The
--dnochoice selects the reader:GdaModelBuilder.LoadNged(paths),UkpnSource.Load(root, note)orNpgSource.Load(root). Each returns anLtdsDataset;Ltds/LtdsReaderandCsv/read the tables andParquet.Netreads the parquet-only sources. GdaModelBuilder.Build(ds, GdaBuildOptions)returnsGdaBuildResult(Model, Diagnostics). Every figure it cannot take from a published table is a named default inGdaBuildDefaults, and every dropped row is counted in the diagnostics.GeoLocatorplaces buses on the geography.GdaModelWriter.WriteDirectory(model, dir)writes the JSON case folder, and--solvesolves the model withbackend.Pick(kind)(--denseor--sparseforce the engine); a non-converged solve exits 3.
Replay, gridsim gda-replay:
RunGdaReplayloads the case (a case folder or a built-in name) and aGdaTimeSeries(GdaTimeSeries.Load, orSynthetic).ForwardInferenceGuard.AssertHistoricalchecks every step stamp at load (GdaTimeSeries.Load) and again once at the top ofGdaReplayLoop.Replay, before the step loop.GdaReplayLoop.Replaysolves each step:step.ApplyTo(base)(GridScenario.AtLoad, thenDispatcher.Balance), the storage fleet if supplied, the passive plant if bound, warm start from the last converged result, thenSteadyStateSolver.Solveon the engine fromSolverSelector.Create(kind, base, 1500).- The
onSolvedcallback writesexports/state-NNNNNN.jsonthroughSolveExport.WriteJsonfor step 0 and every--export-everystep, and the verb writesstate-estimate.csvandrun-metadata.json(RunInfo.WriteJson). - The exit code is
3if any step did not converge,2for a forward-inference rejection. - The output directory is a run directory:
PresolvedReplay.Load(the desktop replay picker and the webCaseCatalog.LoadReplay) turnsexports/*.jsonback intoSimFrames. The CLIreplayverb is unrelated: it plays a NESO frequency CSV (--sps,--seconds,--frames).
Interposer window¶
RunInterposeloads the crosswalk (RtuPointMap.Load), looks up the protocol inRtuProtocolRegistry, and opens a transport withTransportFactory.Open, which binds a TCP listener and accepts one client.- The loop calls
IRtuTransport.TryReadFrame(reassemble one frame, resynchronise on a bad lead-in) with a 200 ms timeout and passes each frame toInterposePipeline.Ingest. IngestrunsFrameCorrection.Validate(the framing gate, with accepted and rejected tallies), thenIRtuProtocol.Decode, which returns anIReadOnlyList<RtuPoint>with quality folded byQualityFold, and appends the points to the window stamped with the frame sequence number.- Every
--window-ms(default1000) and once at shutdown, the verb callsFlushWindow(DateTime.UtcNow, utcNow: DateTime.MaxValue), which hands the window toRtuMeasurementSetMapper.Map. TheDateTime.MaxValueclock defers the horizon guard (the verb prints that the guard is deferred), so a window is written unguarded and the check runs whenserveorestimateloads it. The mapper then does: horizon guard (inert here), latest value per address, drop by quality, resolve against the crosswalk, per-unit conversion, angle datum. The window is cleared even on failure, and a window that does not fit its crosswalk is discarded and counted inMapErrors. - The result is a
MeasurementSetwith sourcertu. Two optional sinks take it:--measurements-dirwrites it throughMeasurementSet.SaveWithoutOverwrite(agridsim-measurements/1file thatserveorestimatereads), and--out-storewrites it throughQuarantineLiveStore.WriteWindow(GDA-shaped Parquet, firewalled from the lake). Both sinks open throughNeverOverwrite, so an existing file is kept and a-dupNname is written; with--monitorand neither flag, nothing is written. - The verb exits 3 when any window was discarded. The estimator runs later and elsewhere: nothing here estimates.
Bridge stream¶
- A host constructs
GridBridgeClient, callsConnectAsync(endpoint)and thenStartAsync(case, start, fps, source). The server (in the GDA repository, specified in ch. 9) sends the topology, then onestatemessage per tick; the client acts ontopology,state,resultanderror, and its dispatch ignores every other type. - The receive loop assembles each message from 64 KiB chunks and
Dispatchroutes byBridgeProtocol.PeekType.HandleTopologybuilds and caches theGridModeland raisesTopologyReceived. HandleStatefirst refuses an undated frame, then callsForwardInferenceGuard.RejectFuture(timestamp, DateTime.UtcNow, "bridge state")before any frame is built. A violation throwsForwardInferenceException, which ends the receive loop, fails every pending query and raisesFaulted.- A passing
statemessage becomes aSimFramethroughStateMessage.ToSimFrame(topology, tick), and the client raisesFrameReceived,StateReceived,MarketDataReceived,EstimateReceivedand the raw-source events. Queries (Query(op, at, params)) are correlated byidand time out afterQueryTimeout(60 s). - The frame enters the same view path as a live frame (WPF
MainWindow, webSimSession.OnBridgeState).
Recorded-event gate¶
gridsim replay-event, scenario, h0-study and evidence share one pipeline. EventCatalog.Load (or
Builtin) reads dated events behind the horizon guard; ReplayValidator.Validate reproduces the recorded RoCoF and nadir
with no device and returns a ReplayResult whose verdict is Reproduced, Marginal or Failed; ScenarioEngine.Run and H0StudyEngine run counterfactual sweeps only when
the baseline is Reproduced and otherwise throw BaselineNotReproducedException; EvidenceReport.Build assembles the
dossier with each column hard-tagged by provenance. Exit 3 means the gate was not passed (ch. 7).
Cross-cutting rules¶
Determinism and bit-identity¶
Bit-identical results come from a short list of mechanisms, each in code:
- One Newton step. Dense and sparse Newton share
NewtonShell.SolveStep(one CPU direct sparse LU) andNewtonShell.ApplyStep(one damped update), and assemble every quantity with the same products and sums in the same order. Two correct LU factorisations round differently, so the sparse elimination, the only one that runs at GB scale, is canonical. - Fixed orders. Parallel sweeps write each result into a pre-allocated slot by index and rank with a stable sort;
workers take a private engine from a
ThreadLocal; Newton engines use ascending bus order. - Bit-exact kernels.
SimdKernels.SubtractScaledRowandAddScaledRowwithfastFma=falsedo a separate multiply and add per lane, matching the scalar loop at every vector width.DenseLuKernelsuses only the bit-exact kernel.SimdCapabilities.Tieris chosen at run time fromVector512,Vector256andVector128IsHardwareAccelerated. - Cached structure, not cached numbers.
CpuSparseLuSolverreuses the RCM ordering because it is a deterministic function of the sparsity pattern.SparseWlsStepreuses the gain pattern and slot maps and refills every value. - One tolerance table.
NumericTolerancesholds the singular-pivot floor (1e-12relative), the Krylov cap and tolerance. A pivot floor decides only what is declared singular, never the arithmetic of a non-singular solve. - Fixed seeds.
LiveSolveSettings.LiveSimSeedseeds the live load jitter;SyntheticMeasurementsis a seeded sampler, exact at zero noise. - No hidden state in the model.
GridModeland the records under it are immutable;SteadyStateSolverrejects re-entry, and the Newton engines keep per-solve state and are not thread-safe, so parallel code builds one engine per thread. - Culture. See Invariant culture below.
- Orchestration changes are proved. A change that only reorganises orchestration must leave every number unchanged,
proved with exact
Assert.Equal. The CI matrix runs the whole suite in three SIMD modes (normal,avx2-off,hwintrinsic-off) on Linux and Windows.
GPU arithmetic is outside the contract. The CPU dense and sparse engines are pinned; a GPU solve is an independent cross-check. Ch. 8 records the evidence per invariant.
Fail loud¶
A non-converged, invalid or unsupported request ends in a message and a non-zero exit, never in a silent default:
- Flags and verbs. An unknown flag, a flag another verb owns, a value flag with no value, an unparseable number, an
unknown enum value, an unknown verb or case and a conflicting pair all exit 2 (
knownFlags,VerbFlags,FailFlag). - Exit codes.
0success;1input error or a failed check;2usage, licence refusal or forward-inference rejection;3validation or gate failure, including any verb whose power flow did not converge;77crossvalonly, when no Python or pandapower exists. The table is in the CLI reference. - Convergence contract.
ConvergenceContract.RequireConvergedthrowsNonConvergedStateException; it is called byProtectionAssessment,FaultAnalysis,ProtectionCascade,LossContingency,SmallSignalAssembler,RmsTransientStabilityandSubtransientInit. Other studies over a solved state (WatchList.Assess,BoundaryAnalysis.Evaluate,PhysicsLaws.Audit, the distribution-factor and loss-allocation studies) do not call it. A non-finite Newton step is never applied. The OLTC loop never steps a tap on a non-converged solve.PowerFlowStatusdistinguishesConverged,MaxIterationsandNonFinite. - Singular matrices. Every elimination refuses a pivot under the shared floor; a singular batch member is isolated and
marked, and a singular system is never solved to
NaNsilently. - Estimator limits.
DenseEstimationLimitsfixes four sizes. Two fail loud:DenseMethodMaxBuses(3000) throwsNotSupportedExceptionfor the dense-only methods, andRecursiveFilterMaxState(2500) fails the Kalman filter at construction. Two degrade: aboveRankProofMaxState(2500) the observability gate skips the numerical rank proof and returns the topology-only report withRank = -1, and aboveStateCovarianceMaxState(2000) the per-bus confidence bounds staynull. - Process safety net. A missing input path or
CaseLoadExceptioniserror: ..., exit 1; any other unhandled exception is a defect and writes a crash log throughCrashReportandCrashLogbefore the runtime's crash exit. - Unimplemented options. An option the engine does not implement throws (
CycleOptions.PipelineBacklogthrowsNotSupportedExceptioninCycleRunner) instead of being accepted and ignored. - Audit.
--audit-logis accepted only onestimate,crossvalandinterpose, the log is opened and verified before the verb runs, and a broken hash chain stops the run (ch. 31).
Forward-inference guard¶
GridSim is a historical state estimator. ForwardInferenceGuard (src/GridSim.Core/Diagnostics/ForwardInferenceGuard.cs)
fixes HorizonHours = 169.0 and throws ForwardInferenceException for any stamp newer than utcNow - 169 h. The check
runs at every dated ingestion seam: MeasurementSet.Load, HarmonicSourceSet.Load, EventCatalog.Validate,
NesoFrequencyCsv.Read, GdaTimeSeries.Load, GdaReplayLoop.Replay, RtuMeasurementSetMapper.Map,
PmuMeasurementSetMapper.Map, the GridBridgeClient state handler, RealtimeEstimatorEngine.Run, RealtimeDseEngine and
the phase-angle feed; two engines also guard the stamp of each output through RestrictionReview. The bridge server holds
the same constant on its side, so each end is a complete defence if the other fails. The CLI verbs that load dated input
print REJECTED (forward inference): <message> and exit 2. The full seam table is in
ch. 31 and the rationale for 169 hours in
ch. 8.
Provenance¶
ProvenanceTag (src/GridSim.Core/Diagnostics/Provenance.cs) has seven members: Recorded, DerivedFromRecorded,
CuratedPlaceholder, Synthetic, Counterfactual, Interpolated and UnvalidatedAtScale. Evidence records, asset
classes (AssetClassProvenance) and audit-log entries carry a tag that the constructor demands, so it cannot be
forgotten, and it survives serialisation. The tags are permissions: data moves down the trust ladder and never up. Two rules sit
on top. A CuratedPlaceholder asset-class parameter must carry a [VERIFY] marker in its note, enforced by
AssetClassLibrary.ValidateProvenance and pinned by AssetProvenanceTests; no general rule extends this to other
records. A Counterfactual result is labelled in its output as never an input to another replay; that is policy, and no
code rejects such an input. Ch. 8 owns the taxonomy.
Invariant culture¶
The CLI (Program.cs), the web host (GridSim.Web/Program.cs) and the case editor (EntryPoint.Main) set
CultureInfo.DefaultThreadCurrentCulture and CultureInfo.CurrentCulture to InvariantCulture before anything else, so
a comma-decimal host cannot parse 0.5 as 5 or write 1,2340 into a CSV that the invariant reader would then
mis-column. Flags parse with NumberStyles.Float and InvariantCulture; NaN and Infinity are refused as flag values.
The desktop app (GridSim.Wpf/App.xaml.cs, OnStartup) does the same before its first window exists, and also sets
the WPF element language (FrameworkElement.LanguageProperty) to the invariant culture, because XAML StringFormat
bindings format with the element language, not the thread culture.
Licence gate¶
Every user-facing model load in the CLI goes through Gated.Dir, Gated.Matpower or Gated.V2Case, so the tier's bus
limit (LicenceGate, Entitlement) is enforced in one place; networks under the repository's data/ tree are bundled and
exempt (BundledNetworks), and so are the built-in IeeeCases. The licence resolves offline, never over the network, in
the order GRIDSIM_LICENCE, the installed licence file, then the free Personal tier. The web host resolves it once and
hands it to CaseCatalog (ch. 22).
Versioning¶
Directory.Build.props holds the single <Version> for every project. No .csproj sets Version, VersionPrefix,
AssemblyVersion, FileVersion or InformationalVersion. The SDK derives AssemblyVersion and FileVersion as
<Version>.0 and InformationalVersion as <Version>+<full commit sha>; IncludeSourceRevisionInInformationalVersion
keeps the suffix, and a container build passes the commit with -p:SourceRevisionId. GridSim.Core.BuildInfo reads the
informational version and splits it into Version, Commit and DisplayLine; gridsim --version, the web footer, the
web OpenAPI document and the desktop About box show it. A bump is one edit to that file plus a matching ## [<Version>]
heading in CHANGELOG.md in the same commit. The CLI additionally stamps the git commit and build configuration into the
assembly (StampGitCommit target) for RunInfo.
Build rules and embedded resources¶
Directory.Build.props turns on Nullable, TreatWarningsAsErrors, the .NET code analysers at latest level and
AnalysisMode Recommended, with rule severities tuned in .editorconfig; a warning is a build failure. The CLI embeds
docs/manual/0019 CLI reference.md as GridSim.Cli.19-cli-reference.md, so gridsim help <verb> prints the manual's own
section and the build fails if the chapter is missing. Core embeds the bundled MATPOWER cases and the schemas, and the
CLI copies the JSON, CSV and MATPOWER files under data/ and the JSON files under schemas/ next to the binary.
Parameters and defaults¶
| Name | Value | Unit | Meaning | Source symbol |
|---|---|---|---|---|
| Power-flow sparse threshold | 0 |
buses | Newton solves above it are sparse; 0 makes every CPU Newton solve sparse |
SolverSelector.DefaultSparseThreshold |
| Dense and sparse crossover | 200 |
buses | GPU routing, solve-export engine and the WLS observability report (the CPU estimate itself is the same sparse arithmetic either side) | SolverSelector.DenseSparseCrossoverBuses, EstimationOptions.SparseBusThreshold |
| Live per-tick threshold | 1500 |
buses | Above it, or when sparse Newton is chosen explicitly, the live and GDA-replay loops run sparse with the options below | LiveSolveSettings.SparseThresholdBuses |
| Live sparse iteration cap | 40 |
iterations | Newton cap of a sparse per-tick solve; no Q-limit switching | LiveSolveSettings.SparseMaxIterations |
| OLTC pass cap | 25 |
passes | Outer tap-loop cap | SteadyStateSolver.DefaultMaxOltcPasses |
| Newton tolerance | 1e-8 |
pu | Largest power mismatch | PowerFlowOptions.Tolerance |
| Newton iteration cap | 30 |
iterations | Inner cap | PowerFlowOptions.MaxIterations |
| Reactive-limit pass cap | 12 |
passes | Outer Q-limit cap | PowerFlowOptions.MaxQLimitPasses |
| Largest undamped Newton step | 0.4 |
rad or pu | Larger steps are scaled down | NewtonShell.MaxUndampedStep |
| Voltage floor after a step | 0.2 |
pu | Floor on a PQ bus Vm |
NewtonShell.VmFloorPu |
| Relative pivot floor | 1e-12 |
relative | A pivot at or below it is singular | NumericTolerances.RelativePivotFloor |
| Krylov iteration cap | 4n + 200 |
iterations | BiCGSTAB cap for order n |
NumericTolerances.DefaultKrylovMaxIterations |
| Dense estimation methods cap | 3000 |
buses | Constrained, Feasible and QR fail loud above it | DenseEstimationLimits.DenseMethodMaxBuses |
| Rank-proof size | 2500 |
states | Sparse backend runs the numerical rank proof at or below it; above it the gate returns the topology-only report | DenseEstimationLimits.RankProofMaxState |
| State-covariance size | 2000 |
states | Per-bus confidence bounds are computed at or below it and are null above it; HtiBadDataEstimation.MaxState reuses it |
DenseEstimationLimits.StateCovarianceMaxState |
| Recursive-filter size | 2500 |
states | The Kalman filter fails at construction above it; the streaming engine applies it to ekf, ukf and enkf |
DenseEstimationLimits.RecursiveFilterMaxState |
| Estimator tolerance | 1e-8 |
radians or pu | Largest state change | EstimationOptions.Tolerance |
| Estimator iteration cap | 30 |
iterations | Gauss-Newton cap | EstimationOptions.MaxIterations |
| Feasible voltage floor | 1e-6 |
pu | Interior guard | FeasibilityConstraints.VmFloor |
| Forward-inference horizon | 169.0 |
hours | Newest processable stamp is now minus this | ForwardInferenceGuard.HorizonHours |
| Cycle window | 1000 |
ms | Wall-clock deadline of a cycle | CycleOptions.WindowMs |
| Cycle workers | processor count minus one, at least 1 | threads | Fan-out width; the web host caps its default at 4 |
CycleOptions.Workers, PresentationLimits.DefaultCycleWorkerCap |
| Cycle N-1 shards | 8 |
shards | Low-tier N-1 rotation | CycleOptions.NMinus1Shards |
| Hot branch loading | 80.0 |
percent | Loading at which a branch joins the High N-1 set | CycleOptions.HotBranchLoadingPercent |
| Cycle harmonics gate | 1500 |
buses | Harmonics job returns nothing above it | CycleOptions.HarmonicsBusLimit |
| Cost-model smoothing | 0.3 |
fraction | Weight of the newest core-ms measurement | CycleOptions.CostEwmaAlpha |
| Live load seed | 20260605 |
seed | Ambient load jitter generator | LiveSolveSettings.LiveSimSeed |
| Live step | 20 |
minutes per tick | Default simulated step | LiveSolveSettings.DefaultStepMinutes |
| Bridge query timeout | 60 |
seconds | Wait for a matching result |
GridBridgeClient.QueryTimeout |
| Bridge default source | presolved |
name | Source of StartAsync |
GridBridgeClient.StartAsync |
| GPU FP64 smoke tolerance | 1e-12 |
absolute | kept for compatibility; the smoke test is now exact ((1 + 2^-40) - 1 == 2^-40) |
GpuContext.Fp64SmokeTolerance |
| Interposer window | 1000 |
ms | Window length | --window-ms of RunInterpose |
Invariants¶
| Invariant | Pinned by |
|---|---|
One <Version> in Directory.Build.props, no project overrides it, CHANGELOG.md has its heading, BuildInfo reports it, and Core, Gda, Gpu, Web and the test assembly carry the identical informational version |
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) |
--version is answered before the licence and the flag gate |
VersionContractTests.Cli_answers_version_before_any_other_processing, CliProcessSmokeTests |
Every dispatched verb is listed by --help, --help names no undispatched verb, and every verb has a section in the CLI reference and its own help |
CliHelpContractTests, VerbHelpTests |
| The CLI reference is embedded in the build, and the help text is plain ASCII with the exit-code rule | VerbHelpTests.The_chapter_is_embedded_in_the_cli_build, Every_help_text_is_plain_ascii_with_a_title_and_the_exit_code_rule |
| Flag helpers never substitute a default for a bad or missing value, and flags the CLI accepts are not ignored | CliFailLoudFlagContractTests, CliLiteralsAndFlagsContractTests |
| Every reported non-convergence exits 3, and fault, protection and the default report refuse or skip a non-converged state | CliExitCodeContractTests, CliProcessSmokeTests |
The engine rule lives in SolverSelector: DefaultSparseThreshold is 0, the crossover is 200, the sweep is never chosen by the Newton rule, and CreateNewton builds the resolved engine |
SolverRoutingTests |
| No CLI file chooses an engine by itself | SolverRoutingTests.No_local_dense_sparse_threshold_remains_in_the_cli, CliEngineChoiceScanTests |
| Contingency screens and the loss sweep match an explicit dense engine bit for bit on the default engine | DefaultEngineRoutingTests |
| Dense and sparse Newton are bit-identical on the CPU across the built-ins, the bundled MATPOWER files and the JSON cases | DenseSparseParityTests |
| A GPU solve that gives up is retried on the exact CPU LU | GpuFallbackTests |
| GPU backends match the CPU solvers within solver tolerance and reject singular and mismatched input | GpuSolverSeamTests, RocmSolverSeamTests |
| The batched in-place LU is bit-identical to the single-system LU | BatchedInPlaceLuBitIdentityTests |
| The bit-exact SIMD kernels equal the scalar loop at every tier, and the active tier is what the hardware flags say | SimdTierSeamTests |
| Estimator kernels equal their scalar references to the bit | ResidualCovarianceBitIdentityTests, ObservabilityRankBitIdentityTests, HouseholderQrLayoutBitIdentityTests, KalmanDenseKernelBitIdentityTests |
| Estimator limits keep their values; the dense-method bus cap fails loud above it | DenseEstimationLimitsTests |
| A study refuses a non-converged state | ConvergenceContractTests, ConvergenceContractStudyTests |
| The horizon is a single knob, every dated seam rejects a fresh stamp, and the output guard matches the input horizon | ForwardInferenceGuardTests, RestrictionInvariantTests |
The inertia model seam routes bit for bit to FrequencyDynamics |
InertiaModelSeamTests |
| The cycle budget is workers times window, High is always admitted and Low is gated by budget (the first Low job of a cycle is always admitted, which no test covers), Low rotates to full coverage, results do not depend on worker count, the fan-out never mutates the shared frame, and a faulting job is isolated | Scheduling/CycleSchedulerTests |
| A cancellation during the High drain stops no High job and starts no Low job; the default token is bit-for-bit identical to no token; a cancelled token aborts a solve | Scheduling/CycleDeadlineDeterministicTests, Scheduling/SolverCancellationTests |
A CuratedPlaceholder without [VERIFY] is a violation; recorded and derived tags need none |
AssetProvenanceTests |
| Result files are written without locale influence | CliResultWriterTests (runs under de-DE) |
| CI covers both operating systems and all three SIMD modes, the release refuses a tag that disagrees with the version, and the vulnerable-package gate exists | WorkflowContractTests |
| Every shipped package appears with its version in the third-party notices | ThirdPartyNoticesTests |
| The real web host starts in memory (middleware order, replay-only switch, version in the footer, OpenAPI document), and its DTOs carry exactly the solver's numbers | WebHostTests, GridSimWebProjectionTests |
Worked example¶
gridsim case9 --once exercises the whole chain of the first flow on the smallest case. The block below is
docs/_captures/start/case9-once.txt with the bus and branch tables elided; its stamps and timings, where a capture
has them, differ by machine and run. case9 loads at its WSCC nominal frequency, so the second line ends 60 Hz; the
power flow does not use the frequency.
$ gridsim case9 --once
GridSim - IEEE 9-bus (WSCC)
9 buses 9 branches 3 generators base 100 MVA 60 Hz
========================================================================
Converged (4 NR iters, 0 OLTC passes, max mismatch 1.65E-014 p.u.)
Generator output 0.32 GW Demand 0.32 GW Losses 4.6 MW Inertia 7.5 GVAs
...
Voltage range: 0.996 - 1.040 p.u. (in band)
Total losses: 4.64 MW, -92.16 MVAr
Protection simulation: no relay would operate on this state (generic settings).
[exit 0]
Reading it against the architecture:
case9is a built-in name, soProgram.cscallsIeeeCases.Case9()and, because it is not an export,PrintAndExitCase9callsPrintDetailed(model, backend.Pick(model)). The model never touchesGated, so the licence gate does not count it.SolverBackend.Pickresolves the engine withSolverSelector.ResolveNewton(model, SparseThreshold). The CPU backend'sSparseThresholdisDefaultSparseThreshold(0), and 9 buses is above 0, so the engine isSparseNewtonRaphsonPowerFlowonCpuSparseLuSolver, wrapped in aSteadyStateSolver. case9 has 9 buses and 9 in-service branches, not buses minus one, soLooksRadialis false and the sweep is not considered.Converged (4 NR iters, 0 OLTC passes, ...)isPowerFlowResult.IterationsandSteadyStateSolver.OltcPasses: four Newton passes to a largest mismatch of1.65E-014pu againstPowerFlowOptions.Toleranceof1e-8, and no OLTC in the case, so the loop is a single pass. The dense engine returns the same bits on this case (DenseSparseParityTests).- The balance is checkable by hand from the elided bus table. The loads are 90, 100 and 125 MW, 315 MW in all. The three
generators supply
71.6 + 163.0 + 85.0 = 319.6MW, and the difference of4.6MW is theLosses 4.6 MWfigure, whose exact value is the4.64 MWline (PowerFlowResult.TotalLossMw, the sum of the net injections).Generator output 0.32 GWisSolvedGeneration.GeneratorOutputMw, the generators' own output, not the sum of positive bus injections. - The last line is
ProtectionAssessment.Assesson the converged state, which is skipped for a non-converged one. The[exit 0]marker is the capture's exit status:PrintDetailedsets exit code 3 only whenConvergedis false.
The version stamp the same chain reports is docs/_captures/start/version.txt:
The release number is the single <Version> and the suffix is the commit the binary was built from; both come from
BuildInfo.InformationalVersion.
Source map¶
| File | Type or function | Responsibility |
|---|---|---|
GridSim.slnx |
solution | The eight projects. |
Directory.Build.props |
build properties | Code-analysis and warning policy, the single <Version>, CI path mapping. |
src/GridSim.Core/BuildInfo.cs |
BuildInfo |
Release number and commit from the informational version. |
src/GridSim.Core/Abstractions/ |
IPowerFlowSolver, ILinearSolver, ISparseLinearSolver, IBatchedLinearSolver, IComplexLinearSolver, IInertiaModel |
The seam contracts. |
src/GridSim.Core/PowerFlow/SolverSelector.cs |
SolverSelector, SolverKind |
Engine choice and the three threshold constants. |
src/GridSim.Core/PowerFlow/SteadyStateSolver.cs |
SteadyStateSolver |
OLTC loop, RegulatedModel, TapsSettled. |
src/GridSim.Core/PowerFlow/NewtonShell.cs |
NewtonShell |
The shared Newton step, update, bus specification, reactive limits, result assembly. |
src/GridSim.Core/PowerFlow/ConvergenceContract.cs |
ConvergenceContract, NonConvergedStateException |
Refusal of a non-converged state. |
src/GridSim.Core/PowerFlow/CpuSparseLuSolver.cs, DenseLuSolver.cs, CpuBatchedLuSolver.cs |
CPU linear backends | The reference solvers. |
src/GridSim.Core/Numerics/NumericTolerances.cs |
NumericTolerances |
The one tolerance table. |
src/GridSim.Core/Numerics/SimdKernels.cs, SimdCapabilities.cs |
SimdKernels, SimdCapabilities |
Vector kernels and run-time tier dispatch. |
src/GridSim.Core/Estimation/WlsStateEstimator.cs, WlsStep.cs, FeasibleWlsStep.cs, JacobianSinks.cs, DenseEstimationLimits.cs |
estimator and step backends | The estimator loop, the IWlsStep seam, limits. |
src/GridSim.Core/Assets/IAssetDynamics.cs, AssetBinding.cs, AssetDynamicsFleet.cs, PassivePlantFleet.cs |
asset seam | Active and passive asset contracts and fleets. |
src/GridSim.Core/Protection/ProtectionRelays.cs, ProtectionAssessment.cs, ProtectionCascade.cs |
relay overlay | The passive protection layer. |
src/GridSim.Core/Simulation/LiveSim.cs, LiveSolveSettings.cs, GdaReplayLoop.cs, PresolvedReplay.cs |
frame producers | SimFrame and its producers. |
src/GridSim.Core/Scheduling/ |
cycle engine | CycleRunner, CycleScheduler, AnalysisJobLibrary, ResidentCycleService. |
src/GridSim.Core/Interpose/ |
interposer | Pipeline, protocols, crosswalk, mapper. |
src/GridSim.Core/Net/GridBridgeClient.cs, src/GridSim.Core/IO/BridgeProtocol.cs |
bridge | Client and wire schema. |
src/GridSim.Core/Diagnostics/ForwardInferenceGuard.cs, Provenance.cs, AuditLog.cs, CrashLog.cs |
guarantees | Horizon, provenance, audit, crash log. |
src/GridSim.Core/Licensing/ |
licensing | Key, gate, entitlement, bundled networks. |
src/GridSim.Cli/Program.cs |
top-level statements | Dispatch, flag gate, every verb. |
src/GridSim.Cli/SolverBackend.cs |
SolverBackend |
CPU or GPU backend and engine selection. |
src/GridSim.Cli/Gated.cs |
Gated |
The licence choke point for model loads. |
src/GridSim.Cli/VerbHelp.cs, VerbFlags.cs |
VerbHelp, VerbFlags |
Per-verb help and flag gate read from the embedded reference. |
src/GridSim.Cli/CliResultWriter.cs, RunInfo.cs, CrashReport.cs, InterposeTransport.cs |
CLI support | Result files, run metadata, crash report, TCP transports. |
src/GridSim.Gpu/GpuContext.cs, GpuDenseLinearSolver.cs, GpuSparseLinearSolver.cs, GpuBatchedDenseSolver.cs |
ILGPU backends | CUDA and OpenCL solvers. |
src/GridSim.Gpu/RocmContext.cs, RocmInterop.cs, RocmDenseLinearSolver.cs, RocmSparseLinearSolver.cs |
ROCm backends | Native HIP solvers. |
src/GridSim.Gda/GdaModelBuilder.cs, GdaModelWriter.cs, GdaPaths.cs |
lake builder | LTDS dataset to GridModel and the case writer. |
src/GridSim.Gda/Interpose/QuarantineLiveStore.cs |
QuarantineLiveStore |
Firewalled Parquet sink of the interposer. |
src/GridSim.Web/Program.cs |
host setup | Culture, licence, services, REST and WebSocket mapping. |
src/GridSim.Web/Sessions/SessionManager.cs, SimSession.cs |
sessions | Feed creation, frame production. |
src/GridSim.Web/Projection/FrameProjector.cs |
FrameProjector |
SimFrame to per-tab DTOs. |
src/GridSim.Web/Api/RestApi.cs, SimWebSocket.cs, SimOutbound.cs |
API | REST under /api, /ws/sim. |
src/GridSim.Wpf/MainWindow.xaml.cs, DashboardViewModel.cs |
desktop shell | Tick loop, view-model update. |
src/GridSim.SchemaEditor/EntryPoint.cs, Ui/ |
case editor | Key map, renderer, views. |
tests/GridSim.Tests/ |
tests | The suite; contract tests read Program.cs and Wpf source. |
Limits¶
- No test pins the dependency directions between projects or Core's zero-package rule; the
.csprojfiles are the pin, andThirdPartyNoticesTestschecks only that every shipped package is named in the notices. IBatchedLinearSolverhas one consumer, thebatchsolveverb; no analysis, including the contingency sweeps, calls it. Batching a full AC N-1 across divergent Newton iteration counts, and memory-chunking very large batches, is not implemented.- GPU backends are outside bit-identity, and the ROCm solvers are built against the ROCm 6.x headers and have had no runtime run on Instinct hardware.
- The dense estimator step (
DenseWlsStep, used to solve an estimate only with a GPU linear solver) and the sparse step are not bit-identical to each other; on the CPU every WLS and robust estimate takes the sparse step at every size (CanonicalWlsParityTests). tools/AssetSmokeis compiled by every build but run by nothing: no CI job or test starts it, so a page that crashes only at template load is still caught only by running it by hand on Windows.- The WPF source picker offers
presolved,estimatedandliveonly;solvesandraware reachable from the web host.
See also¶
- ch. 2, Data model and per-unit conventions - the records every flow carries.
- ch. 3, Power flow - inside
IPowerFlowSolver. - ch. 4, Sparse and GPU numerics - inside the linear-solver seams.
- ch. 6, State estimation - inside
IWlsStep. - ch. 7, The evidence engine - the recorded-event gate.
- ch. 8, Provenance and invariants - the guard, the tags and the numerical invariants.
- ch. 9, GDA integration - the lake builder, the replay loop and the bridge protocol.
- ch. 10, File and wire formats - every schema.
- ch. 5, Frequency dynamics - the swing stack behind
IInertiaModel. - ch. 11, Validation - the cross-checks that validate the solver and the estimator.
- ch. 20, The RTU/SCADA interposer - the interposer pipeline.
- ch. 31, Audit log and diagnostics - the audit log and the guard's seams.
- ch. 35, Deployment and security - containers, the web host, CI and release workflows.
- ../manual/0001 Getting started.md - building and running the projects.
- ../manual/0029 Extending GridSim.md - adding an implementation behind a seam.
- ../manual/0019 CLI reference.md - the verbs that drive these flows.
- ../manual/0008 The desktop app.md - the WPF dashboard.
- ../manual/0033 The case editor.md - the case editor.