Units

Every numeric value in this data declares its unit. A field is megawatts, or ohms, or seconds, because its definition says so — never because of how it is named, and never by convention agreed somewhere else. This page is the vocabulary those declarations draw on and the conventions they follow.

Where do I look?

I have… Look at
A schema field, and want its unit That field’s own .json file (x-unit, or x-unit-discriminator + x-units), read via the decision procedure below
A question about the vocabulary itself — what quantities or units exist at all Core/units.json, read via Reading Core/units.json below
A new unit or annotation to add docs/UNIT_ANNOTATIONS.md — the authoring spec, a different job from reading
A SiennaGridDB database column or row value SiennaGridDB’s units documentation — the registry mirrors this vocabulary; this page covers the JSON Schema side only

The self-contained blob principle

Every component JSON blob is completely interpretable on its own. There is no document-level unit context: Core/SystemDocument.json carries no unit_system and no base_power for the document as a whole. Anyone holding a single component blob — one ThermalStandard, one Line — can read every value on it without the enclosing document, because everything a value needs is recorded on that same blob: a discriminator property that says which basis applies, and, for a per-unit reading, a base_power (and voltage base, where applicable) to per-unitize against.

One consequence: a document can be — and often legitimately is — mixed-basis. One ThermalStandard can carry its power fields in NATURAL_UNITS while the Line next to it carries its in COMPONENT_BASE, each on its own recorded base. Nothing about the document constrains this; each blob is read independently.

Reading a value: the decision procedure

Given a numeric field on a component blob, read it in this order:

  1. Plain x-unit? The field is always in that fixed unit, full stop — no discriminator, no basis, no document context. base_power is always MVA. angle_limits is always rad. time_limits is always min.
  2. x-unit-discriminator + x-units? The field’s unit depends on a sibling property recorded on the same blob. Read that sibling first, then look up the unit for its value in the x-units map. The discriminator is one of:
    • power_units — governs every power-family field on the component: active/reactive/ apparent power, ratings, limits, ramp rates. Required on every component that has such a field, with no default — a producer must always declare it explicitly.
    • the established per-family discriminators: parameter_units (branch impedance — resistance, reactance, susceptance, conductance), admittance_units (shunt admittance), dc_voltage_units / setpoint_voltage_units / voltage_units (voltage setpoints), energy_units (stored energy), and others named on the type’s own reference page.
  3. Discriminator value is COMPONENT_BASE? Per-unitize against the basis this same blob records for itself — its own base_power (in MVA), and its own voltage base where the field is voltage-related. Never a document-wide table, never another component’s base.

UnitSystem’s three scopes

UnitSystem is a two-member enum — COMPONENT_BASE, NATURAL_UNITS — and it is used in exactly three scopes, each read against its own record rather than anything document-wide:

  • A component’s own power_units. Governs that component’s power-family fields, read against that component’s own base_power. Required, no default.
  • A cost payload’s own power_units. The x-axis basis of a CostCurve or FuelCurve — the value curves wrapped by a generation cost’s variable_operation_cost member (renamed from variable in this design pass). Read against the owning component’s base_power, same as any other COMPONENT_BASE reading. Note the field-level asymmetry here: CostCurve.power_units does carry a default (NATURAL_UNITS) even though it stays in the schema’s required list, while FuelCurve.power_units — like every component-level power_units — has none.
  • A time series association’s own unit_system. Governs only that one series; it says nothing about the component it is attached to, or any other series.

There is deliberately no system-wide basis. Data that was historically per-unitized against a system base records that base in the component’s own base_power and rides as COMPONENT_BASE — the basis moved from “the system” to “this component’s copy of what the system’s base happened to be.” Mixed-basis documents are the expected result, not an edge case: different components, in different bases, each self-describing.

Read the discriminator before you read the number. A per-unit value taken for a physical one — or the reverse — is wrong by the size of the base, typically a factor of a hundred, and the number itself will not warn you: 0.85 is an entirely plausible per-unit reactance and an entirely plausible count of ohms.

Quantity types

A quantity type is what is being measured: active power, voltage, a heat rate. Each one fixes a default unit and a dimension — an exponent map over the base dimensions M (mass, kg), L (length, m), T (time, s) and I (electric current, A), plus two pseudo-dimensions, USD for money and Btu for the chemical energy in fuel. Dimensionless quantities carry the empty map.

