Skip to content

Commit e8f7f27

Browse files
committed
fix PR issues and docs
1 parent 97b2d46 commit e8f7f27

6 files changed

Lines changed: 235 additions & 30 deletions

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

docs/src/explanation/emissions_data.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ The `emission_rate` field accepts any [`ValueCurve`](@ref) subtype, typically an
5353
[`IncrementalCurve`](@ref) representing the emission rate (pollutant per unit of
5454
fuel or power):
5555

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

6060
When constructing with a scalar `Real` value, the rate is automatically wrapped in an
6161
`IncrementalCurve` with constant rate. This makes simple constant-rate cases ergonomic
@@ -73,9 +73,9 @@ tie it to the start binary variable in the unit commitment formulation).
7373

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

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)
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,11 @@ export get_gwp
346346
export set_emission_rate!
347347
export set_start_up_adder!
348348
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!
349354

350355
export Service
351356
export AbstractReserve

src/emissions_data.jl

Lines changed: 108 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,6 @@ function _validate_nonneg_finite(val::Real, field::String)
4343
end
4444
end
4545

46-
function _validate_pos_finite(val::Real, field::String)
47-
if !isfinite(val) || val <= 0.0
48-
throw(ArgumentError("$field must be finite and > 0.0, got $val"))
49-
end
50-
end
51-
5246
function _validate_basis_energy_unit(basis::EmissionBasis, energy_unit::EnergyUnit)
5347
if basis == EmissionBasis.FUEL_INPUT
5448
if energy_unit != EnergyUnit.MMBTU && energy_unit != EnergyUnit.GJ
@@ -66,9 +60,63 @@ function _validate_basis_energy_unit(basis::EmissionBasis, energy_unit::EnergyUn
6660
),
6761
)
6862
end
63+
else
64+
throw(
65+
ArgumentError(
66+
"unhandled EmissionBasis $basis; update _validate_basis_energy_unit",
67+
),
68+
)
6969
end
7070
end
7171

