User manual — chapter list

Exports and formats - a field guide

What GridSim writes, which verb writes it, and which tools read it. This is the practical map, not the schema reference - field-by-field definitions live in the technical manual (../technical/10-file-and-wire-formats.md). All JSON is camelCase with enums by name; all numbers are invariant-culture.

Run directories and run-metadata.json

Every report-producing verb writes into an output directory (default under results/) and drops a run-metadata.json sidecar so the run is self-describing: who ran it, on what code, on what machine. Written by RunInfo.WriteJson; read by you, months later, when two results disagree.

{
  "timestampUtc": "2026-07-01T10:18:36.0000000Z",
  "backend": "cpu",
  "gitCommit": "c41ef951",
  "buildConfig": "Release",
  "machineName": "curious-bohr",
  "logicalCores": 28,
  "run": "bench-matrix  cases=153  engines=3  runs=10  parallel=28"
}

The same information heads every Markdown report and every CSV (as # comment lines), and the git commit is stamped into every CSV row's commit column. The WPF app's LOAD RUN picker auto-discovers run directories under results/.

gridsim-solve-export/1 - the solve-math export

The full record of one power-flow solve: the network as solved, the per-iteration math trace, and the converged state.

  • Written by: --enableExport[=path] on any case run; crossval (solve-export.json); transient (exports/slice-*.json); gda-replay (exports/state-*.json); estimate --export-solve (estimated-solve-export.json); GDA's gridsim_export.to_solve_export on the lake side.
  • Read by: tools/crossval/crossval.py and residual_check.py (validation); Core/IO/SolveImport.cs (pre-solved replay in the WPF/web apps); the GDA bridge, whose topology and query payloads reuse this document's model block.
{
  "schema": "gridsim-solve-export/1",
  "meta":   { "case": "case9", "backend": "cpu", "gitCommit": "...", "timestampUtc": "..." },
  "model":  { "baseMva": 100, "buses": [ { "id": 1, "type": "Slack", "baseKv": 16.5, "pd": 0, "vm": 1.04 } ],
              "generators": [ ... ], "branches": [ { "from": 1, "to": 4, "r": 0, "x": 0.0576, "tap": 1.0 } ] },
  "solve":  { "solver": "dense-newton", "converged": true, "iterations": 4,
              "maxMismatch": 1.8E-14, "ybus": [ ... ], "passes": [ { "iterations": [ ... ] } ] },
  "result": { "totalLossMw": 4.641, "buses": [ { "id": 1, "vm": 1.04, "vaDeg": 0.0, "pInjMw": 71.6 } ],
              "lineFlows": [ ... ] }
}

The ybus block is empty for BFS solves (it forms no Y-bus); the Jacobian is captured only for small dense solves unless you pass --export-jacobian.

gridsim-estimate-export/1 - the estimate export (+ trace block)

The entire input contract of the estimation sidecars: model, the original measurement set, the estimated state, the residual table, observability and removals.

  • Written by: estimate (estimate-export.json in the run directory).
  • Read by: tools/crossval/crossval_estimation.py, se_residual_check.py; the contract is pinned in CI by EstimateExportContractTests.
{
  "schema": "gridsim-estimate-export/1",
  "meta": { ... }, "model": { ...same shape as solve-export... },
  "measurements": { "measurementSchema": "gridsim-measurements/1", "count": 1098, "items": [ ... ] },
  "estimate": { "status": "Converged", "iterations": 5, "objectiveJ": 96.3,
                "chiSquareThreshold": 931.2, "chiSquarePassed": true,
                "buses": [ { "id": 1, "vm": 1.0399, "vaDeg": 0.0 } ] },
  "residuals": [ { "measurement": { "kind": "PInjMw", "bus": 4, ... },
                   "predicted": -47.71, "residual": -1.2E-01, "normalizedResidual": 0.43, "critical": false } ],
  "observability": { "observable": true, "rank": 235, "stateSize": 235 },
  "removed": [ { "measurement": { ... }, "normalizedResidual": 21.4, "pass": 1 } ],
  "trace": [ { "pass": 1, "iteration": 1, "maxStateUpdate": 4.2E-02, "stepScale": 1.0 } ]
}

The trace block appears only when the run passed --trace: the per-iteration convergence path (monotone maxStateUpdate on a healthy run), re-checked by se_residual_check.py. Readers ignore unknown fields, so trace is additive.

residuals.csv

The estimate's residual table flattened for spreadsheets - one row per measurement the final estimate used. Written by estimate next to the export; read by humans hunting outliers and by GDA's sigma-calibration tooling.

kind,location,value,sigma,predicted,residual,normalized_residual,critical
VmPu,bus 1,1.0601,0.004,1.06008,2.1E-05,0.005,False
PInjMw,bus 4,-47.83,1.0,-47.71,-1.2E-01,0.43,False

gridsim-measurements/1 - the estimator's input

A versioned, self-describing measurement set over one case.

  • Written by: estimate --synth (persists the sampled set as measurements.json so the sidecars consume the identical input); GDA's NetworkModel/build_estimator_measurements.py for real GB telemetry.
  • Read by: estimate --measurements <m.json>. A takenUtc stamp is checked against the 169 h historical horizon on load.
{
  "schema": "gridsim-measurements/1",
  "case": "case14", "baseMva": 100.0, "takenUtc": "2026-05-01T08:30:00Z",
  "measurements": [
    { "kind": "VmPu",    "bus": 1,  "value": 1.0601, "sigma": 0.004 },
    { "kind": "PFlowMw", "from": 1, "to": 2, "end": "From", "value": 156.9, "sigma": 1.6 }
  ]
}

Kinds: VmPu, PInjMw, QInjMvar, PFlowMw, QFlowMvar (and the PMU angle kind - see the technical manual). Parallel circuits disambiguate with circuit (a name, or "#k" for the k-th branch).

gridsim-event-catalog/1 - recorded events

The catalogue of recorded grid events the evidence verbs replay.

  • Written by: replay-event --dump-catalog <path> (the built-ins, so you can edit and source them); hand-authored files under data/events/.
  • Read by: replay-event, scenario, fleet, evidence via --catalog. Every whenUtc is horizon-checked on load.
{
  "schema": "gridsim-event-catalog/1",
  "events": [ {
    "id": "gb-2019-08-09", "whenUtc": "2019-08-09T15:52:00Z",
    "preEventInertiaMwS": 200000, "preEventDemandMw": 30000,
    "losses":  [ { "atSeconds": 1.0, "deltaPMw": -737, "provenance": "CuratedPlaceholder" } ],
    "outcome": { "initialRocofHzPerS": -0.17, "nadirHz": 48.8, "lfddMwShed": 1000,
                 "nesoTraceCsvPath": null }
  } ]
}

Placeholder figures carry CuratedPlaceholder provenance and [VERIFY] notes until sourced; nesoTraceCsvPath (or the --trace flag) attaches the 1-second NESO trace that enables the per-sample RMSE check.

gridsim-evidence/1 - the dossier

  • Written by: evidence (evidence.json, wrapped as { schema, report }).
  • Read by: reviewers; the summary is mirrored to summary.md in the run directory.
{
  "schema": "gridsim-evidence/1",
  "report": {
    "eventId": "gb-2019-08-09", "baselineVerdict": "Reproduced", "counterfactualUnlocked": true,
    "columns": [ { "label": "recorded", "rocofHzPerS": -0.170, "nadirHz": 48.80,
                   "lfddAtRisk": true, "provenance": "CuratedPlaceholder" } ],
    "riskCurve": { "points": [ ... ] }, "fleet": { ... },
    "avoidedCost": { "lfddMwShedAvoided": 1000, "vollPerMwh": 6000, "avoidedCostGbp": 3000000 }
  }
}

The gating is structural: when the baseline is not Reproduced, the counterfactual column, risk curve, fleet and avoided-cost sections are simply absent - the file cannot carry an ungated claim.

gridsim-frequency-export/1 - the swing export

One frequency-dynamics simulation with its full recurrence, so RoCoF and nadir are hand-checkable from the numbers in the file.

  • Written by: replay-event (baseline-frequency.json) and scenario (counterfactual-frequency.json, the device at the lowest swept inertia, worst corner).
  • Read by: tools/crossval/crossval_freq.py (re-integrates the identical physics with scipy RK45; agreement < 2e-4 Hz).
{
  "schema": "gridsim-frequency-export/1",
  "inertia": { "systemInertiaMwS": 200000, "deviceRotationalInertiaMwS": 0, "effectiveInertiaMwS": 200000 },
  "fastResponse": { "fastResponseMw": 0, "timeConstantS": 0.5, "deadbandHz": 0.015 },
  "events": [ { "atSeconds": 1.0, "deltaPMw": -737 } ],
  "recurrence": [ "rocof = f0/(2E) * pImbalance ;  f += rocof*dt", ... ],
  "headline": { "initialRocofHzPerS": -0.170, "nadirHz": 48.80, "settleHz": 49.98 },
  "points": [ { "t": 0.0, "hz": 50.0, "rocofHzPerS": 0.0 }, ... ]
}

Intrinsic inertia and fast response are kept in separate labelled blocks - FFR is a power injection, never stored energy, and the export says so.

Time-series CSVs

transient -> timeseries.csv - one row per logged slice; the sidecar telemetry that pre-solved replay plays back in the GUI:

tick,t_seconds,freq_hz,rocof_hz_s,gen_mw,demand_mw,iters,max_mismatch,vmin_pu,vmax_pu,solve_ms,converged
1,0.0010,50.00000,0.00000,315.00,315.00,4,1.8E-14,0.9958,1.0400,2.1,True

gda-replay -> state-estimate.csv - one row per reconstructed timestamp:

index,time_utc,load_factor,converged,iterations,max_mismatch,total_loss_mw,vmin_pu,vmax_pu
0,2024-01-01T00:00:00Z,0.6910,True,4,3.1E-11,3.212,0.99310,1.04000

Other verbs write analogous CSVs next to their Markdown reports (matrix.csv and risk-curve.csv for scenario, fleet.csv for fleet, bench's benchmark-results.csv, ...); each begins with the #-commented RunInfo header and carries the commit column where rows may be aggregated across runs.

See also