Quantity type Default unit Description
ActivePower MW Active (real) power
ReactivePower MVAr Reactive power
ApparentPower MVA Apparent power
ActivePowerChangeRate MW/min Rate of change of active power (ramp)
ElectricalEnergy MWh Real (electrical) energy
FractionPerTime 1/min A fraction of a stored quantity lost or decayed per unit of time (storage self-discharge, leakage). Per minute, matching OperationalDuration: the value multiplies the simulation step, so its time basis must be the same one durations use or the decay is wrong by the ratio of the two. Distinct from Frequency despite sharing the T^-1 dimension.
Voltage kV Voltage
CurrentFlow kA Current flow
Resistance ohm Resistance
Reactance ohm Reactance
Impedance ohm Impedance
Conductance S Conductance
Susceptance S Susceptance
Angle rad Angle (allowed units rad and deg)
Frequency Hz Frequency
PowerPerFrequency MW/Hz Active power response per unit of frequency deviation (regulation / droop participation)
Length km Length
Elevation m Elevation / hydraulic head
Duration s Continuous-time duration or time constant, real-valued seconds. Dynamics time constants are inherently fractional, so no integer representation exists for them. Discrete scheduling durations use OperationalDuration; multi-year planning spans use CalendarPeriod.
OperationalDuration min Operational or scheduling duration, expressed in minutes. The unit is the convention; integrality is not. Most of these fields are real-valued because the data model library validates them on an open (0, nothing) range and sub-minute products are legitimate (a 30-second reserve, a fractional-minute saturation window). Declare a field integer only where the domain is genuinely discrete and the ingest path rounds – no current field qualifies. Sub-minute control timing belongs in Duration instead.
CalendarPeriod yr Long-term planning span, a whole number of years. Integer-valued. Deliberately carries no to_default bridge into Duration or OperationalDuration: a calendar year is not a fixed number of seconds, so any such factor would silently bake in a 365-day convention. ‘ucum’ is null for the same reason: every UCUM year atom is longer than 365 days (a/a_j = 365.25, a_g = 365.2425, a_t = 365.24219), so none of them matches the 365-day year this vocabulary actually uses. Emitting “a” would hand a consumer a leap-day convention we do not follow.
Dimensionless 1 Dimensionless quantity
Fraction 1 Fractional quantity (efficiency, losses, cofire level, etc.)
PowerFactor 1 Power factor
HeatRate MMBtu/MWh Heat rate (fuel energy per unit of electrical energy)
StartFuelPerCapacity MMBtu/MW Start-up fuel per unit of capacity
VolumeFlowRate m3/s Volumetric flow rate
HeadRate m/s Rate of change of hydraulic head
Volume m3 Volume
Inductance H Inductance
Capacitance F Capacitance
CostPerCapacity USD/MW Cost per unit of capacity (capital cost)
CostPerEnergy USD/MWh Cost per unit of energy (operation / energy capital cost)
CostPerTime USD/h Cost per unit of time
CO2Emissions t/MMBtu CO2 emission rate (mass per unit of fuel energy)
Mass t Mass
Money USD Monetary value
CostPerVolume USD/m3 Cost per unit of volume (e.g. reservoir operation cost)
CostPerLevel USD/m Cost per unit of hydraulic head (e.g. reservoir operation cost when level_data_type=HEAD)
EmissionRate t/MWh Emission mass per unit of electrical energy
CostPerMass USD/t Cost per unit of mass (e.g. carbon price or tax)

Allowed units and conversions

These are the units a value of each quantity type may be stored in. Nothing else is accepted. Factor to default is the exact number a value is multiplied by to convert it into that quantity’s default unit — 0.001 takes kW to MW.

One kind of entry is not a conversion at all:

  • — conversion is context-dependent rather than a fixed factor. Every such row is a pu unit: what a per-unit value comes out as depends on the base the component recorded, which no constant can capture.

And one entry looks like a conversion but is not. Frequency / Hz carries 60.0, which records the nominal system frequency, not a factor. Hz is already Frequency’s default unit, so a genuine conversion factor there would be 1.0. Do not multiply by it.