72+
# Emission-rate helpers (shared between constructor and setters)
73+
74+
"""Wrap a scalar emission rate in a constant-rate `IncrementalCurve` after validation."""
75+
function _emission_rate_curve(val::Real)
76+
_validate_nonneg_finite(val, "emission_rate")
77+
return IS.IncrementalCurve(LinearFunctionData(0.0, Float64(val)), nothing, nothing)
78+
end
79+
80+
"""
81+
Validate that an `emission_rate` [`ValueCurve`](@ref) holds only finite coefficients and
82+
non-negative rate values (the rate at zero input and every tabulated rate must be `>= 0`).
83+
"""
84+
function _validate_emission_rate_curve(curve::IS.ValueCurve)
85+
_validate_emission_rate_function_data(get_function_data(curve))
86+
return
87+
end
88+
89+
function _validate_emission_rate_function_data(fd::LinearFunctionData)
90+
_validate_nonneg_finite(get_constant_term(fd), "emission_rate (rate at zero input)")
91+
isfinite(get_proportional_term(fd)) || throw(
92+
ArgumentError(
93+
"emission_rate slope must be finite, got $(get_proportional_term(fd))",
94+
),
95+
)
96+
return
97+
end
98+
99+
function _validate_emission_rate_function_data(fd::QuadraticFunctionData)
100+
_validate_nonneg_finite(get_constant_term(fd), "emission_rate (rate at zero input)")
101+
for term in (get_proportional_term(fd), get_quadratic_term(fd))
102+
isfinite(term) ||
103+
throw(ArgumentError("emission_rate coefficients must be finite, got $term"))
104+
end
105+
return
106+
end
107+
108+
function _validate_emission_rate_function_data(
109+
fd::Union{PiecewiseLinearData, PiecewiseStepData},
110+
)
111+
for r in get_y_coords(fd)
112+
_validate_nonneg_finite(r, "emission_rate")
113+
end
114+
return
115+
end
116+
117+
# Safe fallback for any future FunctionData subtype not enumerated above.
118+
_validate_emission_rate_function_data(::IS.FunctionData) = nothing
119+
72120
"""
73121
EmissionsData(; name, pollutant, emission_rate, basis, energy_unit, ...)
74122
@@ -92,20 +140,16 @@ function EmissionsData(;
92140
internal::InfrastructureSystemsInternal = InfrastructureSystemsInternal(),
93141
)
94142
_validate_nonneg_finite(start_up_adder, "start_up_adder")
95-
_validate_pos_finite(gwp, "gwp")
143+
_validate_nonneg_finite(gwp, "gwp")
96144

97145
# Validate basis/energy_unit combination
98146
_validate_basis_energy_unit(basis, energy_unit)
99147

100-
# Convert scalar to IncrementalCurve with constant rate
148+
# Normalize emission_rate to a validated ValueCurve
101149
if emission_rate isa Real
102-
_validate_nonneg_finite(emission_rate, "emission_rate")
103-
rate = IS.IncrementalCurve(
104-
LinearFunctionData(0.0, Float64(emission_rate)),
105-
nothing,
106-
nothing,
107-
)
150+
rate = _emission_rate_curve(emission_rate)
108151
else
152+
_validate_emission_rate_curve(emission_rate)
109153
rate = emission_rate
110154
end
111155

@@ -152,15 +196,14 @@ get_internal(value::EmissionsData) = value.internal
152196

153197
"""Set [`EmissionsData`](@ref) `emission_rate` with a [`ValueCurve`](@ref)."""
154198
function set_emission_rate!(value::EmissionsData, val::IS.ValueCurve)
199+
_validate_emission_rate_curve(val)
155200
value.emission_rate = val
156201
return
157202
end
158203

159204
"""Set [`EmissionsData`](@ref) `emission_rate` with a scalar (wraps in `IncrementalCurve` with constant rate)."""
160205
function set_emission_rate!(value::EmissionsData, val::Real)
161-
_validate_nonneg_finite(val, "emission_rate")
162-
value.emission_rate =
163-
IS.IncrementalCurve(LinearFunctionData(0.0, Float64(val)), nothing, nothing)
206+
value.emission_rate = _emission_rate_curve(val)
164207
return
165208
end
166209

@@ -179,7 +222,54 @@ end
179222

180223
"""Set [`EmissionsData`](@ref) `gwp`."""
181224
function set_gwp!(value::EmissionsData, val::Real)
182-
_validate_pos_finite(val, "gwp")
225+
_validate_nonneg_finite(val, "gwp")
183226
value.gwp = Float64(val)
184227
return
185228
end
229+
230+
"""Set [`EmissionsData`](@ref) `pollutant`."""
231+
function set_pollutant!(value::EmissionsData, val::PollutantType)
232+
value.pollutant = val
233+
return
234+
end
235+
236+
"""Set [`EmissionsData`](@ref) `mass_unit`."""
237+
function set_mass_unit!(value::EmissionsData, val::MassUnit)
238+
value.mass_unit = val
239+
return
240+
end
241+
242+
"""
243+
Set [`EmissionsData`](@ref) `basis`, validating it against the current `energy_unit`. To
244+
switch between FUEL_INPUT and POWER_OUTPUT (which also requires changing `energy_unit`),
245+
use [`set_basis_and_energy_unit!`](@ref) instead.
246+
"""
247+
function set_basis!(value::EmissionsData, val::EmissionBasis)
248+
_validate_basis_energy_unit(val, value.energy_unit)
249+
value.basis = val
250+
return
251+
end
252+
253+
"""Set [`EmissionsData`](@ref) `energy_unit`, validating it against the current `basis`."""
254+
function set_energy_unit!(value::EmissionsData, val::EnergyUnit)
255+
_validate_basis_energy_unit(value.basis, val)
256+
value.energy_unit = val
257+
return
258+
end
259+
260+
"""
261+
Set [`EmissionsData`](@ref) `basis` and `energy_unit` together, validating the combination.
262+
This is the supported way to retarget an attribute between FUEL_INPUT and POWER_OUTPUT,
263+
since neither field can be changed individually without transiently violating the
264+
basis/energy_unit invariant.
265+
"""
266+
function set_basis_and_energy_unit!(
267+
value::EmissionsData,
268+
basis::EmissionBasis,
269+
energy_unit::EnergyUnit,
270+
)
271+
_validate_basis_energy_unit(basis, energy_unit)
272+
value.basis = basis
273+
value.energy_unit = energy_unit
274+
return
275+
end

test/test_emissions_data.jl

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,25 @@
9393
start_up_adder = -0.5,
9494
)
9595

96-
# Zero gwp
96+
# Zero gwp is allowed (e.g. a pollutant excluded from CO2-equivalent accounting)
97+
zero_gwp = EmissionsData(;
98+
name = "zero_gwp",
99+
pollutant = PollutantType.SO2,
100+
emission_rate = 1.0,
101+
basis = EmissionBasis.FUEL_INPUT,
102+
energy_unit = EnergyUnit.MMBTU,
103+
gwp = 0.0,
104+
)
105+
@test get_gwp(zero_gwp) == 0.0
106+
107+
# Negative gwp is rejected
97108
@test_throws ArgumentError EmissionsData(;
98109
name = "bad",
99110
pollutant = PollutantType.CO2,
100111
emission_rate = 1.0,
101112
basis = EmissionBasis.FUEL_INPUT,
102113
energy_unit = EnergyUnit.MMBTU,
103-
gwp = 0.0,
114+
gwp = -1.0,
104115
)
105116

106117
# MWH with FUEL_INPUT
@@ -167,6 +178,103 @@
167178
@test_throws ArgumentError set_gwp!(e, Inf)
168179
end
169180

181+
@testset "ValueCurve emission_rate validation" begin
182+
# Negative constant rate (rate at zero input < 0)
183+
@test_throws ArgumentError EmissionsData(;
184+
name = "bad",
185+
pollutant = PollutantType.CO2,
186+
emission_rate = IS.IncrementalCurve(
187+
LinearFunctionData(0.0, -5.0), nothing, nothing,
188+
),
189+
basis = EmissionBasis.FUEL_INPUT,
190+
energy_unit = EnergyUnit.MMBTU,
191+
)
192+
193+
# Non-finite slope
194+
@test_throws ArgumentError EmissionsData(;
195+
name = "bad",
196+
pollutant = PollutantType.CO2,
197+
emission_rate = IS.IncrementalCurve(
198+
LinearFunctionData(Inf, 1.0), nothing, nothing,
199+
),
200+
basis = EmissionBasis.FUEL_INPUT,
201+
energy_unit = EnergyUnit.MMBTU,
202+
)
203+
204+
# Negative piecewise step rate
205+
@test_throws ArgumentError EmissionsData(;
206+
name = "bad",
207+
pollutant = PollutantType.SO2,
208+
emission_rate = IS.IncrementalCurve(
209+
PiecewiseStepData([0.0, 100.0, 200.0], [50.0, -10.0]),
210+
0.0,
211+
nothing,
212+
),
213+
basis = EmissionBasis.POWER_OUTPUT,
214+
energy_unit = EnergyUnit.MWH,
215+
)
216+
217+
# A decreasing-but-non-negative-at-origin linear rate is allowed
218+
ok = EmissionsData(;
219+
name = "ok",
220+
pollutant = PollutantType.NOX,
221+
emission_rate = IS.IncrementalCurve(
222+
LinearFunctionData(-0.001, 5.0), nothing, nothing,
223+
),
224+
basis = EmissionBasis.FUEL_INPUT,
225+
energy_unit = EnergyUnit.MMBTU,
226+
)
227+
@test get_emission_rate(ok) ==
228+
IS.IncrementalCurve(LinearFunctionData(-0.001, 5.0), nothing, nothing)
229+
230+
# Setter rejects an invalid ValueCurve too
231+
e = EmissionsData(;
232+
name = "setter_curve",
233+
pollutant = PollutantType.CO2,
234+
emission_rate = 1.0,
235+
basis = EmissionBasis.FUEL_INPUT,
236+
energy_unit = EnergyUnit.MMBTU,
237+
)
238+
@test_throws ArgumentError set_emission_rate!(
239+
e,
240+
IS.IncrementalCurve(LinearFunctionData(0.0, -1.0), nothing, nothing),
241+
)
242+
end
243+
244+
@testset "Validated setters for enum fields" begin
245+
e = EmissionsData(;
246+
name = "setters",
247+
pollutant = PollutantType.CO2,
248+
emission_rate = 1.0,
249+
basis = EmissionBasis.FUEL_INPUT,
250+
energy_unit = EnergyUnit.MMBTU,
251+
)
252+
253+
set_pollutant!(e, PollutantType.NOX)
254+
@test get_pollutant(e) == PollutantType.NOX
255+
256+
set_mass_unit!(e, MassUnit.LB)
257+
@test get_mass_unit(e) == MassUnit.LB
258+
259+
# Valid energy_unit change within FUEL_INPUT (MMBTU -> GJ)
260+
set_energy_unit!(e, EnergyUnit.GJ)
261+
@test get_energy_unit(e) == EnergyUnit.GJ
262+
263+
# Individual setters enforce the basis/energy_unit invariant
264+
@test_throws ArgumentError set_basis!(e, EmissionBasis.POWER_OUTPUT)
265+
@test_throws ArgumentError set_energy_unit!(e, EnergyUnit.MWH)
266+
267+
# Combined setter is the supported way to switch basis + energy_unit atomically
268+
set_basis_and_energy_unit!(e, EmissionBasis.POWER_OUTPUT, EnergyUnit.MWH)
269+
@test get_basis(e) == EmissionBasis.POWER_OUTPUT
270+
@test get_energy_unit(e) == EnergyUnit.MWH
271+
272+
# Combined setter still rejects an inconsistent pair
273+
@test_throws ArgumentError set_basis_and_energy_unit!(
274+
e, EmissionBasis.FUEL_INPUT, EnergyUnit.MWH,
275+
)
276+
end
277+
170278
@testset "Default mass_unit is KG" begin
171279
e = EmissionsData(;
172280
name = "test_defaults",

0 commit comments

Comments
 (0)