Changelog

What's new. Every release.

All notable changes to GridSim, newest first.

Changelog

All notable changes to GridSim. Format based on Keep a Changelog. Ordered oldest -> newest (ascending); versions are intentionally undated.


[1.3.1]

Added

  • Offline licence engine (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).
  • CLI 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).
  • Web enforcementLicenceGate 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.
  • Desktop enforcementApp.OnStartup resolves the licence; MainWindow gates model loads and shows the tier in the title bar.
  • Out-of-repo forge (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.
  • TestsLicensingTests (round-trip, tamper/foreign-signer rejection, expiry, bus-limit boundary, unlimited tier, bundled exemption, resolution precedence).

[1.5.0]

Added

  • Phase-angle feed into the state estimator (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).
  • Physics-accurate by construction (this is the point). A per-bus absolute angle is φᵢ = θ_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.
  • CLI 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.
  • GDA 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.
  • Honest GDA scaffoldNetworkModel/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.
  • TestsPhaseAngleFeedTests (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).

[1.4.0]

Added

  • Prioritised parallel analysis cycle (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).
  • Compute budget in core-milliseconds. With 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).
  • Preemptive cancellation through the solver corePowerFlowOptions.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.
  • Batched GPU dense solveIBatchedLinearSolver (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).
  • CLI verbsgridsim 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.
  • Control-room CYCLE tab in BOTH dashboards — the Blazor web (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.
  • Teststests/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).

[1.3.0]