Quantity type Unit Factor to default
ActivePower MW 1.0
ActivePower kW 0.001
ActivePower GW 1000.0
ActivePower TW 1000000.0
ActivePower pu
ReactivePower MVAr 1.0
ReactivePower pu
ApparentPower MVA 1.0
ApparentPower pu
ActivePowerChangeRate MW/min 1.0
ActivePowerChangeRate pu/min
ElectricalEnergy MWh 1.0
ElectricalEnergy kWh 0.001
ElectricalEnergy GWh 1000.0
ElectricalEnergy TWh 1000000.0
ElectricalEnergy MWmin 0.016666666666666666
ElectricalEnergy MJ 0.0002777777777777778
FractionPerTime 1/min 1.0
Voltage kV 1.0
Voltage pu
CurrentFlow kA 1.0
CurrentFlow A 0.001
Resistance ohm 1.0
Resistance pu
Reactance ohm 1.0
Reactance pu
Impedance ohm 1.0
Conductance S 1.0
Conductance pu
Susceptance S 1.0
Susceptance pu
Angle rad 1.0
Angle deg 0.017453292519943295
Frequency Hz 60.0
PowerPerFrequency MW/Hz 1.0
Length km 1.0
Length m 0.001
Elevation m 1.0
Duration s 1.0
OperationalDuration min 1.0
CalendarPeriod yr 1.0
Dimensionless 1 1.0
Fraction 1 1.0
PowerFactor 1 1.0
HeatRate MMBtu/MWh 1.0
StartFuelPerCapacity MMBtu/MW 1.0
VolumeFlowRate m3/s 1.0
HeadRate m/s 1.0
Volume m3 1.0
Inductance H 1.0
Capacitance F 1.0
CostPerCapacity USD/MW 1.0
CostPerEnergy USD/MWh 1.0
CostPerTime USD/h 1.0
CO2Emissions t/MMBtu 1.0
Mass t 1.0
Mass Mt 1000000.0
Mass kg 0.001
Mass lb 0.00045359237
Mass ston 0.90718474
Money USD 1.0
CostPerVolume USD/m3 1.0
CostPerLevel USD/m 1.0
EmissionRate t/MWh 1.0
EmissionRate Mt/MWh 1000000.0
CostPerMass USD/t 1.0

Reading Core/units.json

The vocabulary file has two lists, answering two different questions.

  • quantity_kinds — one entry per kind of quantity: name, its dimension (an exponent map over the base dimensions), default_unit, ucum (the UCUM code for that default unit, or null where none applies), and a description. This is what is being measured, independent of how any one value happens to be stored — the source of the Quantity types table above.
  • allowed_units — one row per (quantity_kind, unit) pair that quantity may actually be stored in, each carrying its own to_default. This is which spellings are legal, and the factor from each to its quantity’s default_unit — the source of the Allowed units table above.

to_default is the multiplier from a value in unit into that quantity’s default_unit0.001 for kW means “multiply by 0.001 to get MW.” Two values are not ordinary multipliers:

  • null — the unit is relative, not absolute: a pu row. There is no fixed factor, because the physical value depends on a base recorded elsewhere, resolved through the annotated field’s own discriminator and the blob’s own base_power (see the decision procedure) — never a constant in this file.
  • 0.0Frequency/Hz’s row; this layer deliberately performs no conversion at all (see the note under Allowed units and conversions).

Every x-unit, and every value inside an x-units map, in every schema file, must name a unit that is an allowed_units row for that field’s quantity type (or the literal "pu") — scripts/validate_units.py enforces this and rejects anything else. Core/units.json is the only place a new unit spelling may be introduced; see docs/UNIT_ANNOTATIONS.md for how — that page covers authoring an annotation, a different job from reading one.

Conventions worth knowing

Reactive power is MVAr, never MW

Active, reactive and apparent power share one dimension, so nothing in the arithmetic separates them. The vocabulary does: three quantity types with three units — MW, MVAr, MVA — and a field’s declared unit says which of the three it holds.

Percent is banned; store fractions

There is no % unit and there will not be one. An efficiency of 95% is stored as 0.95, with the unit 1. Every fraction, loss factor, and cofire level in this data reads that way.

Time is three quantity types, not one

Time is split across three quantity types, each with exactly one allowed unit, so the vocabulary itself enforces which tier a field belongs to:

Quantity type Unit For
Duration s continuous-time constants — machine and controller dynamics
OperationalDuration min scheduling and commitment — minimum up and down times, ramp and reserve windows
CalendarPeriod yr long-term planning spans

Hours are not a unit in this vocabulary. A field holding hours is a tier error, not a conversion — it belongs in one of these three tiers, expressed in that tier’s unit. Only min → s is bridged, and it is bridged by an exact physical factor. Years are deliberately not bridged at all: a calendar year is not a fixed number of seconds, so any factor would quietly commit every consumer to one 365-day convention.

The tier fixes the unit, not the numeric type. CalendarPeriod is a whole number of years throughout. OperationalDuration is real-valued, because a thirty-second reserve product is a legitimate half-minute.

Fuel energy is kept apart from electrical energy

