All notable changes to GridSim, newest first.
All notable changes to GridSim. Format based on Keep a Changelog. Ordered oldest -> newest (ascending); versions are intentionally undated.
src/GridSim.Core/Licensing/) — enforces the LICENSE.md tiers, keyed on
the only licensed quantity, buses per network model. A licence key is an SSH-key-style one-liner
(GSK1 <base64url>) carrying email, org, tier, bus-limit, issue/expiry dates and an id, signed with
ECDSA P-256 / SHA-256 and verified fully offline against a public key embedded in GridSim.Core
(no network call, no server). Signed, not encrypted — integrity/authenticity, not secrecy. Built on the
BCL System.Security.Cryptography only, keeping Core NuGet-free. Keys are delivered as .gridsim
files (a commented header + the key). LicenceGate resolves the active entitlement (precedence:
GRIDSIM_LICENCE env / Licence:Key config → installed %LOCALAPPDATA%\GridSim\licence.gridsim →
free Personal tier) and checks a model's BusCount, exempting Bundled Networks (the shipped
IEEE/CIGRE cases and synthetic/reduced GB networks). British spelling throughout (licence).licence verb (status / import / path) and a --importLicence <file.gridsim> global switch; every user model
load is routed through a single Gated choke point that hard-blocks an over-limit or expired network
with a clear message (src/GridSim.Cli/Gated.cs, LicenceCli.cs).LicenceGate DI singleton; CaseCatalog.LoadModel gates by CaseKind; the REST
API returns HTTP 403 on a licence denial; the dashboard shows the active tier in a footer badge.App.OnStartup resolves the licence; MainWindow gates model loads and shows
the tier in the title bar.D:\Work\GridSimLicensing) — a licenseforge console (keygen / issue /
verify) that links Core's licensing sources so the issuer and verifier can never drift; the private
signing key never enters this repo. Docs: manual ch. 23, technical ch. 22.LicensingTests (round-trip, tamper/foreign-signer rejection, expiry, bus-limit boundary,
unlimited tier, bundled exemption, resolution precedence).GridSim.Core.Estimation.PhaseAngleFeed) — turns per-bus
voltage phase-angle observations into slack-referenced VaDeg measurement rows the WLS estimator consumes.
Bus angles were the estimator's weakest-observed directions (pinned only indirectly through P rows); a
genuine per-bus angle adds a direct 1.0-pivot on that bus's angle column, converting a weakly/indirectly-
observed direction into a directly-observed one and sharpening the angle profile. PhaseAngleObs,
PhaseAngleOptions, Parse (robust BUS:ANGLE:UTC:FREQ — the ISO-8601 UTC keeps its own colons), and
ToMeasurements (frequency/RoCoF time-projection to a common instant, slack re-referencing via
differences at a datum bus, three modes AutoReference|AlreadyReferenced|Raw, and a CommonModeDeg
de-rotation input).φᵢ = θ_sys + δᵢ; re-referencing to the fixed slack gives δᵢ − δ_datum, so the system common-mode
θ_sys cancels exactly. Genuine per-bus differences improve the estimate; the frequency-integrated
system COI angle cannot (it is the same for every bus). Both are proven in tests:
Genuine_per_bus_angles_sharpen_the_estimate (angle RMSE cut >50%) and
Auto_reference_cancels_a_common_mode_system_angle.estimate --phaseangle BUS:ANGLE:UTC:FREQ … (src/GridSim.Cli/Program.cs, RunEstimate) — a
space-separated per-bus angle override (gathered before positional-arg detection), with
--phaseangle-ref auto|referenced|raw, --phaseangle-sigma, --phaseangle-at, and --phaseangle-from-gda.
Injected rows merge into the set before the prior/warm-start and obey the 169 h forward-inference horizon
like every dated input.PHASEANGLE_INFERED reader (src/GridSim.Gda/Neso/PhaseAngleInferedReader.cs) — reads the inferred
system COI angle (θ = θ₀ + 360·∫(f−50)dt, 1 Hz, national) scoped to the ISO week of an instant,
re-zeroed to 0° at the window start. Used correctly — as the absolute reference frame + true
instantaneous frequency/RoCoF and the common-mode to de-rotate an absolute per-bus feed
(--phaseangle-from-gda) — never faked as a per-bus measurement.NetworkModel/build_estimator_measurements.py gains a documented _phase_angle_rows
hook for a future genuine per-bus angle source (PMU/synchrophasor/boundary-anchored), returning empty
today with the physics spelled out; the --phaseangle override is the injection path meanwhile.PhaseAngleFeedTests (parsing incl. UTC colons, frequency projection, common-mode cancellation,
the estimator-improvement proof) and PhaseAngleInferedTests (re-zero, at-or-before lookup, common-mode
de-rotation).GridSim.Core.Scheduling) — the Newton-Raphson base solve is
inherently single-core, but everything downstream of it (N-1 sweeps, harmonics, RoCoF, physics conformance,
the watch list) reads the same solved state and is embarrassingly parallel. Each cycle does one
single-core base solve, then shares the immutable SimFrame to every downstream analysis and fans them out
across a worker pool. AnalysisJob/JobPriority split the work into High (must complete every cycle:
base solve, hot N-1, harmonics, watch list) and Low (deferrable/best-effort: rotating N-1 shards, RoCoF,
conformance). CycleScheduler runs an HP-before-LP worker-pool drain; CycleRunner drives LiveSim.Step
then the fan-out; AnalysisJobLibrary wraps the existing analyses as jobs (nothing re-implemented — sharing
the immutable frame is a reference, zero deep-copy).W workers and a windowMs window a cycle owns W × windowMs
of compute (28 cores × 1000 ms = 28,000 core-ms) — the window is the wall-clock deadline, the budget scales
with core count (CycleBudget, CycleOptions). High always completes and is never cancelled; Low is
admitted only while budget remains, runs under the deadline, and any Low not served this cycle keeps its
place at the FRONT of the rotating pool so a later cycle covers it — round-robin coverage, no starvation. A
per-job EWMA cost model (JobCostModel) feeds admission. CycleResult carries the per-cycle telemetry
(base-solve ms, fan-out wall ms, tier completion, utilization %, backlog depth, N-1 shard coverage, worst
contingency). Opt-in CycleOptions.PipelineBacklog reserves the pipelined-backlog optimisation (drain the
previous cycle's Low backlog on the cores idle during the serial base solve, marked FromStaleFrame).PowerFlowOptions.Cancellation (a CancellationToken,
default None) is checked at the top of every Newton-Raphson inner iteration (NewtonRaphsonPowerFlow,
SparseNewtonRaphsonPowerFlow), so a long deferrable solve aborts at the cycle deadline. default/None is
a guaranteed no-op — every existing solve is bit-for-bit unchanged (73 solver-regression tests green; an
explicit test pins exact Vm/VaDeg equality between the token and no-token paths).
ContingencyAnalysis.ScreenBranchesParallel gained an optional CancellationToken on its ParallelOptions.IBatchedLinearSolver (GridSim.Core.Abstractions) solves count independent
same-sized n×n systems in one call (per-system success mask; a singular system returns NaN, never fails
the batch). CpuBatchedLuSolver is the reference/fallback (parallel DenseLuSolver loop);
GpuBatchedDenseSolver (GridSim.Gpu) runs the same Gaussian-elimination-with-partial-pivoting kernels as
the single-solve path with a batch index threaded through, factoring the whole batch under one
GpuContext.Lock acquisition and one pair of host syncs — amortising launch + lock overhead across the
batch. Surfaced via SolverBackend.DenseBatch(); ROCm uses the CPU reference (no native batched entry yet).gridsim cycle <case|--dir d> [--window-ms 1000] [--workers N] [--cycles K] [--shards N] [--jobs …] [--hold-steady] [--pipeline] runs the real-time cycle and prints per-cycle base-solve/HP/LP/
utilization/backlog/coverage (exits non-zero if High is ever missed); gridsim batchsolve [--count N] [--n M] [--runs K] [--gpu] solves a batch on the selected backend, verifies against the CPU reference, and times it.Components/Tabs/CycleTab.razor) and the WPF
desktop app (Views/CycleVm.cs + CycleView.xaml, before ASSETS). Both consume the shared, host-agnostic
ResidentCycleService (runs the fan-out off the UI/gate thread and publishes telemetry) — so the engine and
its readout can't drift between the twins. Shows the High-tier guarantee + utilization, a budget-accounting
card, and a colour-coded job table; the WPF tab refreshes live on each cycle completion. A Cycle: config
section (Cycle:Enabled default true, WindowMs, Workers, Shards) gates and sizes it per host.tests/GridSim.Tests/Scheduling/: CycleSchedulerTests (budget = W×window, High-always,
rotation coverage, worker-count-independent results, no-frame-mutation, faulted-isolation),
SolverCancellationTests (bit-for-bit default token, cancelled-token abort), BatchedSolverTests (batched
== per-system LU, singular isolation, GPU-vs-CPU).GridSim.Web is now a true twin of GridSim.Wpf - the Blazor dashboard reaches feature parity with
(and in places exceeds) the desktop app, wearing the same cyan control-room theme (matched to
GridSim.Wpf/Theme.xaml) and rendering the same solve, tab-for-tab.wwwroot/css/app.css re-skinned to a tokenised control-room palette; reusable UI
primitives (Components/Ui/ - Card/StatRow/Bar/Tile); a scoped-CSS bundle linked in App.razor.Components/Visuals/{Gauge,Phasor,PqDiagram,FrequencyTrace}.razor + WebPalette (voltage/loading/
condition/stress ramps), plus the shared Overlays/Inspector.razor drill-down.wwwroot/js/map-interop.js + Components/Visuals/NetworkMap.razor): a full Canvas
port of FastSchematic - LOD + viewport culling at GB scale, tube-map styling, marching-dash flow, four
colour modes plus the wave ripple, hover/click detail, double-click drill, right-click trip, minimap,
keyboard shortcuts, and the coastline / DNO / GSP / geocoded-asset overlays (wwwroot/js/map-geo.js +
wwwroot/geo/, projected on the shared equirectangular GB box). Optional /geo static mapping in
Program.cs serves the git-ignored gda-out/geo GeoJSON when present.Components/Assets/) - landing
tiles → per-class fleet registers → detail pages (generators/interconnectors/transformers/circuits/
substations/tap-changers) with gauges/phasor/sparklines, cross-links, and the live-solved / estimated /
static-demo provenance tiers.Result.VaDeg); a real
HARMONICS tab surfacing the Core harmonics engine on demand (penetration, THD/indices, driving-point
resonance scan, IEEE-519 / G5-5 compliance) via SimSession.RunHarmonicsStudy (cached, bus-count gated);
THE MACHINE deepened to the eight-gauge instrument banks + torque-gap; NESO RP1 with purchased-time
and inertia gauges, correlated-trip credible loss and the struck-through 1,800 MW fiction; MECHANICAL
with the P-Q capability envelope + dual-limit RoCoF bars + machine inspector; GENERATION & INERTIA with
the live TSGB synthetic-inertia + FFR what-if (FrequencyTrace + TsgbSizer). The seven read-only tabs
(Operator/Market/Speed Domains/Inverter Fleet/Experimental/Conformance/SCADA) enriched to their WPF depth.dt, 1 ms → 20 min via SimSession.SetStepMinutes), and a network-map substation search
(gridsimMap.focusBus).GridSim.Core.Presentation
(CarbonFactors, EnergyEconomicsConstants, ScadaThresholds, CredibleLossBasis, MechanicalThresholds)
and consumed by both twins - so they can no longer diverge (the recurring "copy-drift disease").tests/GridSim.Tests - CorePresenterConstantsTests pins every
shared constant; WebProjectionAntiDriftTests proves the web projections consume the Core presenters (e.g.
FuelBreakdown.CarbonKgPerKwh equals the value recomputed from CarbonFactors; RP1 design basis ==
CredibleLossBasis.DesignBasisLossMw); WebProjectionRegressionTests guards the map/asset JS-interop path
against non-finite doubles, round-trips the NonFiniteDoubleConverter, and smoke-tests the harmonics study.Dynamics/Emt/ engine - the first time-domain solver of the instantaneous electrical network
(L·di/dt, C·dv/dt), a Dommel nodal companion-model method. Each reactive element becomes an equivalent
conductance in parallel with a history current source; the real nodal system G·v = i_history + i_sources
is solved each fixed timestep, with G factored once and back-substituted every step (constant between
switching events). Complements the phasor estate: ch. 5 electromechanical dynamics and ch. 19
harmonic/impedance work in phasors; EMT resolves the waveform.EmtCompanionStamp/Companion (trapezoidal + backward-Euler R-L and C rules, unified as
I_hist = HistCoeffV·v_prev + HistCoeffI·i_prev), EmtUnits (per-unit L=X/ω0, C=B/ω0; per-unit-only
EMT is first-class when BaseKv=0), EmtElement (SeriesBranch, ShuntElement, VoltageSource -
Thévenin-Norton or ideal Dirichlet node, CurrentSource, SwitchElement), EmtNetwork
(+FromGridModel), EmtEngine, EmtCase/EmtResult/EmtPoint.EmtEngine.InitializeRest seeds the rest state (i_L=0, v_C=0) so a source on at t=0 gives the correct
step response (avoiding the trapezoidal step-from-rest half-di/dt artifact).SwitchElement + SwitchEvent (a fault is a
bus-to-ground switch); switch branches stay in the matrix pattern so an event needs only a numeric
re-factor. On an event step the engine takes two backward-Euler dt/2 half-steps (CDA) to annihilate the
spurious trapezoidal Nyquist oscillation, then resumes trapezoidal (--no-cda / --rule be fallback).emt CLI verb: gridsim emt [case] --dt-us N --duration S [--source <bus>] [--amp] [--freq] [--probe v:<bus>,i:<f>-<t>] [--event fault:<bus>@<t>:<Rpu>] [--event clear:<bus>@<t>] [--rule] [--no-cda] [--out]. Writes a provenance-headed timeseries.csv, summary.md, run-metadata.json. DC settles to the
resistive solution; AC drives a sinusoid.Dynamics/ seam, does not engage the RestrictionReview gate, and labels outputs model trajectories.
Passive elements are grounded in the model's per-unit data; converter/machine internals (future) will be
CuratedPlaceholder [VERIFY].BergeronLine): a lossless distributed line (surge conductance at each
end + a delay-buffered history for the wave from the far end; EmtUnits.SurgeParams gives Zc=√(X/B),
τ=√(XB)/ω0) — reproduces reflections, matched-line absorption and propagation delay exactly.FromGridModel (IncludeLoads, default on): each bus's Pd/Qd becomes a
conductance + inductor/capacitor shunt, damping the otherwise near-lossless network. UseTravellingWaveLines
builds charged branches as lines.ThreePhaseEmtEngine): each bus is three phase nodes; a ThreePhaseBranch
stamps the 3×3 companion of its symmetric self/mutual R,L matrices, so inter-phase mutual coupling is
exact — real three-phase EMT, not three independent circuits. Ideal 120°-apart sources, per-phase
shunts/loads, and shunt faults (ThreePhaseFault, e.g. single-line-to-ground). Reuses the scalar nodal
reduction + SparseLu factor-once path.CuratedPlaceholder [VERIFY]).docs/technical/design/emt-time-domain.md, technical
manual ch. 21. 12 EMT tests: EmtRlcAnalyticTests (closed-form RLC step response, all three damping regimes,
to <1e-5; O(dt²); determinism), EmtSteadyStateTests (DC → resistive solution), EmtSwitchingTests
(TRV ≈ 2×V; CDA suppresses the Nyquist chatter), EmtLineTests (matched-line absorption; open-end doubling
at τ; reflection at 2τ), ThreePhaseEmtTests (balanced coupled line matches the positive-sequence phasor to
<5e-3, zero-sequence≈0; SLG fault collapses the faulted phase and creates a zero-sequence voltage).gridsim interpose verb - a live SCADA/RTU telemetry front-end that receives protocol frames,
validates and corrects them, decodes their object model into typed measurements, maps them through a
user-authored crosswalk, and emits them two ways: as gridsim-measurements/1 windows for the estimator
(serve) and as GDA-shaped, quarantined Parquet. --protocol selects the stack; --list-protocols
enumerates them. This turns the previously framing-only telemetry codecs into a working ingest path.IO/Telemetry/Iec104Asdu - the IEC 60870-5-101/104 ASDU application layer: the Data Unit Identifier
(type id, VSQ/SQ, cause of transmission, common address) and the monitor-direction information objects
for measured (normalised M_ME_NA_1, scaled M_ME_NB_1, short-float M_ME_NC_1), counter
(M_IT_NA_1) and single/double-point types, plus their CP56Time2a-timed siblings (34-37, 30). QDS/SIQ
quality octets and CP56Time2a decode land here.IO/Telemetry/ModbusFrame - the Modbus TCP MBAP framing and read-holding/input-registers (FC 3/4)
response PDU; registers are keyed by response position and scaled to engineering units by the crosswalk.Interpose/ subsystem (GridSim.Core, dependency-free): IRtuProtocol + RtuProtocolRegistry
(selectable stacks), RtuPoint/RtuPointType/RtuQuality, QualityFold (protocol quality bits ->
sigma multiplier or drop; sigma is the weight, there is no quality enum), TimeDecode
(CP56Time2a / DNP3 absolute-time / IEC 61850 UtcTime -> UTC), FrameCorrection (framing/integrity
gate + tallies), RtuPointMap (gridsim-rtu-pointmap/1 crosswalk: address -> bus/branch + kind + base +
scale + sigma + angle datum), RtuMeasurementSetMapper (guard-first, crosswalk-driven; mirrors
PmuMeasurementSetMapper), and InterposePipeline (frame -> correct -> decode -> window -> guarded set).
Active protocols: iec104, modbus. DNP3 / GOOSE / C37.118 remain framing-scaffolded.GridSim.Gda/Interpose/QuarantineLiveStore - writes GDA-shaped Parquet (hive-partitioned
Parquet/<source>/year=/week=, columns timestamp_utc/source/source_class/unit-named value cols/
sigma/provenance) so GDA tooling can read it, but firewalled from the public evidence lake: every
row is tagged source_class="telemetry", a _QUARANTINE.json sidecar marks the partition
evidential:false, and the store refuses in code to open at/below a DataSchema.json (the lake's master
contract). Never merged into GDA's MERGE-only unified tables.GridSim.Cli, the first socket-binding verbs): TcpDelimitedFrameTransport
(start-byte + length framing, IEC-104) and TcpMbapFrameTransport (Modbus MBAP), behind a
timeout-bounded read loop (RunLive idiom, --ticks headless cap).ForwardInferenceGuard deferred
(utcNow = DateTime.MaxValue, the replay-feeder escape); the consuming engine re-checks against the real
clock and skips still-fresh windows (rejected-fresh). Live telemetry becomes estimable only once it has
aged past the horizon - the no-forward-inference invariant is never bypassed for anything evidential.docs/rtu-interposer.md (overview), technical manual ch. 20, user manual
ch. 21. 27 new tests (Iec104AsduTests, ModbusFrameTests, RtuMeasurementSetMapperTests,
InterposePipelineTests, QuarantineLiveStoreTests).The code has undergone both HUMAN and AI Code reviews
Below are almost 100 fixed bugs, its important to understand these changes are not global, and were only present in certan combinations of events and verbs, tools than run only for themself never encountered the bugs which only showed in combinations.
Due to the number of changes, all previously generated results will need to be re-run and validated.
I expect no further breaking changes, this is the Version 1.0 Release Bundle
GdaModelBuilder: the NGED '-' null marker was treated as a join key, collapsing all 375
dash generation rows (13.7 GW) onto one arbitrary bus. Dash/blank primaries are now non-joinable and
counted as unmatched. National/NGED power flow around that bus changes materially.FuelMap: 'Pump Storage' (the NESO TEC spelling) now classifies as synchronous pumped hydro (H=3.0),
not zero-inertia storage - the 2.7 GW GB pumped-hydro fleet re-enters the inertia total (RoCoF/nadir,
synchronous/converter split and dispatch all shift).GdaModelBuilder: --gsp/--all-gsps carves now reconcile the generation-table GSP vocabulary with the
circuit table (GspKey), so a carved model keeps its embedded generation instead of dropping ~92%.GdaModelBuilder: synthesized 3-winding star nodes are no longer promoted to phantom slack buses / "Grid
infeed" generators.GdaPaths: demand table resolved by glob (*table-3*.csv) to match the shipped table-3-2025.csv;
undated files no longer excluded by their mirror-sync mtime under an as-of.NationalFleet: interconnector register is status-filtered and valued at current (not future "Total")
capacity - removes ~29 GW of phantom interconnectors from the connected fleet.UkpnSource: LTDS lookups accept the archive's ukpn- filename prefix (UKPN ingestion now works).LtdsReader/builder honour the demand Removed status; TEC capacity prefers connected over future
cumulative; zero-byte substations.csv degrades geolocation to a note instead of failing the build.EvidenceReport and ScenarioEngine (and the CLI fleet verb) now replay the whole recorded event
window in the counterfactual/fleet/risk-curve columns, not the 30 s default. The baseline was already
full-window; the truncation could falsely lift the counterfactual nadir above LFDD and unlock a bogus
avoided-cost figure. Avoided-cost and device-benefit outputs for late-staged events (e.g. gb-2019-08-09)
change.SparseNewtonRaphsonPowerFlow: Q-limit switching only after a converged sub-solve (matches the dense
twin bit-for-bit; no longer pins PV buses on transient garbage Q).ThreePhaseSweep: returns NonRadial for meshed/parallel input (added the missing n-1 branch-count
check) instead of silently solving a spanning tree.BackwardForwardSweep: weak-mesh compensation carries each link's complex tap/phase; NonRadial result
now reports a non-converged Status.Complex2x2.Inverse relative-singularity test (no 1.0 scale floor); GeneralEigen.Balance no longer
hangs on an Inf entry; BoundedSimplex prices free variables in both directions;
CoupledHarmonicPowerFlow iteration budget fixed (was halved).ProtectionRelays 51: IEC 60255 inverse-time curve uses M = I/pickup, not I/rated - trip times near
pickup change by up to ~23x (previously far too fast); cascade ordering and SOE reconciliation change.ProtectionController recloser reference-counts branch holds (recloses only when the last stage
restores); FaultAnalysis.FaultedFlows keeps index alignment with Branches for out-of-service branches;
SequenceFaultStudy.Thevenin rejects meshed/parallel models instead of understating fault current;
distance-relay (21) margin is negative on blocked/non-operating elements.FrequencyDynamics/AssetFrequencyDynamics: equilibrium start balances the forming damping Dv (forming
replays now start at the recorded frequency); the asset forming branch sub-steps like the lumped baseline
(bit-for-bit reduction restored).NoiseFloorAnalysis: interprets the estimator's absolute (half-normal) normalized residuals correctly -
well-calibrated classes no longer read as miscalibrated (and it no longer recommends shrinking every
correct sigma ~40%).MeasurementModel: out-of-service branch flow/current measurements predict zero; angle residuals
(VaDeg/IAngDeg) wrap to (-pi, pi]. WlsStateEstimator no longer misattributes a failed backfill pass's
residual arrays. EnsembleKalmanStateEstimator propagates members through the process transition;
LinearPmuStateEstimator/BranchCurrentDsse honour observability / InService.CimExporter round-trips loads, generators, base voltages and transformers (was nodes+lines only);
CimGridModelBridge keeps the slack when units share a node; MatpowerParser reads newline-separated
matrix rows; Iec104Frame preserves U-format function bits and rejects oversize ASDUs;
BridgeProtocol.PeekType is case-insensitive; C37118Decoder applies integer-format phasor scalings.--name=value flags are parsed (no more silent-default / infinite hang on uk --ticks=500);
--help works; unrecognized cases and failed loads fail loud (exit 2/1) instead of silently running
case9 or reporting success; list/enum/--dir/--spectra/--device/trip parse errors exit cleanly./ws/sim no longer double-disposes a session; a corrupt realtime JSON file is skipped not fatal; the
topology frame can't be evicted under backpressure; catalog id collisions disambiguated; bridge start
errors surface; map-interop.js listener leak fixed.publish.yml closes a tag/input script-injection hole; ci.yml always runs the required check.FrequencySensitivitySweep): reports the frequency nadir as an
uncertainty band by sweeping the swing constants +/- a fractional range - the per-parameter nadir swing
and the combined band over the joint range.SequenceOfEvents, AlarmEvent): reconciles a predicted
protection cascade against a timestamped alarm log, classifying each event as agreement, predicted-only,
observed-only, or out-of-order. Matching is by branch identity and relative order.ProtectionSettings.PerAsset, ProtectionContext.SettingsFor):
the branch relays (49/50/51/21) use a site-specific settings record for named assets and the generic
settings elsewhere; a null map leaves every existing assessment unchanged.WatchList): aggregates the precursor signals - branch overloads, bus voltages,
convergence health, and optional protection time-to-trip and boundary transfers - into one list ranked by
consequence x proximity, capped to the top N.FrequencyConstantsFitter, Numerics.NelderMead): fits
DampingPctPerHz (and optionally the responsive fraction and governor lag) to a measured loss-of-infeed
trace by least-squares over FrequencyDynamics.Simulate. Validated by parameter recovery on synthetic
traces; the shipped default is unchanged until a measured trace is supplied.NoiseFloorAnalysis): per source-tag x kind, compares the
class's actual noise to its declared sigma from the leverage-corrected residuals and suggests a rescale.IeeeCases.TwoAreaFourMachine, a symmetric two-area
four-machine system, with tests for inter-area vs local modal separation, coherency and synchronism.PowerTransformer / PowerTransformerEnd /
RatioTapChanger / PhaseTapChanger and folds each two-winding transformer into one tapped branch.ZipLoad on Bus): voltage-dependent constant-Z/I/P load in both Newton solvers,
applied as a deviation from nominal. The default is constant power, unchanged from before.RestorationSequencer): energises branches step by step, solving the
live subnetwork with reactive limits enforced, and flags over-voltage or reactive-limited steps.TapOptimizer): a coordinated tap schedule across the OLTC
transformers that flattens the voltage profile, respecting each tap-changer's band and step grid.A sweep of hot-path optimizations, each shipped with a bit-for-bit (or documented-tolerance) regression
pin and a gridsim bench --perf before/after (docs/perf-baseline.md). Every change preserves the
validated numerical output.
.Clone() of a freshly built matrix was pure
waste) and compute post-fault injections by walking the sparse Y cells in ascending column order instead
of the O(n^2) dense loop over a ~99.7%-zero matrix. Bit-identical; ~46% fewer allocations at n=300,
growing toward the 1500-bus cap (removes ~72 MB of transient dense builds).ISparseLinearSolver seam (incl. GPU) is preserved.ComplexCsr.Builder / RCM BFS: dropped per-row/per-node Comparer<int> closure allocations
(key-array sort; hoisted comparer) - byte-identical ordering.vmByBus refilled in place, branch-list
copy deferred until a tap actually moves. Bit-identical settled taps / voltages.DenseComplexLuSolver: the cross-validation seam now reports singular honestly (null on a
scale-invariant floor) instead of clamping to a fabricated answer; the fault-study entry point keeps its
historical clamping. Flat row-major storage for the dense oracle (bounds-check removal, bit-identical).AdmittanceStamp migration: routed all seven surviving inline pi-model stamp copies through the
centralised stamp (the drift-risk the stamp's doc claimed to have removed is now actually removed); the
physics-audit copies' independence is preserved by an independent stamp pin-test.IComplexLinearSolver
(IComplexLinearFactorization), a by-content RCM ordering cache reused across a scan's orders, and
parallel per-order solves (per-worker solver). Bit-identical; the penetration/impedance scan is ~4x
faster with ~83% fewer allocations.YBus.BuildSparse cells ==
YBus.Build cells bit-for-bit - which surfaced and fixed a latent signed-zero divergence;
SparseWlsStep structure reuse) and added the gridsim bench --perf harness.Full frequency-domain harmonic support - a complete new subsystem, grounded in physics with the one assumed input (source spectra) behind a swappable, provenance-tagged, measurement-ready seam. Suite 860 -> 863, 0-warning; validated to machine epsilon against an independent NumPy reference.
src/GridSim.Core/Harmonics/)Abstractions/IComplexLinearSolver, Numerics/ComplexLinearSolvers.cs):
three cross-validated implementations - DenseComplexLuSolver (oracle, lifted from the fault study),
RealExpandedComplexSolver (the scalable [G -B; B G] real 2n-expansion over the tuned real
SparseLu - the default, runs at gb-full scale), and SparseComplexBiCgStabSolver (native complex).
A single shared PowerFlow/AdmittanceStamp replaces the 4x-copied Y stamp (bit-identical).FrequencyDependentYBus - Y(h) from the same stamp (X->hX, B->hB, per-equipment skin R(h),
frequency-dependent shunts, optional load-shunt damping, and the distributed-parameter long-line
correction Z'=Z*sinh(gl)/gl). Reduces exactly to the fundamental YBus at h=1.HarmonicSource/HarmonicSpectrum, a typed HarmonicSourceLibrary
(6/12-pulse, PV, EV, arc-furnace, saturated-transformer - each CuratedPlaceholder/[VERIFY]), and
IO/HarmonicSourceSet (gridsim-harmonics/1, schema-version reject, TakenUtc -> forward-inference
guard). Assumed spectra load CuratedPlaceholder; measured load Recorded - the tag upgrades in place.HarmonicPenetration (solve Y(h)V=I per order), HarmonicImpedanceScan
(driving-point Z(h), resonance + participation), HarmonicSequence (h mod 3 -> +/-/zero),
HarmonicIndices (THD, K-factor, damping-sharpness), HarmonicCompliance (IEEE 519 + G5/5).ThreePhaseHarmonicPenetration on Distribution/MultiPhaseModel):
balanced triplen = zero-sequence, 5th = negative-sequence, unbalanced = all three.CoupledHarmonicPowerFlow + IHarmonicCouplingModel/
NortonCouplingModel) - the converter Norton admittance folded into Y (stable, damps), nonlinear
injection iterated; the coupling model is the one assumed part, provenance-tagged.HarmonicStateEstimator) - a linear complex WLS per order recovering
source injections from sparse HarmonicVoltageMeasurements; recovers to machine precision on exact data.Physics/Experimental/HarmonicKclCheck, law 12,
never in HardGateLaws).harmonics CLI verb (gridsim harmonics <case> [--spectra <gridsim-harmonics/1.json> | --synth-sources] [--enableExport[=path]]) + IO/HarmonicExport (gridsim-harmonic-export/1, with a
headline provenance tag) + HarmonicExportContractTests.GridSim.Web Components/Tabs/HarmonicsTab.razor +
GridSim.Wpf MainWindow.xaml).tools/crossval/crossval_harmonics.py - an independent NumPy harmonic-penetration reference;
GridSim's export agrees to machine epsilon (max |dVm| ~ 6e-16 pu).docs/technical/19-harmonics.md, manual chapter
docs/manual/20-running-a-harmonic-study.md.D:\Work\GDA\v1)NetworkModel/HarmonicDataSchema.json - the authoritative gridsim-harmonics/1 contract, in
GDA's frozen-contract home, mirrored as constants in schemas.py. The GDA->GridSim emit contract
(schemas.py CASE_FILES + gridsim_export.py) and Model.Branch/JsonGridLoader gain the
frequency-dependent line fields GDA already holds - lengthKm, equipment (OHL/cable), rPu0/xPu0.DataSchema.json/lake_paths.py) is the follow-up that lands
real spectra into the same seam.Starter network maps for grids beyond Great Britain, so getting started no longer means
hand-authoring a network - load one exactly like gb-full or an IEEE case.
data/intl/. Ready-made GridSim case dirs (the standard
system / buses / generation / demand / transmission / transformer JSON contract) for
grids other than GB, each with a MODEL_CARD.md and ATTRIBUTION.txt:
ireland-all-island - Ireland (EirGrid + SONI), 60 buses. Converges and passes the
physics-law gate (gridsim physics-check data/intl/ireland-all-island).iberia (Spain + Portugal, 1,497 buses), france (RTE, 1,976 buses) - transmission backbones.continental-europe - the CE synchronous area, 5,824 buses / ~8,000 branches, larger than
gb-full's 3,539.NetworkModel/international) from open, published transmission topology (the
PyPSA-Eur / GridKit ENTSO-E map extraction; the OSM power layer for finer networks), converted onto
the same case format via synthetic per-km impedances by voltage class and a per-island slack.data/intl/README.md documents loading and the per-map bus counts.Estimation / dynamics validation hardening plus one behavioural fix. Suite 823 -> 828, 0-warning.
Estimation/Security/FdiaScreen audited a
least-squares ESTIMATE against the hard physical-law gate at the machine-epsilon tolerance meant
for an exact power-flow solution (FeasibilityConstraints.AuditTolMva = 1e-4 MVA). A WLS/robust
fit reconciling noisy, over-determined measurements never satisfies KCL / complex-power balance to
machine precision - its inherent imbalance is of order the measurement noise (~2-3 MVA on a 100 MVA
IEEE-9 base) - so the physics gate fired on essentially every real estimate (~96% false-positive
rate on clean noisy data; only noiseless fixtures passed). Screen now takes an estimate-appropriate
physicsTolMva (default 0.1 * BaseMva), which sits well above a good fit's imbalance and far below
a gross residual-cloaked violation (a forced out-of-band voltage produces a >500 MVA imbalance).
A new test measures the false-positive rate over 100 noisy trials (<=2%); the attack-detection test
is unaffected (it is a >500 MVA / out-of-band violation).Diagnostics/RestrictionReview.AssertOutputHistorical enforces the review gate's
OutputPresentPastOnly certification at the emit boundary (not just as a static attestation):
an emitted estimate stamped newer than the 169 h historical horizon throws
RestrictionViolationException. Wired into RealtimeDseEngine and RealtimeEstimatorEngine at the
frame-emit point, so a recursive predict step (which integrates the model forward internally) can
never leak a future-dated state into an output frame. Three invariant tests pin it (present/past
permitted, future-dated rejected, boundary matches the input ForwardInferenceGuard horizon).FrequencyRocofFilterTests): on a noisy 50 Hz trace the
naive finite-difference RoCoF is swamped (RMS > 0.3 Hz/s) while the Kalman RoCoF state tracks the
true ramp (RMS < 0.1 Hz/s), beating differentiation by > 4x - pinning the Blue-Cut "estimate, don't
differentiate" rationale that was previously asserted only in prose.Dynamics/Distributed/DistributedConfig.DominanceThreshold,
docs/distributed-inertia.md, docs/technical/05-frequency-dynamics.md): the 0.10 default is
re-tagged as an engineering-judgement value (not theory-derived or fitted), now empirically anchored
init-settable for calibration.Full AMD Instinct / ROCm + OpenCL compute support. Three GPU backends now sit behind the same
ILinearSolver / ISparseLinearSolver seams and the SolverBackend factory - all verifiable
without AMD hardware. Suite 806 -> 823, 0-warning.
GpuContext generalised from CUDA-only to accelerator-type-keyed (CUDA + OpenCL) over one
shared ILGPU Context and lock; AcceleratorFor/IsAvailableFor/DescriptionFor(type) plus the
historical CUDA-typed properties as wrappers. OpenCL device selection requires FP64, proven by
an on-device smoke kernel (1.5+1.5==3.0) rather than a capability string - a device without
cl_khr_fp64 (e.g. an Intel iGPU) is cleanly rejected and skipped, not selected-then-faulted.GpuDenseLinearSolver / GpuSparseLinearSolver parameterised by AcceleratorType (default
CUDA) - the same hand-rolled ILGPU kernels run on OpenCL unchanged, portable across AMD/Intel/NVIDIA.SolverBackend: new Vendor.OpenCl, --gpu=opencl, and the AMD-first auto order
rocm -> opencl -> cuda -> cpu. gpucheck and the bench matrix gain OpenCL probes/engines.RocmInterop re-based onto the native hipSOLVER API (hipsolverDgetrf/Dgetrs with
explicit work/lwork + separate *_bufferSize) instead of the ambiguous cuSOLVER-compat
hipsolverDn* aliases; every former // VERIFY on ROCm marker resolved to a // verified vs ROCm 6.x header citation. The half-declared generic rocsparse_spmv scaffold removed.RocmContext: real device name via the struct-free hipDeviceGetName (avoids the
version-sensitive hipDeviceProp_t), an Instinct-aware FP64 note, and hipSetDevice selection.RocmSparseLinearSolver rewritten fully device-resident: BiCGSTAB vectors stay on the GPU;
vector algebra via rocBLAS-1 (ddot/daxpy/dscal/dcopy), the Jacobi apply as an on-device
Hadamard product via rocblas_ddgmm, and the SpMV via rocsparse_dcsrmv - no per-iteration
host round-trips. rocBLAS added as a fourth resolved native lib.GpuSolverSeamTests extended with five OpenCL cases (same kernels, OpenCL accelerator).RocmSolverSeamTests mirrors the seam facts for the native ROCm dense/sparse solvers
(CPU-fallback on a ROCm-less box; pins the hipSOLVER/rocSPARSE/rocBLAS kernels on an MI box).RocmInteropShapeTests - a reflection ABI guard pinning the enum char-codes (111/112/113,
rocBLAS side 141/142/143), the zero status sentinels, the explicit-workspace getrf/getrs shape,
and the absence of the compat hipsolverDn* / generic-spmv names.gpucheck + gpu-bench.sh); OpenCL and CUDA are exercised today. The FP64
gate was validated live on an Intel UHD OpenCL device (correctly rejected).docs/server-results/mi300/ placeholder added for the pending MI250/MI300X results tree; runbook
gains an AMD first-contact checklist and the OpenCL tier; benchmarking.md sec. 4 and the README
backend table updated.Real-Time State Estimator build-out - the estimation estate grows from a single static WLS
into a layered real-time / distribution / dynamic estimator, plus interop and CNI assurance.
Everything honours UnderstandingRestrictions.md: state estimation is a filter (present/past),
never forward inference; the 169 h horizon guard stands, and any Dynamics/->Estimation/
fusion passes the review gate. Suite 703 -> 806, 0-warning.
UnderstandingRestrictions.md made mechanical: Diagnostics/RestrictionReview gates any
estimation method that fuses a dynamics process model (fail-loud unless registered + certified
present/past-only); static estimators pass unconditionally. RestrictionInvariantTests asserts
the doc's five testable invariants next to the machine-epsilon checks; docs/restriction-review-log.md
mirrors the code table.Simulation/RealtimeEstimatorEngine - the first resident streaming estimator: pulls a dated
measurement window, guards it, warm-starts from the prior estimate, solves, emits - on
permitted-age / replayed data only (structurally incapable of nowcasting).Simulation/RealtimeDseEngine - the same, driving a recursive tracking filter.diag(G^-1) exposed as per-bus 1-sigma bounds (EstimationResult.StateVmSigma
/ StateVaSigmaDeg), validated to machine precision against a brute-force inverse.serve verb; a Web RealtimeEstimatorHostedService (BackgroundService) + a
ReplayDirectoryWindowFeed, config-gated (Realtime:Enabled) in Program.cs.Estimation/Dse/: IProcessModel + RandomWalkProcessModel; EKF (KalmanStateEstimator,
information-form), UKF (UnscentedKalmanStateEstimator), EnKF
(EnsembleKalmanStateEstimator), and a frequency/RoCoF Kalman filter (FrequencyRocofFilter)
that tracks RoCoF as a state rather than differentiating a PLL (the 2016 Blue Cut failure mode).
All gate-registered; each recovers a known trajectory.IMagPu (branch current magnitude) and IAngDeg (current-phasor
angle) - Jacobians verified against finite differences, truth recovered to machine precision.Numerics/HouseholderQr orthogonal least squares and a new EstimationMethod.Qr
(QrWlsStep) - solves without forming the ill-conditioned normal equations.EstimationMethod.SchweppeGm - Schweppe-Huber GM on the leverage-adjusted residual;
ProjectionStatistics (masking-immune leverage); HypothesisTestingBadData (HTI) for
interacting bad data the sequential-LNR loop mis-attributes.Distribution/: per-phase MultiPhaseModel + 3x3 PhaseImpedance (with inverse), a three-phase
backward/forward-sweep power flow (ThreePhaseSweep), SymmetricalComponents, node-voltage
ThreePhaseWlsEstimator (+ robust WLAV mode) and branch-current BranchCurrentDsse - which
cross-validate to machine precision. Sequence-network fault study (SequenceFaultStudy:
three-phase / SLG / LL / LLG), TransformerConnectionModel (Yg-Yg, delta-wye 30-degree shift),
nominal-voltage pseudo-measurements, and the voltage-unbalance factor.Synchrophasor/: an IEEE C37.118.2 decoder (CONFIG-2 parse, fixed & float DATA formats,
CRC-CCITT), a Phasor Data Concentrator (time-alignment + dropout flags), a guarded
PmuMeasurementSetMapper, a linear single-shot PMU state estimator
(Estimation/LinearPmuStateEstimator), and a hybrid multi-rate SCADA+PMU scheduler.Estimation/Security/FdiaScreen - the physics-conformance audit repurposed as a
residual-cloaked False-Data-Injection detector; Diagnostics/AuditLog (append-only SHA-256
hash chain) + EstimationAuditTrail. NCSC CAF mapping, NIS/OES gap assessment, and
RBAC/SBOM/secrets notes under docs/assurance/.IO/Cim/: a CIM / IEC 61970 (CGMES) RDF importer + exporter (information-preserving round-trip,
version-agnostic by local name), a CimGridModelBridge (nodes/lines/loads/generators -> solvable
GridModel, with ohm->per-unit via BaseVoltage). IO/Telemetry/: minimal frame codecs for
IEC 60870-5-104 (APCI I/S/U), DNP3 (data-link + CRC-16/DNP), and IEC 61850 GOOSE.Analysis/ContinuationPowerFlow (voltage-collapse loadability margin) and
Analysis/AcOptimalPowerFlow (loss-aware AC-OPF by successive DC-OPF).docs/technical/design/.Branch), a DSE cross-validation sidecar, sparse null-space
observability naming, and the ASN.1/MMS object models behind the protocol frame codecs.The Saturday bug hunt: a 155-agent adversarial review of the whole estate (25 subsystem/cross-cutting finders, every finding independently re-verified against the code) confirmed 47 GridSim defects - all fixed in this release. Full dossier with failure traces: the estate review report of 2026-07-18.
BuildY now skips out-of-service branches (the review's one critical). The sparse
path stamped every branch unconditionally, so on any model above the sparse threshold an opened
branch stayed electrically connected: the solve was that of the unbroken network while the branch's
LineFlow read 0 MW - recloser trips had no electrical effect and cascade re-solves above 3,000 buses
ran on the wrong network. One line, mirroring the dense YBus.Build; pinned by a regression test
that opens a case9 ring corridor and requires dense/sparse agreement on the weakened network.
(Sub-3,000-bus cascade results - including the GB-spine seven-trip demo - re-verified unchanged:
they ran on the dense path.)BackwardForwardSweep emits zero-flow placeholders for out-of-service branches so LineFlows stays
index-aligned with Branches (the NewtonShell contract that LiveSim, SCADA and the physics
audit all rely on positionally).PhysicsLaws.Audit skips out-of-service branches - it computed phantom current through open
branches and falsely failed the hard-gate laws after any recloser/cascade trip.ProtectionCascade.FindBranch resolves the tripped branch by the verdict's LocationId, not its
display name - on MATPOWER models (null branch names) two parallel circuits both render
"branch F-T" and the cascade always opened the first one regardless of which relay fired.ProtectionController no longer double-counts one infeed: a class with several trip parameters
builds one TripInfeed stage per parameter, and each latching independently added the same
CapacityMw to the deficit again.VaDeg residuals/predictions were scaled by BaseMva instead of rad->deg (the internal angle state
is radians) - angle rows in the estimate export were ~x100 garbage in "engineering units".FeasibleWlsStep honours EnforceGeneratorQ = false (the Q-capability active set ran
unconditionally while the sibling EnforceBranchRatings toggle was respected).JsonGridLoader throws on a demand row referencing a nonexistent bus (it was silently discarded -
MW vanished from the case - asymmetric with the generator check).GraphLayout.EnsureLayout and AssetEnvelope.WithOverrides no longer drop AssetDynamics /
Protections when rebuilding the model: every coordinate-less case lost its authored protection at
load, and every envelope sweep ran with all protective devices removed.GdaModelBuilder percent-convention shunt susceptance: admittance converts INVERSELY to impedance
across the MVA base (100/BaseMva, not BaseMva/100 - identical only at BaseMva = 100, which is how
it hid).GridBridgeClient refuses a bridge state frame with no t timestamp: it parsed to
DateTime.MinValue, which passes the forward-inference guard by construction - an undated frame
slid past the horizon defence entirely.scenario's headline counterfactual export and the whole fleet verb now apply the event's
recorded response services exactly as the matrix/evidence/replay paths do - artifacts of one run
no longer contradict each other (0 vs 1,226 MW of services, 50.00 vs 49.879 Hz start).replay-event exports the config the validator actually simulated with (ReplayResult gained
EffectiveConfig); a --trace path that does not exist fails loud instead of silently reporting
RMSE "n/a (data-gated)".--mnutes silently substituted the default and
"succeeded" with wrong physics); --as-of (ISO-8601 only), --dno, chain --mode and
transient --solver fail loud on bad values; gda failures exit 1 on stderr; transient exits 3
when any slice failed to converge; conformance exits 1 when any case fails the law gate.MarketVm; replay ramp figures on the real 30-minute settlement step (were ~30x overstated);
SCADA corridors by solved loading excluding transformers, P2 >90% / P3 watch >70%, 49.9-50.1 Hz
band, 275 kV EHV cutoff.ForwardInferenceGuard.Horizon minus a settlement margin - the hardcoded
now-2d fell inside the widened 7d+1h horizon, so the server rejected every web bridge start.NetworkMap pushes only nodes+edges through JS interop, and the NonFiniteDoubleConverter is
registered on every HTTP JSON path (/api/solve 500'd on inf Q-limits / +inf purchased time);
/ws/sim surfaces bridge faults as error frames and caps control frames at 16 KB.OperatorVm.Reset() on source switch (lake BM actions/constraints/reserve/warnings survived onto
local cases); ApplyBridgeStream drops the cached frames like LoadBridge (zone restarts resolved
new-topology ids against old data); RP1 clears its top-boundary narrative when the case has no
boundary model; generator/transformer fleet registers prune rows that rotate out of the top-200
(stale MW shown indefinitely); MECHANICAL ramps divide by the frame-clock delta instead of a
hardcoded 30 min; circuit detail Iin/Iout swap both the apparent-power side and the base kV on
reversed flow.GpuDenseLinearSolver mirrors the CPU solver's relative near-singular pivot floor on-device
(the kernel only flagged exact-zero pivots, returning garbage as success on ill-conditioned
systems), and GridSim.Gpu is referenced by the test project for the first time with hardware-free
seam tests (dense/sparse CPU agreement, near-singular rejection, dimension checks) - GPU/CPU drift
was structurally undetectable before.Suite 697 -> 703, 0-warning.
Passive protection-assessment overlay. A detailed relay-simulation layer that reads a SOLVED state and reports which protective devices would operate and why - without ever altering the solve - so a run can conclude, with a straight face, "on branch X, thermal-overload protection would likely operate: loaded to 123% of RateA; the inverse-time element trips in 3.3 s." It is the read-only, evidence-apparatus sibling of chapter 15's active protection controllers (which trip/shed/open during a simulation) and of the physics-conformance audit. Suite 684 -> 692, 0-warning, full solution.
Core/Protection/)ProtectionAssessment.Assess(model, result, settings?, trajectory?) builds a ProtectionContext -
every operating quantity computed ONCE from the solved state (per-bus voltage, per-branch apparent power /
current / loading / apparent impedance V^2/conj(S), the busbar KCL differential residual, system
frequency) - then runs each element in ProtectionRegistry.All as a pure comparison. Read-only: it never
calls the solver and never mutates the model or result (pinned by a test).RelayVerdict per operating location (+ the closest
non-operating one) with the quantity, pickup, signed margin, inverse-time trip, a plain-language basis,
and a confidence grade.ProtectionTrajectory); on a steady snapshot they are NotCheckable, not invented.Gs/Bs) current, or a
capacitor bank's draw reads as a false internal fault (found and fixed via the healthy-case test - bus 9
of IEEE-14 has a 19 MVAr shunt).gridsim protection-check <case> [--uv/--ov pu] [--uf/--of Hz] [--oc xIn] [--thermal frac] [--rocof Hz/s] - solve and print the operating elements, the not-evaluated elements, and the headline
"PROTECTION SIMULATION suggests N protection event(s)...". Flags override the generic pickups.PrintDetailed appends one passive line - "no relay would operate" or "N
event(s) suggested - e.g. on X, relay Y would likely operate (...)". The solve is untouched.docs/technical/17-protection-assessment.md (+ contents entry). ProtectionAssessmentTests (8).Core/Protection/FaultAnalysis.cs)If = V0_k/(Zth_kk + Zf), V_faulted = V0 - Zbus[:,k].If. The faulted branch currents
drive the overlay, so 87/21/27/50/51 operate on a REAL fault. Positive-sequence, so three-phase only (an
earth fault needs a sequence model and is reported not-representable); generator subtransient reactance is
synthesised (generic X"d) and provenance-noted; dense, fails loud above 1,500 buses.ProtectionContext gains an optional per-bus fault current so the busbar differential (87) sees a fault
within its zone; ProtectionAssessment.AssessThreePhaseFault applies + assesses in one call.gridsim fault <case> --bus <id> [--zf pu] - fault current (pu/kA), Thevenin Z, collapsed voltage,
and the operating relays. IEEE-14 bolted fault at bus 4: ~12.4 pu, bus to 0, operating 87 (at the fault
current), 21 Zone 2 on the faulted branches + Zone 3 backup, and 27 across the depressed network.Core/Protection/ProtectionCascade.cs)gridsim protection-check <case> --cascade prints the ordered trip sequence. On the
GB spine the single 123%-overload seeds a seven-trip cascade ending in a non-convergent split - the
cascade-to-blackout story, quantified.FaultAndCascadeTests (5): bolted fault collapses the bus + draws a large current, a fault impedance
limits it, the faulted state operates 87/21/27, an unstressed case does not cascade, and an
overloaded-branch case cascades opening that branch first.Suite 692 -> 697.
Experimental physics + network-analysis suite, and the 0.16.0 follow-ups closed. A gridsim experimental verb and matching UI panels that attribute the current solved frame - where each
generator's power flows, the network's electromechanical wave modes, its carbon intensity, its DC
sensitivities and its cheapest secure dispatch - alongside the physical-law conformance audit. Every
experimental model is provenance-tagged, off the validated solver path, and collapses to established
physics in its degenerate limit. This release also lands the four items 0.16.0 left as tracked
follow-ups: the Ring B branch-thermal and (already-shipped) angle-difference solve-time constraints,
a provably-optimal full-LP DC-OPF, the WPF experimental tab, and the real gb-full case now
loading end-to-end. Suite 653 -> 684, 0-warning, full solution.
Core/Analysis/)DistributionFactors - DC PTDF/LODF from a once-factored reduced susceptance B': PtdfColumn
(per-branch sensitivity to a bus injection), LodfColumn (post-outage redistribution), DcFlows, and
OutageIslands. The shared DC backbone for the tracing and OPF below. DistributionFactorsTests.DcOptimalPowerFlow - security-constrained economic dispatch. With line limits enforced it now
solves the exact bounded-variable LP min sum cost_i.Pg_i s.t. balance, Pmin<=Pg<=Pmax, and a
two-sided -RateA <= f0_l + PTDF.Pg <= RateA per rated line (a per-line slack in [0, 2.RateA] carries
the two-sided limit) - solved to a global optimum across all simultaneously-binding constraints by
the new BoundedSimplex, replacing the previous marginal one-line-at-a-time redispatch (correct for a
single binding line, but not provably optimal and prone to under-securing). Merit-order (cheapest-first)
when limits are off; fuel-defaulted linear costs. On the real GB spine: ~53 GW met at ~£257k/h, fully
secured. DcOptimalPowerFlowTests.GridSim.Core.Numerics.BoundedSimplex - a self-contained dense two-phase primal simplex for
min c.x s.t. Ax=b, lo<=x<=hi (any hi may be +inf). Variable bounds are handled natively (nonbasic
variables rest at a finite bound; the ratio test includes each entering variable's own opposite bound, so
a step can be a bound flip with no basis change); Phase I drives artificials to zero for a feasible basis
(rows pre-negated to an identity start), Phase II optimises the real objective, and the basis inverse is
carried by eta updates with periodic Gauss-Jordan refactorisation. Reports Optimal/Infeasible/
Unbounded/IterationLimit. BoundedSimplexTests (5) pin known optima, native bounds, and the
infeasible/unbounded verdicts.Core/Physics/Experimental/, gridsim experimental)PowerFlowTracing (Bialek proportional sharing) - "where did each generator's power go": an upstream
distribution matrix A_u factored once (SparseLu) to allocate every load's supply back to specific
generators (and every branch flow to its sources). Reports per-generator->load allocations and boundary
transfers; defined only on an acyclic flow graph (reported when a cycle is present).
PowerFlowTracingTests.CarbonTracing - a flow-based system CO2 intensity (gCO2/kWh) and per-load intensities from the
traced generation and per-generator fuel types; unweighted-mean fallback where a slack bus carries no
dispatched real power. On the GB spine ~181 gCO2/kWh. CarbonTracingTests.ContinuumWaveModel + WaveField - the grid as a damped-wave medium: the linearised swing is a
wave equation M d2(delta)/dt2 + D d(delta)/dt + K_L delta = P, whose checkable content is the modal
structure - the generalised eigenproblem K_L phi = w^2 M phi (via the self-contained SymmetricEigen
Jacobi solver) gives the inter-area electromechanical normal modes (~0.1-2 Hz) and the centre-of-inertia
null mode. Collapses to the SMIB analytic w_n = sqrt(w_s P_s / 2H) in its degenerate limit
(test-pinned against the same oracle ClassicalMultiMachineTests uses); needs per-machine inertia and
fails loud without it. WaveField adds a propagating-wave field for the map animation.
ContinuumWaveModelTests, WaveFieldTests.SymmetricEigen - a self-contained cyclic-Jacobi symmetric eigensolver (Eigenvalues,
EigenDecomposition with eigenvectors) backing the wave modes.gridsim experimental <reciprocity|wave|trace|carbon|boundaries>, gridsim ptdf/lodf, and the
full-LP gridsim dcopf verbs; usage header + docs/manual/14-validation-tools.md.EXPERIMENTAL tab (reciprocity / continuum wave + area split / top traced flows / carbon)
and a CONFORMANCE tab (per-law verdict pills + hard-gate verdict) over the live frame, plus a WAVE
overlay on the network map (map-interop.js requestAnimationFrame field animation).docs/technical/16-physics-conformance.md gains "Experimental models" and "On real GB data"
sections (boundary attribution, carbon, DC-OPF, lake conformance).FeasibleWlsStep, opt-in FeasibilityConstraints.EnforceBranchRatings,
default OFF): the nonlinear |S_end| <= RateA is enforced through its squared form S^2 = P^2 + Q^2 with
the row 2P.dP + 2Q.dQ from the new MeasurementModel.BranchFlowRow (the branch's own pi-model
stamps - no copy-drift). Only the more-loaded end is held (the two ends are near-collinear and would
singularise the KKT); a small hysteresis band holds the constraint at the boundary and its RHS is clamped
so it can only hold or pull an over-rating flow down, never push a slack branch up - which converges the
active set instead of limit-cycling against a strong flow measurement. OFF by default because a thermal
rating is operational (a real grid runs over it), unlike a generator's reactive capability; the overload
is always reported on LimitViolations (decoupled from the enforcement flag). FeasibleEstimationTests
gains a rating-binds test.gb-full (3,539 real transmission buses) now loads and audits end-to-end (energy/KCL/S=VI* ~4e-9,
~524 MW losses). A stale export carried two generator names with unescaped inner quotes that broke the
JSON loader; both the C# GdaModelWriter and the Python writer.py emit through a JSON serializer that
escapes correctly, so the file was repaired and a regeneration is clean.EXPERIMENTAL tab (Views/ExperimentalView + ExperimentalVm, before ASSETS) mirroring the Web
panel: the physical-law conformance table + hard-gate verdict, reciprocity, the dominant continuum-wave
mode and its area split, the top Bialek generator->load allocations, and the system carbon intensity -
all over the live frame, with the same large-case trace guard and graceful no-inertia degradation.Physics-constrained state estimation. The estimator could produce physically-impossible states: on real GB data the undamped Gauss-Newton solve "walked Vm through zero", and a VAr-blind measurement set drags Vm to collapse. Those were patched reactively (trust-region damping, a voltage-schedule prior, the zero-injection KKT). This release generalises the patches into one idea - the physical laws define the admissible region, and the estimator is constrained to live inside it - and delivers the audit engine, the constrained solve, an independent cross-language instrument, a corpus conformance sweep, docs, a UI panel and an experimental-law seam. Suite 630 -> 653, 0-warning, full solution.
Core/Physics/)PhysicsLaws.Audit re-derives the network laws from the raw branch data by an independent
per-branch pi-model path (accumulating each branch's S_from/S_to and the bus-shunt draw); it never
calls IPowerFlowSolver, so a pass genuinely corroborates that the solved voltages satisfy the network
equations rather than checking the solver against itself. Returns a PhysicsLawReport (a LawResult
per law + a Gate(hardLaws) verdict). Ten laws, honestly classified for a positive-sequence phasor
model:
1 conservation of energy, 2 Ohm, 3 Joule, 6 Kirchhoff-current, 8 complex power S=VI* are hard -
PhysicsCheckRegistry.HardGateLaws = {1,2,3,6,8} - the laws whose violation is a physically-impossible
state. 4 Faraday and 5 Ampere are constitutive (embodied in transformer ratios / series reactance;
no field to check), 7 Kirchhoff-voltage is structural (nodal potentials are single-valued), 9 AC
synchronism is a dynamics property (scored from a rotor-angle-spread / frequency-excursion margin),
10 Gauss is not modelled (field/geometry, outside a phasor model). Laws 1/6/8 are three views of the
single power-balance residual and are reported as such - no fabricated independence.gridsim physics-check <case> [--experimental] - audit one solved state; prints the per-law table
and the gate verdict (exit 1 on gate fail). PhysicsAuditTests (9) pin that a machine-epsilon solution
holds every hard law and that a negative-voltage state is caught by the gate.EstimationMethod.Feasible)Three nested rings around the existing Gauss-Newton loop; every ring is inert for the other methods, so
Wls/Huber/Lav/Constrained are byte-for-byte unchanged.
WlsStateEstimator.ApplyStep): extends the existing trust-region damping
with a fraction-to-boundary scale and a hard clamp so a voltage magnitude can never reach zero - the
direct fix for "walked Vm through zero". ApplyStep receives the feasibility spec only for the feasible
method (null otherwise, provably the old path).Estimation/FeasibleWlsStep): composes ConstrainedDenseWlsStep (reusing
its zero-injection equalities, observability and covariance) and, each iteration, appends a linearised
constraint row for every violated generator Q-capability bound Qg(x) in [Qmin,Qmax]. The
constraint's Jacobian row is the injection-Q partials the measurement model already emits (new
MeasurementModel.InjectionQRow, reusing the exact InjectionRow math - no copy-drift). The active set
re-detects each iteration and converges with the state. Load-bearing invariant: with no equality and
no active inequality the augmented system is the free normal equations, so the step matches
DenseWlsStep to solver tolerance - the feasible method costs nothing on an already-feasible state
(suite-pinned). Dense only, under the same 3,000-bus fail-loud guard as Constrained.ToPowerFlowResult - pure evaluation, no forward inference) and, on a hard-law breach, demoted to the new
loud terminal state EstimationStatus.Infeasible rather than shipped. Operational limits
(PhysicsFeasibility: generator Q-capability slack-exempt, thermal ratings, voltage band) are always
reported on EstimationResult.LimitViolations but demote only under
FeasibilityConstraints.RejectOnLimitViolation - a real grid legitimately runs branches over rating and
buses outside the nominal band.Estimation/FeasibilityConstraints, EstimationOptions.Feasibility, CLI
--method feasible (alias constrained-physics). FeasibleEstimationTests (7): recovers the true state
and passes the gate, keeps every voltage strictly positive, is bit-equivalent to Constrained on clean
data, demotes to Infeasible when a hard law fails, reports-but-doesn't-demote an operational overload
by default, and - the Ring-B binding proof - holds Qg at a tightened capability where plain WLS
reproduces the violation.tools/crossval/, IO/)physics block in gridsim-solve-export/1 and gridsim-estimate-export/1 (schema
unchanged; readers ignore unknown fields): the per-law report + gate verdict, so an estimated or
presolved state carries its own conformance evidence.tools/crossval/physics_audit.py - the primary instrument: rebuilds Ybus and recomputes laws
1/3/6/8 in NumPy (reusing residual_check.build_ybus), independent of the C# math, and checks the
exported gate verdict agrees with the independent recomputation. Verified agreeing to machine
epsilon on real exports. PhysicsExportContractTests (2) lock the block shape in CI.gridsim conformance sweeps the audit across the whole case corpus, writing a case x law matrix
(results/conformance/conformance.{md,csv} + run-metadata.json, RunInfo-stamped, git commit per CSV
row). Every converged benchmark case is physically admissible (17/17). The lake-scale sweep over
stored parquet estimated/presolved states is a follow-up (physics_audit.py already runs over exported
lake states).docs/technical/16-physics-conformance.md (+ contents entry) - the subsystem reference: the audit
engine, the ten-law table with the hard-gate set, the passivity split, the three-ring estimator, the
verbs and instruments, and the invariants a change must preserve. User-manual note in
docs/manual/14-validation-tools.md.ConformanceDto projected per frame via
PhysicsLaws.Audit; Components/Tabs/ConformanceTab.razor): a gate hero (ADMISSIBLE / IMPOSSIBLE) plus
per-law verdict pills for the current state. (The WPF equivalent is a noted optional follow-up.)Physics/Experimental/ReciprocityCheck (law 11, opt-in) following the PropagationDelay provenance
pattern: a passive network is reciprocal (Y = Y^T); phase-shifting transformers are the deliberate
non-reciprocal exception and are excluded, and an asymmetry over the rest is a model/build defect, not
an impossible state. OFF by default (physics-check --experimental), never in the hard gate, and
collapses to a trivial pass in its degenerate (phase-shifter-free) limit. ReciprocityCheckTests (5,
incl. the degenerate limit and an empty network).case145/case588/case9241pegase on Joule heating with large residuals.
They converge and conserve energy exactly, but carry a branch with negative real loss - their data
has a negative-resistance branch (a network-reduction/equivalencing artefact). A negative I^2 R is a
model passivity property, not a bad state, and the estimator cannot fix the model. So Joule (hard)
now checks only loss consistency (independent total dissipation == reported loss, shunt conductance kept
separate from branch I^2 R), and passivity moved to Ampere (law 5): reported when violated, but NOT
hard-gated. The gate then correctly passes these states while surfacing the non-passive branch; the
Python instrument was aligned to match, and the two gates now agree even on case145.{1,2,3,6,8} (state-level impossibilities); operational
preferences (voltage band, thermal ratings) are reported, not gated by default. Ring B enforces generator
Q-capability at solve time; branch-rating and angle-difference solve-time constraints, the WPF panel and a
lake-scale conformance sweep were tracked follow-ups here and land in 0.17.0 below.Instruction-set-level performance: runtime-detected, width-tiered SIMD across the dense numeric kernels, plus algorithmic wins on the sparse and transient paths. Target hardware is a 128-core AVX-512 EPYC, so the design detects the widest vector width at run time rather than compiling down to the build host (an AVX2 desktop). Suite -> 630, 0-warning, full solution.
Core/Numerics/SimdCapabilities): picks AVX-512 -> AVX2 -> SSE2 ->
scalar by Vector512/256/128.IsHardwareAccelerated, not by what the build machine happens to have -
the same binary runs 8-wide on the EPYC and 4-wide on the desktop. gridsim simdcheck reports the
active tier; every benchmark header and run-metadata.json gains a simd: line, so a result row
records which width executed it.Core/Numerics/SimdKernels): explicit Vector512/Vector256/Vector128
scaled-row axpy (SubtractScaledRow/AddScaledRow) and dot products, with bit-exact (separate
multiply-then-add, matching the scalar loop) and FMA (one rounding, faster, last-bit different)
variants. ref-origin cores drive both flat double[] and the row-major storage of double[,]
(ref m[0,0]), via safe LoadUnsafe/StoreUnsafe (no unsafe block, so the pedantic build stays
clean). Pinned by SimdKernelsTests (bit-exactness across the lane-boundary tail).SinCos (Cody-Waite pi/2 argument reduction + Cephes minimax polynomials,
generic Vector<double>): the transcendental no hardware provides as a vector op. Validated to
< 1e-12 vs Math.SinCos over the quadrant boundaries and thousands of random angles; approximate,
not bit-identical, so it drives only the opt-in transient path.FormNormalEquations), and the residual-covariance LU
(BadDataAnalysis) now dispatch through the shared kernel. The dense LU stays bit-exact (it is the
validated reference the residual/pandapower/GPU checks diff against); the gain formation uses FMA.Math.SinCos consolidation across the injection/Jacobian/transient trig loops (SparseNR, dense NR,
MeasurementModel, ClassicalMultiMachine, ClassicalInit) - one call instead of paired Cos+Sin.
Proven bit-identical (0 mismatches over 20M angles), so it halves the transcendental call count
while leaving every machine-epsilon cross-validation untouched.CpuSparseLuSolver caches the RCM ordering across Newton iterations. A power flow re-solves the
same-structured Jacobian each iteration; recomputing the fill-reducing reverse-Cuthill-McKee ordering
(a sort/BFS over the graph) every time is pure repeated structural work. RCM is a deterministic
function of the pattern, so reuse is bit-identical - a speed-only change. The pattern is compared by
content each call (cheap next to the ordering it saves). Sparse power flow: case1354 -19% (best-of-N
isolated A/B), case2869 -6%.CsrMatrix.Builder sorts by a key array, dropping the per-row Comparer<int>.Create delegate
allocation in every Jacobian rebuild (~thousands of allocations per iteration at PEGASE scale).ClassicalMultiMachine.Deriv vectorised (batch SinCos over the rotor-angle-difference row + two
vectorised dot products over the contiguous G/B rows) and its RK4/Deriv buffers hoisted out of the
step loop (was ~14 double[n] allocated per step, thousands of steps). ~1.85x on the classical
transient kernel (best-of-9 isolated A/B, AVX2), scaling with machine count.Deriv differ ~1e-13 from before, so their
cross-validation sidecar tolerances need a re-baseline; the bit-exact power-flow reference path is
unchanged, so power-flow crossval still holds at machine epsilon.GridSim.Core.Assets.Protection) - protective devices as a generalisation
of the inverter trip block, on the existing asset-dynamics seam. A protection is one or more definite-time
stages (monitored quantity → threshold + dwell → action): ProtectionElement / ProtectionEvaluator
(the extracted dwell/edge state machine) / ProtectionController (an IActiveAssetDynamics).
AssetAwareFrequencyIntegrator.SetNetworkState
(LiveSim); a guard test codifies that they are inert in the pure lumped COI model.Branch.InService flag (skipped by YBus/BFS and the
line-flow computation - bit-identical when all in service), IslandPartition (connected components →
per-island slack election for a generating island, blackout for a dead island with the lost
load/gen booked), and TopologyOps to apply recloser-opened branches + partition. LiveSim applies the
open branches before solving and feeds the blackout net-load back to the swing model; auto-reclose
restores after the delay.asset-classes.json (kind: "protection") + a protections.json
placement file ({name, class, monitorBus, capacityMw, targetBranch?}); a null-gated GridModel.Protections
bundle (reduction gate: absent/never-operating ⇒ byte-identical frequency AND power-flow). Per-parameter
[VERIFY] provenance reused; grounded thresholds Recorded/DerivedFromRecorded, ungrounded settings
(recloser/voltage/thermal timers) CuratedPlaceholder+[VERIFY].NetworkModel/build_protections.py emits the grounded protection classes + placement, wired
into assemble/run.py.trip --asset-dynamics now reports protections and the legacy DG LoM cascade (e.g. 800 MW tripped on
RoCoF). ~24 new tests (evaluator dwell/reclose, each protection firing, dynamic LFDD arrest, island
election/blackout, recloser open/reclose, reduction + power-flow bit-identity).[VERIFY]/generic elsewhere (recloser, voltage, thermal - no operational data exists in the lake).GridSim.Core.Assets) - an authorable, versioned asset-class format
(gridsim-asset-classes/1) describing how a device samples → computes → actuates → protects, bound
to a bus and consumed by the swing solver as a new additive pAssetFleet term. Turns an inverter from
a passive H=0 generator into active feedback.
GridFollowingInverter): PLL-behind current source - transport-delayed local
(Hz, RoCoF, phase) sampling, deadbanded droop + df/dt legs, ramp-limited fast slew + first-order lag,
and RoCoF / vector-shift / LVRT protection trips whose lost dispatched Pg becomes a deficit
(mirrors RocofScreening) - the 2019-08-09 Loss-of-Mains feedback.GridFormingInverter): synthetic inertia as a power injection ∝ df/dt, lowering
initial RoCoF without touching E (intrinsic-vs-synthetic separation preserved).TransformerDynamics (mechanically-delayed OLTC + thermal derating) and
LineThermalDynamics (IEEE-738 single time constant) on the steady-state tick - PlantStateUpdate,
never a swing-power term. PassivePlantFleet.ApplyTo overrides tap ratio / derates rating.asset-classes.json + asset-bindings.json per case; inline dynamicsClass on a
generator/branch overrides a bind-by-technology rule; unknown class fails loud. Per-parameter
provenance with the [VERIFY] discipline enforced at load (any CuratedPlaceholder must carry the
token).AssetEnvelope.Sweep): corner sweep over the unmeasurable [VERIFY] parameters
→ nadir/RoCoF band + tornado sensitivity attribution + a CascadeIsPossible flag; tagged
UnvalidatedAtScale. AssetValidation.Compare back-tests against a recorded trace (nadir/RoCoF/timing
gate).DomainFrequencySampler + AssetDynamicsFleet.AssignDomains bucket
per-domain power (single-domain → equals the lumped net); the swing forcing stays on the validated
COI path (per-domain local response awaits the 4-node Gridradar PMU back-test).DynamicsConfig.EnableAssetDynamics); CLI --asset-dynamics
(trip); an empty/passive fleet appends 0.0 last so the trajectory is bit-identical to the
lumped baseline (suite-pinned).NetworkModel/build_asset_classes.py autogenerates the representative gb-full class
library with grounded per-fuel dispatchRampMwPerMin from Outputs/ramp_correlation.json; wired into the
assembler (assemble/run.py emits the two files after write_case, additive/never-fatal). Bindings use
the assembler's REAL type vocabulary (assemble/fuelmap.py): wind-onshore/wind-offshore (via the
new prefix wildcard), solar, storage, interconnector, tidal - validated to bind 1,995 real UK
inverter units on gb-full while grid/other/synchronous/pumped-storage stay passive. Now also emits
grounded passive-plant classes (transformer OLTC + cable thermal) with transformer/line bindings.docs/technical/13-asset-dynamics-data-grounding.md): 10 Recorded + 7 DerivedFromRecorded class
params. Grounded: modern/legacy RoCoF trip settings and the ALoMCP 500 ms definite-time delay
(rocofTripConfirmS, manual/54); transformer OLTC tap step ~1.25%/step and ±15% span (SCADA
tap_changers.parquet + LTDS); cable seasonal derate summer ≈ 0.89 × winter (LTDS circuit ratios);
per-fuel dispatch ramps. Honestly flagged [VERIFY]/swept where the lake has nothing: BESS storage
duration (capacityMwh), OLTC mechanical delay + thermal time constants, and the legacy-vs-upgraded DG
relay fraction (the RoCoF-cascade driver - hence the envelope sweep, not a point prediction). System
context grounded for validation: GB inertia 101-338 GVA·s (floor 107.5), flex ramp-absorption 0.40-0.55,
per-domain inertia shares (thermal 98.6% / hydro 1.4% / inverter 0%; converters ~57% of online MW), and
sub-cycle RoCoF uplift ~1.16× (max 1.35×) the 1 s COI value - real evidence that LoM relays act on a
local RoCoF above the COI. Elexon per-BMU dynamic params (materialised) ground synchronous ramp/headroom
(CCGT ~15 MW/min, headroom ~0.40) for a future synchronous-machine class.GbResponseServices - the DC/DM/DR arrest layer at real NESO EAC-cleared held volumes (DC ≈ 950 MW,
DM ≈ 520 MW, DR ≈ 490 MW each direction, on their published delivery curves), so scenarios/envelopes can
use a data-grounded "typical GB" response layer instead of assumed volumes.localRocofUpliftFactor - LoM relays trip on the sub-cycle LOCAL RoCoF, grounded ~1.16× the 1 s COI
value (Derived/subcycle_period_features). New per-class param (default 1.0, reduction-gate-safe; GDA
emits 1.16, DerivedFromRecorded) uplifting the RoCoF the trip relay sees (not the slower FFR).Asset2019EventTests): fed only the primary
Hornsea + Little Barford losses at the recorded 215 GVA·s, a legacy DG fleet trips on its own RoCoF
protection, unprompted; an ALoMCP fleet rides through. HONEST scope: the depth is not predicted - the
nadir swings ~2.7 Hz across plausible embedded volumes, so the test asserts the mechanism + that the
recorded 48.787 Hz lies WITHIN the envelope band, not a nadir match. Topology is not used (lumped model);
the scripted replay's agreement is replay of recorded inputs. Tagged UnvalidatedAtScale.docs/technical/13-asset-dynamics-data-grounding.md - the full provenance ledger (grounded vs
[VERIFY], every value with its source path); docs/OVERNIGHT-SESSION-SUMMARY.md - session handoff.* PREFIX wildcard ("wind*" → wind-onshore,
wind-offshore), exact-beats-wildcard, so fuel-vocabulary differences between cases (synthetic
onshore vs assembler wind-onshore) can't silently drop assets from the fleet.BlindPredictionTests,
docs/technical/14-cascade-validation-and-blind-prediction.md): the honest test of predictive power.
Findings, numbers as measured: (1) the cascade cannot be blind-validated on GB data — 9 Aug 2019 is the
only major GB embedded-generation cascade and ALoMCP eliminated the mechanism, so there is no held-out
cascade event (structural, not fixable); (2) the obvious candidate 2026-05-31 is disqualified (gradual
event, 19× step-physics-closure failure); (3) the base swing+response model reproduces 2019 by replay
(0.02 Hz) and forward-estimates 2023-12-22 to ~0.30 Hz too deep with shared unfitted constants. Verdict:
the cascade is a mechanism/envelope tool tagged UnvalidatedAtScale, NOT a validated predictor. The
earlier Asset2019EventTests was corrected from a loose nadir-match (false agreement) to a mechanism +
envelope-bracket assertion.AssetAwareFrequencyIntegrator passed clockS=0 every
substep), silently disabling detection latency, compute-latency arming and vector-shift in the live twin
(batch path was fine) - found by an adversarial review, now advances an accumulated clock; regression-tested.Reset() did not restore state of charge in either inverter controller - a reused fleet carried the
previous run's ending SoC; both now reset _soc to SocInit.ApplyTo (and would collide on a synthetic key).[n,n] G/B pair (~200 MB at gb-full's 3,539 buses) is replaced by neighbour-aligned per-bus
arrays + diagonal vectors, built once at construction in the SAME ascending-k order the dense
loops used - summation order unchanged, so the bitwise injection-agreement gate still passes.
The transient dense YBus.Build spike remains (a sparse YBus is a solver-wide change, tracked
in the bug).MeasurementKind.VaDeg, WS-E): degrees against the slack
reference, linear h - one constant Jacobian entry, both backends via the shared sink path. A
voltage-only set (Vm + VaDeg everywhere) is fully observable with zero power measurements
(suite-pinned, exact recovery); a slack-datum row is legal and uninformative. Production
Gridradar use stays data-gated (token + reference confirmation) -
docs/pmu-gridradar-design-note.md records the re-referencing rule (carry feed angle
DIFFERENCES anchored to the estimator's own datum-bus view; never solve a per-feed offset).--backfill-removals,
EstimationOptions.BackfillRemovedFromPrior): a removed measurement is replaced by a
pseudo-measurement evaluated at the start state (the --prior=solve solved prior), sigma
inflated 3x (floored at 5% of value), tagged source="removal-backfill-prior" and itself never
removable. Fixes the post-event runaway: on 9 Aug 2019 16:00/16:30 UTC, removing
Keith's sole informative P injection had let the under-constrained north drift to a B4 transfer
of +5.9 GW with a passing chi-square - a wrong-but-self-consistent fit. Backfilled, the flows stay in
the plausible band (B4 ~ +1.8 GW) and the genuinely LFDD-straddling settlement averages now
report chi-square FAIL honestly (J ~ 650, removal cap reached) instead. Real-power kinds only
(P injections/flows): reactive rows in the VAr-blind GB layer are themselves pseudos and the
solved prior embeds them, so backfilling a systematically-defective Q row (the LTDS-fold bus-147
family) re-injected its own defect and churned the removal loop to its cap on healthy
periods - found on the first full-week backfill, pinned by a regression test. The GDA
estimated-state driver opts in.StateMessage.Source (live|presolved|estimated) and an EstimateBlock (J, chi-square verdict +
threshold, dof, measurement count, removals, iterations - the per-frame honesty layer of a
measurement-fitted state); GridBridgeClient.EstimateReceived surfaces both per accepted tick
(contract-tested, additive: legacy servers -> nulls). WPF gains a bridge SRC picker
(PRESOLVED / ESTIMATED / LIVE) beside the zone/start pickers plus a live chi-square status pill
(ESTIMATED * <case> * chi-square PASS * J=... n=...), a --bridge-source launch arg, and gb-spine in the
zone list. Web: bridge:<case>@<source> case-id syntax threads the source through
SimSession.BridgeStartAsync, with an "Estimated bridge" card on the home screen.gb-2019-08-09 built-in sheds
its [VERIFY] placeholders: loss sequence, LFDD and outcome now come from the Ofgem
investigation report (paras 2.4.1-2.4.16) - 150 MW vector-shift DG + Hornsea 737 MW +
Little Barford ST 244 MW (not the placeholder 641 - the record says the ST unit was 244) inside
the first second, 350-430 MW RoCoF-protection DG, GT1a 210 MW at ~58 s, 200 MW f-sensitive DG at
49 Hz, LFDD 892 MW at ~76 s with ~550 MW concurrent DG loss, GT1b 187 MW at ~90 s. Operating
point from the measurement apparatus: NESO recorded outturn inertia 215 GVA.s + the WLS
estimator's transmission-net demand 19.6 GW. Baseline verdict: REPRODUCED (RoCoF -0.162 vs
-0.170 recorded, 5% err; nadir 48.76 vs 48.80 Hz, 0.04 Hz err) - counterfactuals unlocked with
sourced provenance end to end. Both placeholder variants FAILED (33%/27% RoCoF err).data/events/gb-2019-08-09-estimated.json - the measured-operating-point catalog emitted by
GDA NetworkModel/build_event_catalog.py (NESO recorded inertia; estimated demand; estimated
pre/post-event injections at the two event sites in the notes). Use via
replay-event gb-2019-08-09 --catalog data/events/gb-2019-08-09-estimated.json.ReplayValidator): the sim duration extends to
the last recorded event + 30 s (the 30 s default clipped the real event's ~76 s LFDD nadir -
a clipped replay reproduces a different event than the record describes).ForwardInferenceGuard.HorizonHours); boundary tests parameterize on the const; fixture stamps
and ~34 doc mentions updated. GDA's Scripts/horizon.py matches (7 d + 1 h) for everything
inference-facing, while raw ACQUISITION keeps its 25.2 h currency via the new ingest_cutoff() -
fetching public source data is not inference.IJacobianSink - dense array sink /
sparse row sink, no copy-drift), CSR gain + Gilbert-Peierls LU (SparseLu.Factor exposed
factor-once/solve-many, keeping the residual covariance exact at scale), shared trust-capped
steps. Sparse observability = island sweep + factorization singularity (rank -1 = not computed).
Dense<->sparse agree to 1e-8 with identical chi-square/bad-data verdicts (SparseEstimatorTests).MaxAngleStepRad 0.2 / MaxVmStepPu 0.1, whole-step scaling) - real
data exposed undamped Gauss-Newton walking voltages through zero. Machine-precision gates
unchanged.estimate verb: GDA/JSON case-directory loading, --warm-start (iterate from the base-case
solve), --export-solve (project x^ through ToPowerFlowResult into a gridsim-solve-export/1
document - the feed for the estimated-state lake).Core/Estimation/, manual Chapter 78) - the
estimator-defining layer gda-replay's demand replay never had. Measurement/MeasurementSet
(wire format gridsim-measurements/1: typed kinds Vm/PInj/QInj/PFlow/QFlow, sigmas,
crosswalk-friendly bus-id/branch-name addressing incl. the "#k" ordinal-circuit form for
unnamed parallels, horizon-guarded takenUtc, offender-naming validation).
WlsStateEstimator: Gauss-Newton normal equations (H^TWH)deltax = H^TW(z-h) on the ILinearSolver
seam, flat/warm start, NewtonShell.BusSpec state conventions, non-finite guards, and
ToPowerFlowResult so estimates flow into replay/UI/export unchanged. h/H evaluate through the
solver's own ComputeInjections (opened to internal - one implementation, no copy-drift) and
the ComputeLineFlows pi-model stamps.Va@bus ...); undeterminable sets return Unobservable with the report,
never NaN.SyntheticMeasurements, seed 20260713,
Box-Muller, coverage strides, NoiseScale=0 exact mode).estimate CLI verb - full pipeline + estimate-export.json (gridsim-estimate-export/1,
contract-locked in CI), residuals.csv, run metadata; --synth persists the sampled set and
scores estimate-vs-truth.tools/crossval/crossval_estimation.py vs pandapower.estimation -
case14 2.3e-13 pu / 1.0e-11 deg, case30 5.6e-16 pu, case118 2.3e-13 pu on identical measurement
sets; --bad-data shows both tools remove exactly the same injected +20sigma gross error.
tools/crossval/se_residual_check.py - the independent NumPy instrument: residual honesty
(~1e-12), objective recomputation, first-order optimality via finite-difference Jacobian
(~1e-11), --leverage criticality arbitration.dgetrs order). The interleaved variant read wrong
multipliers under pivoting and mis-flagged a healthy case14 measurement as critical - caught by
se_residual_check.py --leverage on its first run, root-caused against scipy.lu_solve.The evidence engine (historical state estimator + grounded counterfactuals), pre-solved replay in the WPF app, pandapower cross-validation - then a full code-review remediation pass.
ForwardInferenceGuard (25 h horizon, wired into
every dated ingester), replay-event credibility gate, scenario counterfactual sweep, fleet
single-vs-fleet non-additivity, evidence dossier, gda-replay state estimator. Intrinsic (1/2)J*omega^2
inertia and FFR kept in separate labelled columns; counterfactuals gated behind a reproduced baseline.crossval - diff the solver vs pandapower (case9/14/30/57/118 agree to machine epsilon; swing vs
scipy RK45 < 2e-4 Hz), with a CI-safe export-contract test.SolveImport + PresolvedReplay.Three parallel reviewers (Core / WPF / CLI+tests+CI) with every Critical/High re-verified against the working tree. Root cause -> symptom -> fix below.
fleet ran counterfactuals with no baseline gate.
Bug: replay-event/scenario/evidence all enforce "no counterfactual without a Reproduced
baseline", but RunFleet never called ReplayValidator - it took a recorded event, stripped the
recorded LFDD (ExogenousLossEvents()), applied a device budget and printed/exported nadir/LFDD claims
plus a fleet.csv dossier: the exact ungrounded-claim path the whole pipeline exists to prevent.
Fix: validate the device-free baseline at the top of RunFleet; if the verdict isn't Reproduced,
print GATE FAILED ... and exit 3 (same semantics as scenario's BaselineNotReproducedException).
Verified: fleet --event gb-2019-08-09 (placeholder -> Failed) now refuses; gb-largest-loss-1320
(Reproduced) still runs.FlagDouble/FlagInt parsed with the ambient culture and, on any failure, silently returned the
caller's default. Two compounding modes: on a comma-decimal locale --dt-ms 0.5 parsed "." as a group
separator -> 5.0 (a 10x coarser slice, no warning); and a typo like --trip 100O failed to parse and
became "no trip", so the run "succeeded" having simulated the wrong scenario. These flags feed the physics
(--trip/--dt-ms/--inertia/--voll/--tol/--device-ffr), so a wrong evidence dossier could ship silently.
Fix: parse with NumberStyles.Float/Integer + InvariantCulture; a present-but-unparseable flag now
prints error: <flag> expected ... and exits 2 (FailFlag<T>) rather than defaulting. ParseIntList also
made invariant to match ParseDoubleList.timeseries.csv, state-estimate.csv, matrix/risk-curve/
fleet/bench CSVs, trip export) format with the ambient culture, so a comma-decimal locale emits 1,2340
inside a comma-separated file -> columns shift and PresolvedReplay's invariant parser drops/mis-assigns
telemetry (frequency silently falls back to 50.0 Hz, RoCoF to 0). Every Core-side parser was already
invariant; only the CLI writers weren't.
Fix: pin the whole process at startup (DefaultThreadCurrentCulture = CurrentCulture = InvariantCulture)
Update computed dtHours = (f.Hour - _lastHour + 24) % 24; the % 24 wrap assumes time only
moves forward. Replay's < step, scrubber-drag-left and (rewind) restart move f.Hour backwards, so each
backward step banked ~24 h - delta of fabricated time - one scrub from t=5 h->0 h added 19 h of phantom
Hz*s, "clock error", "missed regen £" and BESS throughput. And no integrator (_accumHzS, _regenMwh,
_bessSoc, _bessMovedMwh, _lastHour, _freqHistory, _voltHistory) was ever reset - so a model
switch carried the previous run's accumulation and the tab showed fabricated £ until app restart.
Fix: (1) new MachineVm.Reset() clears every integrator + history (_lastTick = int.MinValue), wired
via DashboardViewModel.ResetTimeline() from SwitchTo/LoadReplay/Restart/play-from-end; (2) Update
treats a Tick decrease (Tick is monotonic in both live and replay) or the first frame as a discontinuity
-> dtHours = 0, so backward motion never integrates. ResetTimeline also clears the trip lamp and the
per-onset alarm de-dup.t >= unit.StaggerSeconds, i.e. stagger from absolute sim t=0. But both
built-in events place the first loss at t=1.0 s and the shipped default stagger was 0.5-1.0 s, so every
unit was already enabled before frequency left the deadband - 100 staggered units == one aggregate device,
and the "FFR is NON-additive at the nadir" comparison (the verb's entire point) silently reported
delta = 0.00 Hz. The unit test only passed because it used a 4.0 s stagger.
Fix: (a) each unit now arms the first time frequency leaves its own deadband and its output lags that
crossing by StaggerSeconds (a detection/dispatch latency), armAtS[u] = t_cross + StaggerSeconds; with
stagger 0 it arms on the crossing step, keeping N=1/stagger=0 byte-identical to FrequencyDynamics. Simulate. (b) default spread 0.5 s -> 10 s (CLI --stagger) and 1 s -> 10 s (EvidenceSpec.Default),
matching GB primary frequency response (fully delivered by 10 s; a heterogeneous FFR/DSR fleet spreads
across that window). Now non-vacuous - gb-largest-loss-1320: 692 MW online at nadir vs 897 MW (77%),
nadir 0.16 Hz deeper. New regression test places the loss after the whole stagger window (delta=0 under the
old anchoring, non-zero now).SolveExport crashed on Infinity, which the importer explicitly expects.
Bug: SolveExport.Opts had no NumberHandling, so serializing a non-finite double threw. Two live
triggers: PEGASE cases carry Inf gen limits (deliberately parsed by MatpowerParser), and a non-radial
BackwardForwardSweep returns MaxMismatch = +inf - a single non-converged BFS slice killed an entire
transient/gda-replay run at the export call. SolveImport already read these back, so the two sides
disagreed.
Fix: NumberHandling = AllowNamedFloatingPointLiterals on SolveExport.Opts (confirmed against a
case533mt_hi run whose BFS returns Infinity).DrawFlow reused one unfrozen StreamGeometry across all 12 bucket draws.
Bug: FastSchematic.DrawFlow cleared/reopened a single _flowGeometry field per loading bucket and
called DrawGeometry(pen[bkt], _flowGeometry). DrawGeometry does not snapshot an unfrozen
Freezable - retained render data holds the reference and is drawn after OnRender returns, by which point
the field holds only the last non-empty bucket's segments. So all 12 draws rendered the same segments
with 12 pens: the marching-flow overlay (the map's headline visual) was colour/coverage-wrong on any
network spanning >1 bucket.
Fix: build a fresh StreamGeometry per bucket, Freeze(), then draw (the idiom RebuildStatic already
uses); dead field removed. <=12 allocations/frame is trivial.UpdateBoundaries only added/updated and never cleared _boundaryByName/Boundaries. Replay
passes an empty boundary list (SolveExport carries no boundary data), but the panel only stays empty if
it was already empty - GB-model -> replay left the last live model's corridor flows frozen on screen,
indistinguishable from live data (same for GB -> case9).
Fix: seen-set prune like UpdateVoltageBands - an empty list now prunes every row, so replay's panel is
genuinely empty and a model switch drops the old corridors.-- token" or blindly as a[1]. So
replay-event --catalog cat.json gb-2019-08-09 picked "cat.json" (the flag's value) as the event;
transient --minutes 5 case14 checked only a[1] (=--minutes) and ran case9; nminus1-sweep --sample 100 case118 took --sample as the case. A wrong-case run that completes is worse than a crash
for a validation tool.
Fix: Positionals(a) returns tokens that are neither a --flag nor the value one consumes (boolean
flags are a.Removed first by convention, so every remaining flag takes a value); PositionalArg(a) is
the first free arg after the verb. Routed replay-event, crossval, transient, chain, stress and
nminus1-sweep through it. Verified the --catalog ... <id> ordering now reproduces the right event.gridsim evidnce --event ... (typo for evidence) launched an infinite-loop
dashboard that hangs headless scripts.
Fix: the switch maps uk/no-arg to the UK model and routes any other unrecognised token to
UnknownVerb (usage listing cases + verbs, exit 2); the no-arg live path is preserved. Verified
evidnce -> exit 2, no hang.RunPython did sequential synchronous ReadToEnd() on stdout then stderr. pandapower fills stderr
with chatter while case118/300 diffs fill stdout; if the stderr buffer fills while the parent blocks on
stdout, both sides wedge - a classic pipe-buffer deadlock.
Fix: start ReadToEndAsync() on both streams, WaitForExit(), then collect both - neither pipe can
back-pressure the other.crossval with no
Python/pandapower exited 0 (CI green while validating nothing); transient exited 0 even when every case
failed to load - a CI gate keying on status couldn't tell "passed" from "did nothing".
Fix: missing-required-arg paths (replay-event, scenario, evidence, gda-replay, crossval, fleet) exit 2;
transient counts cases that actually ran and exits 1 if none did; an env-gated crossval skip (no
interpreter / no pandapower) exits 77 - the conventional "skipped" code, distinct from 0 (agrees) and
3 (mismatch). Verified scenario -> 2, transient-all-fail -> 1.OnRender (driven by CompositionTarget.Rendering) called Animate(dt) unconditionally, which
advances the dash phase and InvalidateVisual()s a full re-render - even paused or parked on one replay
frame, where nothing moves. Each render does up to ~220 FormattedText allocations + per-ring new Pen.
Fix: guard the animate call with !_paused, so a paused/parked frame does no render work. (The heavier
FormattedText/pen caching the review also suggests is left as a separate perf item.)Bus.Name/Generator.Name.
Bug: the export didn't serialize bus/generator names, so a replayed synthetic-GB run lost all asset
identity - the ASSETS explorer showed "gen@412" instead of "Sizewell B".
Fix: add name to the exported bus/generator objects and to SolveImport's DTOs + record construction
(schema-additive; the importer already tolerated extra fields). New round-trip test asserts named
bus/generator survive. (OLTCs/boundaries still not carried - a documented remaining gap.)TripAtBus set the lamp, but the next Update recomputed status to "SYSTEM NORMAL" because
FreqLocked (the flag meant to let a trip own the lamp) was never set anywhere.
Fix: since TripInfeed drives the excursion through the live frames anyway, fold frequency deviation
into the status priority - |f-50| > 0.2 Hz (GB operational limit) -> " FREQUENCY EVENT" as the top lamp -
so it reflects the live frequency each tick and self-clears on recovery.CircuitFleetVm model-swap precedence bug.
Bug: the rebuild guard A || !ReferenceEquals(model) && oldBusCount != newBusCount parses as
A || (B && C); switching to a different model with the same bus count made B true / C false -> no
rebuild, so the register kept the old model's rows and a stale BranchIndex into the new model -> wrong
rows or a UI-thread ArgumentOutOfRangeException.
Fix: use ScadaVm's clone-stable identity key (Name + BusCount + BranchCount) - survives LiveSim's
per-tick cloning yet changes on any real swap, including a same-bus-count one.Medium: M1 (RoCoF gate compares recorded-initial vs simulated-peak), M2 (ActiveRocofLimitHzPerS mutable
global static / thread-safety), M3 (catalog-forgery: evidence writes artifacts even when the gate fails),
M4/M5 (GdaReplayLoop per-step SolveTrace cost; ReplayValidator RMSE upsamples the whole NESO CSV 50x),
M7 (explicit-Euler swing has no stability guard), M8 (PresolvedReplay.Load is synchronous on the UI
thread), M9 (TripAtBus races the worker Step), M10 (dead per-frame UpdateHeadlineFraming), M13-M15
(hidden-tab work / stale asset pages / picker desync on failed switch), M16/M17 (crossval tolerance vs the
"machine epsilon" claim; FFR-slope duplicated with no contract test), M19 (dependabot branch-protection),
M20 (dead data/events/gb-events.json), M21 (no CLI verb-layer tests) - plus the Lows. None are
correctness-critical to the evidence pipeline.
Rental-ready GPU benchmarking - turn a rented box into "run one script, collect results".
gridsim gpucheck - fail-fast preflight: reports each backend's device and does one tiny
isolated solve on it (CPU/CUDA/ROCm), flushing every step, so a rented box gives go/no-go in seconds
and a native P/Invoke crash leaves a precise trail rather than dying inside a long sweep.bench --note "<hw, $/hr>" - stamps the hardware + cost into the report header and run-metadata,
so CPU/CUDA/MI300 reports are self-describing and the comparison is cost-anchored.DllImportResolver - falls back to versioned .so names (librocsparse.so.1, ...) and
$ROCM_PATH/lib, so the backend loads on a runtime-only ROCm container that lacks the dev symlinks.tools/gpu-bench.sh one-shot runner (preflight -> matrix -> solve exports -> bundled tarball) and
docs/gpu-rental-runbook.md - the 3-phase CPU -> CUDA -> MI300 comparative-analysis runbook
(publish self-contained, preflight, run, compare), with the same-money-by-generation pairing
(A100<->MI250, H100<->MI300X) and the "do you even need a GPU for sparse power flow?" framing.RocmInterop) remains a scaffold with // VERIFY on ROCm flags - never run on
AMD hardware. gpucheck is the tool to validate/fix it on first contact without burning a sweep.Variable simulation time-step with live frequency dynamics. 347 tests total.
,/. change the
step, the speed button changes playback.FrequencyIntegrator): a stateful, sub-stepped centre-of-inertia
swing integrator - the running twin of FrequencyDynamics.Simulate - advanced at the step's dt, so
system frequency is now integrated physics (RoCoF/nadir/recovery) rather than a canned wander. Stable
from 1 ms to 20 min (internal sub-stepping). Ambient load jitter keeps the dial alive.LiveSim.TripInfeed): right-click a generator, or press T to trip the
largest infeed (the canonical worst-case N-1). The loss of infeed plays out through the live
integrator at the current dt and recovers over ~5 min as reserve picks up - replacing the previous
precomputed-trace playback on the main dial. SimFrame now carries RocofHzPerS.FrequencyIntegratorTests (4): holds nominal with no event, expected initial RoCoF on a loss,
matches the batch Simulate trajectory, and a 60 s step equals 3000x20 ms sub-steps (dt-invariant,
finite at a 20-min step). LiveSim trip test: a loss dips the frequency then it recovers.A full-featured benchmark suite and a complete solve export. 342 tests total.
gridsim bench): every case x every available engine - cpu-dense,
cpu-sparse, bfs (radial only) by default, plus cuda-* / rocm-* on bench --gpu when a
device answers (availability-aware; absent accelerators are labelled, not errored). One table per
engine + a casexengine mean-time comparison matrix; CSV gains an engine column. GPU engines run
serially and skip >3000-bus cases (consumer-GPU FP64 is slow). This replaces the old fixed
sparse+dense two-sweep with a solver-and-backend comparison across the whole archive.--enableExport[=path]: dump everything a solve computes to JSON - run metadata, full network
inputs, the admittance matrix (sparse triplets), the per-unit bus spec, every iteration's
mismatch/correction vectors and the Jacobian (dense, gated by size; --export-jacobian forces
it), Q-limit switching events, and the converged state with all line flows. Works for dense,
sparse and BFS (SolveTrace recorder on PowerFlowOptions.Trace; SolveExport serialiser; schema
gridsim-solve-export/1, documented in docs/solve-export.md).SolveExportTests (7): Y-bus/spec/iteration/Jacobian capture, size-gate + force, BFS empty-Y-bus,
JSON round-trip with all sections, determinism, and the no-perturbation guarantee.Bring every IEEE/MATPOWER test case into the live dashboard - selectable, self-solving, and drawn as a proper topology. 335 tests total.
.m cases now start and keep re-solving a
steady state (hold their own loads, warm-started each tick) instead of the GB day/night curve
(LiveSim(holdSteady)). Launch arg (e.g. -- case9) boots straight onto a named case.SolverSelector
(Auto/dense-NR/sparse-NR/backward-forward-sweep) + a SOLVER dropdown and an active-engine readout.
Auto routes radial feeders -> sweep, large -> sparse, else dense; the CLI's LooksRadial now delegates
here so CLI and dashboard agree. Opt-in PV->PQ revert + slack-Q diagnostic (RevertQLimits, default
off; the physically-consistent Vm-vs-Vg revert condition with a one-revert anti-oscillation cap).GraphLayout): a deterministic, aspect-preserving
Fruchterman-Reingold layout with centring gravity for organic (non-boxy) meshed networks, a radial
tidy-tree layout for distribution feeders (trunk->laterals fan), a concentric-ring fallback for very
large graphs, and an iteration budget that scales down with size so big cases don't stall on load.TopologyTiers): a display tier
from graph structure (tree subtree-size / mesh betweenness centrality, source pinned to the backbone)
mapped to GB-like nominal kV, so the renderer's kV-driven roundel sizing/level-of-detail draws the
primary-HV -> mid -> distribution hierarchy. True kV is kept for tooltips - the tier is display-only.GraphLayout, TopologyTiers, SolverSelector, LiveSim steady mode, and
the Q-limit revert path.FastSchematic culled everything below the GB 200 kV EHV
level-of-detail floor, so sub-200 kV cases (case33bw 12.66 kV, case14/118 138 kV, cigre_mv 20 kV) showed
nothing. Small networks now draw in full at any zoom (DisplayKv-driven sizing, size-graded roundels).Remediation of an external code review. 279 tests total.
ReferenceEquals of a per-tick-cloned model,
so it rebuilt every 900 ms and wiped tap-events/alarm-dedup. Now keys on clone-stable identity.GdaPaths.Newest now chronological (date-parse + LastWriteTime), not lexicographic.TotalLossMw), per-fuel CO2 (was flat 0.38 on all
generation incl. nuclear/wind), dead MachineVm outputs bound, stripped glyphs restored.GridModel.Validate() (unique ids, one slack, resolvable endpoints, LV-side OLTC) - hard-throws
at the JSON/MATPOWER entry points, warn-only for GDA (keeps the future pipeline flowing).NewtonShell) - ~120 duplicated lines removed, bit-identical.LiveSim/SimFrame moved to Core + unit tests (was untestable in WPF).SteadyStateSolver marked/guarded non-thread-safe (re-entrancy check for parallel N-1); dropped the
product-surface AI-attribution footer; dark-theme contrast, over-frequency trace clamp, frozen
Sparkline pen, AssetHome O(n^2)->O(1) + visibility gate.MatpowerUnitNormalizer) - recognises distribution files stored in
ohms + kW (e.g. case33bw) and refers them to per-unit/MW; conservative 3-tell heuristic that
leaves correctly-per-unit cases (CIGRE MV) untouched..m files on load and sends strictly-radial networks to
the backward/forward-sweep solver automatically - gridsim case33bw.m now converges out of the box
(Vmin 0.913). Meshed cases (case9, ...) stay on Newton-Raphson.Vector<double> SIMD row-update (no FMA, so
bit-identical). 1.5-1.9x faster end-to-end (more on the isolated factorization).RocmContext, RocmDenseLinearSolver (hipSOLVER dense LU) and
RocmSparseLinearSolver (rocSPARSE / Jacobi-BiCGSTAB) in GridSim.Gpu, behind the same
ILinearSolver / ISparseLinearSolver seams as the CPU and CUDA backends.--gpu now auto-detects AMD-first (ROCm, then CUDA), with
explicit --gpu=rocm / --gpu=cuda; unavailable devices warn and fall back to CPU. The active
backend is stamped into every report.BackwardForwardSweep : IPowerFlowSolver) - the
"two-way" radial power flow that converges high-R/X distribution feeders the transmission
Newton-Raphson cannot. Detects radial topology, sweeps currents up then voltages down; carries line
charging, bus shunts and unity taps so it solves the same pi-model as the Newton solvers. Selectable
with --bfs. Validated: matches the dense solver on CIGRE MV, and reproduces the Baran & Wu 33-bus
published solution (min V ~ 0.9131, loss ~ 0.2 MW) once its raw ohm/kW data is referred to p.u.data/matpower/dist/ (case33bw/69/85/18/22); note case33bw
ships in raw units (ohms + kW), not MATPOWER p.u./MW.sqrt(...), * / + -, parentheses,
pi) in data matrices - e.g. the 135/sqrt(3) base voltages in the ACTIVSg cases (case533mt_hi
/ _lo) that previously failed to load..m parser does not evaluate arithmetic expressions (e.g. 135/sqrt(3)), so a couple of
ACTIVSg cases fail to load.docs/: architecture & solver internals, CLI
reference, case libraries & MATPOWER parser, and benchmarking & GPU.--cpu / --gpu switch (SolverBackend factory) threaded through every verb; warns
and falls back to CPU when no usable device is present, and stamps the active backend into reports.GridSim.Gpu project (ILGPU) - GpuContext (thread-safe CUDA accelerator probe + device
lock), GpuDenseLinearSolver (GPU LU with partial pivoting), and GpuSparseLinearSolver
(GPU BiCGSTAB with Jacobi preconditioner, CPU fallback on stagnation).Abstractions.ISparseLinearSolver + CpuSparseLuSolver;
SparseNewtonRaphsonPowerFlow now takes the solver by injection (default CPU direct LU), mirroring
the existing dense ILinearSolver seam.tools/run-regime.sh - the full server test regime (bench -> sampled N-1 -> full PEGASE N-1 -> stress
-> warm-start chains -> tarball), with a managed-heap hard limit (catchable OOM instead of OS-kill)
and per-phase timeout guards. Self-contained linux-x64 publish so no SDK is needed on target.tools/fetch-cases.sh - fetches the full PGLib-OPF archive plus all MATPOWER cases (~161 systems,
3 -> ~78,000 buses), auto-discovered by the bench/sweep verbs.chain - consecutive warm-started solves (GridScenario.WithSolvedVoltages) in fixed, ramp,
and daily modes, tracking warm-start iteration counts and solution drift.stress - load-to-collapse sweep via GridScenario.AtLoad, reporting per-case max loadability
(the P-V nose point) with parallel load steps.nminus1-sweep - parallel full/sampled N-1 branch contingency screening
(ContingencyAnalysis.ScreenBranchesParallel, per-thread solvers), reporting security counts and
throughput (solves/s). --sample N bounds the largest systems (logged, never silent).OOM. Large dense solves are
memory-gated and ordered small->large.RunInfo): every report and a run-metadata.json sidecar are
stamped with machine spec, runtime, active backend, and the git commit captured at build time
(MSBuild target) - the baseline mechanism for tracking optimization gains across runs.bench parallelised across cases (--parallel N, default =
core count) with deterministic output ordering; added CSV output alongside the markdown table.docs/benchmark-results.md: dense and sparse agree on
iteration count and final mismatch across every case both can solve, up to the 9241-bus PEGASE
system - the trust signal for the sparse backend.data/matpower/README.md provenance + BSD/CC-BY licence notes for the committed cases.bench verb: a dense-vs-sparse convergence table (iterations, mismatch, timing) over the
built-in/library cases.data/matpower/**/*.m (ModelCatalog, MainWindow load path), so the
libraries appear in the desktop app.<file>.m and --mpc <file>, plus a built-in case
switch (case14 ... case300, case24/rts, cigre).MatpowerParserTests - tokenizer units, case9 parse-vs-hand-coded round-trip, and guarded
PEGASE/RTS-96/NESTA/CIGRE convergence checks.IeeeCasesTests now compile and pass. Corrected the AssertPublished helper to
re-anchor both solved and published angles to the slack bus (it previously only re-anchored the
published side, which failed cases whose slack sits at a non-zero reference angle, e.g. IEEE-118).GridSim.Gda/Csv/Csv.cs (UTF-8 BOM trim) and in the CLI replay gauge glyphs - both blocked
compilation of the test and CLI projects.data/matpower/benchmarks/cigre_mv.m, IeeeCases.CigreMv).data/matpower/: MATPOWER 8.0 lib/, PEGASE case1354/2869/9241,
PGLib RTS-96 (73-bus), and a NESTA-lineage subset. Provenance + licences recorded.IeeeCases factory methods for the standard systems - Case14/30/57/118/300/145 and
Case24Rts - sourced from embedded .m resources; Case9 retained as the hand-coded ground truth.Inf/-Inf generator limits, isolated (type-4) bus dropping with incident
element removal, out-of-service (status-0) skip, transformer detection by tap ratio, and
non-contiguous bus ids preserved in file order (PEGASE numbering).ParseWithDiagnostics returning notes (dropped/skipped elements, multi-slack)..m parser (GridSim.Core.IO.Matpower.MatpowerParser) - the single import path for
every standard test system. Parses mpc.baseMVA, mpc.bus, mpc.gen, mpc.branch into a
GridModel with exact per-unit values (no dispatch reinterpretation). Comment stripping and ...
line-continuation handling.