Context
The active-power upper/lower-bound constraint machinery across thermal and load device families uses a mix of constraint types, meta strings, and row-counting conventions that isn't documented anywhere. This issue captures the current state on paper, as a prerequisite to deciding what's intentional design vs. accumulated technical debt.
All keys below are ConstraintKey(ConstraintType, ComponentType, meta).
Loads
InterruptiblePowerLoad with PowerLoadInterruption + forecast attached:
| Key |
RHS |
Source |
(ActivePowerVariableTimeSeriesLimitsConstraint, InterruptiblePowerLoad, "") |
p ≤ forecast_t · pmax |
electric_loads.jl:118-140 → add_parameterized_upper_bound_range_constraints |
(ActivePowerVariableLimitsConstraint, InterruptiblePowerLoad, "binary") |
p ≤ u · pmax |
electric_loads.jl:142-169 |
Plus a JuMP variable lower bound p ≥ 0 from get_variable_lower_bound (electric_loads.jl:9). There is no ActivePowerVariableLimitsConstraint row for the nameplate — the nameplate UB is enforced via the JuMP variable's upper bound, not a constraint.
PowerLoadDispatch omits the "binary" row (no OnVariable) and otherwise looks the same.
Thermals
AbstractThermalDispatchFormulation (no commitment, no forecast):
| Key |
RHS |
Source |
(ActivePowerVariableLimitsConstraint, ThermalGen, "lb"/"ub") |
pmin ≤ p ≤ pmax via add_range_constraints! |
thermal_generation.jl:294-310 |
AbstractThermalUnitCommitment (commitment, no forecast):
| Key |
RHS |
Source |
(ActivePowerVariableLimitsConstraint, ThermalGen, "lb"/"ub") |
pmin · u ≤ p ≤ pmax · u via add_semicontinuous_range_constraints! |
thermal_generation.jl:377-391 |
AbstractThermalUnitCommitment + forecast attached — adds on top of commitment:
| Key |
RHS |
Source |
(ActivePowerVariableTimeSeriesLimitsConstraint, ThermalGen, "") |
p ≤ forecast_t · pmax |
thermal_generation.jl:462-484 |
ThermalMultiStartUnitCommitment — multiple metas under the same constraint type:
| Key |
RHS |
Source |
(ActivePowerVariableLimitsConstraint, ThermalMultiStart, "on") |
p ≤ (pmax−pmin)·u − max(pmax−startup, 0)·v |
thermal_generation.jl:489-567 |
(ActivePowerVariableLimitsConstraint, ThermalMultiStart, "off") |
startup/shutdown trajectory UB |
same method |
(ActivePowerVariableLimitsConstraint, ThermalMultiStart, "lb") |
p ≥ 0 |
same method |
(ActivePowerVariableLimitsConstraint, ThermalMultiStart, "ubon") |
via ActivePowerRangeExpressionUB |
thermal_generation.jl:614-681 |
(ActivePowerVariableLimitsConstraint, ThermalMultiStart, "uboff") |
same |
same |
(ActivePowerVariableLimitsConstraint, ThermalMultiStart, "lb") (expression) |
expression ≥ 0 via ActivePowerRangeExpressionLB |
thermal_generation.jl:569-612 |
Observed inconsistencies
Descriptive, not prescriptive — flagging for discussion, not proposing fixes:
-
Commitment coupling lives under different keys across device families.
- Thermals:
(ActivePowerVariableLimitsConstraint, …, "lb"/"ub") for both dispatch and UC, plus "on"/"off"/"ubon"/"uboff" for multi-start.
- Loads:
(ActivePowerVariableLimitsConstraint, …, "binary") — a meta string that exists nowhere on the thermal side.
-
Forecast + commitment are never folded into a single row. Thermal UC with forecast produces two separate rows (p ≤ pmax·u and p ≤ forecast_t · pmax) under different constraint types. Loads do the same. ThermalMultiStartUnitCommitment does fold commitment and startup/shutdown trajectories into a single row — but not forecast.
-
The meta-string vocabulary is ad-hoc. Thermals use "lb", "ub", "on", "off", "ubon", "uboff". Loads use "binary". Nothing enforces or documents the vocabulary; no registry of valid metas per constraint type.
-
Lower bounds are enforced three different ways.
- JuMP variable bound (loads; thermal dispatch via
add_range_constraints!)
- Explicit constraint row with meta
"lb" (thermal multi-start)
- Expression-based
≥ 0 under the same "lb" meta (thermal multi-start with ActivePowerRangeExpressionLB)
The ThermalMultiStart case reuses the "lb" meta for two semantically different rows (one on the variable, one on an expression), which would collide if both code paths ran for the same device.
-
Nameplate UB materialization is asymmetric. Thermals always create an ActivePowerVariableLimitsConstraint row. Loads push the nameplate into the JuMP variable's upper bound and only materialize a constraint row when commitment (PowerLoadInterruption) or a forecast is involved.
Context
The active-power upper/lower-bound constraint machinery across thermal and load device families uses a mix of constraint types, meta strings, and row-counting conventions that isn't documented anywhere. This issue captures the current state on paper, as a prerequisite to deciding what's intentional design vs. accumulated technical debt.
All keys below are
ConstraintKey(ConstraintType, ComponentType, meta).Loads
InterruptiblePowerLoadwithPowerLoadInterruption+ forecast attached:(ActivePowerVariableTimeSeriesLimitsConstraint, InterruptiblePowerLoad, "")p ≤ forecast_t · pmaxelectric_loads.jl:118-140→add_parameterized_upper_bound_range_constraints(ActivePowerVariableLimitsConstraint, InterruptiblePowerLoad, "binary")p ≤ u · pmaxelectric_loads.jl:142-169Plus a JuMP variable lower bound
p ≥ 0fromget_variable_lower_bound(electric_loads.jl:9). There is noActivePowerVariableLimitsConstraintrow for the nameplate — the nameplate UB is enforced via the JuMP variable's upper bound, not a constraint.PowerLoadDispatchomits the"binary"row (noOnVariable) and otherwise looks the same.Thermals
AbstractThermalDispatchFormulation(no commitment, no forecast):(ActivePowerVariableLimitsConstraint, ThermalGen, "lb"/"ub")pmin ≤ p ≤ pmaxviaadd_range_constraints!thermal_generation.jl:294-310AbstractThermalUnitCommitment(commitment, no forecast):(ActivePowerVariableLimitsConstraint, ThermalGen, "lb"/"ub")pmin · u ≤ p ≤ pmax · uviaadd_semicontinuous_range_constraints!thermal_generation.jl:377-391AbstractThermalUnitCommitment+ forecast attached — adds on top of commitment:(ActivePowerVariableTimeSeriesLimitsConstraint, ThermalGen, "")p ≤ forecast_t · pmaxthermal_generation.jl:462-484ThermalMultiStartUnitCommitment— multiple metas under the same constraint type:(ActivePowerVariableLimitsConstraint, ThermalMultiStart, "on")p ≤ (pmax−pmin)·u − max(pmax−startup, 0)·vthermal_generation.jl:489-567(ActivePowerVariableLimitsConstraint, ThermalMultiStart, "off")(ActivePowerVariableLimitsConstraint, ThermalMultiStart, "lb")p ≥ 0(ActivePowerVariableLimitsConstraint, ThermalMultiStart, "ubon")ActivePowerRangeExpressionUBthermal_generation.jl:614-681(ActivePowerVariableLimitsConstraint, ThermalMultiStart, "uboff")(ActivePowerVariableLimitsConstraint, ThermalMultiStart, "lb")(expression)expression ≥ 0viaActivePowerRangeExpressionLBthermal_generation.jl:569-612Observed inconsistencies
Descriptive, not prescriptive — flagging for discussion, not proposing fixes:
Commitment coupling lives under different keys across device families.
(ActivePowerVariableLimitsConstraint, …, "lb"/"ub")for both dispatch and UC, plus"on"/"off"/"ubon"/"uboff"for multi-start.(ActivePowerVariableLimitsConstraint, …, "binary")— a meta string that exists nowhere on the thermal side.Forecast + commitment are never folded into a single row. Thermal UC with forecast produces two separate rows (
p ≤ pmax·uandp ≤ forecast_t · pmax) under different constraint types. Loads do the same.ThermalMultiStartUnitCommitmentdoes fold commitment and startup/shutdown trajectories into a single row — but not forecast.The meta-string vocabulary is ad-hoc. Thermals use
"lb","ub","on","off","ubon","uboff". Loads use"binary". Nothing enforces or documents the vocabulary; no registry of valid metas per constraint type.Lower bounds are enforced three different ways.
add_range_constraints!)"lb"(thermal multi-start)≥ 0under the same"lb"meta (thermal multi-start withActivePowerRangeExpressionLB)The
ThermalMultiStartcase reuses the"lb"meta for two semantically different rows (one on the variable, one on an expression), which would collide if both code paths ran for the same device.Nameplate UB materialization is asymmetric. Thermals always create an
ActivePowerVariableLimitsConstraintrow. Loads push the nameplate into the JuMP variable's upper bound and only materialize a constraint row when commitment (PowerLoadInterruption) or a forecast is involved.