Btu is its own pseudo-dimension rather than a form of electrical energy. Heat rates (MMBtu/MWh) and start-up fuel (MMBtu/MW) therefore never share a dimension with a cost quantity, and a mix-up between fuel intensity and cost is a dimension error rather than a plausible-looking number.

Angle converts, so degrees are accepted

rad is the default, and deg is registered with the exact factor π/180. Source data in degrees converts on the way in rather than being rejected at the door.

Dimensionless quantities use the unit 1

The empty exponent map, and a single unit spelled 1. Dimensionless, Fraction and PowerFactor all share it: what separates them is what the number means, not how it is scaled.

Worked example: reading a ThermalStandard blob

Take one component, ThermalStandard, and its power-family fields: active_power, rating, active_power_limits, ramp_limits, plus the fixed-unit base_power and time_limits. Below is the same generator recorded twice — once with power_units: NATURAL_UNITS, once with power_units: COMPONENT_BASE — at the same physical operating point, on a 100 MVA base. (Abbreviated to the fields this example needs — a real ThermalStandard blob also carries status, bus, operation_cost, and the rest of its required fields.)

NATURAL_UNITS:

{
  "id": 1,
  "name": "GEN-1",
  "power_units": "NATURAL_UNITS",
  "base_power": 100.0,
  "active_power": 100.0,
  "rating": 150.0,
  "active_power_limits": { "min": 20.0, "max": 150.0 },
  "ramp_limits": { "up": 50.0, "down": 50.0 },
  "time_limits": { "up": 480.0, "down": 480.0 }
}

COMPONENT_BASE, same generator, same operating point:

{
  "id": 1,
  "name": "GEN-1",
  "power_units": "COMPONENT_BASE",
  "base_power": 100.0,
  "active_power": 1.0,
  "rating": 1.5,
  "active_power_limits": { "min": 0.2, "max": 1.5 },
  "ramp_limits": { "up": 0.5, "down": 0.5 },
  "time_limits": { "up": 480.0, "down": 480.0 }
}

Walking the decision procedure on three of these fields:

  • base_power carries a plain x-unit: MVA. It is always megavolt-amperes, in both blobs, regardless of power_units — it is the anchor the discriminated fields are read against, not itself subject to the discriminator.
  • rating carries x-unit-discriminator: power_units with x-units: {NATURAL_UNITS: MVA, COMPONENT_BASE: pu}. Read power_units first. In the first blob it is NATURAL_UNITS, so rating: 150.0 means 150 MVA directly. In the second it is COMPONENT_BASE, so rating: 1.5 is per-unit on this blob’s own base_power — multiply by base_power (100 MVA) to recover the same 150 MVA. Nothing outside this one blob is needed to make that conversion.
  • time_limits carries a plain x-unit: min. It reads {up: 480.0, down: 480.0} minutes in both blobs — power_units governs power-family fields only, so a fixed-unit field is never affected by it, one of the reasons step 1 of the decision procedure comes before step 2.

Reading a unit on a type page

Every type’s reference page carries a Unit column, and it shows one of two things.

A plain unit — the field is always in that unit. ThermalStandard’s base_power is MVA in every blob, whatever else the blob says.

A discriminated unit, written alt1 / alt2 (by <property>) — the unit depends on the value of another property on the same component. Branch impedance is the standard case: a Line stores r and x in either physical ohms or per-unit, and its parameter_units property says which.

Line property Unit Description
rating MVA / pu (by power_units) Thermal rating.
power_units Unit basis for this component’s power-family fields (active/reactive/apparent power, ratings, limits, ramp rates).
parameter_units Unit basis for this line’s impedance and shunt admittance fields (r, x, b, g).
r ohm / pu (by parameter_units) Resistance.

So on a Line whose parameter_units reads COMPONENT_BASE, r is per-unit on that line’s own base_power; on a Line that reads NATURAL_UNITS, the same field is ohms. The same line’s rating is governed separately, by its own power_units — the two discriminators are independent properties on the same blob, and a Line can (and often does) carry one basis for its impedance and a different one for its power fields. Read the relevant discriminator first, then the number.

For schema authors

Adding or changing a unit annotation is a different job from reading one, and it has its own spec: docs/UNIT_ANNOTATIONS.md covers the x-unit, x-unit-base, and x-units + x-unit-discriminator keywords, where an annotation may be placed, and what the validator enforces. The vocabulary itself is Core/units.json, which is where both tables on this page come from.


This site uses Just the Docs, a documentation theme for Jekyll.