Architecture
This chapter maps the codebase for someone modifying or auditing it: the projects and their dependency directions, the seams (the interfaces the system pivots on and every implementation behind each), the end-to-end data flows, and where each wire format crosses a process or project boundary.
Projects
The solution GridSim.slnx carries six src/ projects and one test project.
Dependency facts below are taken from the .csproj files.
| Project | Kind | TFM | Purpose | References |
|---|---|---|---|---|
GridSim.Core |
class library | net10.0 |
The physics: model, power flow, sparse numerics, dynamics, estimation, evidence, IO, simulation loops, prioritised analysis scheduling. No package references. | - |
GridSim.Gpu |
class library | net10.0 |
GPU linear-solver backends (ILGPU/CUDA and ROCm/HIP) behind the Core seams. | Core + ILGPU, ILGPU.Algorithms |
GridSim.Gda |
class library | net10.0 |
Ingestion of the GB data lake (NESO + DNO LTDS sources: Ltds/, Neso/, Npg/, Ukpn/, Csv/, Geo/) into GridModels. No grid data is embedded. |
Core + Parquet.Net |
GridSim.Cli |
console exe | net10.0 |
Every verb (solve, bench, N-1, transient, replay-event, scenario, fleet, evidence, gda-replay, estimate, crossval, ...), backend wiring, run metadata. | Core, Gda, Gpu |
GridSim.Wpf |
WinExe | net10.0-windows |
The desktop dashboard (MVVM; tabs NETWORK -> CYCLE -> ASSETS, transport bar, inspector, bridge/replay pickers). | Core |
GridSim.Web |
web exe | net10.0 |
Blazor Server dashboard plus a REST/WebSocket API over Core (Api/, Components/, Projection/, Sessions/, Model/). |
Core |
tests/GridSim.Tests |
xUnit | net10.0 |
The test suite (case9 validated against the published MATPOWER solution and cross-checked against pandapower). | Core, Gda, Web |
Dependency directions are strictly one-way, with Core at the root:
GridSim.Core (Model, PowerFlow, Numerics, Estimation, Dynamics,
/ | | \ \ Analysis, Simulation, Scheduling, IO, Net, Events, Evidence, ...)
/ | | \ \
Gpu Gda Wpf Web Tests
\ | (Tests also references Gda and Web)
\ |
GridSim.Cli (argv parsing, backend selection, drives everything)
Two consequences to preserve when you change things:
- Core has zero package references and no UI/OS dependencies beyond the
BCL (its bridge client uses
System.Net.WebSocketsdirectly). Anything that needs a NuGet package or a device driver goes in a leaf project behind a Core seam. - Nothing references
GridSim.Gpuexcept the CLI. The GPU backends reach the solvers only throughILinearSolver/ISparseLinearSolver, so Core never needs to know CUDA or HIP exists.
The seam catalogue
A "seam" is an interface deliberately placed where an alternative implementation is expected. Each is listed with its contract, its implementations, and who owns the call site.
ILinearSolver - dense linear solve
src/GridSim.Core/Abstractions/ILinearSolver.cs:ILinearSolver. Solves the
dense real system Ax = b in place (SolveInPlace(double[,], double[]); the
caller's buffers are consumed - this is the allocation-free path the
Newton-Raphson inner loop uses). Every dense Newton iteration and every dense
estimator gain solve goes through here.
| Implementation | File | Notes |
|---|---|---|
DenseLuSolver |
src/GridSim.Core/PowerFlow/DenseLuSolver.cs |
Default. Partial-pivoting Gaussian elimination over a flat row-major copy with SIMD row updates (DenseLuKernels.SubtractScaledRow); bit-for-bit identical to the scalar textbook order. Throws on a near-singular pivot. |
GpuDenseLinearSolver |
src/GridSim.Gpu/GpuDenseLinearSolver.cs |
ILGPU elimination over an augmented [A|b] matrix, AcceleratorType-selectable (CUDA or OpenCL - same kernels); falls back to DenseLuSolver when no such device exists. |
RocmDenseLinearSolver |
src/GridSim.Gpu/RocmDenseLinearSolver.cs |
Native hipSOLVER Dgetrf/Dgetrs (explicit workspace) via P/Invoke; falls back to DenseLuSolver when no HIP device exists. |
ISparseLinearSolver - sparse linear solve
src/GridSim.Core/Abstractions/ISparseLinearSolver.cs:ISparseLinearSolver.
double[]? Solve(CsrMatrix a, double[] b) - returns the solution or null
when the matrix is structurally/numerically singular, in which case the sparse
Newton solver falls back to its ILU(0)/BiCGSTAB path.
| Implementation | File | Notes |
|---|---|---|
CpuSparseLuSolver |
src/GridSim.Core/PowerFlow/CpuSparseLuSolver.cs |
Default. Wraps SparseLu.Solve (Gilbert-Peierls LU + RCM ordering); the reference the GPU sparse backends are validated against. |
GpuSparseLinearSolver |
src/GridSim.Gpu/GpuSparseLinearSolver.cs |
ILGPU Jacobi-preconditioned BiCGSTAB, AcceleratorType-selectable (CUDA or OpenCL - same kernels); returns null on breakdown so the caller's fallback contract still holds. |
RocmSparseLinearSolver |
src/GridSim.Gpu/RocmSparseLinearSolver.cs |
Native ROCm: fully device-resident BiCGSTAB (rocsparse_dcsrmv SpMV + rocBLAS-1 vector algebra + rocblas_ddgmm Jacobi apply); ABI matched to ROCm 6.x, awaiting first Instinct runtime run. |
IBatchedLinearSolver - many same-sized dense solves in one dispatch
src/GridSim.Core/Abstractions/IBatchedLinearSolver.cs:IBatchedLinearSolver.
bool[] SolveBatch(int count, int n, double[] aFlat, double[] bFlat) - solves
count independent n x n systems in one call over row-major flat buffers; the
solution overwrites each system's bFlat segment and the per-system success
mask is returned (a singular system's segment is filled with NaN and reported
false - one bad system never fails the whole batch). The point is amortising
per-solve launch and global-lock overhead across a batch, so a contingency sweep
of many same-sized outages becomes a handful of dispatches instead of hundreds of
locked single solves.
| Implementation | File | Notes |
|---|---|---|
CpuBatchedLuSolver |
src/GridSim.Core/PowerFlow/CpuBatchedLuSolver.cs |
Reference/fallback: loops DenseLuSolver across the batch in parallel. The ground truth the GPU path is validated against. |
GpuBatchedDenseSolver |
src/GridSim.Gpu/GpuBatchedDenseSolver.cs |
ILGPU batched Gaussian elimination with partial pivoting (the GpuDenseLinearSolver algorithm with a batch index threaded through every kernel): one flat augmented buffer factored under a single GpuContext.Lock acquisition and one pair of host syncs. Falls back to CpuBatchedLuSolver when no device is present, so it is always safe to construct. ROCm has no native batched entry point yet, so it uses the CPU reference. |
src/GridSim.Cli/SolverBackend.cs:SolverBackend.DenseBatch() vends the batched
solver for the selected backend (CUDA/OpenCL -> GpuBatchedDenseSolver, else
CpuBatchedLuSolver); the CLI batchsolve verb exercises it against the CPU
reference.
IPowerFlowSolver - the whole steady-state solve
src/GridSim.Core/Abstractions/IPowerFlowSolver.cs:IPowerFlowSolver.
PowerFlowResult Solve(GridModel, PowerFlowOptions?).
| Implementation | File | Chosen when |
|---|---|---|
NewtonRaphsonPowerFlow |
src/GridSim.Core/PowerFlow/NewtonRaphsonPowerFlow.cs |
Meshed networks up to the sparse threshold (3,000 buses). |
SparseNewtonRaphsonPowerFlow |
src/GridSim.Core/PowerFlow/SparseNewtonRaphsonPowerFlow.cs |
Above the threshold. |
BackwardForwardSweep |
src/GridSim.Core/PowerFlow/BackwardForwardSweep.cs |
Radial / weakly-meshed distribution feeders. |
src/GridSim.Core/PowerFlow/SolverSelector.cs:SolverSelector is the single
source of truth for the choice (see 03-power-flow.md); the
CLI and both dashboards route through it so a given case always solves with
the same engine. SteadyStateSolver wraps any IPowerFlowSolver in the OLTC
outer loop.
PowerFlowOptions.Cancellation (a CancellationToken, default None) lets a
deferrable solve abort at a cycle deadline: NewtonRaphsonPowerFlow and
SparseNewtonRaphsonPowerFlow check it at the top of every inner iteration.
default/None is a guaranteed no-op (ThrowIfCancellationRequested never
throws for it), so every existing solve is bit-for-bit unchanged - a test asserts
exact Vm/VaDeg equality between the token and no-token paths.
ContingencyAnalysis.ScreenBranchesParallel takes the same optional token on its
ParallelOptions so a deferred N-1 shard aborts between outages.
IInertiaModel - frequency dynamics
src/GridSim.Core/Abstractions/IInertiaModel.cs:IInertiaModel. Turns a model,
a demand baseline and power-step events into a FrequencyResponse, plus a
CoiInertiaGvaS aggregate for scalar readouts and an optional
LastDistributedResult for spatial detail.
| Implementation | File | Notes |
|---|---|---|
LumpedScalarInertiaModel |
src/GridSim.Core/Dynamics/LumpedScalarInertiaModel.cs |
Default; single swing equation on the system aggregate. |
DistributedInertiaModel |
src/GridSim.Core/Dynamics/Distributed/DistributedInertiaModel.cs |
Experimental multi-domain model; returns the COI-collapsed response so scalar consumers keep working. |
Selected at runtime by
src/GridSim.Core/Dynamics/InertiaModelSelector.cs:InertiaModelSelector (CLI
flag --inertia-model=lumped|distributed).
IWlsStep - the estimator's step backend
src/GridSim.Core/Estimation/WlsStep.cs:IWlsStep (internal). The
WlsStateEstimator owns the loop (start point, trust-capped damping,
convergence, chi-square, bad-data removal); everything representation-dependent - how
H is held, how the gain matrix is formed and solved, how observability is
gated, how the residual covariance is computed - lives behind this seam.
Contract: Begin (bind to a fixed model/measurements/weights, do structural
work once per pass), GateObservability, TrySolve (one undamped
Gauss-Newton correction; false = singular gain), NormalizedResiduals.
| Implementation | Notes |
|---|---|
DenseWlsStep |
Dense normal equations G = H^TWH on ILinearSolver; at or below the sparse threshold. Proves observability by gain-matrix rank. |
SparseWlsStep |
CSR gain + Gilbert-Peierls LU; the gb-full-scale backend. Builds the gain's sparsity pattern, per-row slot maps and the RCM ordering once in Begin, then only refills values per iteration (SparseLu.Ordering + SparseLu.FactorPrepared). |
ConstrainedDenseWlsStep |
--method constrained: rows tagged virtual-zero-injection become hard equalities via a KKT (Hachtel-style) augmented system. Dense only; above the threshold it throws rather than silently degrading. |
The robust methods (Huber, LAV) are not separate steps: the estimator wraps
the same step backend in an IRLS reweighting outer loop
(src/GridSim.Core/Estimation/WlsStateEstimator.cs:WlsStateEstimator.Estimate).
IJacobianSink - one Jacobian evaluation path, two storages
src/GridSim.Core/Estimation/JacobianSinks.cs:IJacobianSink (internal).
MeasurementModel.EvaluateJacobian computes each measurement partial exactly
once and pushes it into a sink, so the dense and sparse estimator backends
share a single evaluation path (no copy-drift):
DenseJacobianSink- writes into a caller-cleared densedouble[m,s].SparseJacobianRows- collects per-measurement sparse rows (an injection row touches only its bus's electrical neighbourhood, a flow row four columns, a voltage row one).
Bridge sources - which reconstruction streams
Not a C# interface but a wire-level seam: every bridge stream carries a
source string, live | presolved | estimated
(src/GridSim.Core/Net/GridBridgeClient.cs:GridBridgeClient.StartAsync,
default presolved).
live- the server solves per tick.presolved- replays the demand-scaled pre-solved lake frame by frame.estimated- replays the measurement-conditioned WLS estimate lake; eachstateframe then carries a per-frame estimation-quality block (BridgeProtocol.EstimateBlock: objective J, chi-square verdict, redundancy, removals) so a view can show how well the frame fitted its telemetry.
The WPF SRC picker (src/GridSim.Wpf/MainWindow.xaml.cs, _bridgeSource,
launch arg --bridge-source) and the web's bridge:<case>@<source> case-id
suffix (src/GridSim.Web/Sessions/SessionManager.cs) select it; the server
echoes the choice back in StateMessage.Source.
Compute backends - CPU/CUDA/ROCm selection
src/GridSim.Cli/SolverBackend.cs:SolverBackend maps the runtime switch
(--cpu default, --gpu = AMD-first auto-detect ROCm -> OpenCL -> CUDA -> CPU,
or explicit --gpu=rocm|opencl|cuda) onto the dense and sparse linear-solver seams and
records the choice in RunInfo so reports are comparable. An unavailable
accelerator warns and falls back to CPU; availability probes
(GpuContext.IsAvailable, RocmContext.IsAvailable) never throw. See
04-sparse-and-gpu-numerics.md for why the
CPU backends are the production path.
Data flow: case -> solve -> frames -> views
- Case sources. A
GridModelarrives from one of: built-in factories (src/GridSim.Core/IO/IeeeCases.cs- case9 is the physics ground truth); the MATPOWER parser (src/GridSim.Core/IO/Matpower/MatpowerParser.cs) for any.mfile; a JSON case directory (src/GridSim.Core/IO/Json/JsonGridLoader.cs:JsonGridLoader.LoadDirectorysystem.json,buses.json,generation.jsonrequired;demand.json,transmission.json,transformer.json,onloadtapchanger.jsonoptional), which covers the GDA-materialised gb-full/gb-spine/zone cases; the GDA builder itself (src/GridSim.Gda/GdaModelBuilder.cs); or a bridgetopologymessage.
- Solve.
SolverSelector.Createpicks the engine;SteadyStateSolverruns it inside the OLTC loop and exposesRegulatedModel(the model whose tap ratios match the result - the loop solves internal clones, so consumers needing post-solve taps must read it, never the input model). The output is aPowerFlowResult. - Frames. Three producers assemble
SimFrame(Model, Result, Hour, Tick, FrequencyHz, GenerationMw, DemandMw, RocofHzPerS)(src/GridSim.Core/Simulation/LiveSim.cs:SimFrame):LiveSim(steps a day, re-dispatches, re-solves, integrates frequency);PresolvedReplay(src/GridSim.Core/Simulation/PresolvedReplay.cs- loads a run directory ofexports/*.jsonsolve-exports into a frame cursor, telemetry from the sidecar CSV where present); andGridBridgeClient(rebuilds frames from streamedstatemessages against the cached topology). - Views. The WPF shell binds tabs to the current frame
(
src/GridSim.Wpf/DashboardViewModel.cs); the web re-projects each frame into per-tab DTOs on the server (src/GridSim.Web/Projection/FrameProjector.cs) rather than serialising view models. All three frame producers feed the same views - replay and bridge modes are branches in the shell, not separate UIs.
Data flow: case -> solve -> cycle fan-out -> telemetry
The base Newton-Raphson solve is single-core, but everything a control room needs
after it converges (N-1 sweeps, harmonics, RoCoF screening, physics
conformance, the watch list) reads the same solved frame and is embarrassingly
parallel. The GridSim.Core.Scheduling namespace turns that into one base solve
plus a prioritised parallel fan-out per cycle:
- One base solve.
CycleRunnerdrivesLiveSim.Step(the single-core NR solve) then hands the immutableSimFrame(GridModel+PowerFlowResult, both immutable-by-convention, so sharing it to every job is a reference, not a deep copy - jobs treat the baredouble[]arrays read-only) toCycleScheduler.RunCycle. - Fan-out. The frame's post-solve work is expressed as
AnalysisJobs - small units with a stableId, aJobPriority(High= must complete every cycle;Low= deferrable/best-effort) and aSeedCoreMscost seed.AnalysisJobLibrarywraps the existing analyses as jobs, nothing re-implemented (ContingencyAnalysis.ScreenBranchesParallelas a hot High set plus rotating Low branch-index shards,HarmonicPenetration.Solve,RocofScreening.Screen,PhysicsLaws.Audit,WatchList.Assess). - Budget. Currency is core-milliseconds:
Wworkers over awindowMswindow ownW x windowMsof compute (CycleBudget,CycleOptions.WindowMs/Workers).CycleSchedulerdrains High-before-Low over a worker pool (aParallel.Forover an atomic cursor; not re-entrant - cycles are sequential, the fan-out inside a cycle is parallel). High always completes and is never cancelled; Low is admitted only while budget remains and any Low not served keeps its place at the front of the rotating pool, so later cycles cover it (round-robin, no starvation).JobCostModelkeeps a per-job EWMA of measured core-ms feeding admission. - Telemetry.
CycleResultreports per-cycle base-solve ms, fan-out wall ms, High done/total, Low admitted/completed/deferred/cancelled, budget total/spent core-ms, utilization, backlog depth, N-1 shard coverage and worst contingency.
ResidentCycleService is the host-agnostic background driver both dashboards
wrap (so the engine and its telemetry are shared, with no WPF<->web copy-drift):
a UI hands each solved frame to Offer(frame, baseMs); it runs the scheduler off
the caller's thread, coalesces to the newest frame, publishes the latest
CycleResult plus rolling shard coverage, and raises Updated when a cycle
completes. A new CYCLE tab renders it in both shells - the Blazor web
dashboard (src/GridSim.Web/Components/Tabs/CycleTab.razor) and the WPF desktop
app (src/GridSim.Wpf/Views/CycleVm.cs + CycleView.xaml, refreshed on the
dispatcher from the Updated event) - showing the High-tier-met/utilization
hero, a budget-accounting card and a High-first job table. The CLI cycle verb
is the soak driver (it exits non-zero if High is ever missed); Cycle: web
config gates it (Cycle:Enabled, default true for live sessions).
Data flow: measurements -> estimate -> lake -> bridge
- Measurements. A
gridsim-measurements/1JSON document is loaded bysrc/GridSim.Core/Estimation/MeasurementSet.cs:MeasurementSet.Load, which rejects unknown schemas and - like every dated input - checksTakenUtcagainst the historical horizon (src/GridSim.Core/Diagnostics/ForwardInferenceGuard.cs:ForwardInferenceGuard.HorizonHours, 169 h): the engine reconstructs the past only. Synthetic sets for testing come fromSyntheticMeasurements(estimate --synth). - Estimate. The
estimateverb builds aMeasurementModeland runsWlsStateEstimator(method/backends per theIWlsStepseam above), producing anEstimationResultwith residuals, chi-square verdict, observability report and removals. - Export.
EstimateExportwrites thegridsim-estimate-export/1document; with--export-solvethe CLI also writesestimated-solve-export.json- an ordinarygridsim-solve-export/1document of the estimated state, so all replay/bridge plumbing consumes estimated states unchanged. - Lake and bridge. The GDA pipeline batches those estimates into the
lake's
Derived/estimated_stateproduct; the bridge server streams them back assource=estimatedframes with the per-frameEstimateBlockquality attached, into the same views as any other stream.
Wire formats and the boundaries they cross
Every format is JSON unless noted. "Boundary" names where the bytes change hands.
| Schema / format | Written by | Read by | Boundary |
|---|---|---|---|
gridsim-solve-export/1 |
src/GridSim.Core/IO/SolveExport.cs (CLI --enableExport, transient, gda-replay, estimate --export-solve); the GDA python exporter |
src/GridSim.Core/IO/SolveImport.cs; PresolvedReplay; web CaseCatalog |
disk: CLI run directories <-> WPF/web replay; python <-> .NET |
Bridge messages (topology, state, result, info/error) |
GDA bridge server | src/GridSim.Core/IO/BridgeProtocol.cs via GridBridgeClient |
one WebSocket, python server <-> .NET client (WPF and web). The topology model block and the result payload reuse the solve-export shapes. |
gridsim-measurements/1 |
GDA measurement pipeline; SyntheticMeasurements |
MeasurementSet.Load (CLI estimate --measurements) |
disk: lake -> estimator |
gridsim-estimate-export/1 |
src/GridSim.Core/IO/EstimateExport.cs |
GDA build_estimated_state pipeline; validation tooling |
disk: estimator -> lake |
gridsim-frequency-export/1 |
src/GridSim.Core/IO/FrequencyExport.cs |
analysis tooling | disk |
gridsim-evidence/1 |
src/GridSim.Core/Evidence/EvidenceReport.cs:EvidenceReport.ToJson |
downstream consumers of the dossier | disk |
| Event catalogue JSON | curated files under data/events/ |
src/GridSim.Core/Events/EventCatalog.cs |
disk: recorded-event evidence -> replay gate |
MATPOWER .m |
external tools | MatpowerParser |
disk: MATPOWER ecosystem -> GridSim |
| JSON case directory | synth, GDA materialisation, hand-editing |
JsonGridLoader.LoadDirectory |
disk: case library -> any entry point |
REST /api/* + WebSocket /ws/sim |
src/GridSim.Web/Api/RestApi.cs, SimWebSocket.cs |
external consumers (the Blazor UI drives its SimSession in-process) |
HTTP/WebSocket out of GridSim.Web |
Deserialisation on every inbound path is case-insensitive and tolerant of
named float literals (Infinity generator limits) - SolveImport,
BridgeProtocol.JsonOptions and MeasurementSet all pin the same options, and
both the CLI and the web pin InvariantCulture process-wide so numbers never
change meaning with the host locale.
See also
- 02-data-model-and-per-unit.md - the record types every flow above carries.
- 03-power-flow.md - inside
IPowerFlowSolver. - 04-sparse-and-gpu-numerics.md - inside the linear-solver seams.
- ../manual/01-getting-started.md - building and running the projects.
- ../manual/18-extending-gridsim.md - adding an implementation behind a seam.
- ../manual/19-cli-reference.md - the verbs that drive these flows.