Skip to content
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pages = OrderedDict(
"explanation/dynamic_data.md",
"explanation/supplemental_attributes.md",
"explanation/plant_attributes.md",
"explanation/emissions_data.md",
],
"Model Library" => Any[],
"Reference" =>
Expand Down
3 changes: 2 additions & 1 deletion docs/src/api/public.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ Modules = [PowerSystems]
Pages = ["outages.jl",
"contingencies.jl",
"impedance_correction.jl",
"plant_attribute.jl"
"plant_attribute.jl",
"emissions_data.jl"
]
Public = true
Private = false
Expand Down
81 changes: 81 additions & 0 deletions docs/src/explanation/emissions_data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# [Emissions Data](@id emissions_data)

## Motivation

`EmissionsData` is a [`SupplementalAttribute`](@ref supplemental_attributes) that pairs a
pollutant identity (CO2, NOx, SO2, etc.) with an emission rate expressed as a
[`ValueCurve`](@ref). This supports both constant rates and nonlinear
relationships between fuel consumption / power output and emissions. By modeling
emissions as a supplemental attribute rather than a field on each generator type, a single
`EmissionsData` instance can be shared across multiple components (one-to-many attachment).
This avoids data duplication when several units at the same plant share the same emission
profile and allows emissions metadata to be added or removed without changing the component
struct definitions.

## Example

```julia
using PowerSystems
using PowerSystemCaseBuilder

# Load a test system with thermal generators
sys = build_system(PSITestSystems, "c_sys5_uc")
thermals = collect(get_components(ThermalStandard, sys))

# Create a constant-rate emissions attribute (scalar wraps into IncrementalCurve)
co2 = EmissionsData(;
name = "co2_ccgt",
pollutant = PollutantType.CO2,
emission_rate = 117.6, # kg/MMBtu (constant rate)
basis = EmissionBasis.FUEL_INPUT,
energy_unit = EnergyUnit.MMBTU,
)

# Create an emissions attribute with a linearly varying incremental rate
nox = EmissionsData(;
name = "nox_ccgt",
pollutant = PollutantType.NOX,
emission_rate = IncrementalCurve(LinearFunctionData(0.001, 0.01), nothing, nothing),
basis = EmissionBasis.FUEL_INPUT,
energy_unit = EnergyUnit.MMBTU,
start_up_adder = 5.0, # 5 kg per cold start
)

# Attach to generators — the same CO2 attribute is shared
add_supplemental_attribute!(sys, thermals[1], co2)
add_supplemental_attribute!(sys, thermals[2], co2) # shared instance
add_supplemental_attribute!(sys, thermals[1], nox)
```

## Emission Rate as ValueCurve

The `emission_rate` field accepts any [`ValueCurve`](@ref) subtype, typically an
[`IncrementalCurve`](@ref) representing the emission rate (pollutant per unit of
fuel or power):

- **`IncrementalCurve(LinearFunctionData(0, rate), ...)`** — constant emission rate
- **`IncrementalCurve(LinearFunctionData(slope, intercept), ...)`** — linearly varying rate
- **`IncrementalCurve(PiecewiseStepData(...), ...)`** — piecewise step rate (`PiecewiseIncrementalCurve`)

When constructing with a scalar `Real` value, the rate is automatically wrapped in an
`IncrementalCurve` with constant rate. This makes simple constant-rate cases ergonomic
while supporting complex nonlinear relationships for advanced use cases.

## Start-Up Adder

The `start_up_adder` field captures the transient pollutant pulse that occurs during a cold
or warm start before combustion controls and post-combustion controls reach steady state.
The adder is expressed in `mass_unit` per start event. How it is multiplied by start events
is the responsibility of the consumer (e.g., a future PowerSimulations.jl integration will
tie it to the start binary variable in the unit commitment formulation).

## Scope and Future Work

The following features are out of scope for this version and tracked in follow-up issues:

- Time-varying emission rates (time series support)
- Hot/warm/cold split of the start-up adder
- `EmissionsCap` and `EmissionsPrice` supplemental attribute types
- Removal rate / pollution control fraction
- PowerSimulations.jl integration
- Parser support (CSV, Matpower, PSS/E, RTS data format)
23 changes: 23 additions & 0 deletions src/PowerSystems.jl
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,28 @@ export get_impedance_correction_curve
export get_transformer_winding
export get_transformer_control_mode

# Emissions Data
export EmissionsData
export PollutantType
export EmissionBasis
export MassUnit
export EnergyUnit
export get_pollutant
export get_emission_rate
export get_basis
export get_start_up_adder
export get_mass_unit
export get_energy_unit
export get_gwp
export set_emission_rate!
export set_start_up_adder!
export set_gwp!
export set_pollutant!
export set_mass_unit!
export set_basis!
export set_energy_unit!
export set_basis_and_energy_unit!

export Service
export AbstractReserve
export Reserve
Expand Down Expand Up @@ -876,6 +898,7 @@ include("models/supplemental_setters.jl")
# Supplemental attributes
include("contingencies.jl")
include("outages.jl")
include("emissions_data.jl")

# Definitions of PowerSystem
include("base.jl")
Expand Down
78 changes: 78 additions & 0 deletions src/definitions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -589,3 +589,81 @@ const TRANSFORMER3W_PARAMETER_NAMES = [
"COD", "CONT", "NOMV", "WINDV", "RMA", "RMI",
"NTP", "VMA", "VMI", "RATA", "RATB", "RATC",
]

# Emissions enums

IS.@scoped_enum(
PollutantType,
CO2 = 1,
CO2E = 2,
CH4 = 3,
N2O = 4,
NOX = 10,
SO2 = 11,
PM25 = 20,
PM10 = 21,
HG = 30,
HAP = 40,
CUSTOM = 99,
)
@doc """
Enumeration of pollutant types for emissions tracking.

# Values
- `CO2 = 1`: Carbon dioxide
- `CO2E = 2`: Carbon dioxide equivalent
- `CH4 = 3`: Methane
- `N2O = 4`: Nitrous oxide
- `NOX = 10`: Nitrogen oxides
- `SO2 = 11`: Sulfur dioxide
- `PM25 = 20`: Particulate matter (2.5 μm)
- `PM10 = 21`: Particulate matter (10 μm)
- `HG = 30`: Mercury
- `HAP = 40`: Hazardous air pollutants
- `CUSTOM = 99`: User-defined pollutant
""" PollutantType

IS.@scoped_enum(
EmissionBasis,
FUEL_INPUT = 1,
POWER_OUTPUT = 2,
)
@doc """
Enumeration of emission rate basis types.

# Values
- `FUEL_INPUT = 1`: Mass per unit of heat input (e.g., lb/MMBtu, kg/GJ)
- `POWER_OUTPUT = 2`: Mass per unit of electrical output (e.g., lb/MWh, kg/MWh)
""" EmissionBasis

IS.@scoped_enum(
MassUnit,
KG = 1,
LB = 2,
SHORT_TON = 3,
METRIC_TON = 4,
)
@doc """
Enumeration of mass units for emissions.

# Values
- `KG = 1`: Kilograms
- `LB = 2`: Pounds
- `SHORT_TON = 3`: Short tons (2000 lb)
- `METRIC_TON = 4`: Metric tons (1000 kg)
""" MassUnit

IS.@scoped_enum(
EnergyUnit,
MMBTU = 1,
GJ = 2,
MWH = 3,
)
@doc """
Enumeration of energy units for emissions rate denominator.

# Values
- `MMBTU = 1`: Million British thermal units
- `GJ = 2`: Gigajoules
- `MWH = 3`: Megawatt-hours
""" EnergyUnit
Loading
Loading