31. Exports and formats¶
GridSim writes a file for almost every result it computes, and reads a small number of documents you author yourself. This chapter is the field guide to those files: for each one, which verb writes it, where it lands by default, its schema id, what every top-level field means to you, and a real excerpt from a captured run. It is the practical map. The field-by-field wire reference, with serialiser names and reader tolerances, is technical chapter 10; the v2 case and library schemas are technical chapter 28 and are only pointed to here.
Every excerpt below is pasted from a run of the current build under docs/_captures/exports/. The $ line is the
command that produced it. Timings, timestamps, host names, commit stamps and build paths differ by machine and by run.
Iteration counts, mismatches and estimates are reproducible: the acceptance rule is bit-identical results across runs
and CPU modes (see chapter 30).
Conventions that hold for every file¶
Where files land. A verb with an output directory defaults to a folder under results/ in the current directory
(results/estimate-case14, results/emt-out, results/replay-gb-2019-08-09). The path is relative, so it resolves
against the directory you ran the command from. An explicit --out (or --enableExport=<path>) is used exactly as
given. A missing folder is created. The desktop and web apps look for run directories in a results/ folder near
the application, so runs started from the repository root are found.
Casing and numbers. JSON field names are camelCase and enums are written by name ("Slack", "Recorded"), with
two exceptions. The estimate export writes chiSquareTest as a number (0, 1 or 2, see
the estimate export). The meta block of the solve, estimate, harmonic and impedance-stability exports, and the three trace
records of the solve export (ybus, busSpec, qLimitEvents), keep their C# PascalCase property names
(meta.Case, meta.GitCommit, ybus[].G). The meta block of the frequency export and the H=0 study is camelCase.
Readers bind names case-insensitively, so both spellings read back. Numbers are invariant-culture (a . decimal
point), and the JSON exports write doubles in round-trip form, so reading a file back gives the exact bits GridSim
computed. Timestamps are UTC ISO-8601.
Nulls. Exports omit a field whose value is null, with two deliberate exceptions. The H=0 study has no omit rule:
it keeps every null field, including the figures that are undefined (RoCoF, nadir, settle frequency and the other
cell figures). The estimate export writes an explicit null for a chi-square test that did not run.
Non-finite values. Each writer has its own rule for a value that is not finite.
| Writer | A NaN, Infinity or -Infinity |
|---|---|
| Solve, estimate, harmonic, impedance-stability and small-signal exports | Written as the string "NaN", "Infinity" or "-Infinity" (a generator limit of infinity, the normalised residual of a critical measurement). |
--json result files (gridsim-cli-result/1) |
The same three strings; the CSV files write the same spellings unquoted. |
| H=0 study | A NaN is written as null. An infinity makes the writer throw. |
Every other JSON file GridSim writes (frequency, evidence, event catalogue, measurement set, spectra, point map, demand series, classical export, fdia-screen.json, gridviz.json) |
The serialiser throws, so the verb fails rather than write a number that JSON cannot hold. |
Additive fields. A schema id ends in a version (/1). New fields are added inside the same version, and readers
ignore fields they do not know, so a file written by a newer build still loads in an older reader. Your own files are
held to the opposite rule where a typo would silently change a result: the cost, switched-shunt and three-phase feeder
files refuse an unknown key and name it.
The 169-hour horizon. Every dated input is checked when it is read: a measurement set's takenUtc, a harmonic
set's takenUtc, an event's whenUtc and every timestamp of a GDA demand series. A date less than 169 hours before
now, or in the future, is refused. GridSim replays history and never nowcasts; see
chapter 22.
Where each file comes from¶
| File | Written by | Default path | Schema id or format | Section |
|---|---|---|---|---|
solve-export.json and exports/slice-*.json, exports/state-*.json |
gridsim <case> --once --enableExport, transient, gda-replay, crossval, estimate --export-solve |
results/exports/<case>-solve.json; the others in their run directory |
gridsim-solve-export/1 |
Solve export |
estimate-export.json and its sidecars |
estimate |
results/estimate-<case> |
gridsim-estimate-export/1 |
Estimate export |
fdia-screen.json |
estimate --fdia-screen |
results/estimate-<case> |
gridsim-fdia-screen/1 |
The estimate sidecars |
measurements.json, interposer windows |
estimate --synth, interpose |
results/estimate-<case>; the --measurements-dir you name |
gridsim-measurements/1 |
Measurement sets |
spectra file, harmonic-export.json |
you, harmonics |
none; results/harmonic-export.json |
gridsim-harmonics/1, gridsim-harmonic-export/1 |
Harmonic files |
smallsignal.json, impedance-stability.json, classical export |
modal, impedance-stability, stability |
the --export path you name; results/impedance-stability-export.json; the --export path you name |
gridsim-smallsignal/1, gridsim-impedance-stability/1, gridsim-classical-export/1 |
Stability exports |
| RMS event study export | stability --rms --events --events-out |
the --events-out folder you name |
gridsim-rms-events-export/1 and CSV |
Stability exports |
timeseries.csv, summary.md |
emt |
results/emt-out |
CSV and Markdown | EMT run files |
| event catalogue | you, replay-event --dump-catalog |
the path you name | gridsim-event-catalog/1 |
Event catalogue |
| frequency exports, evidence, H=0 study | replay-event, scenario, evidence, h0-study |
results/replay-<event>, results/scenario-<event>, results/evidence-<event>, results/h0-study-<event> |
gridsim-frequency-export/1, gridsim-evidence/1, gridsim-h0-study/1 |
Event study outputs |
fleet.csv |
fleet |
results/fleet-<event> |
CSV | Run directories |
| time-series CSVs, frequency trace, PV curves | transient, gda-replay, trip --export, cpf --pv-out, stress --pv-out |
results/transient-<case>-<stamp>, results/gda-replay-<case>; the path you name |
CSV | Time-series CSVs |
| reports | bench, chain, stress, nminus1-sweep, conformance |
results/benchmark-results.md, results/chain-<case>.md, results/stress.md, results/nminus1-sweep.md, results/conformance |
Markdown and CSV | Reports |
| RoCoF screening table | screen --csv |
the path you name | CSV only | Reports |
crossval-diff.json |
crossval (its Python sidecar) |
results/crossval-<case> |
JSON, no schema id | Run directories |
--json / --csv results |
losses, dcopf, dcpf, cpf, nminus1 and the other verbs of ch. 19 |
the paths you name | gridsim-cli-result/1 |
Result files |
| cost, switched-shunt, device, demand-series files | you | none | gridsim-costs/1, gridsim-switched-shunts/1, device JSON, GDA series JSON |
Authored inputs |
| point map (crosswalk), quarantine marker | you, interpose |
none; <dir>/Parquet/<source id>/_QUARANTINE.json |
gridsim-rtu-pointmap/1, _QUARANTINE.json |
Crosswalks |
| audit log | --audit-log |
the file you name | gridsim-audit/1 |
Audit log |
| v2 documents | the case editor, convert (an output ending in .json), dist3ph to-v2; checked by validate |
the path you save or name | gridsim-case/2, gridsim-network/2, gridsim-library/2, gridsim-kind/2, gridsim-dist3ph/2 |
v2 documents |
MATPOWER .m file or v1 JSON case folder |
convert |
the output you name | MATPOWER .m, or the v1 case folder (system.json, buses.json, ...) |
Other files you may meet |
| PMU measurement sets, live quarantine capture | pmu-ingest --measurements-dir, pmu-ingest --live --live-quarantine |
the folders you name | gridsim-measurements/1; raw C37.118 frames with a gridsim-pmu-quarantine/1 sidecar |
ch. 25 |
gridviz.json |
export |
results/gridviz.json |
none | Visualiser scene |
Run directories and run-metadata.json¶
A verb that writes a report writes it into a run directory (or next to a report file) together with a
run-metadata.json sidecar, so the run is self-describing: when it ran, on what code, on what machine, with what
parameters. You read it months later when two results disagree.
| Verb | Default location | Files written |
|---|---|---|
transient |
results/transient-<case>-<stamp> (several cases: results/transient-<stamp>/<case>) |
timeseries.csv, summary.md, exports/slice-NNNNNNNNN.json, run-metadata.json |
replay-event |
results/replay-<event> |
baseline-frequency.json, summary.md, run-metadata.json |
scenario |
results/scenario-<event> |
matrix.csv, risk-curve.csv, counterfactual-frequency.json, summary.md, run-metadata.json |
h0-study |
results/h0-study-<event> |
h0-study.json, matrix.csv, summary.md, run-metadata.json |
evidence |
results/evidence-<event> |
evidence.json, summary.md, run-metadata.json |
fleet |
results/fleet-<event> |
fleet.csv, run-metadata.json |
gda-replay |
results/gda-replay-<case> |
state-estimate.csv, exports/state-NNNNNN.json, storage-counterfactual.csv (with --bess), run-metadata.json |
emt |
results/emt-out |
timeseries.csv, summary.md, run-metadata.json |
estimate |
results/estimate-<case> |
estimate-export.json, residuals.csv, run-metadata.json, and the sidecars listed under the estimate export |
estimate-noise-floor |
results/estimate-noise-floor |
noise-floor.csv, run-metadata.json |
conformance |
results/conformance |
conformance.md, conformance.csv, run-metadata.json |
bench |
results/benchmark-results.md |
the report, benchmark-results.csv, and benchmark-results.run-metadata.json beside it |
nminus1-sweep |
results/nminus1-sweep.md |
the report, a .csv and a <report>.run-metadata.json beside it |
stress |
results/stress.md |
the report, a .csv and a <report>.run-metadata.json beside it |
chain |
results/chain-<case>.md |
the report, a .csv and a <report>.run-metadata.json beside it |
crossval writes solve-export.json and crossval-diff.json into results/crossval-<case>, and no
run-metadata.json: it records the same provenance in the export's meta block. crossval-diff.json is the
comparison written by the Python sidecar tools/crossval/crossval.py. It carries no GridSim schema id, and it is
absent when the sidecar did not run. chain with --bess writes results/chain-storage-<case>.md and its
.csv and no metadata sidecar. bench --perf writes results/perf-baseline.md and .csv and no sidecar: the header
lines are in the Markdown, and the CSV (path, case, buses, metric, value, runs, commit) has a commit
column and no # lines. bench --estimate writes results/bench-estimate/bench-estimate.csv (case, buses,
measurements, states, backend, converged, iterations, mean_ms, runs, commit), also with a commit
column and no header lines. losses, dcopf and the other verbs of
chapter 19 that take --json and --csv write only the files you name.
fleet.csv has one row per fleet size after the # header: units, rocof_hz_s, nadir_hz, ffr_at_nadir_mw,
peak_fleet_ffr_mw and lfdd_at_risk.
run-metadata.json has the same fields for every verb. This is the one transient wrote:
$ cat results/exports-docs/transient/run-metadata.json
{
"timestampUtc": "2026-10-20T05:26:20.0011475Z",
"backend": "cpu",
"gitCommit": "v1.95.294-173-g203c6b9c",
"buildConfig": "Release",
"coreVersion": "1.95.467.0",
"runtime": ".NET 10.0.12",
"arch": "X64",
"serverGc": true,
"simdTier": "Avx256",
"simdLanes": 4,
"simdFma": true,
"machineName": "LAPTOP",
"os": "Microsoft Windows 10.0.26300",
"cpu": "Intel(R) Core(TM) i3-N305",
"logicalCores": 8,
"ramGb": 8.3,
"run": "transient case=case9 minutes=1 dt=100ms slices=600 solver=Newton-Raphson (dense)"
}
[exit 0]
| Field | Meaning |
|---|---|
timestampUtc |
The instant the sidecar was written. |
backend |
The solver backend: cpu, gpu/<vendor> (<device>), or cpu (<vendor> requested but unavailable). |
gitCommit |
The short commit baked into the build; a -dirty suffix means uncommitted changes. |
buildConfig |
Debug or Release. |
coreVersion |
The GridSim.Core assembly version. |
runtime, arch, serverGc |
The .NET runtime, the process architecture, and whether server garbage collection is on. |
simdTier, simdLanes, simdFma |
The active vector tier, doubles per vector step, and whether fused multiply-add is available, as gridsim simdcheck reports them. |
machineName, os, cpu, logicalCores, ramGb |
The host. cpu is the model name on Linux and the architecture elsewhere. |
run |
The verb and its key parameters as one string. |
The same information heads the reports. A Markdown report starts with the header as plain lines indented by four
spaces. A CSV starts with the header as #-prefixed lines, then the column names. These CSVs carry the # header:
timeseries.csv (transient and emt), state-estimate.csv, matrix.csv and risk-curve.csv, fleet.csv,
residuals.csv, hti-removals.csv, hti-corrections.csv, noise-floor.csv and conformance.csv.
storage-counterfactual.csv has no such header: its first line is a single # COUNTERFACTUAL comment, then the
column names. The CSVs of bench, chain, stress and nminus1-sweep have no header lines. They carry a commit
column on every row instead, so rows pooled from several runs stay attributable. The screen CSV, the --bess
variant of chain, the --pv-out curves and the shared-writer CSVs (--csv) have neither a header nor a commit
column. A CSV with # lines needs comment='#' in a pandas or R reader.
gridsim-solve-export/1 - the solve-math export¶
The full record of one power-flow solve: the network as solved, the solver's per-iteration working, and the converged state, with a physical-law audit. Recording it is observational: attaching the recorder does not change the solution.
- Written by:
gridsim <case> --once --enableExport[=path](defaultresults/exports/<case>-solve.json, for exampleresults/exports/case9-solve.json;--export-jacobianforces the Jacobian);transient(exports/slice-NNNNNNNNN.json, one per--export-everyslice, plus slice 1 and the trip window);gda-replay(exports/state-NNNNNN.json);crossval(solve-export.json);estimate --export-solve(estimated-solve-export.json, written only when the estimate converged). GDA'sgridsim_export.to_solve_exportwrites the same shape on the lake side. - Read by:
tools/crossval/crossval.pyandresidual_check.py;SolveImport(pre-solved replay in the apps, chapter 8); the GDA bridge (chapter 24), whose topology and query payloads reuse themodelblock. - Produced in: chapter 5, chapter 14, chapter 21, chapter 27.
$ gridsim case9 --once --enableExport=results/exports-docs/solve-export.json
[export] dense-newton: converged=True iters=4 maxMis=1.65E-014 -> results\exports-docs\solve-export.json
[exit 0]
The document has six top-level fields.
| Field | Meaning |
|---|---|
schema |
gridsim-solve-export/1. |
meta |
Provenance: Case, Backend (the CLI backend, or the verb name for harmonics and impedance-stability), GitCommit, TimestampUtc, ElapsedMs (the wall time of the computation that produced the document, measured by every writer), Machine. |
model |
The network exactly as solved. |
solve |
The solver's working. |
result |
The converged state and every branch flow. |
physics |
The physical-law audit of the state. |
$ sed -n '1,24p' results/exports-docs/solve-export.json
{
"schema": "gridsim-solve-export/1",
"meta": {
"Case": "case9",
"Backend": "cpu",
"GitCommit": "v1.95.294-173-g203c6b9c",
"TimestampUtc": "2026-10-20T05:26:16.4773195Z",
"ElapsedMs": 31.8409,
"Machine": "LAPTOP / Microsoft Windows 10.0.26300"
},
"model": {
"name": "IEEE 9-bus (WSCC)",
"baseMva": 100,
"frequencyHz": 60,
"busCount": 9,
"buses": [
{
"id": 1,
"type": "Slack",
"baseKv": 345,
"pd": 0,
"qd": 0,
"gs": 0,
"bs": 0,
[exit 0]
model carries the solve-relevant network, and an exported state re-imports as the same network.
| Block | Fields |
|---|---|
| top level | name, baseMva, frequencyHz (the nominal frequency), busCount |
buses[] |
id, type (Slack, Pv or Pq), baseKv, pd and qd (demand, MW and Mvar), gs and bs (shunt, MW and Mvar at 1 pu), vm (voltage set-point or start), name, zip (the six coefficients ap bp cp aq bq cq of a ZIP load; omitted for constant power), vaDeg (initial angle), x and y (layout), vminPu and vmaxPu (the bus's own voltage limits; omitted when the global band applies) |
generators[] |
Every unit, in service or not: bus, pg, qg, vg, qmin, qmax, pmin, pmax, mBase (MVA), h (inertia constant, seconds), type, name, inService, dynamicsClass, the fault data subtransientXdPu, negativeSeqXPu, unitTransformerXPu, the converter flags converterInterfaced, converterFaultCurrentPu, gridForming, powerFlowOnly, and the cost curves cost and reactiveCost |
branches[] |
from, to, r, x, b (per unit on baseMva), tap, phaseShiftDeg, rateAMva, name, isTransformer, inService, the short-circuit data r0, x0, fromWinding, toWinding, fromNeutralXPu, toNeutralXPu, and lengthKm, equipment, rateBMva, rateCMva, dynamicsClass |
oltcs[] |
transformer (the branch name), controlsBus, targetVpu, minTap, maxTap, stepPu, deadbandPu |
boundaries[] |
name, limitMw, branches[] of {branch, direction} (the transfer boundaries of the GB models) |
A limit such as qmax may be the string "Infinity". Per-asset dynamics classes and protection bundles are not part
of the export.
solve is what the solver did.
| Field | Meaning |
|---|---|
solver |
dense-newton, sparse-newton or bfs. |
n |
The number of buses solved. |
converged, status, iterations, maxMismatch |
The outcome; status is Converged, MaxIterations or NonFinite; maxMismatch is the largest power mismatch in per unit. |
jacobianOmittedForSize |
True when a dense solve left the Jacobian out because the system exceeds 300 buses. |
slackQNote |
A note on the slack bus reactive output; omitted when there is none. |
ybus[] |
The non-zero admittance entries {I, J, G, B} (zero-based bus indices, per unit). Empty for a BFS solve, which forms no Y-bus. |
busSpec[] |
Per bus {Index, Id, Type, VmInit, VaInitDeg, Pspec, Qspec, Qd, Vg, Qmin, Qmax}: the specification and start point (Pspec and Qspec per unit, Qd, Qmin and Qmax MVAr, on every engine). |
qLimitEvents[] |
Every switch of a bus between PV and PQ in the reactive-limit loop: {BusIndex, BusId, PinnedAt, Pass}. |
passes[] |
One entry per reactive-limit pass: pass, effectiveTypes, pvpqIndices and pqIndices (the equation ordering), and iterations[] of {iteration, maxMismatch, mismatch[], correction[], jacobian[][]}. |
$ sed -n '418,432p' results/exports-docs/solve-export.json
"solve": {
"solver": "dense-newton",
"n": 9,
"converged": true,
"status": "Converged",
"iterations": 4,
"maxMismatch": 1.6456133601332757E-14,
"jacobianOmittedForSize": false,
"ybus": [
{
"I": 0,
"J": 0,
"G": 0,
"B": -17.36111111111111
},
[exit 0]
mismatch and correction follow the pass ordering: the P equations of every non-slack bus first, then the Q
equations of the PQ buses. The Jacobian, captured before factorisation, appears only for the dense engine, for
systems of up to 300 buses unless you pass --export-jacobian. A sparse solve records the mismatch and correction
vectors without it. A BFS solve records the per-bus voltage change as mismatch and leaves correction empty.
result is the converged state.
| Field | Meaning |
|---|---|
totalLossMw, totalLossMvar |
System losses. For Newton solves these are the sums of the bus net injections, so they include bus-shunt consumption; for BFS they are the sums of the branch losses. |
buses[] |
id, vm (pu), vaDeg, pInjMw, qInjMvar. |
lineFlows[] |
from, to, pFromMw, qFromMvar, pToMw, qToMvar, pLossMw, qLossMvar, name, ratingMva, loadingPercent. An out-of-service branch is an all-zero row. |
$ sed -n '1795,1817p' results/exports-docs/solve-export.json
"result": {
"totalLossMw": 4.6410214744828835,
"totalLossMvar": -92.16012521906433,
"buses": [
{
"id": 1,
"vm": 1.04,
"vaDeg": 0,
"pInjMw": 71.64102147448251,
"qInjMvar": 27.045923533493067
},
{
"id": 2,
"vm": 1.025,
"vaDeg": 9.280005481642787,
"pInjMw": 162.99999999999997,
"qInjMvar": 6.653660318427831
},
{
"id": 3,
"vm": 1.025,
"vaDeg": 4.664751333136755,
"pInjMw": 84.99999999999999,
[exit 0]
physics is the audit of that state against the physical laws: gatePass (true when every hard law holds),
hardGateLaws (the numbers of the laws that gate), and laws[] of {number, name, statement, status, residual, unit,
tolerance, detail}. tools/crossval/physics_audit.py recomputes each law from model and result and checks these
values independently.
$ sed -n '1983,2005p' results/exports-docs/solve-export.json
"physics": {
"gatePass": true,
"hardGateLaws": [
1,
2,
3,
6,
8
],
"basis": "case-dispatch",
"laws": [
{
"number": 1,
"name": "Conservation of energy",
"statement": "Sigma(generation - load) = Sigma losses",
"status": "Holds",
"residual": 4.263256414560601E-14,
"unit": "MW",
"tolerance": 1E-06,
"detail": "independent dissipation 4.641021 MW (branch 4.6410 \u002B shunt 0.0000) vs reported loss 4.641021 MW"
},
{
"number": 2,
[exit 0]
gridsim-estimate-export/1 - the estimate export¶
Everything one state estimation computed: the network, the measurement set it was asked to fit, the estimated state, the residual table, observability and removals. It is the whole input contract of the estimation sidecars.
- Written by:
gridsim estimateasestimate-export.jsonin the run directory. - Read by:
tools/crossval/crossval_estimation.pyandse_residual_check.py;estimate-noise-floor. - Produced in: chapter 21.
$ gridsim estimate case14 --synth --trace --export-solve --out results/exports-docs/estimate
=== WLS state estimate: case14 ===
status: Converged iterations: 5 max |dx|: 2.72E-009
measurements: 122 states: 27 redundancy (dof): 95
observability: observable (rank 27/27)
consistency: J = 80.01 vs chi2(95%) = 118.75 -> PASS
vs synthetic truth: max |dVm| = 1.330E-003 pu max |dVa| = 2.248E-001 deg
-> results\exports-docs\estimate
[exit 0]
| Field | Meaning |
|---|---|
schema |
gridsim-estimate-export/1. |
meta |
The same meta as the solve export; ElapsedMs is the time of the estimate. |
model |
name, baseMva, busCount, buses[], generators[] and branches[] with the fields of the solve export's blocks except pmin, zip and the short-circuit data. It has no frequencyHz, oltcs or boundaries. |
measurements |
The set as handed to the estimator: measurementSchema (gridsim-measurements/1), case, takenUtc, note, count, items[]. Gross errors are still in it; so are the replaced voltage-schedule rows under --prior solve. |
estimate |
The outcome, below. |
residuals[] |
One entry for every measurement that survived to the final fit: measurement, predicted, residual (engineering units), normalizedResidual, critical. |
observability |
observable, rank (-1 only when the sparse backend skipped the rank proof because the state size exceeds 2,500), stateSize, unobservableBuses[], unobservableStates[], unmeasuredIslandBuses[]. |
removed[] |
The bad-data removals in order: measurement, normalizedResidual, pass. |
trace[] |
Only with --trace: the per-iteration path {pass, iteration, maxStateUpdate, stepScale}. |
physics |
The same audit as the solve export, evaluated at the estimated state. |
estimate is the verdict:
$ sed -n '1495,1506p' results/exports-docs/estimate/estimate-export.json
"estimate": {
"status": "Converged",
"converged": true,
"iterations": 5,
"maxStateUpdate": 2.7236422301635937E-09,
"objectiveJ": 80.01052231478407,
"degreesOfFreedom": 95,
"chiSquareThreshold": 118.74899343100537,
"chiSquarePassed": true,
"chiSquareTest": 1,
"irlsCapReached": false,
"buses": [
[exit 0]
status is the estimation status (Converged, MaxIterations, NonFinite, Unobservable, ...). maxStateUpdate
is the last Gauss-Newton step, objectiveJ the weighted sum of squared residuals, degreesOfFreedom the redundancy,
and chiSquareThreshold the 95 % chi-square limit that objectiveJ is tested against (NaN when no test ran).
irlsCapReached is true when a reweighting method (huber, schweppe-gm, lav-irls) stopped re-weighting on its cap
before the weights settled, so the state is not a fixed point of that estimator; it is always present, and false for
the other methods. buses[] is the estimated state: id,
vm, vaDeg.
chiSquarePassed is true or false only when the chi-square test ran. It is null, never a pass, when the test
did not run: no redundancy, --no-bad-data, an unobservable fit or a fit that did not converge. chiSquareTest says
the same as a number: 0 not run, 1 passed, 2 failed. A script that treats the field as a verdict must accept null.
A measurement inside items[], residuals[] or removed[] is {kind, value, sigma, bus | from + to + end, circuit,
source}, with the bus fields absent for branch kinds and the branch fields absent for bus kinds.
$ sed -n '1579,1592p' results/exports-docs/estimate/estimate-export.json
"residuals": [
{
"measurement": {
"kind": "VmPu",
"value": 1.0603366153490539,
"sigma": 0.004,
"bus": 1,
"source": "synthetic"
},
"predicted": 1.0600177496207248,
"residual": 0.00031886572832906523,
"normalizedResidual": 0.08517669708450266,
"critical": false
},
[exit 0]
When bad-data removal runs (the default for wls), each removal is recorded with the normalised residual that
condemned it and the pass it happened in. This is the export of the same case with three gross errors planted, run
without --hti:
$ sed -n '3284,3299p' results/exports-docs/estimate-lnr/estimate-export.json
"removed": [
{
"measurement": {
"kind": "VmPu",
"value": 0.9791469785239025,
"sigma": 0.004,
"bus": 9,
"source": "synthetic"
},
"normalizedResidual": 18.347391438257883,
"pass": 0
},
{
"measurement": {
"kind": "PInjMw",
"value": -17.94245975275534,
[exit 0]
observability is the proof the estimate means something:
$ sed -n '3327,3345p' results/exports-docs/estimate/estimate-export.json
"observability": {
"observable": true,
"rank": 27,
"stateSize": 27,
"unobservableBuses": [],
"unobservableStates": [],
"unmeasuredIslandBuses": []
},
"removed": [],
"trace": [
{
"pass": 0,
"iteration": 0,
"maxStateUpdate": 0.2,
"stepScale": 0.6483659052714864
},
{
"pass": 0,
"iteration": 1,
[exit 0]
With --backfill-removals, each removed real-power row is replaced in the fit by a loose pseudo-measurement from the
prior, with source set to removal-backfill-prior, and that row appears in residuals[].
The estimate sidecars¶
| File | Written when | Content |
|---|---|---|
residuals.csv |
always | The residual table flattened for a spreadsheet. |
measurements.json |
--synth |
The sampled set, so the sidecars consume the identical input. A gridsim-measurements/1 file. |
estimated-solve-export.json |
--export-solve and the estimate converged |
The estimate projected into a gridsim-solve-export/1 document. Its solve block names the estimator and carries its status and last state update, because no power-flow solve ran. |
hti-removals.csv |
--hti |
The Hypothesis-Testing-Identification removals. |
hti-corrections.csv |
--hti-correct |
The in-place corrections. |
fdia-screen.json |
--fdia-screen |
The false-data-injection verdict, schema gridsim-fdia-screen/1. |
noise-floor.csv |
estimate-noise-floor |
Pooled residual statistics per source and kind. |
The solve block of estimated-solve-export.json:
"solve": {
"solver": "state-estimate (Wls)",
"converged": true,
"status": "Converged",
"iterations": 5,
"maxStateUpdate": 2.7236424064891863E-09
},
maxStateUpdate is the estimator's last state update, not a power mismatch, so the block has no maxMismatch.
residuals.csv. One row per measurement in the final fit, after the # header. value and predicted are
round-trip (G17), sigma is G6, residual is E6 and normalized_residual is F3 (NaN for a critical row,
or with --lean on a passing fit). location reads bus 4, branch 1->2 (from) or branch 1->2 'name' (from).
$ head -n 14 results/exports-docs/estimate/residuals.csv
# GridSim - 2026-10-20 05:26:11 UTC
# code: git v1.95.294-173-g203c6b9c | GridSim.Core v1.95.467.0 | build Release
# runtime: .NET 10.0.12 X64 GC=Server
# machine: LAPTOP Microsoft Windows 10.0.26300
# cpu: Intel(R) Core(TM) i3-N305 - 8 logical cores - 8 GB RAM
# simd: Avx256 (4 doubles/step) FMA=True
# backend: cpu
# run: estimate case=case14 measurements=122 source=synthetic
kind,location,value,sigma,predicted,residual,normalized_residual,critical
VmPu,bus 1,1.0603366153490539,0.004,1.0600177496207248,3.188657E-004,0.085,False
PInjMw,bus 1,230.50994250325303,1,231.53597775062065,-1.026035E+000,1.633,False
QInjMvar,bus 1,-16.387551951145515,1,-16.02951780147939,-3.580341E-001,0.538,False
VmPu,bus 2,1.0458945760543186,0.004,1.0448823911434171,1.012185E-003,0.269,False
PInjMw,bus 2,17.170793445468924,1,17.517053380283386,-3.462599E-001,0.669,False
[exit 0]
hti-removals.csv. The # lines are followed by a # passes=... total_iterations=... stop_reason=... line, then
one row per removal: pass (counted from 0, pass 0 being the first estimate; N256, it was 1-based), kind, location, value, sigma, estimated_error with its estimated_error_unit
(pu for voltage and current magnitudes, rad for angles, pu_base_mva for powers and flows),
estimated_error_measured with its measured_unit (pu, deg, MW or Mvar), and test_statistic.
hti-corrections.csv has the same shape with original_value and corrected_value in place of value.
$ head -n 14 results/exports-docs/estimate-planted/hti-removals.csv
# GridSim - 2026-10-20 05:26:10 UTC
# code: git v1.95.294-173-g203c6b9c | GridSim.Core v1.95.467.0 | build Release
# runtime: .NET 10.0.12 X64 GC=Server
# machine: LAPTOP Microsoft Windows 10.0.26300
# cpu: Intel(R) Core(TM) i3-N305 - 8 logical cores - 8 GB RAM
# simd: Avx256 (4 doubles/step) FMA=True
# backend: cpu
# run: estimate case=case14 measurements=122 source=docs/_captures/estimation/case14-planted.json
# passes=2 total_iterations=10 stop_reason=ChiSquarePassed
pass,kind,location,value,sigma,estimated_error,estimated_error_unit,estimated_error_measured,measured_unit,test_statistic
0,VmPu,bus 9,0.97914697852390253,0.004,-0.076494769013953995,pu,-0.076494769013953995,pu,17.926056699398455
0,PFlowMw,branch 2->3 (from),96.567423708461746,1.6,0.23807965107852463,pu_base_mva,23.807965107852464,MW,13.597861020478678
0,PInjMw,bus 4,-17.94245975275534,1,0.27292734923735545,pu_base_mva,27.292734923735544,MW,7.2052784005966659
[exit 0]
fdia-screen.json. schema, verdict (suspected, clear, chi-square-failed or no-verdict), summary
(one sentence), status (the estimation status) and chiSquareTest (Passed, Failed or NotRun, by name). A
suspected or no-verdict result makes estimate exit 3.
$ cat results/exports-docs/estimate-planted/fdia-screen.json
{
"schema": "gridsim-fdia-screen/1",
"verdict": "clear",
"summary": "chi-square PASSED and the state is physically plausible and anchor-consistent - no FDIA indicator.",
"status": "Converged",
"chiSquareTest": "Passed"
}
[exit 0]
noise-floor.csv. One row per measurement class (source tag and kind): count of finite samples,
spread_robust and spread_std (the ratio k of true noise to declared sigma, by the median and by the standard
deviation), bias, suggested_sigma_scale and miscalibrated. It is a study, not a gate.
$ head -n 16 results/exports-docs/noise-floor/noise-floor.csv
# GridSim - 2026-10-04 14:35:09 UTC
# code: git 98048023 | GridSim.Core v1.95.154.0 | build Release
# runtime: .NET 10.0.12 X64 GC=Server
# machine: LAPTOP Microsoft Windows 10.0.26200
# cpu: X64 - 8 logical cores - 8 GB RAM
# simd: Avx256 (4 doubles/step) FMA=True
# backend: cpu
# run: estimate-noise-floor exports=1 skipped-non-converged=0 samples=122
source,kind,count,spread_robust,spread_std,bias,suggested_sigma_scale,miscalibrated
synthetic,QInjMvar,14,1.1825479925667144,0.93302204184988125,-0.17391889373940495,1.1825479925667144,False
synthetic,PFlowMw,40,1.0758801392421329,0.79909543407213657,-0.11921574898423581,1.0758801392421329,False
synthetic,QFlowMvar,40,1.0481207722311126,0.8865119790428293,-0.05958625259041006,1.0481207722311126,False
synthetic,VmPu,14,0.98388255398924207,0.84922574108164928,-0.18734780464406331,0.98388255398924207,False
synthetic,PInjMw,14,1.0109505299492152,0.87123781863344785,-0.0057258981188841318,1.0109505299492152,False
[exit 0]
gridsim-measurements/1 - the estimator's input¶
A versioned, self-describing measurement set over one case. You can write one by hand, estimate --synth samples one,
and interpose writes one per window.
- Written by:
estimate --synth(measurements.json);interpose(one file per window, named<UTC>-<sequence>.json; a clash is kept as-dupN); GDA'sNetworkModel/build_estimator_measurements.pyfor real GB telemetry; your editor. - Read by:
estimate --measurements <file>,serve --measurements-dir <dir>. - Produced in: chapter 21, chapter 25.
$ head -n 20 results/exports-docs/estimate/measurements.json
{
"schema": "gridsim-measurements/1",
"case": "IEEE 14-bus",
"baseMva": 100,
"takenUtc": "2024-03-01T12:00:00Z",
"note": "synthetic seed=20260713 noise=1 coverage=vm:1/inj:1/flow:1",
"measurements": [
{
"kind": "VmPu",
"value": 1.0603366153490539,
"sigma": 0.004,
"bus": 1,
"from": 0,
"to": 0,
"end": "From",
"source": "synthetic",
"isBranch": false
},
{
"kind": "PInjMw",
[exit 0]
| Field | Meaning |
|---|---|
schema |
Must equal gridsim-measurements/1; any other value is refused. |
case |
The case the set was taken on. Informational. |
baseMva |
Checked against the model (default 100). |
takenUtc |
Optional. When present it must clear the 169-hour horizon. |
note |
Free provenance text; the sampler records its seed and coverage here. |
measurements[] |
The rows below. |
A row needs kind and value; a row without either is refused and named. sigma must be finite and greater than 0.
| Row field | Meaning |
|---|---|
kind |
VmPu (voltage magnitude, pu), PInjMw and QInjMvar (bus injection), PFlowMw and QFlowMvar (branch flow), VaDeg (PMU phase angle in degrees against the slack), IMagPu (branch current magnitude, pu) and IAngDeg (branch current angle, degrees). |
value, sigma |
The reading and its standard deviation, in the units above. |
bus |
The bus id, for the bus kinds. |
from, to, end |
The branch ends and which terminal is measured (From, the default, or To), for the branch kinds. |
circuit |
Picks one of several parallel branches: the branch name, or "#k", where k is the zero-based position of the branch in the case's branch list. A "#k" branch must have the row's from and to, or the row is refused. |
source |
A provenance tag, default scada. Some tags act: virtual-zero-injection rows are hard equalities under --method constrained, pseudo-voltage-schedule rows are the ones --prior solve replaces. |
A file GridSim writes carries every field of every row, including from, to and isBranch as 0, 0 and false on a
bus row. A file you write needs only the fields its kind uses.
Harmonic files¶
The spectra file (gridsim-harmonics/1) is the input of harmonics --spectra: a set of harmonic current sources
on a case. Measured spectra are tagged Recorded and assumed library spectra CuratedPlaceholder; the solver is the
same either way. Produced in chapter 17.
$ cat docs/_captures/harmonics/spectra-case9.json
{
"schema": "gridsim-harmonics/1",
"case": "IEEE 9-bus (WSCC)",
"baseMva": 100.0,
"takenUtc": "2026-08-01T12:00:00Z",
"note": "Metered 6-pulse drive at bus 9 (example)",
"sources": [
{ "bus": 9, "sourceType": "6-pulse", "fundamentalCurrentPu": 0.5, "provenance": "Recorded",
"spectrum": [ { "order": 5, "magnitudePercent": 18.0, "phaseDeg": 0.0 },
{ "order": 7, "magnitudePercent": 11.0, "phaseDeg": 0.0 },
{ "order": 11, "magnitudePercent": 4.5, "phaseDeg": 0.0 },
{ "order": 13, "magnitudePercent": 3.0, "phaseDeg": 0.0 } ] }
]
}
[exit 0]
| Field | Meaning |
|---|---|
schema |
gridsim-harmonics/1. |
case, baseMva |
The case and system base the set was authored against; baseMva is checked against the model. |
takenUtc, note |
Optional measurement time (horizon-checked) and provenance text. |
sources[] |
bus, sourceType, fundamentalCurrentPu, provenance, and spectrum[] of {order, magnitudePercent, phaseDeg}. Magnitude is a percentage of the fundamental current. |
A component at an order outside the analysed range, or a triplen source, is refused (exit 1); see chapter 19.
The harmonic export (gridsim-harmonic-export/1) is written by harmonics --enableExport[=path] (default
results/harmonic-export.json).
gridsim harmonics case9 --spectra docs/_captures/harmonics/spectra-case9.json --enableExport=results/exports-docs/harmonic-export.json
$ gridsim harmonics case9 --spectra docs/_captures/harmonics/spectra-case9.json --enableExport=results/exports-docs/harmonic-export.json
harmonics: case IEEE 9-bus (WSCC), 1 source(s), orders 24
worst voltage THD : 4.71% at bus 9 (relative to the solved fundamental voltage)
resonance (bus 9): order 7, |Z|=0.658 pu, sharpness 3.8
fine sweep peak : h=7.4750 (448.50 Hz), |Z|=0.688 pu, positive sequence
IEEE-519 : 1/9 buses compliant
export -> results\exports-docs\harmonic-export.json
[exit 0]
| Field | Meaning |
|---|---|
schema, meta |
As for the solve export; meta.Backend is harmonics. |
provenance |
Recorded only when every source is recorded or derived from a recording, else CuratedPlaceholder. |
model |
name, baseMva, busCount, and buses[] of {id, baseKv, name}. |
orders |
The harmonic orders analysed. |
harmonicModel |
Every input beyond the network that the harmonic admittance used: which model options were on, the skin-effect coefficients, generatorSources[] (bus, name, xSystemPu) and loads[] (bus, pdMw, qdMvar), so an independent checker can rebuild the same matrix. |
sources[] |
The injected sources, as in the spectra file. |
voltage[] |
The non-zero harmonic bus voltages: order, bus, vmPu, vaDeg. |
indices |
worstBusId, worstThdPct, and thd[] of {bus, thdPct} (voltage THD against each bus's solved fundamental). |
resonance |
The driving-point resonance scan at one bus: bus, order, zMag, sharpness. |
resonanceSweep |
The fine sweep's refined peak: bus, peakOrder, peakHz, zMag, sequence, minOrder, maxOrder, gridPoints. |
compliance[] |
The IEEE 519 verdict per bus: bus, baseKv, thdPct, thdLimitPct, worstIhdPct, worstIhdOrder, ihdLimitPct, compliant, voltageLevelKnown. |
$ sed -n '448,474p' results/exports-docs/harmonic-export.json
"resonance": {
"bus": 9,
"order": 7,
"zMag": 0.6575669200741651,
"sharpness": 3.83763679770113
},
"resonanceSweep": {
"bus": 9,
"peakOrder": 7.47497016807171,
"peakHz": 448.4982100843026,
"zMag": 0.687623565377445,
"sequence": "Positive",
"minOrder": 1.5,
"maxOrder": 50,
"gridPoints": 486
},
"compliance": [
{
"bus": 1,
"baseKv": 345,
"thdPct": 1.3947496157044652,
"thdLimitPct": 1.5,
"worstIhdPct": 0.9086523860832977,
"worstIhdOrder": 7,
"ihdLimitPct": 1,
"compliant": true,
"voltageLevelKnown": true
[exit 0]
Stability exports¶
smallsignal.json (gridsim-smallsignal/1) is written by gridsim modal --export <file.json>. It has no
meta block. It holds the linearised model and its modes; produced in
chapter 15.
$ head -n 20 results/exports-docs/smallsignal.json
{
"schema": "gridsim-smallsignal/1",
"label": "WSCC-9",
"f0": 60,
"inverterStatesIncluded": false,
"states": [
{
"index": 0,
"kind": "SwingAngle",
"ownerId": 1,
"name": "d.delta_gen1",
"synthetic": false
},
{
"index": 1,
"kind": "SwingAngle",
"ownerId": 2,
"name": "d.delta_gen2",
"synthetic": false
},
[exit 0]
| Field | Meaning |
|---|---|
label, f0 |
The case label and the nominal frequency. |
inverterStatesIncluded |
True with --enable-inverters. |
states[] |
The state list: index, kind, ownerId, name, synthetic. |
aMatrix |
The state matrix, row by row. |
angleNodeBuses, kext |
The bus order and the external coupling matrix the model was built from. |
eigenvalues[], dampingRatios, frequenciesHz |
One entry per mode: the eigenvalue as {re, im}, its damping ratio and its frequency in Hz. |
participation |
The participation factors, one row per mode, one entry per state. |
rhpEigenvalueCount, minDampingRatio |
The right-half-plane count (non-zero means small-signal unstable) and the smallest damping ratio. |
participationAvailable |
False when the state matrix is defective; the participation rows are then all-zero placeholders and participationUnavailableReason says why. |
exitCode |
The exit code the run ended with, so the file carries its own verdict (1 for an unstable result). |
$ sed -n '214,217p' results/exports-docs/smallsignal.json
"rhpEigenvalueCount": 0,
"minDampingRatio": 0.00014298128375036154,
"participationAvailable": true,
"exitCode": 0
[exit 0]
impedance-stability.json (gridsim-impedance-stability/1) is written by
impedance-stability --enableExport[=path] (default results/impedance-stability-export.json).
$ head -n 30 results/exports-docs/impedance-stability.json
{
"schema": "gridsim-impedance-stability/1",
"meta": {
"Case": "IEEE 9-bus (WSCC)",
"Backend": "impedance-stability",
"GitCommit": "v1.95.294-104-g5fd850f6",
"TimestampUtc": "2026-10-29T18:21:03.7392753Z",
"ElapsedMs": 31.4133,
"Machine": "LAPTOP"
},
"provenance": "CuratedPlaceholder",
"model": {
"name": "IEEE 9-bus (WSCC)",
"baseMva": 100,
"busCount": 9
},
"inverterBus": 2,
"mode": "GridFollowingInverter",
"modelDetail": "PllOuter",
"criterion": "Mimo",
"operatingPoint": {
"vd0Pu": 1.025,
"vq0Pu": 0,
"id0Pu": 0.8282520325203251,
"iq0Pu": -0.033809249585507274,
"pPu": 0.8489583333333331,
"qPu": 0.034654480825144954,
"vd0Clamped": false
},
"converterParams": {
[exit 0]
| Field | Meaning |
|---|---|
meta, model |
Provenance and {name, baseMva, busCount}. |
provenance |
Recorded for measured inner-loop parameters, CuratedPlaceholder for flag-built ones. |
inverterBus, mode, modelDetail, criterion |
The attached converter's bus, kind (GridFollowingInverter or grid-forming), detail level and criterion (Siso or Mimo). |
operatingPoint |
vd0Pu, vq0Pu, id0Pu, iq0Pu, pPu, qPu, vd0Clamped. |
converterParams |
The converter parameters used (PLL gains, current-loop bandwidth, SCR, X/R, rating, frequency). |
frequenciesHz, gridImpedance[], converterAdmittance[], minorLoop[] |
The scan: the frequency grid, the grid impedance and converter admittance as {fHz, re, im}, and the loop gain and its eigenvalues per frequency. At reporting frequency f, gridImpedance is the positive-sequence impedance in the converter's dq frame, the stationary-frame impedance at f + f0; converterAdmittance is Y_pp and the loop gain is their product L_pp. converterImpedance[] repeats the admittance under its old, misleading name for older readers; it is deprecated. |
nyquist |
encirclements, unstableModeCount, stable, gainMarginDb, phaseMarginDeg, criticalFreqHz, minDistToMinusOne. |
scrSweep[], boundary |
The short-circuit-ratio sweep and the weak-grid boundary (criticalScr, found). |
crossval |
The right-half-plane count of the small-signal model and whether it agrees with the Nyquist verdict. |
$ sed -n '4326,4337p' results/exports-docs/impedance-stability.json
"nyquist": {
"encirclements": 0,
"unstableModeCount": 0,
"stable": true,
"gainMarginDb": "Infinity",
"phaseMarginDeg": "Infinity",
"criticalFreqHz": "NaN",
"minDistToMinusOne": 0.8740273367032224
},
"scrSweep": [
{
"scr": 1,
[exit 0]
The classical export (gridsim-classical-export/1) is written by stability --export <file> for the classical
engine as one unindented line: label, f0, stepSeconds, durationSeconds, bumpMachine, bumpRad, machines[]
(bus, h, pm, eq, delta0), delta0Perturbed, the reduced admittance as yReducedG and yReducedB, and the
sampled trajectory as sampleTimes and sampleDelta. It has no meta block.
$ head -c 420 results/exports-docs/classical.json
{"schema":"gridsim-classical-export/1","label":"WSCC-9","f0":60,"stepSeconds":0.001,"durationSeconds":5,"bumpMachine":2,"bumpRad":0.1,"machines":[{"bus":1,"h":58.50900000000001,"pm":0.7164102147448185,"eq":1.0465252938930831,"delta0":0.016170595145975637},{"bus":2,"h":12.288000000000002,"pm":1.6300000000000003,"eq":1.0338230694271975,"delta0":0.2580929329264051},{"bus":3,"h":3.8528,"pm":0.85,"eq":1.0168004124084893,"
[exit 0]
RMS event study export (gridsim-rms-events-export/1) is written by gridsim stability --rms --events <list>
--events-out <dir> (chapter 15). The folder holds:
| File | Content |
|---|---|
rms-events-machines.csv |
Every machine trace at every written step: t_s,trace,bus,delta_rad,speed_pu,eqp_pu,edp_pu,eqpp_pu,eddp_pu, each number in its shortest round-trip form. |
rms-events-converters.csv |
Written when the case has converter plant: t_s,converter,name,bus,v_pu,p_pu,q_pu,ip_pu,iq_pu,in_dip_mode. |
rms-events.json |
The case, the nominal frequency, the step, the duration, the events in time order, the verdict, the converter-iteration failures, the machine list with island references marked, everySteps and samplesWritten, provenance Synthetic and whether the result is INDICATIVE. |
run-metadata.json |
The shared run record (Run directories). |
Both CSVs start with the run header and a # provenance: line: Synthetic, a model trajectory and not a record, and
INDICATIVE when the GENROU data is synthesised. --events-every N writes every N-th integration step (steps 0, N,
2N, ...); the default 1 writes every step, and on a large case the files grow with every machine at every step.
EMT run files¶
gridsim emt writes timeseries.csv, summary.md and run-metadata.json into results/emt-out (or --out).
Produced in chapter 16.
gridsim emt case14 --dt-us 20 --duration 0.05 --source 1 --amp 1 --freq 50 --event fault:5@0.02:0.01 --out results/exports-docs/emt
$ gridsim emt case14 --dt-us 20 --duration 0.05 --source 1 --amp 1 --freq 50 --event fault:5@0.02:0.01 --out results/exports-docs/emt
emt case14: 2,500 steps @ 20us, 3 factorization(s), peak |probe| 6.903, 15 ms -> results/exports-docs/emt
emt: 2 floating node(s) pinned at 0 V in the initial-condition solve.
[exit 0]
timeseries.csv has t_seconds and one column per probe, after the # header. The default probes are the
source-bus voltage, named v:<bus>, and the first series branch current, named i:<from>-><to>. With --probe a
column is named exactly as you typed the probe (i:1-2 stays i:1-2). Values are instantaneous, in per unit.
$ head -n 12 results/exports-docs/emt/timeseries.csv
# GridSim - 2026-10-20 05:26:10 UTC
# code: git v1.95.294-173-g203c6b9c | GridSim.Core v1.95.467.0 | build Release
# runtime: .NET 10.0.12 X64 GC=Server
# machine: LAPTOP Microsoft Windows 10.0.26300
# cpu: Intel(R) Core(TM) i3-N305 - 8 logical cores - 8 GB RAM
# simd: Avx256 (4 doubles/step) FMA=True
# backend: cpu
# run: emt case14 dt=20us dur=0.05s src=bus1 amp=1 freq=50Hz rule=Trapezoidal
t_seconds,v:1,i:1->2
0,0,0
2E-05,0.999980261,0.105867021
4E-05,0.999921044,0.210673229
[exit 0]
summary.md records the step count and timestep, the source, the number of matrix refactorisations (one for the
initial matrix plus one per switching event), the peak absolute probe value, the wall time, the integration rule and whether critical damping
adjustment was on. Its last line says the run is a forward model trajectory and not a claim about the real grid.
$ cat results/exports-docs/emt/summary.md
# EMT run: case14
- steps: 2,500 @ 20 us (dur 0.05s)
- source: bus 1, amp 1 pu, 50 Hz
- refactorizations: 3
- peak |probe|: 6.90333
- wall: 15.3 ms (163,564 steps/s)
- rule: Trapezoidal, CDA: on
A forward model trajectory, not a claim about the real grid.
[exit 0]
The event catalogue¶
The schema gridsim-event-catalog/1 is the catalogue of recorded grid events that the evidence verbs replay. You author new events here.
- Written by:
replay-event --dump-catalog <path>writes the three built-ins (gb-2019-08-09,gb-2023-12-22,gb-largest-loss-1320) so you can copy and edit them; hand-authored files, for example underdata/events/. - Read by:
replay-event,scenario,h0-study,fleetandevidencethrough--catalog <file>. EverywhenUtcis checked against the 169-hour horizon on load. - Produced in: chapter 18, chapter 20.
$ head -n 26 results/exports-docs/catalog.json
{
"schema": "gridsim-event-catalog/1",
"events": [
{
"id": "gb-2019-08-09",
"title": "GB 9 Aug 2019 disturbance (Hornsea \u002B Little Barford)",
"whenUtc": "2019-08-09T15:52:33Z",
"preEventInertiaMwS": 215000,
"preEventDemandMw": 21100,
"losses": [
{
"atSeconds": 0.1,
"deltaPMw": -150,
"what": "distributed generation vector-shift trip (Ofgem 2.4.2)",
"provenance": "Recorded"
},
{
"atSeconds": 0.5,
"deltaPMw": -737,
"what": "Hornsea 1 deload 799-\u003E62 MW (Ofgem 2.4.3)",
"provenance": "Recorded"
},
{
"atSeconds": 1,
"deltaPMw": -244,
"what": "Little Barford steam turbine trip (Ofgem 2.4.4)",
[exit 0]
The file is {schema, events[]}. Each event has these fields.
| Field | Meaning |
|---|---|
id, title, whenUtc |
The event's key (matched case-insensitively), its name and its onset in UTC. |
preEventInertiaMwS, preEventDemandMw |
System inertia (MW.s) and demand (MW) before the event. |
losses[] |
Power steps {atSeconds, deltaPMw, what, provenance}. A negative deltaPMw is a loss and a positive one a restoration. |
recordedActions[] |
The same shape for protective and operator actions. Low-frequency demand disconnection enters as a positive step because load is removed. |
outcome |
What the record shows: initialRocofHzPerS, nadirHz, settleHz, lfddMwShed, provenance, notes, rocofMeasurementWindowS (0 for an instantaneous figure, above 0 for a windowed average that the simulation is scored on too) and nesoTraceCsvPath (a 1-second frequency trace that enables the per-sample RMSE check; replay-event --trace sets it for one run). |
provenance, notes |
The event's provenance tag, and free text that carries [VERIFY] where a figure is unsourced. |
responseServices[] |
Frequency-response services the system held: mw, deadbandHz, fullDeliveryDeviationHz, timeConstantS, what, provenance, kneeDeviationHz, kneeFraction. |
responseServicesComplete |
True when responseServices lists all the response held, so the generic governor term is zeroed. Default false. |
preEventFrequencyHz |
The recorded frequency the trip landed on; the replay starts in equilibrium there. Absent means nominal. |
A figure without a source carries CuratedPlaceholder and a [VERIFY] note; the tags are features, and
they stay on the figure until it is sourced. Null fields are omitted on write.
Event study outputs¶
The frequency export (gridsim-frequency-export/1) is one frequency-dynamics simulation with its full
recurrence, so RoCoF and nadir can be checked by hand from the numbers in the file. replay-event writes
baseline-frequency.json. scenario writes counterfactual-frequency.json: the device at the lowest swept inertia,
on the worst corner if swept, otherwise the first corner. tools/crossval/crossval_freq.py re-integrates the same
physics independently (chapter 27). It integrates the governor, the generic fast
response and an optional forming block, starting from nominal frequency. It checks exports without recorded response
services (the scenario counterfactual of gb-largest-loss-1320). It reports NOT CHECKED with exit 77 for an export
that has recorded services (the replay-event exports of gb-2019-08-09, 6 services, and gb-2023-12-22) and for one
that starts off nominal (config.initialFrequencyHz).
$ head -n 34 results/exports-docs/replay-2019/baseline-frequency.json
{
"schema": "gridsim-frequency-export/1",
"meta": {
"event": "gb-2019-08-09",
"gitCommit": "v1.95.294-173-g203c6b9c",
"timestampUtc": "2026-10-20T05:26:17.2316358Z",
"elapsedMs": 0,
"machine": "LAPTOP / Microsoft Windows 10.0.26300",
"provenance": "DerivedFromRecorded"
},
"inertia": {
"systemInertiaMwS": 215000,
"deviceRotationalInertiaMwS": 0,
"effectiveInertiaMwS": 215000,
"note": "E excludes fast response; FFR is a power injection (see fastResponse), never stored energy."
},
"fastResponse": {
"fastResponseMw": 0,
"timeConstantS": 0.5,
"deadbandHz": 0.015,
"note": "Separate labelled column - a power injection that arrests the nadir, distinct from inertia."
},
"demandMw": 21100,
"config": {
"nominalHz": 50,
"droopPu": 0.05,
"dampingPctPerHz": 3.3175355450236967,
"responsiveFraction": 0,
"governorTimeConstantS": 8,
"stepSeconds": 0.02,
"durationSeconds": 120
},
"systemResponseServices": [
{
[exit 0]
| Block | Fields |
|---|---|
meta |
event, gitCommit, timestampUtc, elapsedMs, machine, provenance. The scenario export names the event as <id>+<device>@<inertia>GVAs and tags it Counterfactual. |
inertia |
systemInertiaMwS, deviceRotationalInertiaMwS (real stored energy), effectiveInertiaMwS (the E of the swing equation, never below 1), note. |
forming |
Only under a grid-forming mode: mode, virtualFormingInertiaMwS (a control parameter, never summed into real inertia), formingDampingMwPerHz, formingSetpointMw, effectiveFormingInertiaMwS, note. |
fastResponse |
fastResponseMw, timeConstantS, deadbandHz, note. Fast frequency response is a power injection and is not part of E. |
demandMw |
The demand the recurrence used. |
config |
nominalHz, droopPu, dampingPctPerHz, responsiveFraction, governorTimeConstantS, stepSeconds, durationSeconds, and initialFrequencyHz when the event began off nominal. |
systemResponseServices[] |
Only when the event's recorded services were delivered: mw, deadbandHz, fullDeliveryDeviationHz, timeConstantS, kneeDeviationHz, kneeFraction. |
events[] |
The power steps, time-ordered: {atSeconds, deltaPMw}. |
recurrence[] |
The discrete recurrence as plain-text lines (more under a forming mode). |
headline |
initialRocofHzPerS (the peak rate), nadirHz, nadirSeconds, settleHz. |
points[] |
The whole trajectory: {t, hz, rocofHzPerS}, t in seconds from the first sample. |
$ sed -n '125,144p' results/exports-docs/replay-2019/baseline-frequency.json
"recurrence": [
"df = f - f0",
"pEvent(t) = sum of deltaPMw over events with atSeconds \u003C= t \u002B 1e-6*dt",
"pGovTarget = -(df/f0)/droopPu * responsiveFraction*demand ; pGov \u002B= (pGovTarget-pGov)/governorTau * dt",
"ffrTarget = clamp(-(FFR/0.5)*df, -FFR, \u002BFFR) if |df|\u003Edeadband else 0 ; pFfr \u002B= (ffrTarget-pFfr)/max(dt,ffrTau) * dt",
"pImbalance = pEvent \u002B pGov \u002B pFfr - damping*df",
"rocof = f0/(2E) * pImbalance ; f \u002B= rocof*dt"
],
"headline": {
"initialRocofHzPerS": -0.17097330951595094,
"nadirHz": 48.84096136852519,
"nadirSeconds": 76,
"settleHz": 48.89159962877177
},
"points": [
{
"t": 0,
"hz": 50,
"rocofHzPerS": 0
},
[exit 0]
The evidence dossier (gridsim-evidence/1) is written by evidence as evidence.json, with summary.md
beside it, and has the shape {schema, report}. Produced in chapter 20.
$ gridsim evidence --event gb-2019-08-09 --out results/exports-docs/evidence-2019
=== Evidence dossier: GB 9 Aug 2019 disturbance (Hornsea + Little Barford) ===
when: 2019-08-09T15:52:33.0000000Z event provenance: DerivedFromRecorded baseline gate: Reproduced
column RoCoF nadir LFDD dev inertia dev FFR
--------------------------------------------------------------------------------------------
recorded -0.170Hz/s 48.80Hz YES 0.0GVAs 0.00GW [Recorded]
simulated (no device) -0.171Hz/s 48.84Hz no 0.0GVAs 0.00GW [DerivedFromRecorded]
counterfactual (device @ 215 GVA.s) -0.147Hz/s 49.07Hz no 30.0GVAs 1.00GW [Counterfactual]
single-vs-fleet @ 215 GVA.s: aggregate nadir 49.07 Hz vs 100-unit fleet 49.07 Hz (FFR@nadir 1000 vs 1000 MW).
AVOIDED COST: 892 MW shed x 0.50 h x GBP 6,000/MWh = GBP 2,676,000 [CuratedPlaceholder]
(recorded LFDD energy avoided (MW shed x duration h) x VoLL (GBP/MWh) - grounded in the record, not a marketing figure)
VoLL [CuratedPlaceholder] duration [CuratedPlaceholder]
Baseline Reproduced; counterfactual device DOES keep the nadir above LFDD; avoided cost GBP 2,676,000 (CuratedPlaceholder)
-> results\exports-docs\evidence-2019
[exit 0]
report field |
Meaning |
|---|---|
eventId, eventTitle, whenUtc, eventProvenance |
The replayed event. |
baselineVerdict |
Reproduced, Marginal or Failed. |
counterfactualUnlocked |
True exactly when the baseline is Reproduced. |
columns[] |
Recorded, simulated and (when unlocked) counterfactual: label, provenance, rocofHzPerS, nadirHz, settleHz, lfddAtRisk (nadir at or below 48.8 Hz), deviceRotationalInertiaGvaS, deviceFfrGw. The two device fields are separate on purpose and are non-zero only on the counterfactual column. |
riskCurve |
deviceName and points[], one per inertia level, comparing no device with the device: inertiaGvaS, rocofNoDevice, nadirNoDevice, lfddNoDevice, rocofWithDevice, nadirWithDevice, lfddWithDevice, lfddAvoidedByDevice. |
fleet |
inertiaGvaS, units, staggerSeconds, aggregateNadirHz, fleetNadirHz, aggregateFfrAtNadirMw, fleetFfrAtNadirMw. |
avoidedCost |
lfddMwShedAvoided, vollPerMwh, durationHours, avoidedCostGbp, provenance, basis. |
summary |
One line of verdict. |
$ sed -n '97,115p' results/exports-docs/evidence-2019/evidence.json
"fleet": {
"inertiaGvaS": 215,
"units": 100,
"staggerSeconds": 10,
"aggregateNadirHz": 49.06951716271372,
"fleetNadirHz": 49.06953264157351,
"aggregateFfrAtNadirMw": 999.9999999999986,
"fleetFfrAtNadirMw": 999.9999999999993
},
"avoidedCost": {
"lfddMwShedAvoided": 892,
"vollPerMwh": 6000,
"durationHours": 0.5,
"avoidedCostGbp": 2676000,
"provenance": "CuratedPlaceholder",
"basis": "recorded LFDD energy avoided (MW shed x duration h) x VoLL (GBP/MWh) - grounded in the record, not a marketing figure",
"vollProvenance": "CuratedPlaceholder",
"durationProvenance": "CuratedPlaceholder"
},
[exit 0]
The gating is structural. When the baseline is not Reproduced, columns stops at two, riskCurve, fleet and
avoidedCost are absent, and evidence exits 3, so the file cannot carry an ungated claim. avoidedCost appears
only when the device keeps the nadir above the disconnection threshold and the record actually shed load. The cost is
recorded MW shed x duration x value of lost load; the defaults (6,000 GBP per MWh, 0.5 h) are CuratedPlaceholder
until you name a source with --voll-source.
The H=0 study (gridsim-h0-study/1) is written by h0-study as h0-study.json, with matrix.csv. It sweeps
synchronous inertia towards zero against a grid-forming fleet fraction. Produced in
chapter 20.
$ sed -n '1,22p' results/exports-docs/h0-2019/h0-study.json
{
"schema": "gridsim-h0-study/1",
"meta": {
"event": "gb-2019-08-09",
"gitCommit": "v1.95.294-173-g203c6b9c",
"timestampUtc": "2026-10-20T05:26:14.4192800Z",
"machine": "LAPTOP / Microsoft Windows 10.0.26300",
"gfmClass": "gfm-vsm-study-2024",
"gflClass": "gfl-wind-2024",
"provenance": "Counterfactual"
},
"baseline": {
"verdict": "Reproduced"
},
"system": {
"largestLossMw": 2768,
"totalExogenousLossMw": 2768,
"rocofSettingLossMw": 1521,
"rocofSettingWindowS": 5,
"largestSingleLossMw": 737,
"lfddFirstStageHz": 48.8,
"nominalHz": 50
[exit 0]
| Field | Meaning |
|---|---|
meta |
event, gitCommit, timestampUtc, machine, gfmClass, gflClass, provenance (Counterfactual). |
baseline |
verdict: the study runs only from a reproduced baseline. |
system |
largestLossMw (kept for existing readers: it is the total of every exogenous loss, not the largest single one), totalExogenousLossMw (the same total), rocofSettingLossMw (the losses within the first rocofSettingWindowS seconds, the dP of bareInertiaRocofHzPerS), rocofSettingWindowS, largestSingleLossMw, lfddFirstStageHz, nominalHz. |
invariantNote, idealizationNote |
Plain-text statements of what is real inertia and what the grid-forming idealisation leaves out. |
swing |
What a sidecar needs to re-integrate a cell: demandMw, config, services[], events[], cornerFactors. |
axes |
syncInertiaScales, gfmFractions, corners. |
inertiaTable[] |
The untransformed per-unit inertia: name, bus, type, hMwsPerMva, mBaseMva. |
cells[] |
One per grid point, below. |
recurrence[] |
The relations the cells follow, in plain text. |
A cell holds syncInertiaScale, gfmFraction (the requested share), realisedGfmFraction (the share whole units
realise, which can exceed the request by up to one unit), corner, systemInertiaGvaS (real), deviceRotationalInertiaMwS (always 0: the
study has no device), virtualInertiaMwS (the
grid-forming fleet's control inertia, kept apart from the real figure), formingDampingMwPerHz,
effectiveFormingInertiaMwS (real plus virtual, with no floor), the bare-system figures bareInertiaRocofHzPerS,
bareFormedRocofHzPerS and bareFormedNadirHz, the simulated rocofHzPerS, nadirHz, nadirSeconds and settleHz,
a fleet block (trippedCount, totalTrippedMw, netFleetMwAtNadir), formingDependent, formedBy,
lfddAtRisk, cascadeLossOfMains and a text assessment. A figure that is undefined, because nothing forms the
grid, is an explicit null; so is every other unset field in the file.
$ sed -n '689,711p' results/exports-docs/h0-2019/h0-study.json
"cells": [
{
"syncInertiaScale": 1,
"gfmFraction": 0,
"realisedGfmFraction": 0,
"corner": "Best",
"systemInertiaGvaS": 171.7,
"deviceRotationalInertiaMwS": 0,
"virtualInertiaMwS": 0,
"formingDampingMwPerHz": 0,
"effectiveFormingInertiaMwS": 171700,
"bareInertiaRocofHzPerS": -0.2214618520675597,
"bareFormedRocofHzPerS": -0.2088442503075672,
"bareFormedNadirHz": 48.40425498654036,
"rocofHzPerS": -0.24433439907031654,
"nadirHz": 49.58837208544303,
"nadirSeconds": 79.74,
"settleHz": 49.60001254694652,
"fleet": {
"trippedCount": 1,
"totalTrippedMw": 800,
"netFleetMwAtNadir": 2297.16961723434
},
[exit 0]
The device file that scenario --device and evidence --device read is plain JSON with no schema id:
name, rotationalInertiaMwS, fastResponseMw, fastResponseTimeConstantS and fastResponseDeadbandHz. Unknown
keys, such as a _provenance note, are ignored. Real stored inertia and fast response are separate fields and are
never merged.
$ head -n 6 data/devices/tsgb-fleet.json
{
"name": "tsgb-fleet-1.14GW",
"rotationalInertiaMwS": 13700,
"fastResponseMw": 1140,
"fastResponseTimeConstantS": 0.05,
"fastResponseDeadbandHz": 0.015,
[exit 0]
Time-series CSVs¶
transient writes timeseries.csv: one row per logged slice (--log-every), after the # header. The slice at
the start, the end and the trip window are always logged. The same slices are the telemetry that pre-solved replay
plays back in the apps. A run with any non-converged slice exits 3.
$ gridsim transient case9 --minutes 1 --dt-ms 100 --export-every 100 --out results/exports-docs/transient
[transient] case9: done in 0.3s nadir 60.000Hz maxRoCoF -0.001Hz/s exports 7 -> results\exports-docs\transient
[transient] case9: 600 slices @ 100ms (1 min) on Newton-Raphson (dense)...
[exit 0]
$ head -n 14 results/exports-docs/transient/timeseries.csv
# GridSim - 2026-10-20 05:26:20 UTC
# code: git v1.95.294-173-g203c6b9c | GridSim.Core v1.95.467.0 | build Release
# runtime: .NET 10.0.12 X64 GC=Server
# machine: LAPTOP Microsoft Windows 10.0.26300
# cpu: Intel(R) Core(TM) i3-N305 - 8 logical cores - 8 GB RAM
# simd: Avx256 (4 doubles/step) FMA=True
# backend: cpu
# run: transient case=case9 minutes=1 dt=100ms slices=600 solver=Newton-Raphson (dense)
tick,t_seconds,freq_hz,rocof_hz_s,gen_mw,demand_mw,iters,max_mismatch,vmin_pu,vmax_pu,solve_ms,converged
1,0.1000,60.00003,0.00032,319.64,315.00,4,1.646E-014,0.99563,1.04000,36.539,True
2,0.2000,60.00004,0.00006,319.64,315.00,0,1.600E-014,0.99563,1.04000,1.640,True
3,0.3000,60.00001,-0.00032,319.64,315.00,0,1.600E-014,0.99563,1.04000,0.074,True
4,0.4000,60.00000,-0.00007,319.64,315.00,0,1.600E-014,0.99563,1.04000,0.155,True
5,0.5000,59.99999,-0.00012,319.64,315.00,0,1.600E-014,0.99563,1.04000,0.086,True
[exit 0]
| Column | Meaning |
|---|---|
tick, t_seconds |
The slice number and the simulated time. |
freq_hz, rocof_hz_s |
The swing-equation frequency and its rate of change. |
gen_mw, demand_mw |
Total generation and demand. |
iters, max_mismatch, converged |
The slice's power-flow outcome; iters is 0 when the warm start was already converged. |
vmin_pu, vmax_pu |
The voltage range. |
solve_ms |
The solve time of the slice on this machine. |
summary.md beside it holds the run totals: slice count, wall time, solver, frequency nadir and its time, maximum
RoCoF, the number of non-converged slices and the number of math exports.
$ head -n 24 results/exports-docs/transient/summary.md
# Transient - case9
GridSim - 2026-10-20 05:26:20 UTC
code: git v1.95.294-173-g203c6b9c | GridSim.Core v1.95.467.0 | build Release
runtime: .NET 10.0.12 X64 GC=Server
machine: LAPTOP Microsoft Windows 10.0.26300
cpu: Intel(R) Core(TM) i3-N305 - 8 logical cores - 8 GB RAM
simd: Avx256 (4 doubles/step) FMA=True
backend: cpu
run: transient case=case9 minutes=1 dt=100ms slices=600 solver=Newton-Raphson (dense)
- slices: 600 @ 100 ms = 1 sim-min
- wall: 0.3 s (600 slices/s)
- solver: Newton-Raphson (dense)
- frequency nadir: 59.9998 Hz at t=38.700 s
- max |RoCoF|: -0.0011 Hz/s at t=57.400 s
- non-convergence: 0
- math exports: 7
[exit 0]
gda-replay writes state-estimate.csv: one row per reconstructed timestamp (index, time_utc, load_factor,
converged, iterations, max_mismatch, total_loss_mw, vmin_pu, vmax_pu). With --bess, state-estimate.csv
gains one # COUNTERFACTUAL comment line after its column names, and the run adds storage-counterfactual.csv, which
starts with its own single # COUNTERFACTUAL line and then its column names, with no # header block. The comment
marks hypothetical storage on recorded history, never an input to another replay. The columns of
storage-counterfactual.csv are index, site, request_mw, delivered_mw,
aux_mw, soc, temp_c and limited (1 when a limit curtailed the request). Produced in
chapter 22 and chapter 13.
This state-estimate.csv is from a --bess run:
$ head -n 12 results/exports-docs/gda-replay/state-estimate.csv
# GridSim - 2026-10-20 05:26:12 UTC
# code: git v1.95.294-173-g203c6b9c | GridSim.Core v1.95.467.0 | build Release
# runtime: .NET 10.0.12 X64 GC=Server
# machine: LAPTOP Microsoft Windows 10.0.26300
# cpu: Intel(R) Core(TM) i3-N305 - 8 logical cores - 8 GB RAM
# simd: Avx256 (4 doubles/step) FMA=True
# backend: cpu
# run: gda-replay case=case9 series=synthetic-case9 steps=48
index,time_utc,load_factor,converged,iterations,max_mismatch,total_loss_mw,vmin_pu,vmax_pu
# COUNTERFACTUAL: hypothetical storage added to recorded history (ProvenanceTag.Counterfactual) - never an input to another replay
0,2024-01-01T00:00:00.0000000Z,0.7000,True,4,3.698E-015,2.189,1.02356,1.04076
1,2024-01-01T00:30:00.0000000Z,0.6782,True,2,2.242E-009,2.086,1.02500,1.04117
[exit 0]
$ head -n 12 results/exports-docs/gda-replay/storage-counterfactual.csv
# COUNTERFACTUAL: hypothetical storage on recorded history (ProvenanceTag.Counterfactual)
index,site,request_mw,delivered_mw,aux_mw,soc,temp_c,limited
0,LFP 5MW/10MWh @5,0.000,0.000,0.0100,0.49998,25.00,0
1,LFP 5MW/10MWh @5,-5.000,-5.000,0.0100,0.71318,27.58,0
2,LFP 5MW/10MWh @5,-5.000,-5.000,0.0117,0.92458,30.00,0
3,LFP 5MW/10MWh @5,-5.000,-0.606,0.0144,0.95000,29.93,1
4,LFP 5MW/10MWh @5,-5.000,-0.000,0.0100,0.95000,29.81,1
5,LFP 5MW/10MWh @5,-5.000,-0.000,0.0100,0.95000,29.70,1
6,LFP 5MW/10MWh @5,-5.000,-0.000,0.0100,0.95000,29.58,1
7,LFP 5MW/10MWh @5,-5.000,-0.000,0.0100,0.95000,29.47,1
8,LFP 5MW/10MWh @5,-5.000,-0.000,0.0100,0.95000,29.37,1
9,LFP 5MW/10MWh @5,-5.000,-0.000,0.0100,0.95000,29.26,1
[exit 0]
The demand series that gda-replay --series reads is JSON: a name and steps[] of {timeUtc,
systemLoadFactor}, with an optional perBusFactor object (bus id to factor) on a step for per-bus demand. Every
timestamp is checked against the 169-hour horizon.
$ cat results/exports-docs/inputs/series.json
{
"name": "docs-three-steps",
"steps": [
{ "timeUtc": "2024-01-01T00:00:00Z", "systemLoadFactor": 0.70 },
{ "timeUtc": "2024-01-01T00:30:00Z", "systemLoadFactor": 0.68 },
{ "timeUtc": "2024-01-01T01:00:00Z", "systemLoadFactor": 0.66 }
]
}
[exit 0]
gridsim gda-replay --series results/exports-docs/inputs/series.json --case case9 --out results/exports-docs/gda-series
$ gridsim gda-replay --series results/exports-docs/inputs/series.json --case case9 --out results/exports-docs/gda-series
=== Historical state estimate: case9 over docs-three-steps ===
reconstructed 3 timestamps (2024-01-01T00:00:00.0000000Z .. 2024-01-01T01:00:00.0000000Z)
non-converged: 0
system loss over the series: 1.9 .. 2.2 MW
-> results\exports-docs\gda-series
[exit 0]
trip --export <file.csv> writes a frequency trace: two columns, dtm (a timestamp with millisecond
resolution) and f (hertz), at the 20 ms step of the simulation. It is the same format as the NESO trace in
data/neso/, so gridsim replay <file> plays it back and gridsim freq <file> summarises it.
$ head -n 4 results/exports-docs/trip.csv
dtm,f
2000-01-01 00:00:00.000,50.00000
2000-01-01 00:00:00.020,50.00000
2000-01-01 00:00:00.040,50.00000
[exit 0]
cpf --pv-out and stress --pv-out write PV curves: one row per converged point and bus, in solved order, in
round-trip form, with no header lines. cpf names the multiplier lambda; stress names it load_factor.
$ head -n 3 results/exports-docs/pv.csv
case,lambda,bus_id,vm_pu
IEEE 9-bus (WSCC),1,1,1.04
IEEE 9-bus (WSCC),1,2,1.025
[exit 0]
dcpf --out <file.csv> writes every branch flow (index, name, from, to, flow_mw, rate_mva,
loading_pct) and, in <stem>-buses.csv, every bus angle and net injection (bus_id, va_deg, p_inj_mw). The
shared writer adds <stem>-islands.csv and <stem>-summary.csv, and --json writes the same tables as one
document; see the result files.
Markdown and CSV reports¶
The reports of bench, chain, stress, nminus1-sweep and conformance are a Markdown table for reading and a
CSV for scripts. The Markdown starts with the indented header block; the CSVs of the first four carry a commit
column instead of a header. The exceptions are chain --bess, whose files have neither, and screen, which writes
only a CSV. Produced in chapter 28,
chapter 11 and chapter 27.
chain writes results/chain-<case>.md and .csv: one row per warm-started step.
$ head -n 24 results/exports-docs/chain-case14.md
GridSim - 2026-10-20 05:26:07 UTC
code: git v1.95.294-173-g203c6b9c | GridSim.Core v1.95.467.0 | build Release
runtime: .NET 10.0.12 X64 GC=Server
machine: LAPTOP Microsoft Windows 10.0.26300
cpu: Intel(R) Core(TM) i3-N305 - 8 logical cores - 8 GB RAM
simd: Avx256 (4 doubles/step) FMA=True
backend: cpu
run: chain case=case14 steps=5 mode=ramp ramp=0.5%
Each step feeds the previous solved voltages in as the initial condition (warm start).
mode=fixed: same operating point re-solved (expect iters->1, no drift). ramp: +0.5% load/step. daily: 24h demand curve.
| Step | Load x | Warm-start iters | Mismatch | Vmin | Vmax |
|-----:|-------:|-----------------:|---------:|-----:|-----:|
| 0 | 1.000 | 2 | 1.32E-010 | 1.010 | 1.090 |
| 1 | 1.005 | 2 | 8.02E-013 | 1.010 | 1.090 |
| 2 | 1.010 | 2 | 7.94E-013 | 1.010 | 1.090 |
| 3 | 1.015 | 2 | 8.09E-013 | 1.010 | 1.090 |
| 4 | 1.020 | 2 | 8.00E-013 | 1.010 | 1.090 |
Cold (flat-start) iters: 4. Max warm-start iters over 4 warm-started step(s) (steps 1-4; step 0 starts from the case): 2.
[exit 0]
$ head -n 6 results/exports-docs/chain-case14.csv
step,load_factor,iters,mismatch,vmin,vmax,commit
0,1.0000,2,1.316E-010,1.0100,1.0900,v1.95.294-173-g203c6b9c
1,1.0050,2,8.017E-013,1.0100,1.0900,v1.95.294-173-g203c6b9c
2,1.0100,2,7.936E-013,1.0100,1.0900,v1.95.294-173-g203c6b9c
3,1.0150,2,8.088E-013,1.0100,1.0900,v1.95.294-173-g203c6b9c
4,1.0200,2,8.003E-013,1.0100,1.0900,v1.95.294-173-g203c6b9c
[exit 0]
| Column | Meaning |
|---|---|
step, load_factor |
The step number and the demand multiplier. |
iters, mismatch |
Newton iterations and the final mismatch. |
vmin, vmax |
The voltage range. |
commit |
The commit that produced the row. |
With --bess, chain writes results/chain-storage-<case>.md and .csv instead. Neither file has the header block,
and the CSV has no commit column. The CSV has step, hour,
load_factor, demand_mw, price, then seven columns per site prefixed s0_, s1_, ... (request_mw,
delivered_mw, aux_mw, soc, temp_c, fade, limited), then slack_mw, vmin, vmax, loss_mw and
converged.
$ head -n 4 results/exports-docs/chain-storage.csv
step,hour,load_factor,demand_mw,price,s0_request_mw,s0_delivered_mw,s0_aux_mw,s0_soc,s0_temp_c,s0_fade,s0_limited,slack_mw,vmin,vmax,loss_mw,converged
0,0,1,315,,5,5,0.01,0.265934,26.9214,0.000145671,0,66.6067,0.995624,1.04,4.59671,1
1,0.5,1,315,,5,4.48479,0.01,0.0499972,28.9905,0.000206137,1,67.1262,0.995625,1.04,4.60094,1
2,1,1,315,,5,0,0.01,0.0499764,28.9346,0.000249144,1,71.6511,0.995631,1.04,4.64112,1
[exit 0]
bench writes results/benchmark-results.md, benchmark-results.csv and benchmark-results.run-metadata.json (or the paths you
give with --out): the solver-by-backend matrix over the whole case archive. The CSV columns are engine, case,
buses, gens, branches, conv, iters, mismatch, mean_ms, runs and commit.
$ head -n 5 results/exports-docs/bench.csv
engine,case,buses,gens,branches,conv,iters,mismatch,mean_ms,runs,commit
cpu-dense,case14,14,5,20,True,2,1.316E-010,32.72,1,98048023
cpu-dense,case18,18,1,17,True,4,5.969E-014,7.37,1,98048023
cpu-dense,pglib_opf_case3_lmbd,3,3,3,False,100,6.763E+000,18.39,1,98048023
cpu-dense,case9 (WSCC),9,3,9,True,4,1.646E-014,32.16,1,98048023
[exit 0]
stress writes results/stress.md and .csv (case, buses, max_loadability, vmin_at_max, commit).
nminus1-sweep writes results/nminus1-sweep.md and .csv (case, buses, branches, screened, secure,
violations, islanded, solves_per_s, wall_s, sampled, commit, base_violations, not_solved).
$ head -n 4 results/exports-docs/stress.csv
case,buses,max_loadability,vmin_at_max,commit
case14,14,1.75,0.714,v1.95.294-173-g203c6b9c
[exit 0]
$ head -n 4 results/exports-docs/sweep.csv
case,buses,branches,screened,secure,violations,islanded,solves_per_s,wall_s,sampled,commit,base_violations,not_solved
case14,14,20,20,16,3,0,477.4,0.04,False,v1.95.294-173-g203c6b9c,1,1
[exit 0]
conformance writes conformance.md, conformance.csv and run-metadata.json into results/conformance. The CSV
has # header lines, then case, buses, converged, gate_pass, law_number, law_name, status, residual,
unit, tolerance and commit: one row per case and law.
screen --csv <file> writes the only file screen produces, with no Markdown report, no header lines and no
commit column. It is the RoCoF screening table: name, lost_mw, remaining_inertia_gva_s,
peak_rocof_hz_per_s, nadir_hz, rocof_breach, statutory_breach, lfdd_risk.
$ head -n 5 results/exports-docs/screen-uk.csv
name,lost_mw,remaining_inertia_gva_s,peak_rocof_hz_per_s,nadir_hz,rocof_breach,statutory_breach,lfdd_risk
Midlands CCGT (marginal),5625.0,126.70,-1.1099,46.486,True,True,True
East Anglia Offshore Wind,4000.0,171.70,-0.5824,47.757,True,True,True
Hinkley Point Nuclear,2880.0,158.90,-0.4531,48.340,True,True,True
Yorkshire CCGT,2400.0,151.70,-0.3955,48.594,True,True,True
[exit 0]
gridsim-cli-result/1 - result files¶
The verbs that print a console table can also write their result as a file. --json <file.json> writes one document,
--csv <file.csv> writes CSV files, and --out <file.json|file.csv> picks the format by extension. The console output
does not change; every message about the files goes to stderr. Chapter 19 lists
the verbs and the rules, including that the files are written only once the verb has a result, and that a failing
verdict is still written with its exit code inside.
| Field | Meaning |
|---|---|
schema |
gridsim-cli-result/1. |
tool |
gridsim <version>+<commit>. |
verb, args |
The verb and the full argument list as run. |
exitCode |
The exit code the run ends with. |
summary |
The verb's scalar results, as {key: value}. |
tables |
The verb's tables, each an array of {column: value} rows. |
$ head -n 26 results/exports-docs/losses.json
{
"schema": "gridsim-cli-result/1",
"tool": "gridsim 1.95.467+203c6b9cc0edff55f2bd73642c94dc0963a82b92",
"verb": "losses",
"args": [
"losses",
"case14",
"--json",
"results/exports-docs/losses.json",
"--csv",
"results/exports-docs/losses.csv"
],
"exitCode": 0,
"summary": {
"case": "IEEE 14-bus",
"buses": 14,
"total_p_loss_mw": 13.393272357431766,
"total_q_loss_mvar": 30.122388044873993,
"shunt_p_mw": 0,
"reconciliation_residual_mw": 2.362554596402333E-13
},
"tables": {
"branches": [
{
"name": "",
"from": 1,
[exit 0]
Keys and column names are snake_case, and numbers are in round-trip form. With --csv the main table goes to the
file you name, every other table to <stem>-<table>.csv, and the summary to <stem>-summary.csv (key,value,
beginning with verb and exit_code). The CSV files carry no # header.
$ head -n 4 results/exports-docs/losses.csv
name,from,to,p_loss_mw,q_loss_mvar,loading_pct,type
,1,2,4.297600336020395,7.271958043463716,0,line
,1,5,2.762872592516459,6.084349851734967,0,line
,2,3,2.3232693341752366,5.162435824094027,0,line
[exit 0]
$ head -n 12 results/exports-docs/dcopf-summary.csv
key,value
verb,dcopf
exit_code,0
case,IEEE 14-bus
buses,14
line_limits_enforced,true
feasible,true
total_load_mw,258.99999999999994
total_generation_mw,258.99999999999994
total_cost_gbp_per_h,12949.999999999996
network_constrained,false
[exit 0]
Authored inputs¶
The generator cost file (gridsim-costs/1) is read by dcopf --costs <file.json> and acopf --costs <file.json> and laid over the case's
generators. A unit is named by index (1-based, in the case's generator order, out-of-service units included) or by
name, never both. model is polynomial (coefficients, highest order first, as MATPOWER writes them) or
piecewiseLinear (points of [MW, cost per hour]); startup and shutdown are optional and carried only. A unit
the file does not name keeps the case's own curve. An unknown key is refused, and a bad curve is refused naming its
entry. Produced in chapter 6.
$ cat results/exports-docs/inputs/costs.json
{
"format": "gridsim-costs/1",
"generators": [
{ "index": 1, "model": "polynomial", "coefficients": [0.11, 5, 150] },
{ "index": 2, "model": "piecewiseLinear", "points": [[0, 0], [100, 2500], [163, 6000]] }
]
}
[exit 0]
$ gridsim dcopf case9 --costs results/exports-docs/inputs/costs.json
DC-OPF - IEEE 9-bus (WSCC) (9 buses) [cheapest secure dispatch on generator cost curves: costs.json]
demand 315 MW generation 315 MW total cost 6,314.98 cost units/h (no line limit binds)
dispatch (by output):
gen bus 3 110.32 MW marginal 28.0290 cost units/MWh
gen bus 1 104.68 MW marginal 28.0290 cost units/MWh
gen bus 2 100.00 MW marginal 25.0000 cost units/MWh
[exit 0]
The switched-shunt file (gridsim-switched-shunts/1) is read by pf --switched-shunts <file.json>. Each shunt
needs bus, stepMvar, maxSteps, vminPu and vmaxPu; minSteps, initialSteps and name are optional. The
solver switches steps until the bus voltage sits inside the band. An unknown key is refused.
$ cat results/exports-docs/inputs/shunts.json
{
"format": "gridsim-switched-shunts/1",
"shunts": [
{ "bus": 5, "stepMvar": 10, "minSteps": 0, "maxSteps": 4, "initialSteps": 0, "vminPu": 1.02, "vmaxPu": 1.04, "name": "C5" }
]
}
[exit 0]
$ gridsim pf case9 --switched-shunts results/exports-docs/inputs/shunts.json
Power flow - IEEE 9-bus (WSCC) (9 buses) [1 switched shunt(s)]
converged in 4 iteration(s), mismatch 2.16E-014 pu losses 4.619 MW / -103.799 MVAr
voltage range 0.9984 - 1.0400 pu
switched shunts: 2 pass(es), settled
C5 bus 5 steps 1 of 0..4 10.00 MVAr at 1 pu V 1.0221 pu (band 1.02-1.04)
lowest voltages:
bus 9 0.9984 pu -3.967 deg
bus 7 1.0175 pu 0.738 deg
bus 5 1.0221 pu -3.720 deg
bus 2 1.0250 pu 9.281 deg
bus 3 1.0250 pu 4.658 deg
bus 8 1.0269 pu 3.727 deg
bus 4 1.0292 pu -2.209 deg
bus 6 1.0345 pu 1.966 deg
bus 1 1.0400 pu 0.000 deg
[exit 0]
The three-phase feeder files gridsim-dist3ph/1 and gridsim-dist3ph-measurements/1 are the inputs of the
experimental dist3ph verb. They are matched exactly by schema id, case-sensitively, and an unknown member is
refused; see technical chapter 18.
Crosswalks and the interposer files¶
The point map (gridsim-rtu-pointmap/1) is the crosswalk from a protocol's point addresses to estimator
measurements. You write one per RTU, because an address has no meaning of its own; gridsim interpose --crosswalk
<map.json> reads it. Produced in chapter 25.
$ cat docs/_captures/interpose/iec104-case14.json
{
"schema": "gridsim-rtu-pointmap/1",
"case": "case14",
"baseMva": 100.0,
"bindings": [
{ "address": "IOA:1001", "kind": "VmPu", "bus": 4, "sigma": 0.01 },
{ "address": "IOA:1002", "kind": "VmPu", "bus": 5, "sigma": 0.01 },
{ "address": "IOA:1003", "kind": "VmPu", "bus": 9, "baseKv": 132.0, "sigma": 0.01 },
{ "address": "IOA:2001", "kind": "PFlowMw", "from": 1, "to": 2, "scale": 0.1, "sigma": 2.0 },
{ "address": "IOA:2002", "kind": "PFlowMw", "from": 1, "to": 5, "scale": 0.1, "sigma": 2.0 },
{ "address": "IOA:3001", "kind": "VaDeg", "bus": 4, "datumAngleDeg": 10.0, "sigma": 0.5 }
]
}
[exit 0]
| Field | Meaning |
|---|---|
schema, case, baseMva |
The schema id, the case the bindings were authored against, and the system base (default 100). |
bindings[].address |
The native address: IOA:1001 for IEC 104, MB:U<unit>:<fc>:@<register> for a Modbus register by its absolute address, for example MB:U1:3:@100, or MB:U<unit>:<fc>:<index> for its position in the response. An address bound twice is refused. |
bindings[].kind |
The measurement kind, as in a measurement set. |
bindings[].bus or from, to, circuit, end |
What the point measures. |
bindings[].scale |
A raw-to-engineering factor applied first (default 1). Modbus registers commonly hold the value times 10 or 100, so set scale to the reciprocal (0.1 or 0.01). |
bindings[].baseKv, baseAmps |
The divisors that convert a reading in kV or amperes to per unit (default 1, meaning the RTU already reports per unit). |
bindings[].sigma |
The measurement standard deviation (default 0.01). |
bindings[].datumAngleDeg |
Subtracted from angle kinds, to re-reference an external feed to the slack. |
bindings[].dataType, wordOrder |
Modbus only: AsDecoded (the default: the value as the protocol decoded it), U16, S16, U32, S32 or F32, and HighWordFirst or LowWordFirst for the 32-bit types. A signed quantity needs S16. |
interpose --measurements-dir <dir> writes each window as a gridsim-measurements/1 file; its rows carry
"source": "rtu".
$ head -n 22 docs/_captures/interpose/iec104-window-1.json
{
"schema": "gridsim-measurements/1",
"case": "case14",
"baseMva": 100,
"takenUtc": "2026-07-20T13:45:13.25Z",
"note": "mapped from RTU interposer",
"measurements": [
{
"kind": "VmPu",
"value": 1.0210000276565552,
"sigma": 0.01,
"bus": 4,
"from": 0,
"to": 0,
"end": "From",
"source": "rtu",
"isBranch": false
},
{
"kind": "VmPu",
"value": 0.9869999885559082,
"sigma": 0.04,
[exit 0]
--out-store <dir> also archives each window as Parquet under <dir>/Parquet/<source id>/year=YYYY/week=WW/, and
writes a firewall marker, <dir>/Parquet/<source id>/_QUARANTINE.json, beside the year= folders. The marker states that the store is live SCADA telemetry,
not evidence, and must never be merged into the GDA public tables. GridSim refuses to open a store inside the GDA
lake.
$ cat docs/_captures/interpose/quarantine-sidecar.json
{
"store": "rtu_live",
"source_class": "telemetry",
"evidential": false,
"reason": "live SCADA/RTU telemetry - GDA explicit exclude-by-rule (README \u0027does NOT use SCADA data\u0027; manual Table 1.3)",
"never_merge_into": "GDA unified (MERGE-only) tables",
"horizon_hours": 169,
"written_by": "gridsim interpose"
}[exit 0]
gridsim-audit/1 - the audit log¶
The global --audit-log <file> records every estimate, crossval and interpose run in a tamper-evident log. It
is UTF-8 with no byte-order mark, one compact JSON object per line, and every line has these fields in this order.
| Field | Meaning |
|---|---|
format |
Always gridsim-audit/1. |
seq |
The line's zero-based position; it must equal the position in the file. |
runUtc |
When the record was written. |
stateUtc |
The grid instant the record describes, or null. |
provenance |
A provenance tag name. |
summary |
actor=<who> action=<verb> inputs=<name>:<sha256>,... outcome=<text>. A file input is hashed by its bytes, a case folder by a hash over its files, a bundled case by the hash of its name. |
prevHash |
The hash of the previous line, or GENESIS on the first. |
hash |
The SHA-256, in upper-case hex, over the line's fields and prevHash. |
gridsim --audit-log results/exports-docs/audit.log estimate case9 --synth --out results/exports-docs/estimate-audit
$ gridsim --audit-log results/exports-docs/audit.log estimate case9 --synth --out results/exports-docs/estimate-audit
=== WLS state estimate: case9 ===
status: Converged iterations: 5 max |dx|: 1.82E-011
measurements: 63 states: 17 redundancy (dof): 46
observability: observable (rank 17/17)
consistency: J = 43.07 vs chi2(95%) = 62.83 -> PASS
vs synthetic truth: max |dVm| = 5.920E-004 pu max |dVa| = 6.102E-002 deg
-> results\exports-docs\estimate-audit
audit: results\exports-docs\audit.log entries=1 head=1408D399580FD9521E26323D5CCA2A9C00DDB3D2E225D20E11665701BA2A0BA9
[exit 0]
$ head -n 1 results/exports-docs/audit.log
{"format":"gridsim-audit/1","seq":0,"runUtc":"2026-10-04T14:31:32.5952612Z","stateUtc":"2024-03-01T12:00:00.0000000Z","provenance":"Synthetic","summary":"actor=analyst@workstation action=estimate inputs=case-id:D784A646F9FED50DEE41E22A244DBAC3BF6B790D2476876CCB2B3A737613CBEB,synthetic-measurements:E9281C961D5E9D1471BB0178820A6EBED4DC3FA850FF67BB3486DA351A69CBEE outcome=exit=0 method=Wls status=Converged observable=True J=43.07228520174633 chi2=PASS dof=46 removed=0 measurements=63","prevHash":"GENESIS","hash":"E9701F8399B010179142F145A733B2C2BA2BD19931AECCEE808A071299B8068A"}
[exit 0]
gridsim audit verify <file> recomputes the chain and prints OK or TAMPERED with the first broken entry's index
and reason; audit show prints every entry when the log verifies. Verification catches an edited, deleted, inserted
or reordered line. Removing entries from the end, or rewriting the whole file with recomputed hashes, leaves a
consistent chain, and is caught only against an anchor (the entry count and head hash from an earlier verify) kept
where the writer cannot change it. See chapter 26 and
technical chapter 31.
$ gridsim audit verify results/exports-docs/audit.log
OK results/exports-docs/audit.log: 1 entries, head 1408D399580FD9521E26323D5CCA2A9C00DDB3D2E225D20E11665701BA2A0BA9
[exit 0]
v2 documents¶
The v2 schema describes a case as a manifest (gridsim-case/2) over network files (gridsim-network/2), with
libraries (gridsim-library/2) and kind specifications (gridsim-kind/2). The documents are validated against the
JSON Schema files under schemas/v2, and a gridsim-case/2 manifest loads as a case through the same model builder
as a v1 case folder. Every other file in this chapter is v1. Technical chapter 28
defines the members. You meet these files in two places. The case editor writes them
(chapter 33), and gridsim validate checks any of them and picks the schema from the
document's schema tag:
gridsim validate tests/GridSim.Tests/fixtures/v2/networks/good.network.json tests/GridSim.Tests/fixtures/v2/networks/bad-bus-type.network.json
$ gridsim validate tests/GridSim.Tests/fixtures/v2/networks/good.network.json tests/GridSim.Tests/fixtures/v2/networks/bad-bus-type.network.json
OK tests/GridSim.Tests/fixtures/v2/networks/good.network.json (gridsim-network/2)
FAIL tests/GridSim.Tests/fixtures/v2/networks/bad-bus-type.network.json (gridsim-network/2): 1 error(s)
/buses/0/type: value "swing" is not one of the allowed values ["pq", "pv", "slack"].
validate: 1 of 2 document(s) valid (schemas: src\GridSim.Cli\bin\Release\net10.0\schemas\v2)
[exit 3]
A v2 file allows // comments and trailing commas, and a network file names its members in plain words (baseKv,
inertiaH, rPu, ratingMva). The writer of the v1 case folder is gridsim convert
(chapter 2).
gridviz.json - the visualiser scene¶
gridsim export [<case>] [<out.json>] [--dir <folder>] [--frames N] solves the model over a simulated day (72 frames by
default, model data/uk) and writes results/gridviz.json for an external visualiser. It carries no schema string
and is written unindented. The network is static: name, baseMva, buses[] of {id, name, kv, type, x, y}, and
branches[] of {from, to, name, transformer}. Each entry of frames[] is one load level: hour, loadGw, genGw,
lossMw, freqHz (the model's nominal frequency), converged, and per-bus arrays vm, vaDeg, pInjMw and
qInjMvar in buses order. A frame with converged false is not a network state and must not be drawn as one.
$ gridsim export results/exports-docs/gridviz.json --frames 4
Exported 20 buses, 24 branches, 4 frames -> results\exports-docs\gridviz.json
[exit 0]
$ head -c 520 results/exports-docs/gridviz.json
{"name":"GB reduced (10-zone)","baseMva":10000,"buses":[{"id":1,"name":"North Scotland 400kV","kv":400,"type":"Pv","x":0.45,"y":0.97},{"id":2,"name":"South Scotland 400kV","kv":400,"type":"Pv","x":0.42,"y":0.85},{"id":3,"name":"North England 400kV","kv":400,"type":"Pv","x":0.45,"y":0.72},{"id":4,"name":"Yorkshire \u0026 Humber 400kV","kv":400,"type":"Pv","x":0.57,"y":0.66},{"id":5,"name":"North Wales \u0026 Mersey 400kV","kv":400,"type":"Pv","x":0.36,"y":0.6},{"id":6,"name":"Midlands 400kV","kv":400,"type":"Slack",
[exit 0]
Other files you may meet¶
- Crash logs. An unhandled exception writes a plain-text crash log (the exception, build, runtime, operating
system and arguments) to the folder named by
GRIDSIM_CRASH_DIR, else to%LOCALAPPDATA%\GridSim\crashon Windows. See chapter 30. - The GDA data products. The estimation pipeline's Parquet tables (
estimated_state,state_divergence,estimation_closureand the GSP measurement tables) and the bridge's wire messages are defined in technical chapter 10 and described in chapter 24. - MATPOWER and JSON case folders. These are loader inputs, documented in
chapter 2.
gridsim convert <input> <output>also writes them: an output that ends in.mis written as a MATPOWER file, an output that ends in.jsonas a JSON v2 case (agridsim-case/2manifest and itsgridsim-network/2file, with the asset classes as case-local templates, the binding rules and the protective devices), and any other output as a v1 JSON case folder. It never overwrites, and it reads the result back and solves both to prove the conversion (see chapter 19). - Case-folder documents for dynamics and protection.
asset-classes.jsonandprotections.jsonsit in a case folder and are read with it. Each is a bare JSON array, with noschemamember and no schema id. They are described in chapter 29.
Pitfalls¶
- A CSV that will not parse. Reports with a
#header needcomment='#'; reports without one (bench,chain,stress,nminus1-sweep,screen, the shared-writer CSVs) do not.storage-counterfactual.csvhas a single# COUNTERFACTUALline and needs it too. chiSquarePassedisnull. The test did not run. ReadchiSquareTestor thestatus; do not treatnullas a pass.- A date is refused on load. A
takenUtcorwhenUtcwithin 169 hours of now, or in the future, is rejected. Use a date older than that. - A measurement row is refused. A row without
kindorvaluefails, naming the row. Aschemaother thangridsim-measurements/1fails. - A cost or shunt file is refused. An unknown key fails by name; a misspelt field cannot silently drop a cost.
- No Jacobian in an export. A sparse solve, a BFS solve and a system above 300 buses omit it; pass
--export-jacobianwith the dense engine to force it. rankis -1 in an estimate export. The state size exceeds 2,500 (DenseEstimationLimits.RankProofMaxState), so the sparse backend proved observability by an island sweep and skipped the rank. At or below 2,500 states it reports a real rank, as the dense backend always does.- An audit log shorter than you expect. Verification cannot see entries removed from the end; keep an anchor.
- Two runs overwrite each other. A run directory is reused as given, and an existing output is replaced (an input,
such as the case being read, never is: ch. 19, Output files). Pass a distinct
--outper run.
Limits¶
- The solve export omits per-asset dynamics classes and protection bundles, so a re-imported export runs power flow and short-circuit studies but not asset-driven dynamics.
- The estimate export's
modelblock omitsfrequencyHz, ZIP loads, OLTCs, boundaries and the short-circuit data (it carriespmin), so it rebuilds the network for the estimation sidecars but not for a full solve replay; a reader that needs the frequency must take it from elsewhere. - The classical export has no
metablock and no provenance tag. - Tail truncation of an audit log is invisible without an external anchor.
See also¶
- Technical chapter 10, File and wire formats - the field-by-field reference.
- Technical chapter 28, The v2 schema - the case, network, library and kind documents.
- Chapter 19, CLI reference - every verb, its flags and its output paths, and the result files.
- Chapter 5, Running power flows - the solve export.
- Chapter 21, State estimation - the estimate export, sidecars and measurement sets.
- Chapter 18, Replaying recorded events and chapter 20, Counterfactuals - the event catalogue and the study outputs.
- Chapter 17, Harmonic studies, chapter 16, Electromagnetic transients and chapter 15, RMS, modal and impedance stability - their exports.
- Chapter 25, Interposing live telemetry - the point map and window files.
- Chapter 26, Audit log, deployment and assurance - the audit log.
- Chapter 27, Validation tools - the tools that consume the exports.