Skip to content

Commit 0c2231b

Browse files
jd-laraCopilot
andauthored
Add EmissionsData SupplementalAttribute for tracking emissions (#1682)
* Initial plan * Add EmissionsData supplemental attribute with enums, validation, accessors, and tests Agent-Logs-Url: https://github.com/Sienna-Platform/PowerSystems.jl/sessions/963273c4-85bf-46b6-a9d4-0a0824357b46 Co-authored-by: jd-lara <16385323+jd-lara@users.noreply.github.com> * Remove trailing blank line from emissions_enums.jl Agent-Logs-Url: https://github.com/Sienna-Platform/PowerSystems.jl/sessions/963273c4-85bf-46b6-a9d4-0a0824357b46 Co-authored-by: jd-lara <16385323+jd-lara@users.noreply.github.com> * Address review: move enums to definitions.jl, add isfinite checks, fix mktempdir and docs Agent-Logs-Url: https://github.com/Sienna-Platform/PowerSystems.jl/sessions/df0d07d8-e910-4e2e-8bbe-56624e2e2b80 Co-authored-by: jd-lara <16385323+jd-lara@users.noreply.github.com> * Address review: emission_rate as FunctionData, energy_unit mandatory, mass_unit defaults to KG - Changed emission_rate field type from Float64 to IS.FunctionData - Scalar Real values are auto-wrapped in LinearFunctionData - Made energy_unit a mandatory constructor argument (no default) - Changed mass_unit default from MassUnit.LB to MassUnit.KG - Added FunctionData-based set_emission_rate! method - Extracted _validate_basis_energy_unit helper - Updated tests for new API (50 tests passing) - Updated docs with FunctionData examples * Change emission_rate from IS.FunctionData to IS.ValueCurve - emission_rate field type is now IS.ValueCurve (typically IncrementalCurve) - Scalar convenience constructor wraps in IncrementalCurve with constant rate - set_emission_rate! accepts ValueCurve or scalar Real - Updated tests and docs to use ValueCurve/IncrementalCurve terminology * Add JSON round-trip tests with ValueCurve variants (linear, piecewise) * fix PR issues and docs --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jd-lara <16385323+jd-lara@users.noreply.github.com> Co-authored-by: Jose Daniel Lara <jdlara@berkeley.edu>
2 parents 11bb231 + e8f7f27 commit 0c2231b

7 files changed

Lines changed: 1009 additions & 1 deletion

File tree

docs/make.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ pages = OrderedDict(
7575
"explanation/dynamic_data.md",
7676
"explanation/supplemental_attributes.md",
7777
"explanation/plant_attributes.md",
78+
"explanation/emissions_data.md",
7879
],
7980
"Model Library" => Any[],
8081
"Reference" =>

docs/src/api/public.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ Modules = [PowerSystems]
3636
Pages = ["outages.jl",
3737
"contingencies.jl",
3838
"impedance_correction.jl",
39-
"plant_attribute.jl"
39+
"plant_attribute.jl",
40+
"emissions_data.jl"
4041
]
4142
Public = true
4243
Private = false
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# [Emissions Data](@id emissions_data)
2+
3+
## Motivation
4+
5+
`EmissionsData` is a [`SupplementalAttribute`](@ref supplemental_attributes) that pairs a
6+
pollutant identity (CO2, NOx, SO2, etc.) with an emission rate expressed as a
7+
[`ValueCurve`](@ref). This supports both constant rates and nonlinear
8+
relationships between fuel consumption / power output and emissions. By modeling
9+
emissions as a supplemental attribute rather than a field on each generator type, a single
10+
`EmissionsData` instance can be shared across multiple components (one-to-many attachment).
11+
This avoids data duplication when several units at the same plant share the same emission
12+
profile and allows emissions metadata to be added or removed without changing the component
13+
struct definitions.
14+
15+
## Example
16+
17+
```julia
18+
using PowerSystems
19+
using PowerSystemCaseBuilder
20+
21+
# Load a test system with thermal generators
22+
sys = build_system(PSITestSystems, "c_sys5_uc")
23+
thermals = collect(get_components(ThermalStandard, sys))
24+
25+
# Create a constant-rate emissions attribute (scalar wraps into IncrementalCurve)
26+
co2 = EmissionsData(;
27+
name = "co2_ccgt",
28+
pollutant = PollutantType.CO2,
29+
emission_rate = 117.6, # kg/MMBtu (constant rate)
30+
basis = EmissionBasis.FUEL_INPUT,
31+
energy_unit = EnergyUnit.MMBTU,
32+
)
33+
34+
# Create an emissions attribute with a linearly varying incremental rate
35+
nox = EmissionsData(;
36+
name = "nox_ccgt",
37+
pollutant = PollutantType.NOX,
38+
emission_rate = IncrementalCurve(LinearFunctionData(0.001, 0.01), nothing, nothing),
39+
basis = EmissionBasis.FUEL_INPUT,
40+
energy_unit = EnergyUnit.MMBTU,
41+
start_up_adder = 5.0, # 5 kg per cold start
42+
)
43+
44+
# Attach to generators — the same CO2 attribute is shared
45+
add_supplemental_attribute!(sys, thermals[1], co2)
46+
add_supplemental_attribute!(sys, thermals[2], co2) # shared instance
47+
add_supplemental_attribute!(sys, thermals[1], nox)
48+
```
49+
50+
## Emission Rate as ValueCurve
51+
52+
The `emission_rate` field accepts any [`ValueCurve`](@ref) subtype, typically an
53+
[`IncrementalCurve`](@ref) representing the emission rate (pollutant per unit of
54+
fuel or power):
55+
56+
- **`IncrementalCurve(LinearFunctionData(0, rate), ...)`** — constant emission rate
57+
- **`IncrementalCurve(LinearFunctionData(slope, intercept), ...)`** — linearly varying rate
58+
- **`IncrementalCurve(PiecewiseStepData(...), ...)`** — piecewise step rate (`PiecewiseIncrementalCurve`)
59+
60+
When constructing with a scalar `Real` value, the rate is automatically wrapped in an
61+
`IncrementalCurve` with constant rate. This makes simple constant-rate cases ergonomic
62+
while supporting complex nonlinear relationships for advanced use cases.
63+
64+
## Start-Up Adder
65+
66+
The `start_up_adder` field captures the transient pollutant pulse that occurs during a cold
67+
or warm start before combustion controls and post-combustion controls reach steady state.
68+
The adder is expressed in `mass_unit` per start event. How it is multiplied by start events
69+
is the responsibility of the consumer (e.g., a future PowerSimulations.jl integration will
70+
tie it to the start binary variable in the unit commitment formulation).
71+
72+
## Scope and Future Work
73+
74+
The following features are out of scope for this version and tracked in follow-up issues:
75+
76+
- Time-varying emission rates (time series support)
77+
- Hot/warm/cold split of the start-up adder
78+
- `EmissionsCap` and `EmissionsPrice` supplemental attribute types
79+
- Removal rate / pollution control fraction
80+
- PowerSimulations.jl integration
81+
- Parser support (CSV, Matpower, PSS/E, RTS data format)

src/PowerSystems.jl

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,28 @@ export get_impedance_correction_curve
330330
export get_transformer_winding
331331
export get_transformer_control_mode
332332

333+
# Emissions Data
334+
export EmissionsData
335+
export PollutantType
336+
export EmissionBasis
337+
export MassUnit
338+
export EnergyUnit
339+
export get_pollutant
340+
export get_emission_rate
341+
export get_basis
342+
export get_start_up_adder
343+
export get_mass_unit
344+
export get_energy_unit
345+
export get_gwp
346+
export set_emission_rate!
347+
export set_start_up_adder!
348+
export set_gwp!
349+
export set_pollutant!
350+
export set_mass_unit!
351+
export set_basis!
352+
export set_energy_unit!
353+
export set_basis_and_energy_unit!
354+
333355
export Service
334356
export AbstractReserve
335357
export Reserve
@@ -876,6 +898,7 @@ include("models/supplemental_setters.jl")
876898
# Supplemental attributes
877899
include("contingencies.jl")
878900
include("outages.jl")
901+
include("emissions_data.jl")
879902

880903
# Definitions of PowerSystem
881904
include("base.jl")

src/definitions.jl

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,3 +589,81 @@ const TRANSFORMER3W_PARAMETER_NAMES = [
589589
"COD", "CONT", "NOMV", "WINDV", "RMA", "RMI",
590590
"NTP", "VMA", "VMI", "RATA", "RATB", "RATC",
591591
]
592+
593+
# Emissions enums
594+
595+
IS.@scoped_enum(
596+
PollutantType,
597+
CO2 = 1,
598+
CO2E = 2,
599+
CH4 = 3,
600+
N2O = 4,
601+
NOX = 10,
602+
SO2 = 11,
603+
PM25 = 20,
604+
PM10 = 21,
605+
HG = 30,
606+
HAP = 40,
607+
CUSTOM = 99,
608+
)
609+
@doc """
610+
Enumeration of pollutant types for emissions tracking.
611+
612+
# Values
613+
- `CO2 = 1`: Carbon dioxide
614+
- `CO2E = 2`: Carbon dioxide equivalent
615+
- `CH4 = 3`: Methane
616+
- `N2O = 4`: Nitrous oxide
617+
- `NOX = 10`: Nitrogen oxides
618+
- `SO2 = 11`: Sulfur dioxide
619+
- `PM25 = 20`: Particulate matter (2.5 μm)
620+
- `PM10 = 21`: Particulate matter (10 μm)
621+
- `HG = 30`: Mercury
622+
- `HAP = 40`: Hazardous air pollutants
623+
- `CUSTOM = 99`: User-defined pollutant
624+
""" PollutantType
625+
626+
IS.@scoped_enum(
627+
EmissionBasis,
628+
FUEL_INPUT = 1,
629+
POWER_OUTPUT = 2,
630+
)
631+
@doc """
632+
Enumeration of emission rate basis types.
633+
634+
# Values
635+
- `FUEL_INPUT = 1`: Mass per unit of heat input (e.g., lb/MMBtu, kg/GJ)
636+
- `POWER_OUTPUT = 2`: Mass per unit of electrical output (e.g., lb/MWh, kg/MWh)
637+
""" EmissionBasis
638+
639+
IS.@scoped_enum(
640+
MassUnit,
641+
KG = 1,
642+
LB = 2,
643+
SHORT_TON = 3,
644+
METRIC_TON = 4,
645+
)
646+
@doc """
647+
Enumeration of mass units for emissions.
648+
649+
# Values
650+
- `KG = 1`: Kilograms
651+
- `LB = 2`: Pounds
652+
- `SHORT_TON = 3`: Short tons (2000 lb)
653+
- `METRIC_TON = 4`: Metric tons (1000 kg)
654+
""" MassUnit
655+
656+
IS.@scoped_enum(
657+
EnergyUnit,
658+
MMBTU = 1,
659+
GJ = 2,
660+
MWH = 3,
661+
)
662+
@doc """
663+
Enumeration of energy units for emissions rate denominator.
664+
665+
# Values
666+
- `MMBTU = 1`: Million British thermal units
667+
- `GJ = 2`: Gigajoules
668+
- `MWH = 3`: Megawatt-hours
669+
""" EnergyUnit

0 commit comments

Comments
 (0)