Added - Web dashboard brought to full parity with the WPF control room

  • 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.
  • Design system: 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.
  • Visual component library (inline SVG, DOM-diffable, ports of the WPF owner-drawn visuals): Components/Visuals/{Gauge,Phasor,PqDiagram,FrequencyTrace}.razor + WebPalette (voltage/loading/ condition/stress ramps), plus the shared Overlays/Inspector.razor drill-down.
  • GB network map (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.
  • Assets explorer: the flat tab replaced by a navigable asset graph (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.
  • Tabs: a new HUMAN SCALE tab (torsion-bar rotor-lead / relatable units, from 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.
  • Shell: a header system-status lamp + active-solver readout, a transport step-size picker (variable live dt, 1 ms → 20 min via SimSession.SetStepMinutes), and a network-map substation search (gridsimMap.focusBus).
  • Copy-drift eliminated at the source: the presentation constants that used to be duplicated between the WPF view-models and the web projections (BESS sizing, regen price, per-fuel carbon factors, SCADA thresholds, the 3,600 MW / 1,800 MW credible-loss basis) are extracted into 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: a parity/regression suite in 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.
  • Docs: user manual ch. 4 ("The desktop app" → now covers the web dashboard twin), README control-room section.

[1.2.0]

Added - EMT (electromagnetic transient) time-domain solver

  • 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.
  • Types: 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).
  • Switching / faults with Critical Damping Adjustment: 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.
  • Governance: an EMT run is Solving (forward simulation about the model, not inference); it sits in the 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].
  • Bergeron travelling-wave lines (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.
  • Constant-impedance loads in 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.
  • Three-phase (abc) engine (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.
  • Scope now: single-phase companion engine, travelling-wave lines, constant-impedance loads, and three-phase abc with mutual coupling. The remaining future phase is converter/machine models (behind the current-source seam, CuratedPlaceholder [VERIFY]).
  • Docs: 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).

[1.1.0]

Added - RTU/SCADA interposer

  • 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.
  • Protocol object-model decoders on top of the existing framing codecs:
    • 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.
  • CLI transports (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).
  • Horizon handling: live windows are written with the 169 h 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: docs/rtu-interposer.md (overview), technical manual ch. 20, user manual ch. 21. 27 new tests (Iec104AsduTests, ModbusFrameTests, RtuMeasurementSetMapperTests, InterposePipelineTests, QuarantineLiveStoreTests).

[1.0.0] - BREAKING RELEASE

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

Changed - BREAKING (GDA / national model assembly)

  • CRITICAL 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.

Changed - BREAKING (evidence engine credibility gate)

  • 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.

Changed - BREAKING (power flow / numerics)

  • 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).

Changed - BREAKING (protection)

  • 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.

Changed - BREAKING (dynamics / estimation)

  • 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.

Changed - BREAKING (IO / protocols)

  • 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.

Changed (CLI / web / WPF / GPU / CI robustness)

  • CLI: --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.
  • Web: /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.
  • WPF: bridge/replay render is guarded (a bad frame no longer crashes the app); fleet marker batching works; UI-thread trips no longer race the solver; stale fleet/OLTC rows are pruned on model switch; the NESO RP1 trace isn't double-filled in bridge mode.
  • GPU: ROCm availability reflects the whole stack (hipsolver/rocsparse/rocblas); the CUDA driver probe resolves the real driver per OS.
  • CI: publish.yml closes a tag/input script-injection hole; ci.yml always runs the required check.

[0.25.0]

Added - operations and analysis tooling

  • Frequency sensitivity sweep (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.
  • Sequence-of-events cross-reference (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.
  • Opt-in site-specific relay layer (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.
  • Ranked watch-list (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.

[0.24.0]

Added - calibration tooling and model fidelity

  • Frequency nadir/recovery fitter (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.
  • Measurement noise-floor characterisation (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.
  • Intermediate-N transient validation: IeeeCases.TwoAreaFourMachine, a symmetric two-area four-machine system, with tests for inter-area vs local modal separation, coherency and synchronism.
  • CGMES transformers: the CIM importer parses PowerTransformer / PowerTransformerEnd / RatioTapChanger / PhaseTapChanger and folds each two-winding transformer into one tapped branch.
  • ZIP load model (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.
  • Sparse observability diagnosis: within a state-size cap the sparse estimator names the free state directions and distinguishes a metering-deficient bus from an unmeasured island.
  • Restoration sequence checker (RestorationSequencer): energises branches step by step, solving the live subnetwork with reactive limits enforced, and flags over-voltage or reactive-limited steps.
  • Multi-transformer tap optimisation (TapOptimizer): a coordinated tap schedule across the OLTC transformers that flattens the voltage profile, respecting each tap-changer's band and step grid.

[0.23.0]

Changed - performance programme

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.

  • Fault study: build the network admittance once (the .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).
  • Sparse Newton-Raphson Jacobian: structural pattern + value-slot map built once per pass, values refilled in place each iteration (was a full triplet rebuild + per-row sort + reallocation every iteration). ~31-37% fewer per-solve allocations. The 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.
  • OLTC outer loop: transformer name->index map computed once, 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.
  • Harmonic solver reuse: a factor-once / multi-RHS handle on 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.
  • Safety net: pinned the two comment-only equivalence claims (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.

[0.22.0]

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.

Added - the harmonic engine (src/GridSim.Core/Harmonics/)

  • Complex-solver seam (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.
  • Source seam: 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.
  • Analysis: 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).
  • Three-phase / unbalanced (ThreePhaseHarmonicPenetration on Distribution/MultiPhaseModel): balanced triplen = zero-sequence, 5th = negative-sequence, unbalanced = all three.
  • Coupled harmonic power flow (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.
  • Harmonic state estimation (HarmonicStateEstimator) - a linear complex WLS per order recovering source injections from sparse HarmonicVoltageMeasurements; recovers to machine precision on exact data.
  • Opt-in per-order harmonic-KCL physics law (Physics/Experimental/HarmonicKclCheck, law 12, never in HardGateLaws).

Added - interfaces, docs, validation

  • 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.
  • HARMONICS dashboard tab in both twins (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 chapter docs/technical/19-harmonics.md, manual chapter docs/manual/20-running-a-harmonic-study.md.

Added - GDA (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.

Notes

  • Supraharmonics (2-150 kHz) are noted as a documented future extension. A measured harmonic-data acquirer + lake dataset (registered in DataSchema.json/lake_paths.py) is the follow-up that lands real spectra into the same seam.

[0.21.0]

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.

Added

  • International starter maps under 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.
  • Built by GDA (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.

Notes

  • These are indicative maps, not planning-grade models: line impedances are synthesised per voltage class (the open sources carry geography + voltage + length but no R/X) and demand is a synthetic snapshot - the topology and geography are real. Ireland (the flagship) fully converges; the larger European cases route to the sparse solver and their auto snapshot may not fully converge, but they load and render as real network maps. Licence: (c) OpenStreetMap contributors (ODbL) + PyPSA-Eur (CC-BY-4.0).

[0.20.1]

Estimation / dynamics validation hardening plus one behavioural fix. Suite 823 -> 828, 0-warning.

Fixed

  • FDIA screen false positives on noisy data. 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).

Added

  • Mechanical present/past output guard for Dynamic State Estimation. 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).
  • RoCoF-as-state vs differentiation test (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.

Changed

  • Dominance-threshold provenance (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
    • over 103,433 GB samples the dominance gap has P10 ~ 0.106, so 0.10 flags ~the bottom decile of gap conditions (fires ~9.4% of the time; 0.08 -> 7.5%, 0.12 -> 11.4%). init-settable for calibration.

[0.20.0]

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.

Added - OpenCL backend (via ILGPU, reusing the CUDA kernels)

  • 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.

Changed - ROCm/HIP native path hardened (ABI matched to ROCm 6.x)

  • 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.

Added - hardware-free tests

  • 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.

Notes

  • The native ROCm path is complete and header-matched but awaits its first runtime run on an AMD Instinct box (via 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.

[0.19.0]

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.

Added - restriction guardrails (governing)

  • 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.

Added - real-time engineering

  • 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.
  • Per-state covariance diag(G^-1) exposed as per-bus 1-sigma bounds (EstimationResult.StateVmSigma / StateVaSigmaDeg), validated to machine precision against a brute-force inverse.
  • CLI serve verb; a Web RealtimeEstimatorHostedService (BackgroundService) + a ReplayDirectoryWindowFeed, config-gated (Realtime:Enabled) in Program.cs.

Added - Dynamic State Estimation (gated)

  • 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.

Added - measurement model & numerics

  • New measurement kinds IMagPu (branch current magnitude) and IAngDeg (current-phasor angle) - Jacobians verified against finite differences, truth recovered to machine precision.
  • Correlated (full 2x2 block-R) weighting in the linear PMU estimator.
  • Numerics/HouseholderQr orthogonal least squares and a new EstimationMethod.Qr (QrWlsStep) - solves without forming the ill-conditioned normal equations.

Added - robust estimation & bad data

  • 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.

Added - distribution / unbalanced three-phase (DSSE)

  • 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.

Added - synchrophasor / PMU

  • 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.

Added - security & assurance (CNI)

  • 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/.

Added - standards & interop

  • 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.

Added - solver breadth

  • Analysis/ContinuationPowerFlow (voltage-collapse loadability margin) and Analysis/AcOptimalPowerFlow (loss-aware AC-OPF by successive DC-OPF).

Notes

  • Design specs for the larger subsystems live under docs/technical/design/.
  • Deferred (documented): earth faults wired into the positive-sequence protection layer (needs sequence fields on the core Branch), a DSE cross-validation sidecar, sparse null-space observability naming, and the ASN.1/MMS object models behind the protocol frame codecs.

[0.18.1]

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.

Fixed - solvers & physics core

  • Sparse Newton 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.
  • Newton Q-limit pinning now only happens after a converged sub-solve (MATPOWER convention): a transient Q from an unconverged pass could permanently convert a PV bus with no way back.
  • 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.

Fixed - estimation

  • 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).

Fixed - model integrity & IO

  • 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.

Fixed - CLI (evidence-pipeline consistency + fail-loud)

  • 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)".
  • Unknown flag names exit 2 at entry (a typo like --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.

Fixed - Web twin re-anchored on WPF (the recurring copy-drift disease)

  • RP1 purchased time from the live frequency (was fixed 50.0); credible loss / largest unit from synchronous units only; the OPERATOR tab no longer prefers the lake's 1,800 MW secured-event convention (Policy 0021: live-largest-now + the honest 3,600 MW basis); carbon factors copied from 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.
  • Bridge start derived from 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.

Fixed - WPF dashboards

  • 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.

Added - GPU seam under test

  • 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.


[0.18.0]

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.

Added - the engine (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).
  • Relay library (ANSI device numbers): 27/59 under/overvoltage, 32 reverse power, 37 undercurrent (an ABSOLUTE de-energized floor, not a fraction of rating - so normally lightly-loaded lines are not flagged), 49 thermal overload (first-order thermal replica -> operating time), 50 instantaneous + 51 IEC 60255 standard-inverse overcurrent (-> operating time), 21 distance (three forward zones scaled from the line's own impedance; load encroachment shows here), 81U/81O under/overfrequency, 81R RoCoF, 78 vector-shift / loss-of-mains, 87 busbar differential. Each yields a 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.
  • Honest scope, in the project's tradition: pickups are Grounded only for the statutory GB frequency band and the modern ALoMCP RoCoF limit, Generic elsewhere, and NotCheckable where a positive-sequence phasor model has nothing to evaluate - the earth-fault / negative-sequence elements (50N/51N/21N/46) report NotCheckable with what an unbalanced sequence-impedance model would need, rather than a fabricated verdict (the protection analogue of "Gauss's law is not modelled"). RoCoF/vector-shift need a dynamic run (a ProtectionTrajectory); on a steady snapshot they are NotCheckable, not invented.
  • Differential shunt fix: the busbar differential nets off the bus shunt (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).

Added - surfacing

  • 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.
  • End of a plain solve: 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.
  • On the real GB spine: two events (a thermal overload + its time-overcurrent on the branch at 123% of its 1,500 MVA rating) and nothing else - the noise-free, defensible statement the overlay is for.
  • docs/technical/17-protection-assessment.md (+ contents entry). ProtectionAssessmentTests (8).

Added - fault injection (Core/Protection/FaultAnalysis.cs)

  • Balanced three-phase fault by the classic Thevenin / bus-impedance superposition: from the prefault voltages it builds the fault-study admittance (network Ybus + loads as constant-impedance shunts + generators as their subtransient reactance to ground), solves one bus-impedance column for the fault bus, and superposes 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.

Added - cascade propagation (Core/Protection/ProtectionCascade.cs)

  • A read-only "what-if" that follows a protection event as it propagates: assess -> trip the first branch element (shortest inverse-time) -> open it in a COPY -> re-solve -> repeat, until nothing trips, the re-solve fails (a split / blackout - the honest terminal state), or a step cap. Never touches the caller's solve. 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.

[0.17.0]

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.

Added - network sensitivities and optimal dispatch (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.

Added - experimental models (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.

Added - surfacing

  • 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.
  • Web: an 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).

Added - 0.16.0 follow-ups closed

  • Ring B branch-thermal constraint (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.
  • WPF 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.

[0.16.0]

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.

Added - the feasibility oracle (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.

Added - the constrained estimator (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.

  • Ring A - interior guard (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).
  • Ring B - active-set KKT (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.
  • Ring C - acceptance gate: after convergence the state is audited (evaluated at x^ via 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.
  • Spec/plumbing: 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.

Added - independent instruments (tools/crossval/, IO/)

  • An additive 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.

Added - corpus conformance sweep

  • 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).

Added - documentation and UI

  • 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.
  • A CONFORMANCE tab in the Blazor control room (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.)

Added - experimental-law seam

  • 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).

Fixed - the Joule-vs-Ampere passivity split (found by the sweep)

  • The conformance sweep flagged 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.

Note - honest scope

  • "Physically possible" is defined by the hard laws {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.

[0.15.1]

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.

Added

  • Runtime SIMD tier detection (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.
  • Shared tiered kernels (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).
  • Vectorised batch 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.

Changed

  • Dense LU, estimator normal-equations gain (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.

Note

  • The width-tiering's headline payoff (8-wide vs 4-wide) is unobservable on the AVX2 dev box; it lands on the AVX-512 EPYC. The FMA gain path and the vectorised 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.

[0.15.0]

Added

  • Generic protections layer (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).
    • Frequency-domain (grounded by the GDA event-detector grades): RoCoF / Loss-of-Mains (legacy 0.125, modern 1.0 Hz/s + 500 ms delay, sub-cycle uplift), under/over-frequency, df/dt, and dynamic LFDD - an endogenous demand-shed that fires when the simulated frequency crosses 48.8 Hz (turning the previously-scripted LFDD into a closed-loop protection).
    • Voltage/thermal (Phase 2): under/over-voltage and thermal-overload trips, fed real per-bus voltage and per-branch loading from the solved model via AssetAwareFrequencyIntegrator.SetNetworkState (LiveSim); a guard test codifies that they are inert in the pure lumped COI model.
    • Full topology-aware recloser (Phase 3): a new 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.
    • JSON: protection classes in 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].
    • GDA: NetworkModel/build_protections.py emits the grounded protection classes + placement, wired into assemble/run.py.
    • CLI 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).

Note

  • Honest scope: real relay settings are unobtainable, so this is a generic protections model, not a protection simulator - grounded where the GDA event data supports it (RoCoF/LoM, LFDD, U/O-frequency) and [VERIFY]/generic elsewhere (recloser, voltage, thermal - no operational data exists in the lake).

[0.14.0]

Added

  • Per-asset dynamics framework (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.
    • Grid-following (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.
    • Grid-forming (GridFormingInverter): synthetic inertia as a power injection ∝ df/dt, lowering initial RoCoF without touching E (intrinsic-vs-synthetic separation preserved).
    • Passive plant: 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.
    • JSON: optional 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).
    • Envelope tooling (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).
    • Distributed attribution seam: 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).
    • Reduction gate: default OFF (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).
    • UI: an INVERTER FLEET tab (WPF + Blazor) before ASSETS - per-class online MW / SoC / trips, net response, LoM deficit, and the UnvalidatedAtScale/[VERIFY] banner.
  • GDA integration: 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.
  • Data-grounded parameters (mined from the GDA lake, real values not catalogue claims - see the new 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).
  • 2019-08-09 cascade MECHANISM reproduced dynamically (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.
  • Corrected the Gridradar phasor network from "5-node" to 4-node (london/buckingham/manchester/ strathclyde) across code, docs and plan; recorded the real 2019-08-09 anchor (nadir 48.787 Hz, RoCoF −0.151 Hz/s) in a validation test.

Changed

  • Asset binding matching gains a trailing-* 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.

Validation

  • Blind cross-event validation of the cascade (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.

Fixed

  • Live integrator froze the controller clock (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.
  • Passive plant skipped unnamed branches whose tap/derate updates could never be matched back in ApplyTo (and would collide on a synthetic key).

[0.13.4]

Changed

  • MeasurementModel admittance storage sparsified : the retained dense [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).

[0.13.3]

Added

  • PMU phase-angle measurement kind (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).

[0.13.2]

Added

  • Observability restoration on bad-data removal (A-6, --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.

[0.13.1]

Added

  • The estimated source is a first-class product surface . The bridge protocol gains 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.

[0.13.0]

Added

  • The 9 Aug 2019 event is sourced and REPRODUCED (D-3/D-4). The 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.

Fixed

  • Replay window covers the recorded sequence (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).
  • Inferred-inertia cross-check uses the RoCoF-window losses (<=5 s), not a staged sequence's multi-minute total: recorded RoCoF + first-second losses now imply 223.7 GVA.s against NESO's recorded 215 - a real two-source consistency check (4%), where the old total-loss version reported a meaningless 407 GVA.s.

[0.12.0]

Changed

  • Forward-inference horizon widened: 25 hours -> 169 hours (7 days + 1 hour) (owner decision, 2026-07-14). The horizon is a defence and widening it strengthens it twice over: (1) forward-inference defence in depth - a week-old state cannot be mistaken for or repurposed as a nowcast under any publication-lag or clock-skew argument, and the guard margin is no longer comparable to the slowest feed's lag; (2) reduced operational constraint space - a week-lagged state carries no operationally actionable information (unit commitment, outage windows, market positions and constraint patterns have all rolled over), so the system's output space sits wholly outside any operational or market-sensitive reading. The extra hour keeps a full settlement week strictly behind the boundary across DST transitions. Single knob (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.

[0.11.0]

Added

  • The estimator runs on the real GB grid (M2/M3). Sparse WLS backend above the 3,000-bus threshold: one Jacobian evaluation path for both backends (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).
  • Trust-region damping (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).
  • Real-data results (2026-05-01 08:30 UTC, GDA measurement pipeline: B1610 per-GSP generation via the BMU crosswalk + interconnector settlement flows + demand/voltage pseudos): gb-spine (316 buses) converged in 24 iterations, chi-square PASS (J=201 vs 358), one LTDS reactive outlier auto-removed (r^N=22.9); gb-full (3,539 buses, 7,076 states, 10,617 measurements) converged in 8 iterations (~107 s), chi-square PASS (J=394 vs 3,681). Manual sec. 78.9 documents the pipeline and both real-data failure modes (VAr-blind collapse -> voltage-schedule prior; undamped overshoot -> trust caps).
  • Tests: 536 -> 540.

[0.10.0]

Added

  • The measurement-based WLS state estimator (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.
  • Observability analysis - unmeasured-island sweep + gain-matrix pivot-rank check naming the free state directions (Va@bus ...); undeterminable sets return Unobservable with the report, never NaN.
  • chi-square bad-data detection - Wilson-Hilferty 95% quantile (dependency-free, table-checked), largest-normalized-residual identification/removal with factor-once LU residual covariance; critical measurements (sole observers, structurally undetectable errors) flagged and protected.
  • Deterministic synthetic measurement sampler (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.
  • Cross-validation: 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.
  • Tests: 499 -> 536 (exact-recovery machine-precision gate, FD-Jacobian policing, observability, bad-data incl. the critical-measurement undetectability trap, sampler determinism, export contract, covariance-vs-DenseLuSolver regression).

Fixed

  • Factor-once LU pivot order in the residual-covariance path: row interchanges are applied to the RHS before forward substitution (LAPACK 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.

[0.9.0]

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.

Added

  • No-forward-inference evidence engine (P0-P6): 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.
  • WPF pre-solved replay - load a run directory and step/scrub through exported frames; a global transport bar (Pause/Step/Speed/Colour/Fleet) on every tab. SolveImport + PresolvedReplay.

Fixed (code-review remediation, 2026-07-03)

Three parallel reviewers (Core / WPF / CLI+tests+CI) with every Critical/High re-verified against the working tree. Root cause -> symptom -> fix below.

Critical

  • C1 - 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.
  • C2 - culture-sensitive flag parsing silently substituted wrong physics values. Bug: 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.
  • C3 - CSV writers used CurrentCulture; the replay importer reads InvariantCulture. Bug: interpolated-string writers (transient 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)
    • the correct posture for a data tool, covering worker threads too - fixing every writer at once.
  • C4 - THE MACHINE's integrators corrupted by replay scrubbing/restart/switch, and never resettable. Bug: 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.

High

  • H1 - fleet stagger anchored to t=0 made the headline result exactly zero. Bug: a unit engaged when 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).
  • H2 - 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).
  • H3 - 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.
  • H4 - boundary rows never pruned -> replay/switch showed stale corridors as live. Bug: 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.
  • H5 - positional case/event args were order-fragile and silently ran the wrong case. Bug: verbs picked their positional as "the first non--- 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.
  • H6 - an unknown verb silently started the live UK simulator. Bug: the model-load fallthrough mapped any unrecognised first token to the UK model and ran the interactive live sim, so 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.
  • H7 - the pandapower subprocess read could deadlock. Bug: 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.
  • H8 - exit-code semantics made scripting gates unsound. Bug: verbs printed usage on a missing required arg and returned exit 0; 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.
  • H9 - the schematic re-rendered at 60 fps even while paused. Bug: 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.)

Medium

  • M6 - solve-export round-trip dropped 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.)
  • M11 - the "FREQUENCY EVENT" lamp was overwritten on the next tick. Bug: 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.
  • M12 - 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.
  • M18 - doc/test-count drift. Bug: the README and the project notes disagreed on the test count (393 vs 401 vs "expect 109 passing"). Fix: reconciled to the runner's authoritative 403 (401 prior + the two regression tests added this pass) across the docs.

Deferred (from the same review - not yet actioned)

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.

[0.8.0]

Rental-ready GPU benchmarking - turn a rented box into "run one script, collect results".

Added

  • 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.
  • ROCm 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.

Note

  • The ROCm interop (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.

[0.7.0]

Variable simulation time-step with live frequency dynamics. 347 tests total.

Added

  • Variable time-step (dt) + playback speed, decoupled. The dashboard gains a STEP control (1 ms -> 20 min sim-time per solve) separate from the playback speed (real-time cadence). Fine steps let you slow-mo a transient; coarse steps fast-forward the day. Keyboard: ,/. change the step, the speed button changes playback.
  • Live frequency dynamics (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.
  • Live generator trip (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.

Tests

  • 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.

[0.6.0]

A full-featured benchmark suite and a complete solve export. 342 tests total.

Added

  • Benchmark matrix (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).

Guarantees

  • The trace recorder is observational: a solve with a trace attached is bit-identical to one without (regression-tested, dense + sparse). Export JSON is deterministic given a deterministic solve.

Tests

  • 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.

[0.5.0]

Bring every IEEE/MATPOWER test case into the live dashboard - selectable, self-solving, and drawn as a proper topology. 335 tests total.

Added

  • Test cases live in the dashboard. The source picker's .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.
  • All three solvers wired to the frontend via a single-source-of-truth 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).
  • Auto tube-map layout for coordinate-less cases (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.
  • Synthesised voltage hierarchy for flat single-voltage imports (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.
  • Deterministic tests for GraphLayout, TopologyTiers, SolverSelector, LiveSim steady mode, and the Q-limit revert path.

Fixed

  • Imported cases drew a blank map. 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).

Changed

  • Roundel radius graded across voltage levels (400->11 px down to distribution) instead of a binary EHV/non-EHV size, so the hierarchy reads at a glance on both GB and imported maps.

[0.4.0]

Remediation of an external code review. 279 tests total.

Fixed

  • Case9 inertia corrected to the published WSCC / Anderson-Fouad dynamic data (H = 23.64/6.40/3.01 s on 247.5/192/128 MVA; total 7.47 GVA.s, was 1.62) + branch ratings; pinned by test. The ground-truth case's frequency/RoCoF screens are now real.
  • SCADA per-tick rebuild bug - the register keyed on 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.
  • Solve faults surfaced - a faulted async solve now drives the status lamp + alarm + log instead of silently freezing the dashboard.
  • Dispatcher multi-generator slack double-count fixed (GDA/synthetic-safe).
  • GdaPaths.Newest now chronological (date-parse + LastWriteTime), not lexicographic.
  • Unified losses (MACHINE now uses solved TotalLossMw), per-fuel CO2 (was flat 0.38 on all generation incl. nuclear/wind), dead MachineVm outputs bound, stripped glyphs restored.

Added

  • 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).
  • Configurable dual RoCoF threshold (legacy 0.125 / modern 1.0 Hz/s, active defaults to 1.0).
  • Inertia "vs secure minimum" headroom framing on the headline tiles; SIMULATED markers on invented panels/sparklines; Theme resources (track colour, font scale, gauge bands).
  • BFS exact at non-unity transformer taps (ideal-transformer current transform + |N|^2 shunt scaling).
  • Shared dense/sparse Newton shell (NewtonShell) - ~120 duplicated lines removed, bit-identical.
  • LiveSim/SimFrame moved to Core + unit tests (was untestable in WPF).
  • Frequency calibration harness vs the NESO trace + documented constant provenance (the bundled trace is ambient - a real trip trace is needed to pin governor/damping, flagged in-code).

Changed

  • 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.

[0.3.30]

Added

  • Weakly-meshed & PV support in the BFS solver - Shirmohammadi-style breakpoint/loop compensation (up to 6 loops) and voltage-controlled (PV) buses held to setpoint within Q-limits; densely meshed grids still decline. Radial results unchanged.
  • Raw-unit auto-detection (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.
  • Auto-routing: the CLI normalises raw .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.

Added - validation

  • +87 tests (238 total): solver invariants/property tests (power balance, dense==sparse, determinism, Q-limits, warm start), N-1 sequential-vs-parallel agreement, load-scaling, RoCoF round-trip, and MATPOWER stored-solution cross-checks.

[0.3.20]

Changed

  • GPU solvers de-stalled - the CUDA dense solver now does on-device pivot search + a deferred singularity flag, cutting host<->device syncs from ~5*n to 2; sparse BiCGSTAB dot-products moved to on-device reductions. ROCm dense/sparse hardened to match (correct hipSOLVER getrf/getrs buffer sizing, rocSPARSE SpMV analysis). Both keep the CPU fallback; compile-clean (hardware validation pending).

[0.3.10]

Changed

  • Sparse LU rewritten - Gilbert-Peierls left-looking factorization (flat CSC arrays, symbolic reach per column, RCM ordering, no per-call dictionaries). 6-7x faster on the hot linear-solve path (case1354 44->6.3 ms, case2869 96->16 ms/solve); results bit-for-bit identical.
  • Dense LU vectorized - flat row-major buffer + Vector<double> SIMD row-update (no FMA, so bit-identical). 1.5-1.9x faster end-to-end (more on the isolated factorization).

[0.3.0]

Added

  • AMD-first ROCm/HIP GPU backend - 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.
  • Vendor-aware backend switch: --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.

Notes

  • The ROCm interop is validated to compile and is structured for MI-series hardware; runtime validation on an AMD Instinct is the remaining step (the RTX A6000 finding is why AMD is first).

[0.2.258]

Added

  • Backward/forward-sweep distribution solver (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.

Notes

  • Committed a few radial feeders under data/matpower/dist/ (case33bw/69/85/18/22); note case33bw ships in raw units (ohms + kW), not MATPOWER p.u./MW.

[0.2.240]

Fixed

  • MATPOWER parser now evaluates arithmetic expressions (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.

[0.2.229]

Changed

  • Backlog captured for the next cycle: a backward/forward-sweep (bidirectional) solver for radial distribution networks (the failing feeder cases), and full AMD-first ROCm/HIP support (rocSOLVER/hipBLAS/rocSPARSE behind the solver seams) as the first-class GPU backend.

Known limitations

  • The .m parser does not evaluate arithmetic expressions (e.g. 135/sqrt(3)), so a couple of ACTIVSg cases fail to load.
  • Radial distribution feeders do not converge under the transmission Newton-Raphson (pending the backward/forward-sweep solver).
  • The naive ILGPU dense LU is correctness-first and slow; it needs a blocked/library implementation.

[0.2.222]

Added

  • Comprehensive developer documentation under docs/: architecture & solver internals, CLI reference, case libraries & MATPOWER parser, and benchmarking & GPU.

[0.2.215]

Added

  • GPU backend validated on an NVIDIA RTX A6000: results correct (case9 exact), but the card's 1:64 FP64 made the dense GPU solve ~500x slower than CPU - empirical evidence that double-precision power flow belongs on full-rate-FP64 hardware (AMD Instinct), not throttled consumer/workstation NVIDIA cards.

[0.2.205]

Added

  • README: the GPU section and a prominent "Compute backends - AMD-first, by design" section - priority order AMD/ROCm -> CPU -> open drivers (OpenCL/Vulkan) -> CUDA (last) - with the double-precision rationale for prioritising AMD Instinct.

[0.2.190]

Added

  • Runtime --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.

[0.2.170]

Added

  • 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).

[0.2.140]

Added

  • Sparse linear-solver seam - Abstractions.ISparseLinearSolver + CpuSparseLuSolver; SparseNewtonRaphsonPowerFlow now takes the solver by injection (default CPU direct LU), mirroring the existing dense ILinearSolver seam.

[0.2.96]

Added

  • Regime deployed and run on an AMD EPYC 7763 (28-core, 57 GB) server, streaming versioned results per row. Sparse solver scales cleanly to 25,000-bus systems.

Changed

  • Confirmed the expected non-convergence classes: PGLib OPF operating points (not valid flat-start power flows) and radial distribution feeders (transmission NR unsuited).

[0.2.83]

Added

  • 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.

[0.2.70]

Added

  • 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.

[0.2.57]

Added

  • chain - consecutive warm-started solves (GridScenario.WithSolvedVoltages) in fixed, ramp, and daily modes, tracking warm-start iteration counts and solution drift.

[0.2.44]

Added

  • stress - load-to-collapse sweep via GridScenario.AtLoad, reporting per-case max loadability (the P-V nose point) with parallel load steps.

[0.2.31]

Added

  • 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).

[0.2.18]

Changed

  • Two-pass bench: a complete sparse sweep followed by a best-effort dense sweep.

Fixed

  • Removed the premature dense "infeasible" cap (mis-calibrated to laptop RAM). Dense is now attempted on every case; only a genuine allocation failure is recorded as OOM. Large dense solves are memory-gated and ordered small->large.

[0.2.9]

Added

  • Run metadata / version stamping (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.

[0.2.0]

Changed

  • Parallel benchmarking harness. bench parallelised across cases (--parallel N, default = core count) with deterministic output ordering; added CSV output alongside the markdown table.

[0.1.198]

Added

  • Laptop benchmark results captured to 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.

[0.1.190]

Added

  • README: "MATPOWER importer", "two solvers", and benchmark sections documenting the new capability.

[0.1.185]

Added

  • Size-aware backend selection (sparse past the dense memory ceiling) in the CLI solve paths.

Changed

  • data/matpower/README.md provenance + BSD/CC-BY licence notes for the committed cases.

[0.1.178]

Added

  • First bench verb: a dense-vs-sparse convergence table (iterations, mismatch, timing) over the built-in/library cases.

[0.1.171]

Added

  • WPF source picker discovers data/matpower/**/*.m (ModelCatalog, MainWindow load path), so the libraries appear in the desktop app.

[0.1.165]

Added

  • CLI can run any MATPOWER case: positional <file>.m and --mpc <file>, plus a built-in case switch (case14 ... case300, case24/rts, cigre).

[0.1.158]

Added

  • MatpowerParserTests - tokenizer units, case9 parse-vs-hand-coded round-trip, and guarded PEGASE/RTS-96/NESTA/CIGRE convergence checks.

[0.1.152]

Fixed

  • The staged 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).

[0.1.146]

Fixed

  • Pre-existing build breaks unrelated to the new work: an empty character literal in 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.

[0.1.140]

Added

  • CIGRE MV distribution benchmark authored to the CIGRE TF C6.04.02 spec (data/matpower/benchmarks/cigre_mv.m, IeeeCases.CigreMv).

[0.1.134]

Added

  • Committed case libraries under data/matpower/: MATPOWER 8.0 lib/, PEGASE case1354/2869/9241, PGLib RTS-96 (73-bus), and a NESTA-lineage subset. Provenance + licences recorded.

[0.1.128]

Added

  • 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.

[0.1.121]

Added

  • Parser robustness: 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).

[0.1.114]

Added

  • MATPOWER .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.