Outputs and provenance
This page is for reading the JSON NEMSweep produces, or consuming it programmatically. Every
artifact described here is written by NEMSweep.CLI: the framework itself returns objects, not
files. Results and sweeps go under the output root, and the imported inputs a scenario reads go
under the data root. Both are yours to choose; see the
CLI reference.
The storageCapacity and limitations blocks and sweep summaries described below are included
in the 0.3.0 release target (see the publication notice); older artifacts
do not gain them automatically. The 0.3.0 whole-system detail, overview and sweep-index schema
versions are 18, 7 and 14 respectively (0.2.0 emitted 17, 6 and 13).
Artifact map
| Path | What it is |
|---|---|
results.json |
Whole-system dispatch evidence for the run started by --run-scenario. The same shape is used whether the scenario declares one region or several. |
results-overview.json |
A compact version of the same run: totals and summaries, without the interval-by-interval series. |
results-{region}.json |
Full dispatch evidence for one region of a whole-system run. |
results-{region}-overview.json |
The compact counterpart of results-{region}.json. |
demand-{region}.json |
The imported operational-demand artifact for one region. |
weather-{region}.json |
The imported weather artifact for one region. |
generation-information.json |
The imported generation-fleet workbook, as data. |
sweeps/index.json |
Manifest of every published sweep: sweep ID, name, and the path to its index. Rewritten from what is on disk after every full sweep run; unchanged by partial runs. |
sweeps/{sweepId}/index.json |
One sweep's index: its axis, scope, provenance, and one entry per point. |
sweeps/{sweepId}/points/{pointId}.json |
Full dispatch result for one sweep point (same shape as a results.json / results-{region}.json). |
sweeps/{sweepId}/points/{pointId}.status.json |
Whether that point succeeded or failed, the failure detail if it did, and reliabilityTargetMet. |
sweeps/{sweepId}/summary.json |
One row per point: the headline result of the sweep. See the summary. |
sweeps/{sweepId}/summary.csv |
The same rows as CSV. |
sweeps/{sweepId}/configs/{pointId}.json |
The fully-resolved scenario config that point ran, produced by merging the point's overrides onto the sweep's baseline config. |
sweeps/{sweepId}/series/base-demand-{sha256}.json |
A base-demand series externalised from one or more points that share it byte-for-byte. See below. |
Read NEMSweep.CLI/Scenarios/DispatchResultsExport.cs and NEMSweep.CLI/Scenarios/SweepArtifactExport.cs if
you need the exact shape beyond what this page covers.
The output tree
What the two commands actually leave on disk, under whatever you passed to --output. A scenario
run writes flat files; a sweep writes a directory per sweep.
<output root>/
results.json whole-system detail, every interval
results-overview.json whole-system totals <- start here
results-<region>.json per-region detail
results-<region>-overview.json per-region totals
csv/ only with --csv
<output root>/
sweeps/
index.json every published sweep
<sweepId>/
index.json this sweep's axis, provenance, and every point's scalars
configs/<pointId>.json the resolved scenario each point ran
points/<pointId>.json that point's full dispatch result
points/<pointId>.status.json succeeded or failed, why, and the reliability verdict
summary.json one row per point: what the sweep found
summary.csv the same rows as CSV
series/base-demand-<sha>.json demand series shared between points
csv/ only with --csv
<sweepId> is the sweep definition's own sweepId field, not a name derived from the file. See
Sweeps.
One sweep artifact is not under the output root. --fan-out-sweep writes its generated point
configs, and the points.csv point-to-axis-value table beside them, to
sweeps/<sweepId>/configs/ relative to your working directory: that is a working area next to
the sweep definition, deliberately separate from the published directory a run copies each config
into. See Mapping point IDs to axis values.
Headline fields in results-overview.json
The compact whole-system file, and the one to read first. Field names are camelCase, and the same
blocks appear for each region under regionSummariesById.<regionId>.
| Field | Unit | Meaning |
|---|---|---|
reliability.achievedUsePercentageOfDemand |
percent of demand energy | Unserved energy actually reached. See the units warning below. |
reliability.targetUsePercentageOfDemand |
percent of demand energy | The scenario's target, from reliability.targetUsePercentage. |
reliability.withinTarget |
boolean | Whether the target was met. |
reliability.standardName |
label | Descriptive only; no effect on dispatch. |
metrics.demandMwh |
MWh | Total demand over the period, any declared data-centre load included. |
metrics.unservedEnergyMwh |
MWh | Demand not served. |
metrics.unservedEnergyPercentageOfDemand |
percent | The same quantity and units as reliability.achievedUsePercentageOfDemand. |
metrics.unservedHours |
hours | Count of intervals with any unserved demand. |
metrics.peakUnservedPowerMw |
MW | Worst single interval. |
metrics.deliveredGenerationMwh |
MWh | Generation after curtailment and charging; not energy served. |
metrics.curtailedEnergyMwh |
MWh | Available output not used. |
cost.slcoeAudPerMwh |
AUD/MWh served | System levelised cost. See SLCoE. |
cost.generationSlcoeAudPerMwh |
AUD/MWh served | Generation component. |
cost.storageSlcoeAudPerMwh |
AUD/MWh served | Storage component. |
cost.transmissionSlcotAudPerMwh |
AUD/MWh served | Transmission component. cost.transmissionCostStatus says calculated or notModelled. |
cost.totalAnnualisedCostAud |
AUD per year | The SLCoE numerator. |
emissions.totalEmissionsTonnesCO2e |
t CO2-e | Operational combustion emissions, on gross generated energy. |
emissions.emissionsIntensityTonnesCO2ePerMwh |
t CO2-e per MWh served | Not the per-technology input assumption, which is per MWh generated. |
storageSizing.initialEnergyMwh, initialPowerMw |
MWh, MW | Battery as declared, before the search. |
storageSizing.finalEnergyMwh, finalPowerMw |
MWh, MW | Battery after the search. Battery only. |
storageSizing.outcome |
label | notRequired, resized, or what stopped the search. |
storageSizing.passesUsed |
count | Dispatch passes spent, against storageSizing.maximumPasses. |
storageSizing.trajectory |
array | Every candidate the search tried, with its unserved energy. |
executionMode |
label | fixedFleet or sizeBattery. |
periodStart, periodEnd, resolution |
timestamps, duration | The modelled period in market time, and the timestep. |
runId |
GUID | Identifies this run. The one value that differs between identical reruns. |
regionIds, topology.links |
n/a | The regions, and the directed interconnectors between them. |
dataSourcesByRegion.<regionId> |
n/a | Per-region input digests and the weather basis. See Provenance. |
storageCapacity |
MWh, MW | Installed storage split into declared, added, battery, pumped hydro and other. See below. |
limitations |
n/a | What this result cannot be read as saying. See below. |
A whole-system run also carries a provenance block with gitCommitSha. results-overview.json
has no top-level storageEnergyMwh: the installed-storage totals are per region, and the
battery-only figures are in storageCapacity, which the next section is about.
Battery-only figures versus totals that include pumped hydro
This is the easiest figure in NEMSweep to get wrong. Only Battery fleets are sized
(NEMSweep.Model/StorageSizing/StorageSizingSearch.cs); PumpedHydro, and any other storage
technology, is dispatched exactly as the scenario declares it.
storageCapacity publishes the split directly, on the whole-system result and on each entry of
regionSummariesById:
| Field | Unit | What it counts |
|---|---|---|
storageCapacity.declaredBatteryEnergyMwh / declaredBatteryPowerMw |
MWh, MW | Battery the scenario declared, before sizing |
storageCapacity.addedBatteryEnergyMwh / addedBatteryPowerMw |
MWh, MW | Battery the sizing search added. Zero in fixedFleet |
storageCapacity.totalBatteryEnergyMwh / totalBatteryPowerMw |
MWh, MW | Battery installed in the dispatched system |
storageCapacity.pumpedHydroEnergyMwh / pumpedHydroPowerMw |
MWh, MW | Declared pumped hydro. Never sized |
storageCapacity.otherStorageEnergyMwh / otherStoragePowerMw |
MWh, MW | Any other declared storage technology. Never sized |
Total battery + pumped hydro + other storage reconciles with the installed storage energy the result reports elsewhere, and the corresponding power fields likewise. Declared and added battery are the components of total battery, not additional storage to sum again. From the worked example's 4,500 MW point:
{
"declaredBatteryEnergyMwh": 14387.1,
"addedBatteryEnergyMwh": 83253.7,
"totalBatteryEnergyMwh": 97640.8,
"pumpedHydroEnergyMwh": 15680,
"otherStorageEnergyMwh": 0
}
The older fields remain, and remain easy to misread:
| Field | Where | What it counts |
|---|---|---|
storageSizing.finalEnergyMwh |
system and per region | Battery only, after sizing. Equals storageCapacity.totalBatteryEnergyMwh |
storageSizing.initialEnergyMwh |
system and per region | Battery only, as declared |
regionSummariesById.<id>.storageEnergyMwh |
per region only | All storage fleets, battery and pumped hydro together |
storageEnergyMwh sweep scalar |
sweeps/<id>/index.json, fact_scalars.csv |
All storage fleets: the sum of every region's storageEnergyMwh (NEMSweep.CLI/Scenarios/SweepArtifactExport.cs) |
storagePowerMw sweep scalar |
same | All storage fleets, by the same rule |
Sweep scalars and the CSV carry the split too, as declaredBatteryEnergyMwh,
addedBatteryEnergyMwh, totalBatteryEnergyMwh, totalBatteryPowerMw, pumpedHydroEnergyMwh and
otherStorageEnergyMwh. Use totalBatteryEnergyMwh where you would have reached for
storageEnergyMwh and meant battery.
In the NEM worked example the difference between the two is 15,680 MWh of existing pumped hydro,
held fixed across every point of the sweep. Quoting storageEnergyMwh as "battery built" therefore
overstates the battery by that amount at every point, and proportionally most at the points where
little battery was added.
Reliability is a percent, not a fraction
achievedUsePercentageOfDemand, targetUsePercentageOfDemand and
unservedEnergyPercentageOfDemand are already percentages. The model computes
100 * (unservedEnergy / totalDemand) (NEMSweep.Model/Simulation/ReliabilityMetrics.cs), and a
scenario's reliability.targetUsePercentage is declared on the same scale, over (0, 100].
So 0.0241 means 0.0241% of demand, roughly one part in four thousand. Multiplying it by 100 again
is a hundredfold error that lands on a plausible-looking number. The NEM reliability standard,
0.002% of demand, appears in these fields as 0.002, not 0.00002.
The console line every run prints names the unit for the same reason:
reliability: achieved 0.0014% vs target 0.0020% of demand: met
How a missed reliability target is signalled
Every run prints its reliability verdict, met or not, after the results path
(NEMSweep.CLI/Scenarios/ScenarioCommand.cs):
reliability: achieved 22.2222% vs target 0.0100% of demand: NOT met
WARNING: reliability target not met (achieved 22.2222% unserved energy, target 0.0100%).
The WARNING line appears only on a miss; the reliability: line appears always, so a run that
says nothing about reliability is a run that did not get far enough to assess it, rather than one
whose reliability nobody checked.
A missed target still exits 0 by default, because the run itself completed. Pass
--fail-on-unmet to make it exit 1 instead:
nemsweep --run-scenario scenarios/sized-scenario.json --data-root ./data --output ./out --fail-on-unmet
On a sweep, --fail-on-unmet exits 1 if any point missed its target and names those points on
standard error. Without the flag the exit code is unchanged: 0, or 1 only when a point failed to
run at all.
Five places record the verdict:
The
reliability:console line above, and theWARNINGline on a miss.reliability.withinTargetinresults-overview.jsonandresults.json, and in each region's ownreliabilityblock.For a sweep, each point's entry in
sweeps/<sweepId>/index.jsoncarries the samereliabilityblock, andsummary.json/summary.csvcarryreliabilityTargetMet.points/<pointId>.status.jsoncarriesreliabilityTargetMet, alongside thestatusthat answers the different question of whether the run completed:{"axisValue":50,"failure":null,"pointId":"p50","reliabilityTargetMet":true,"status":"succeeded"}A point that dispatched a full period and missed its target is
succeededwithreliabilityTargetMet: false. Read the field, not the status.In
sizeBatterymode,storageSizing.outcomesays what stopped the search. Only the outcome reporting system generation energy below demand energy establishes that no battery size could have met the target; the others are a ceiling or a budget the search ran into. Thelimitationsblock states which, in prose.
limitations: what this result cannot say
Every whole-system result and overview carries a limitations block, so the figures do not travel
without them. Three parts:
| Field | What it holds |
|---|---|
limitations.engine |
Statements true of every run, whatever the data. Fixed text, so two results at the same model version carry the same list. |
limitations.thisRun |
Statements derived from this run's own evidence: the period the target was tested over, what stopped the sizing search, what the weather input represents, and whether the declared system includes storage the search cannot grow. |
limitations.reference |
A link to Limitations, where the reasoning behind each engine statement is set out. |
Nothing in the block is specific to a dataset. A statement about the data a run consumed is derived
from that run's own input provenance (dataSourcesByRegion), not from knowledge of any particular
dataset, so a run on your own inputs gets statements about your inputs. From the worked example's
4,500 MW point:
[
"The reliability target of 0.002 per cent of demand energy was tested over 2025-07-01 to 2026-07-01 in market time and over no other period.",
"The search grew battery energy by 83,253.7 MWh over 113 dispatch passes to reach the target. That is a capacity sufficient under this dispatch rule, not a least-cost or otherwise optimal one.",
"The weather input is a typical meteorological year, a composite of representative months applied to the dispatch period by calendar hour. It does not contain the tail weather events that drive storage and reliability outcomes, so storage need is likely understated.",
"The declared system includes 15,680.0 MWh of storage energy the search cannot grow, counted in the installed-storage totals but not in the battery figures. Read storageCapacity rather than storageEnergyMwh for battery."
]
It is a summary, not a substitute: read Limitations before quoting a figure.
SLCoE: what it is, and what it divides by
One definition, used everywhere: the annualised cost of building and running the declared system, divided by energy served.
- The numerator is
cost.totalAnnualisedCostAud: generation, storage and transmission capital annuitised over each asset'stechnicalLifeYearsat the scenario'scostBasis.realDiscountRate, plus fixed and variable operating cost and fuel. - The denominator is energy served, which is demand minus unserved energy
(
NEMSweep.Model/Simulation/SystemDispatchOutcome.cs), published as the sweep scalarenergyServedMwh. It is not demand, and it is not delivered generation: the three differ whenever a run has unserved energy, curtailment or storage. NEMSweep.Model/Economics/PowerSystemCostBreakdown.csdivides by that same denominator for the generation, storage and transmission components, sogenerationSlcoeAudPerMwh + storageSlcoeAudPerMwh + transmissionSlcotAudPerMwhreconstructsslcoeAudPerMwh.- The unit is AUD per MWh served, in the scenario's
costBasis.yeardollars.
SLCoE is not a bill. Network, retail, scheme and tax costs are all outside it, so it is not
comparable to a retail tariff. Per-region SLCoE additionally divides a region's own plant cost by
the load that region served, which understates cost in a net importer; cost.netImportedEnergyMwh
is published so that distortion is visible rather than silent.
Rerunning one published sweep point
Each point config is a complete resolved scenario, so it is sufficient to rerun that point: use the matching NEMSweep version, the point's config, and access to the demand and weather artifacts it names. No baseline scenario or sweep definition is needed to rerun an individual point.
The data root is deliberately external to the configuration and can change or disappear. Results still record each consumed demand and weather artifact's filename, schema version, and SHA-256 digest, while demand artifacts retain descriptive archive filenames. NEMSweep does not bundle the inputs or reconstruct their upstream retrieval history; a rerun against different input artifact bytes is a different run and can produce different results.
Schema versions
Every artifact carries a schemaVersion field, and the current value for each artifact type is
defined in one place: NEMSweep.Contracts/ArtifactSchemaVersions.cs. This page does not repeat those
numbers, because they would go stale the moment a schema changes. Consult the
API reference for the current values.
Schema versions cover serialized meaning as well as JSON structure. The current system and regional dispatch details, overviews, and sweep index distinguish region-specific storage-sizing blockers from the whole-run aggregate outcome. Earlier regional artifacts inherited the run's failure outcome even when growth had not been tested for that region. Do not apply the current regional interpretation to those older artifacts.
Consumers must support the corresponding artifact versions before newly generated outputs are published. In particular, the results site's Contracts package and schema registry must be upgraded alongside its regenerated artifacts. Updating these schema markers does not itself publish a NuGet package or cut a release; older published artifacts remain on their original versions until that coordinated publication.
Sweep scalar vocabulary
A sweep publishes one set of scalars per point per region (and one for the system as a whole), in
sweeps/{sweepId}/index.json and alongside each point's full result. The catalogue in
NEMSweep.Contracts/SweepScalarCatalog.cs is the full vocabulary: the JSON name each scalar is emitted
under, its label, and its unit.
| JSON name | Label | Unit |
|---|---|---|
slcoeAudPerMwh |
System levelised cost | AUD/MWh served |
generationSlcoeAudPerMwh |
Generation levelised cost | AUD/MWh served |
storageSlcoeAudPerMwh |
Storage levelised cost | AUD/MWh served |
demandMwh |
Demand | MWh |
energyServedMwh |
Energy served | MWh |
deliveredGenerationMwh |
Delivered generation | MWh |
achievedRenewableShareGridScale |
Achieved renewable share (grid scale) | fraction |
achievedRenewableShareNative |
Achieved renewable share (native) | fraction |
storagePowerMw |
Storage power capacity | MW |
storageEnergyMwh |
Storage energy capacity (all technologies) | MWh |
unservedEnergyMwh |
Unserved energy | MWh |
unservedEnergyPercentageOfDemand |
Unserved energy | % of demand |
unservedHours |
Unserved hours | h |
hoursServedFraction |
Hours served | fraction |
peakUnservedPowerMw |
Peak unserved power | MW |
curtailedEnergyMwh |
Curtailed energy | MWh |
emissionsTonnesCO2e |
Emissions | t CO2-e |
emissionsIntensityTonnesCO2ePerMwh |
Emissions intensity | t CO2-e/MWh served |
transmissionSlcotAudPerMwh |
Transmission levelised cost | AUD/MWh served |
transmissionCostStatus |
Transmission cost status | status |
netImportedEnergyMwh |
Net imported energy | MWh |
declaredBatteryEnergyMwh |
Battery energy capacity declared | MWh |
addedBatteryEnergyMwh |
Battery energy capacity added by sizing | MWh |
totalBatteryEnergyMwh |
Battery energy capacity | MWh |
totalBatteryPowerMw |
Battery power capacity | MW |
pumpedHydroEnergyMwh |
Pumped-hydro energy capacity | MWh |
otherStorageEnergyMwh |
Other storage energy capacity | MWh |
The six battery and storage-technology scalars are the same split
storageCapacity publishes on a
dispatch result. storageEnergyMwh and storagePowerMw count every storage technology;
totalBatteryEnergyMwh and totalBatteryPowerMw are battery alone.
transmissionCostStatus is the one entry the catalogue marks as not chartable. It is a status
label (calculated or notModelled), not a numeric series.
Energy balance: delivered generation is not energy served
deliveredGenerationMwh excludes both curtailment and storage charging.
It includes generator output serving local load and sending-end exports, but
not storage discharge or received imports. energyServedMwh is demand minus
unserved energy, including demand met by storage discharge and imports.
The per-technology deliveredGenerationByTechnologyMw series and
deliveredGenerationByTechnologyMwh totals use that same delivered-generation
definition. Charging allocations are bookkeeping, not a physical tracing of
co-mingled electricity.
For one region, each hourly interval and its integrated run obey:
G + B + I + U = D + C + E + K
L = G - C - K
S = D - U = L + B + I - E
| Term | Meaning | Published field in the detail result |
|---|---|---|
D |
Total demand | dataSeries.demand.totalDemandMw; total metrics.demandMwh / sweep scalars.demandMwh |
U |
Unserved demand | dataSeries.unservedDemandMw; total metrics.unservedEnergyMwh / sweep scalars.unservedEnergyMwh |
S |
Energy served | D - U; sweep scalars.energyServedMwh |
L |
Generation after curtailment and charging | Sum of dataSeries.deliveredGenerationByTechnologyMw; total metrics.deliveredGenerationMwh / sweep scalars.deliveredGenerationMwh |
C |
Grid-side storage charge, all technologies and charge sources | dataSeries.chargeMw |
B |
Grid-side storage discharge, all technologies | dataSeries.dischargeMw |
K |
Remaining curtailment, after exports and surplus charging | dataSeries.curtailmentMw; total metrics.curtailedEnergyMwh / sweep scalars.curtailedEnergyMwh |
I |
Imports received, after link losses | dataSeries.importsMw |
E |
Exports sent, before link losses | dataSeries.exportsMw |
G |
Engine generation before curtailment and charging | No direct series; reconstruct the aggregate as L + C + K |
T |
Transmission loss | dataSeries.transmissionLossesMw; system link evidence in interconnectors[].lossesMw |
Integrate MW series by summing and multiplying by resolution.TotalHours to
obtain MWh. G includes available intermittent output subsequently curtailed,
whereas dispatchable fleets record dispatched output; it is not installed
capacity or every dispatchable fleet's unused potential output.
System imports and exports are sums of internal regional transfers, not
flows across an external system boundary. Sending-end exports minus received
imports equals transmission loss: E - I = T. Hence system-wide:
G + B + U = D + C + K + T
S = L + B - T
S - L = B - T
Regional transmission loss is attributed to the receiving region. Do not
subtract it again in the regional equation: I is already net of loss.
Likewise, do not subtract charging again from L.
The system scalars.netImportedEnergyMwh is zero because its cost-accounting
boundary excludes internal transfers; use the import/export series and link
loss evidence for this grid balance instead.
Storage losses are applied once on charge: input energy times the fleet's round-trip efficiency becomes stored energy; discharge removes stored energy one-for-one. For the storage inventory, separately from the grid balance:
openingStoredMwh + chargeMwh = dischargeMwh + storageLossMwh + closingStoredMwh
storageLossMwh = sum over fleets and intervals of chargeMwh * (1 - efficiency)
Thus chargeMwh - dischargeMwh is not necessarily storage loss: it also
includes the change in stored energy. Installed storage has an opening seed;
sizing does not increase that seed. Detail results already export charge,
discharge and transmission-loss series, but do not export a storage-loss
series or terminal inventory. stateOfChargeByTechnologyMwh records interval
beginnings, so its last value is not the end-of-run inventory. Aggregate
charge cannot in general be split between fleets with different efficiencies
from these published fields alone. The grid-balance check does not need that
split, and inferred generation is not an independent check of gross generation.
Checking the published sweep
PublishedEnergyBalanceTests checks every interval, integrated regional and
system balances, metric/scalar agreement, regional aggregation and summed
link losses for p0, p9, p10 and p24. It prints every grid-balance term in MWh,
then reruns the emitted configurations against the supplied data to check
independently recorded, unrounded engine generation as well.
It pins the four documented v0.2.0 engine totals (served, delivered, discharge
and transmission loss) to within 0.0001 MWh, the original baseline-config
SHA-256, and all ten ingested demand/weather SHA-256 digests, checking the
input identities in both published evidence and the rerun. A different,
internally balanced sweep is not sufficient to pass this reproduction check.
The unrounded tolerance is
max(0.000001 MWh, 1e-9 * max(abs(left), abs(right))).
Published series and energy scalars are independently rounded to 0.1 MW/MWh.
The artifact checks therefore add the worst-case rounding error: 0.05 times
the number of independently rounded values in the equation, times interval
hours for MW samples. For L + B + I = D - U + E, that is
0.05 * (technologyCount + 5) * intervalHours per interval, summed over the
horizon for the integrated check. For this five-technology, 8,760-hour sweep
the bound is 0.5 MWh per interval and 4,380 MWh per year. It is a serialization
bound, not permission for an engine imbalance. Metric and aggregation checks
similarly account for only the rounded values in each comparison.
Small synthetic cases run normally; the external-data acceptance is explicitly skipped unless both directories are supplied:
$env:NEMSWEEP_ENERGY_BALANCE_SWEEP = (Resolve-Path .\study\out\sweeps\datacentre-nameplate-fy2026).Path
$env:NEMSWEEP_ENERGY_BALANCE_DATA = (Resolve-Path .\study\data).Path
dotnet test .\NEMSweep.CLI.Tests\NEMSweep.CLI.Tests.csproj -c Release --filter FullyQualifiedName~PublishedEnergyBalanceTests --logger "console;verbosity=detailed"
To reproduce the 0.2.0 check, build the CLI at 7f44412, prepare the
Advanced starter, and run the original committed
scenario/sweep configurations with the starter's ingested data. You can select
only the four requested points without changing their overrides or baseline:
# Run from the 0.2.0 source root after preparing .\study with --quick-start advanced.
$definition = Get-Content .\sweeps\datacentre-nameplate-fy2026.json -Raw | ConvertFrom-Json
$definition.points = @($definition.points | Where-Object { $_.pointId -in @('p0', 'p9', 'p10', 'p24') })
[System.IO.File]::WriteAllText(
(Join-Path $PWD 'study\sweeps\energy-balance-four-points.json'),
($definition | ConvertTo-Json -Depth 100))
dotnet .\NEMSweep.CLI\bin\Release\net10.0\nemsweep.dll --run-sweep .\study\sweeps\energy-balance-four-points.json --data-root .\study\data --output .\study\out
Run the acceptance test from a checkout containing the test, pointing its environment variable at the generated 0.2.0 sweep. No new artifact schema or change to existing totals is needed to check this balance.
Verified 0.2.0 result
The four-point run at 7f44412, using the original committed configs and
the verified Advanced data release v2026.10.1, reproduces the reported
delivered-generation and served-energy scalars. These are unrounded engine
totals shown to six decimal places in TWh:
| Point | Delivered generation L |
Energy served S |
Discharge B |
Transmission loss T |
S - L = B - T |
|---|---|---|---|---|---|
| p0 | 186.684136 | 186.643742 | 0.024973 | 0.065368 | -0.040394 |
| p9 | 225.408514 | 226.060638 | 1.073309 | 0.421185 | 0.652124 |
| p10 | 229.475558 | 230.388178 | 1.386342 | 0.473722 | 0.912620 |
| p24 | 271.480899 | 278.114778 | 7.522816 | 0.888937 | 6.633880 |
Every tested regional/hourly balance closes. The largest unrounded integrated system residual is less than 0.000002 MWh. Integrating the independently rounded published series gives system residuals of 30.7, 17.5, 23.6 and 53.1 MWh respectively, within the stated serialization bound.
This is a definition difference, not an energy-creation or scalar-summing bug. No existing field or dispatch result changes. In particular, the worked example's per-technology delivered-generation breakdown is unchanged; the p24-minus-p0 delivered-generation increase remains approximately 84.797 TWh. It should be described as delivered generation excluding storage charging and discharge, rather than gross generation or extra energy served.
Base-demand externalisation
Sweep points frequently share an identical base-demand series, because varying a cost parameter or
a storage limit does not change demand at all. Rather than repeating that series inside every
point's result, a sweep run externalises it: the first point to produce a given series writes it
once to series/base-demand-{sha256}.json, where the hash is computed from the serialised series
itself, and every point whose demand series is byte-identical simply references that file's path.
A run also prunes series files nothing in the freshly-written index still references, so the
series directory does not accumulate stale content-addressed files across regenerations.
Provenance and reproducibility
Every dispatch result records the exact input artifacts it consumed, whether that result is a
scenario run, a region within one, or a sweep point. For each of the demand and weather inputs it
records the filename, the schema version, and the SHA-256 digest of the exact bytes that were
parsed (DispatchInputArtifactDTO in NEMSweep.Contracts/DispatchResultsDTO.cs). The digest, not the
configured file path, is the reproducibility boundary: a path can be overwritten with different
content later, but the digest identifies the bytes that actually produced this result.
A whole-system result also records the model build behind it, in a provenance block carrying the
git commit SHA and a flag for whether the working tree had uncommitted changes. A sweep index.json
records the same for the sweep as a whole. Digests pin the bytes a run consumed, but publishing a
result is a manual step, so without the commit a file copied somewhere else could not say which
version of the model read them.
Citing a run
Cite the run, not just the tool. "Modelled with NEMSweep" cannot be checked by anyone; a version, a commit and a scenario can be. The paste-ready form:
Modelled with NEMSweep <version> at commit <commit>, scenario <scenario config>, run <runId>.
https://github.com/HasinthaAttanayake/NEMSweep
nemsweep --version reports the version, and provenance.gitCommitSha and runId on the result
supply the commit and the run. The scenario config is the one field you have to carry yourself: a
dispatch result records no scenario identifier, so note which file you ran at the time you run it.
A sweep is better placed, because its index provenance lists the baseline scenario config by path
and SHA-256 under the baseline-scenario-config purpose.
CITATION.cff carries the
citation metadata for the software itself.
Which commit is recorded
The commit is the one the binary was built at, stamped in by its build, not whatever commit the directory you ran from happens to be standing on. That distinction matters once the CLI is installed or run as a container, where the two are unrelated. The dirty flag is only ever true when the run was made from a checkout standing on that same commit, because that is the only case where the source the binary was built from is in front of the tool to inspect. The block is absent when the binary was built outside a checkout and so carries no commit to report.
Provenance paths in a sweep index are recorded relative to the data root, or to the working
directory for the sweep definition and baseline config, which is what keeps them citing
sweeps/… and scenarios/… rather than a location on one machine. The digest remains the
reproducibility boundary; the path is there to say where the run was configured from.
Writing conventions
Published artifacts follow the conventions in NEMSweep.CLI/Infrastructure/JsonFile.cs:
- Property names are camelCase.
- Object keys are sorted ordinally, so a rerun that changes one value produces a small, reviewable diff rather than a reordered file.
- Numeric values are rounded according to their unit, so
*Mwand*Mwhfields carry one decimal place, AUD fields two, and fractional shares four, rather than full floating-point precision that no one reads. - Published artifacts are written unindented. Indentation was roughly seventy percent of the bytes in these files, and nothing ever read the whitespace: not the CLI, not the site, not a person.
What changes between two identical reruns
Rerun a scenario against unchanged inputs with the same CLI build and every modelled value is reproduced exactly. The artifacts are not byte-identical, though, and what differs depends on the artifact:
| Artifact | Differs between identical reruns |
|---|---|
results*.json |
runId only, a fresh GUID per run |
demand-{region}.json, weather-{region}.json, generation-information.json |
generatedAt, a UTC timestamp stamped at import |
sweeps/{sweepId}/index.json |
Each point's measured durationMs, and the sweep's totalDurationMs when recorded |
The runId-only guarantee assumes unchanged input-path layout and build metadata as well as
unchanged input bytes. It was verified by regenerating all twelve committed dispatch artifacts and
diffing them leaf by leaf. It applies to the dispatch artifacts; it does not extend to the imported
inputs or to the sweep index, which record when and how long a run took.
Atomic publication
Publishing a scenario result is atomic: the new files are staged in a temporary directory, the
previous versions of the target files are moved aside, and only then are the staged files moved
into place. If anything fails partway through, the move is rolled back and the previous artifacts
are restored, so a failed run never leaves a half-written result where a complete one used to be
(DispatchResultsExport.WritePublication).
A sweep has no equivalent guarantee at the sweep level. Each point's dispatch result lands
through that same staged path, but it is then rewritten in place to externalise its base-demand
series, and the generated configs, per-point status files, the sweep index and the manifest are
written incrementally as the run proceeds. An interrupted sweep can therefore leave a partially
updated sweeps/{sweepId}/ directory. Rerunning the sweep restores it.
These files are generated
Every file described here is generated. Do not hand-edit one. Regenerate it by rerunning the
command that produced it (--ingest for the input artifacts, --run-scenario or --run-sweep for
results), so what is on disk stays traceable to the inputs and commit behind it.
The published artifact set is what the results site displays. Publishing to it is a deliberate act: use the site's publication workflow when you intend to update it, and let ordinary runs land in your own directory the rest of the time.
That set is an illustrative example, not a dataset. It is one FY2026 run and one sweep, retained
so the site has something to show. It is not carried in this repository, so a clone sources its own
upstream data and runs --ingest before it can reproduce a run. The
provenance block on it is real, which is exactly why it is worth saying plainly: a demo carrying
input digests and a commit can read as more authoritative than it is. Run your own scenario before
quoting a figure, and see DATA-LICENSE.md
for the terms on the data behind it.
CSV
Everything on this page is JSON, which is the contract but not something you can open. Add --csv
to a run and it also writes a star schema of CSV tables projected from these same values, rounded
by the same rule. See CSV tables.