Formulation Library

Introduction

A PowerOperationsModels problem is assembled from three kinds of formulation choices:

  • one network formulation (NetworkModel{N}) for the whole problem — how the transmission system is represented,
  • a device formulation (DeviceModel{D, F}) per device type — how each component type is modelled,
  • a service formulation (ServiceModel{S, F}) per service — how each service is modelled.

Every formulation implements construct_device! (or construct_service! / construct_network!) for two dispatch stages, and the split matters when reading the tables below:

StageWhat it may add
ArgumentConstructStagevariables, parameters, expressions, feedforward arguments
ModelConstructStageconstraints, feedforward constraints, objective terms, constraint duals

Expressions must be wired before the constraints that consume them, which is why the balance expressions (ActivePowerBalance, ReactivePowerBalance) are populated during the argument stage and only closed with an == 0 constraint during the model stage.

Not every (device formulation, network formulation) pair is valid. Pairs that are rejected are caught by template validation, which throws IS.ConflictingInputsError rather than failing deep in the build. The tables mark unsupported pairs explicitly.

How to read these tables

  • Variables are the JuMP variables the formulation creates.
  • Constraints are the constraint containers it creates.
  • Expressions are written as Target ← Source, meaning the source variable is added into the target balance expression.
  • meta lists the meta strings used to slice a single key type into several containers (see When to use meta versus a new key type below).
  • no-op means a method exists and returns without emitting anything — the pair is legal but contributes nothing. This is distinct from unsupported, where no method exists.

Network Formulations

Network model type hierarchy

The abstract bounds that drive dispatch:

IOM.AbstractNetworkModel
├── AbstractActivePowerModel
│   ├── AbstractDCPNetworkModel
│   │   ├── DCPNetworkModel
│   │   ├── AbstractPTDFNetworkModel
│   │   │   ├── PTDFNetworkModel
│   │   │   └── AreaPTDFNetworkModel
│   │   ├── AbstractNFANetworkModel   → NFANetworkModel
│   │   └── AbstractDCPLLNetworkModel → DCPLLNetworkModel
│   ├── CopperPlateNetworkModel
│   └── AreaBalanceNetworkModel
└── AbstractReactivePowerNetworkModel
    ├── AbstractACPModel          → ACPNetworkModel
    ├── AbstractACRNetworkModel   → ACRNetworkModel
    ├── AbstractLPACCNetworkModel → LPACCNetworkModel
    └── AbstractIVRNetworkModel   → IVRNetworkModel

Note that AbstractPTDFNetworkModel <: AbstractDCPNetworkModel, so any method bound to <:AbstractDCPNetworkModel also matches the PTDF models. CopperPlateNetworkModel and AreaBalanceNetworkModel are not DCP subtypes.

The shorthands used throughout these tables:

  • ACP — AC power flow in polar coordinates (voltage magnitude and angle),
  • ACR — AC power flow in rectangular coordinates (voltage real and imaginary),
  • IVR — AC current-voltage rectangular formulation,
  • LPACC — linear-programming AC approximation (cold-start),
  • DCP — DC power flow; DCPLL — DC power flow with line losses,
  • NFA — network flow approximation (no voltage angles),
  • PTDF / AreaPTDF — power-transfer-distribution-factor transport, nodal / area-keyed.

What each network model adds

The balance expression containers themselves are allocated up front by initialize_system_expressions!, before any construct_*! runs. A network model does not create them; it wires the system slacks into them (argument stage) and closes them with a balance constraint (model stage).

Network modelArgument stage: variablesModel stage: constraints
DCPNetworkModelVoltageAngle, system slacks (active only)ReferenceBusConstraint, NodalBalanceActiveConstraint
DCPLLNetworkModelVoltageAngle, system slacks (active only)ReferenceBusConstraint, NodalBalanceActiveConstraint
NFANetworkModelsystem slacks (active only); no voltage variablesNodalBalanceActiveConstraint only — no ReferenceBusConstraint
PTDFNetworkModelsystem slacks (active only); no voltage variablesCopperPlateBalanceConstraint
AreaPTDFNetworkModelsystem slacks (active only); no voltage variablesCopperPlateBalanceConstraint (keyed on PSY.Area)
CopperPlateNetworkModelsystem slacks (active only); no voltage variablesCopperPlateBalanceConstraint
AreaBalanceNetworkModelsystem slacks (active only); no voltage variablesCopperPlateBalanceConstraint (keyed on PSY.Area)
ACPNetworkModelVoltageAngle, VoltageMagnitude, system slacks (active + reactive)ReferenceBusConstraint, NodalBalanceActiveConstraint, NodalBalanceReactiveConstraint
ACRNetworkModelVoltageReal, VoltageImaginary, system slacks (active + reactive)ReferenceBusConstraint, VoltageMagnitudeConstraint, NodalBalanceActiveConstraint, NodalBalanceReactiveConstraint
IVRNetworkModelVoltageReal, VoltageImaginary, system slacks (active + reactive)ReferenceBusConstraint, VoltageMagnitudeConstraint, NodalBalanceActiveConstraint, NodalBalanceReactiveConstraint
LPACCNetworkModelVoltageAngle, VoltageDeviation, system slacks (active + reactive)ReferenceBusConstraint, NodalBalanceActiveConstraint, NodalBalanceReactiveConstraint — no VoltageMagnitudeConstraint

All network variables, including the slacks, are created in the argument stage. System slacks are only added when use_slacks = true on the NetworkModel, and they are penalized in the objective at BALANCE_SLACK_COST during the model stage.

System slack containers

The slack container's key and axis depend on the network model, and the AC models are the only ones that use a meta:

Network modelsSlack container keyAxismeta
CopperPlateNetworkModel, PTDFNetworkModelPSY.Systemreference buses—
AreaBalanceNetworkModel, AreaPTDFNetworkModelPSY.Areaarea names—
DCPNetworkModel, NFANetworkModel, DCPLLNetworkModelPSY.ACBusbus numbers—
ACPNetworkModel, ACRNetworkModel, IVRNetworkModel, LPACCNetworkModelPSY.ACBusbus numbers"P", "Q"

The AC models split SystemBalanceSlackUp / SystemBalanceSlackDown into two containers — one for active power ("P") and one for reactive ("Q") — because a single bus axis has to carry both. These metas are passed positionally, not as a meta = keyword.

Reference bus constraint

ReferenceBusConstraint pins the slack bus of each subnetwork, and what it pins depends on the voltage coordinates:

Network modelsmetaPins
DCPNetworkModel, DCPLLNetworkModel, LPACCNetworkModel—$\theta_{ref} = 0$
ACPNetworkModel"va", "vm"$\theta_{ref} = 0$ and $v_{ref} = v_{set}$
ACRNetworkModel, IVRNetworkModel"vi", "vr"$v_{i,ref} = 0$ and $v_{r,ref} = v_{set}$

The AC models additionally error if the reference-bus setpoint falls outside the bus voltage limits.

Branch Formulations

AC branch formulations

The branch constructors delegate to shared helpers, so a constructor body that looks short still emits several containers. The helpers expand as follows:

HelperEmits
_add_static_branch_flow_variables!FlowActivePowerFromToVariable, FlowActivePowerToFromVariable, FlowReactivePowerFromToVariable, FlowReactivePowerToFromVariable
_wire_static_branch_flow_to_balance!ActivePowerBalance ← FlowActivePower{FromTo,ToFrom}Variable and ReactivePowerBalance ← FlowReactivePower{FromTo,ToFrom}Variable
_add_flow_slacks!FlowActivePowerSlackUpperBound and FlowActivePowerSlackLowerBound
_add_static_branch_balance_arguments!FlowActivePowerSlackUpperBound only (when use_slacks), then the four balance wirings, then BranchRatingTimeSeriesParameter if configured
_add_hvdc_active_flow_arguments!FlowActivePowerVariable and ActivePowerBalance ← FlowActivePowerVariable

Note the asymmetry: _add_flow_slacks! adds an upper and lower slack, while _add_static_branch_balance_arguments! — the path taken by StaticBranch on the AC network models — adds only the upper slack.

StaticBranch support matrix

Network modelArgument stageModel stage
DCPNetworkModelFlowActivePowerVariable; slacks (upper + lower); ActivePowerBalance ← FlowActivePowerVariable; BranchRatingTimeSeriesParameter if configuredFlowRateConstraint ("lb"/"ub"), NetworkFlowConstraint, AngleDifferenceConstraint
NFANetworkModelsame as DCPFlowRateConstraint ("lb"/"ub") only — no NetworkFlowConstraint, no AngleDifferenceConstraint (transportation model)
DCPLLNetworkModelFlowActivePowerFromToVariable, FlowActivePowerToFromVariable; slacks or hard flow bounds (mutually exclusive); ActivePowerBalance ← each directional flow. No rating time-series parameterFlowRateConstraint ("ft_ub", "ft_lb", "tf_ub", "tf_lb"), NetworkFlowConstraint, NetworkLossConstraint, AngleDifferenceConstraint
PTDFNetworkModelno flow variables; slacks (upper + lower); rating parameters if configuredPTDFBranchFlow expression, then FlowRateConstraint ("lb"/"ub") on that expression. No NetworkFlowConstraint
CopperPlateNetworkModelno-opno-op
AreaBalanceNetworkModelno-opno-op
ACPNetworkModelfour directional flow variables; upper slack only; four balance wirings; BranchRatingTimeSeriesParameter if configuredFlowRateConstraintFromTo, FlowRateConstraintToFrom, NetworkFlowConstraint ("p_ft", "q_ft", "p_tf", "q_tf"), AngleDifferenceConstraint
ACRNetworkModelsame as ACPsame as ACP
LPACCNetworkModelsame as ACP, plus CosineApproximationsame as ACP, plus CosineRelaxationConstraint
IVRNetworkModelsame as ACP, plus the six branch current variables (BranchCurrent{FromTo,ToFrom}{Real,Imaginary}, BranchSeriesCurrent{Real,Imaginary})same as ACP, plus CurrentLimitConstraint ("from"/"to"). NetworkFlowConstraint carries ten metas here

DCPLLNetworkModel enforces the rating either with slacks or with hard JuMP bounds on the directional flow variables, never both: when use_slacks = false the FlowRateConstraint builder returns without emitting anything and the rating lives entirely in the variable bounds.

StaticBranchBounds

`StaticBranchBounds` ≡ `StaticBranch` on the AC networks

On ACPNetworkModel, ACRNetworkModel, LPACCNetworkModel and IVRNetworkModel the two formulations build the same apparent-power quadratic (FlowRateConstraintFromTo/FlowRateConstraintToFrom, pft² + qft² ≤ rating²) — the generic add_constraints! method is written for U <: AbstractBranchFormulation and dispatches identically for both. That quadratic already implies a box on $(p, q)$, so StaticBranchBounds is not adding a mathematically new limit; it additionally calls branch_rate_bounds! to set an explicit JuMP variable bound (min_max_flow_limits) on the directional flow variables. The two formulations are mathematically equivalent relaxations on AC — the difference is solver-facing: StaticBranchBounds gives the solver an explicit box the presolve/branch-and-bound can exploit directly, StaticBranch leaves the box implicit in the quadratic row.

Rating enforcement per network model, StaticBranch vs StaticBranchBounds side by side:

Network modelStaticBranchStaticBranchBounds
ACPNetworkModel, ACRNetworkModel, LPACCNetworkModel, IVRNetworkModelpft² + qft² ≤ rating² quadratic row only (no variable bounds)Same quadratic row plus explicit JuMP bounds on the four directional flow variables (branch_rate_bounds!)
PTDFNetworkModel, AreaPTDFNetworkModelFlowRateConstraint ("lb"/"ub") rows on PTDFBranchFlow, no variable boundsExplicit JuMP bounds on FlowActivePowerVariable, plus a NetworkFlowConstraint equality tying PTDFBranchFlow to FlowActivePowerVariable — with use_slacks that tie becomes PTDFBranchFlow - FlowActivePowerVariable == slack_up - slack_lo instead of == 0
DCPNetworkModelFlowRateConstraint ("lb"/"ub") rows on FlowActivePowerVariableSame constraint-row style — "lb"/"ub" constraints, not variable bounds
DCPLLNetworkModelSlacked FlowRateConstraint rows or hard directional-variable bounds, mutually exclusive on use_slacksSame mutually-exclusive choice: slacks add row constraints, no slacks fall back to _set_dcpll_flow_bounds!
NFANetworkModelFlowRateConstraint ("lb"/"ub") rows on FlowActivePowerVariableHard JuMP bounds only (branch_rate_bounds!); no constraint row exists to relax
CopperPlateNetworkModel, AreaBalanceNetworkModelno-opno-op

StaticBranchBounds supports use_slacks on every network model except NFANetworkModel — the NFA rating has no equality or constraint row for a slack to relax. Which pairs have slack machinery is declared once, per (formulation, network) pair, by the slack_spec trait (core/branch_slack_specs.jl); the supports_flow_slacks gate derives from it, so any pair whose constructors build no slack containers (StaticBranchBounds × NFA, StaticBranchUnbounded × anything) is rejected at template validation with IS.ConflictingInputsError rather than left to silently ignore the request; a construct-time backstop (_check_flow_slack_support) throws ArgumentError for any direct-construct path that bypasses template validation. On CopperPlateNetworkModel/AreaBalanceNetworkModel the branch model is a no-op, so use_slacks is accepted but inert — validation emits a warning instead of erroring so templates stay reusable on aggregated networks.

Slack mechanisms

StaticBranch and StaticBranchBounds relax the rating through structurally different mechanisms, following directly from where each formulation puts its rating:

FormulationWhat the slack relaxes
StaticBranchThe rating row itself: a slacked FlowRateConstraint/FlowRateConstraintFromTo/FlowRateConstraintToFrom on the linear DC networks (_add_flow_slacks! adds upper and lower), or a one-sided subtraction from the AC quadratic (pft² + qft² - s ≤ rating², meta-less FlowActivePowerSlackUpperBound, upper only)
StaticBranchBoundsThe flow-definition equality, not the rating row: on the AC natives this is the Ohm's-law NetworkFlowConstraint, with one slack pair per directional row (metas "p_ft"/"q_ft"/"p_tf"/"q_tf"); on PTDF/AreaPTDF it's the PTDFBranchFlow == FlowActivePowerVariable tie; on DCP/DCPLL there is no separate flow-definition equality, so the slack falls back to the same row style as StaticBranch

On IVRNetworkModel both mechanisms are present at once, on different equations:

  • StaticBranch relaxes the terminal CurrentLimitConstraint quadratic (cr² + ci² ≤ c_rating²) with one-sided metaed slacks "c_from"/"c_to".
  • StaticBranchBounds leaves CurrentLimitConstraint hard and instead relaxes the four terminal KCL current-defining equalities with their own metaed pairs — "cr_fr", "ci_fr", "cr_to", "ci_to" — one pair per definition because the from-side row scales the current by tm² while the to-side row does not, so a shared pair would relax the two ends unequally under off-nominal taps.
Pricing asymmetry between the two slack domains

Every slack container above is priced identically at CONSTRAINT_VIOLATION_SLACK_COST in the objective, but the domain being relaxed differs. StaticBranchBounds's "p_ft"/"p_tf"/"q_ft"/"q_tf"/"cr_*"/"ci_*" pairs are flow-domain (MW / per-unit current); StaticBranch's meta-less and "c_from"/"c_to" slacks relax a squared domain (MVA² / A²) because they are subtracted directly from a quadratic left-hand side. The same price per unit therefore represents a different marginal relaxation depending on which formulation is active — a one-unit squared-domain slack does not correspond to a one-unit flow violation, so shadow costs and slack magnitudes are not directly comparable between StaticBranch and StaticBranchBounds.

StaticBranchUnbounded

A no-op on every network model except PTDF, where the model stage still builds the PTDFBranchFlow expression (so flows are reportable, but unconstrained).

SecurityConstrainedStaticBranch

N-1 security constraints are built with Modified Outage Distribution Factors (PNM VirtualMODF).

Network modelBehaviour
PTDFNetworkModel, AreaPTDFNetworkModelFull support: PTDFBranchFlow and PostContingencyBranchFlow expressions, FlowRateConstraint and PostContingencyFlowRateConstraint ("lb"/"ub"), optional post-contingency slacks. The post-contingency flow is the MODF-redistributed expression MODF ⋅ nodal balance (on these networks the balance holds only injections)
DCPNetworkModelFull support: replicates the DCP StaticBranch construction (FlowActivePowerVariable, DC Ohm's law, angle-difference and rate limits) and builds the genuine MODF post-contingency flow MODF ⋅ nodal injections, with the injections recovered exactly (by KCL) from the branch-flow terms of the zero-constrained nodal balance
ACPNetworkModel, ACRNetworkModel, IVRNetworkModel, LPACCNetworkModel, DCPLLNetworkModelBlocked at template validation with an IS.ConflictingInputsError: the MODF post-contingency formulation is a lossless linear DC construct and is not available on AC or lossy network models
NFANetworkModel, CopperPlateNetworkModel, AreaBalanceNetworkModelDeliberate no-op — a @warn states the security constraints are inert

Transformer tap control

Tap control is not a separate formulation. It is an opt-in attribute on the ordinary StaticBranch / StaticBranchBounds transformer models, driven by the component data:

DeviceModel(
    PSY.TwoWindingTransformer,
    StaticBranch;
    attributes = Dict(POM.ENABLE_CONTROLS_KEY => true),   # "enable_controls"
)

With the attribute set, each PSY.TransformerCircuit whose control_objective POM models gets a continuous TapRatioVariable bounded by the circuit's tap_ratio_limits, which enters the AC flow equations in place of the fixed tap. Control is per circuit, so each winding of a ThreeWindingTransformer is controlled independently.

control_objectiveConstraint addedRegulated quantity
VOLTAGEVoltageControlConstraintvoltage magnitude at regulated_bus_number, banded by controlled_voltage_limits
REACTIVE_POWER_FLOWReactivePowerFlowControlConstraintFlowReactivePowerFromToVariable at the circuit's winding-one bus, banded by controlled_reactive_power_flow_limits

Every other TransformerControlObjective is inert. UNDEFINED (the field default), FIXED and the *_DISABLED codes are treated as "no control block" and pass silently; the positive objectives POM does not yet model (ACTIVE_POWER_FLOW, CONTROL_OF_DC_LINE, ASYMMETRIC_ACTIVE_POWER_FLOW) emit a @warn and are ignored.

regulated_bus_number must name a bus that exists in the system; a VOLTAGE-controlled circuit whose regulated bus cannot be resolved is rejected at template validation.

NetworkSupport
ACPNetworkModel, ACRNetworkModel, IVRNetworkModelFull support on both StaticBranch and StaticBranchBounds
LPACCNetworkModelSupported, but a variable tap makes the model non-convex — a @warn says so; use a nonlinear solver
DCPNetworkModel, DCPLLNetworkModelBoth objectives are AC quantities, so no tap variable or constraint is built; a @warn states the control is ignored
all othersThe attribute is not offered (get_default_attributes adds it only for the two control formulations) and is inert if forced

A controlled circuit and its regulated bus are pinned irreducible, so a network reduction cannot absorb them. A controlled circuit that would be merged into a parallel-branch equivalent is a hard error rather than a silent demotion.

HVDC formulations

Two-terminal HVDC

FormulationSupported networksKey variablesKey constraints
HVDCTwoTerminalUnboundedall except AreaBalanceNetworkModelFlowActivePowerVariable (+ reactive from/to on AC networks, all unbounded)none — active and reactive flows are unconstrained
HVDCTwoTerminalLosslessall except AreaBalanceNetworkModelFlowActivePowerVariable (+ reactive from/to on ACP/ACR/IVR/LPACC, bounded by reactive_power_limits_from/to)FlowRateConstraint ("ub"/"lb")
HVDCTwoTerminalDispatchPTDF, and all active-power + AC-native modelsFlowActivePower{FromTo,ToFrom}Variable, HVDCLosses, HVDCFlowDirectionVariableFlowRateConstraint{FromTo,ToFrom}, HVDCPowerBalance (nine metas)
HVDCTwoTerminalPiecewiseLossallHVDCActivePowerReceived{From,To}Variable, HVDCPiecewiseLossVariable, HVDCPiecewiseBinaryLossVariableFlowRateConstraint{FromTo,ToFrom}, HVDCFlowCalculationConstraint ("ft", "tf", "bin")
HVDCTwoTerminalLCCACPNetworkModel, ACRNetworkModel, IVRNetworkModel, LPACCNetworkModel (rejected at template validation elsewhere)17 variables, including HVDCRectifierActivePowerVariable/HVDCInverterActivePowerVariable and their reactive counterparts (rectifier/inverter angles, DC voltages, AC currents, taps)11 constraints (DC line voltage, overlap angle, power-factor angle, AC current, power calculation)
HVDCTwoTerminalVSCall except AreaBalanceNetworkModelFlowActivePower{FromTo,ToFrom}Variable, DCLineCurrentFlowVariable, HVDC{From,To}DCVoltageHVDCCableOhmsLawConstraint, HVDCVSCConverterPowerConstraint ("ft"/"tf"), HVDCVSCApparentPowerLimitConstraint
VoltageControlVSCACPNetworkModel, ACRNetworkModel, IVRNetworkModel, LPACCNetworkModel (auto-dropped from active-power templates)as HVDCTwoTerminalVSC, plus RegulatedVoltageMagnitude ("from"/"to") on ACR/IVRas above, plus HVDCDCControlConstraint ("from"/"to")
HVDCTwoTerminalDispatch loses its losses on CopperPlate

On CopperPlateNetworkModel the constructor warns, then skips HVDCPowerBalance entirely: HVDCLosses and HVDCFlowDirectionVariable are created but left unconstrained and unwired, so the line's losses vanish from the single system balance. Use HVDCTwoTerminalPiecewiseLoss on CopperPlate when the losses matter. On every other supported network the losses are accounted for: the nodal models (NFA/DCP/DCPLL/ACP/ACR/IVR/LPACC) carry them implicitly through the HVDCPowerBalance coupling ft + tf == losses with both directional flows entering their terminal balances, while the PTDF/AreaPTDF paths add HVDCLosses to the aggregated system/area row explicitly. On AreaBalanceNetworkModel the Dispatch formulation is not built at all (warn no-op).

HVDCTwoTerminalLossless pins reactive flow to zero on default VSC/LCC data

PSY.TwoTerminalVSCLine and PSY.TwoTerminalLCCLine default both reactive_power_limits_from and reactive_power_limits_to to (min = 0.0, max = 0.0). Because HVDCTwoTerminalLossless bounds the reactive flow variables by those limits, such a device can neither inject nor absorb reactive power at the affected terminal, which can make an AC network model infeasible. This is valid data, so the build warns (naming the device) rather than erroring.

The apparent-power limit on the VSC formulations depends on the "bilinear_approximation" device attribute. With the default "none" it is an exact quadratic disk ("from"/"to"); with a linearizing scheme it becomes a box of eight half-planes, plus eight more octagon cuts when "use_octagon" is true (the default).

AreaInterchange tie-line metering

AreaInterchange (StaticBranch/StaticBranchUnbounded) builds one LineFlowBoundConstraint per interchange, summing the measured export of every tie crossing its area boundary at the terminal on the interchange's from_area side. Each tie formulation resolves to a different container variable and sign, converted to a common "export at the measured terminal" convention:

Tie formulationMeasured variableSign convention
AC lines/monitored lines, HVDCTwoTerminalDispatchFlowActivePower{FromTo,ToFrom}Variable on the terminal matching the interchange's from_areaAlready an export: coefficient +1.0
HVDCTwoTerminalLossless, HVDCTwoTerminalUnboundedFlowActivePowerVariableSigned from -> to: +1.0 if the arc runs from_area -> to_area, -1.0 if reversed
HVDCTwoTerminalPiecewiseLossHVDCActivePowerReceived{From,To}Variable on the matching terminalInjection, not export: coefficient -1.0
HVDCTwoTerminalLCCHVDCRectifierActivePowerVariable (arc's from_area terminal) or HVDCInverterActivePowerVariable (arc's to_area terminal)Rectifier is already an export (+1.0); inverter is an injection (-1.0)

On PTDF/AreaPTDF networks the same table applies on top of each tie's own PTDFBranchFlow nodal-injection response, so the metering coefficient surfaces as the difference between the constraint's coefficient on a tie variable and that tie's own PTDF row.

Multi-terminal HVDC (PSY.InterconnectingConverter, PSY.TModelHVDCLine)

FormulationSupported networksKey variablesKey constraints
LosslessConverterallActivePowerVariable (+ ReactivePowerVariable on AC networks)ConverterPowerCapabilityConstraint on AC networks
LinearLossConverterallActivePowerVariable, CurrentAbsoluteValueVariable (+ ReactivePowerVariable on AC networks)CurrentAbsoluteValueConstraint ("ge_pos"/"ge_neg"); ConverterPowerCapabilityConstraint on AC networks
QuadraticLossConverterallActivePowerVariable, ConverterCurrent, CurrentAbsoluteValueVariable (ConverterACCurrentVariable instead on ACP/ACR/IVR; + ReactivePowerVariable on AC networks)ConverterLossConstraint, CurrentAbsoluteValueConstraint ("ge_pos"/"ge_neg"); ConverterACCurrentConstraint and ConverterPowerCapabilityConstraint on AC networks
VoltageControlConverterACPNetworkModel, ACRNetworkModel, IVRNetworkModel, LPACCNetworkModelActivePowerVariable, ReactivePowerVariable, ConverterCurrent, ConverterACCurrentVariable (ACP/ACR/IVR) or CurrentAbsoluteValueVariable (LPACC)ConverterLossConstraint, ConverterPowerCapabilityConstraint, HVDCDCControlConstraint; ConverterACCurrentConstraint on ACP/ACR/IVR
LosslessLineallFlowActivePowerVariablenone
DCLossyLineallDCLineCurrentDCLineCurrentConstraint

LinearLossConverter models the loss b·|I| + c from the converter loss_function proportional/constant terms, approximating |I| by |P| at nominal DC voltage; a non-zero quadratic term is rejected at build.

The HVDC network model is a separate template slot from the AC network model:

HVDC network modelAddsRequired by
TransportHVDCNetworkModelNodalBalanceActiveConstraint on each DC busLosslessConverter, LinearLossConverter, LosslessLine
VoltageDispatchHVDCNetworkModelDCVoltage variable per DC bus; NodalBalanceCurrentConstraintQuadraticLossConverter, VoltageControlConverter, DCLossyLine

ThermalGen Formulations

Nine concrete thermal formulations, under three abstract branches:

AbstractConcrete
AbstractStandardUnitCommitmentThermalBasicUnitCommitment, ThermalStandardUnitCommitment
AbstractCompactUnitCommitmentThermalBasicCompactUnitCommitment, ThermalCompactUnitCommitment, ThermalMultiStartUnitCommitment
AbstractThermalDispatchFormulationThermalBasicDispatch, ThermalStandardDispatch, ThermalDispatchNoMin, ThermalCompactDispatch

The standard family dispatches ActivePowerVariable directly. The compact family instead dispatches PowerAboveMinimumVariable and reconstructs total power as $p = P_{min} u + \hat{p}$, wiring both the above-minimum variable and the commitment status into ActivePowerBalance.

FormulationVariablesConstraints
ThermalBasicDispatchActivePowerVariable, ReactivePowerVariableActivePowerVariableLimitsConstraint, ReactivePowerVariableLimitsConstraint
ThermalDispatchNoMinas above, with lower bound relaxed to 0as above
ThermalStandardDispatchas aboveas above + RampConstraint
ThermalCompactDispatchPowerAboveMinimumVariable, ReactivePowerVariable, PowerOutput (aux)range limits + RampConstraint. Adds OnStatusParameter and wires it into ActivePowerBalance
ThermalBasicUnitCommitmentActivePowerVariable, ReactivePowerVariable, OnVariable, StartVariable, StopVariable, TimeDurationOn/Off (aux)range limits + CommitmentConstraint. No ramp, no duration
ThermalStandardUnitCommitmentas aboveas above + RampConstraint + DurationConstraint
ThermalBasicCompactUnitCommitmentcompact variable set + commitment variablesrange limits + CommitmentConstraint. No ramp, no duration
ThermalCompactUnitCommitmentas aboveas above + RampConstraint + DurationConstraint
ThermalMultiStartUnitCommitment (PSY.ThermalMultiStart only)compact set + ColdStartVariable, WarmStartVariable, HotStartVariableas above + StartupTimeLimitTemperatureConstraint ("hot"/"warm"), StartTypeConstraint, StartupInitialConditionConstraint, ActiveRangeICConstraint

All nine implement both a <:AbstractNetworkModel and a <:AbstractActivePowerModel method, so all eleven network models are supported. The active-power method simply omits ReactivePowerVariable, the ReactivePowerBalance wiring, and ReactivePowerVariableLimitsConstraint.

Metas on thermal keys: "lb"/"ub" (range limits), "up"/"dn" (ramp and duration), "aux" (the CommitmentConstraint start+stop ≤ 1 container), "hot"/"warm" (startup temperature), and "ubon"/"uboff" on the MultiStart range limits.

FixedOutput supports AC networks: for thermal devices its ArgumentConstructStage wires both ActivePowerTimeSeriesParameter and ReactivePowerTimeSeriesParameter into their respective balances under any <:AbstractNetworkModel.

RenewableGen Formulations

FormulationVariablesConstraints
RenewableFullDispatchActivePowerVariable, ReactivePowerVariableActivePowerVariableTimeSeriesLimitsConstraint ("ub", against ActivePowerTimeSeriesParameter), ActivePowerVariableLimitsConstraint ("lb"), ReactivePowerVariableLimitsConstraint ("lb"/"ub")
RenewableConstantPowerFactoras aboveas above, except the reactive limits are replaced by an EqualityConstraint pinning $q = p \tan(\arccos(pf))$
FixedOutputnonenone — the time-series parameter is wired straight into the balance

Both dispatch formulations support all eleven network models. Renewable injection carries a negative objective multiplier, and the renewable cost expressions include a CurtailmentCostExpression (populated only for a CostCurve{LinearCurve}).

Note that RenewableConstantPowerFactor emits no ReactivePowerVariableLimitsConstraint at all — the power-factor equality replaces it.

Hydro Formulations

Hydro is the largest family. It splits by which PSY device the formulation attaches to.

On PSY.HydroGen

FormulationVariablesConstraints
HydroDispatchRunOfRiverActivePowerVariable, ReactivePowerVariable, HydroEnergyOutput (aux)range limits + ActivePowerVariableTimeSeriesLimitsConstraint
HydroDispatchRunOfRiverBudgetas above, + HydroEnergyShortageVariable when use_slacksas above + EnergyBudgetConstraint (optional "interval" meta container)
HydroCommitmentRunOfRiveras above + OnVariablesemicontinuous range limits + TS limits

On PSY.HydroReservoir

FormulationVariablesConstraints
HydroEnergyModelReservoirEnergyVariable, WaterSpillageVariable, HydroEnergyShortage/SurplusVariableEnergyBalanceConstraint; optional EnergyTargetConstraint, EnergyBudgetConstraint
HydroWaterModelReservoirHydroReservoirHeadVariable, HydroReservoirVolumeVariable, WaterSpillageVariable, water shortage/surplusReservoirInventoryConstraint, ReservoirLevelLimitConstraint ("ub"/"lb"), ReservoirHeadToVolumeConstraint; optional WaterTargetConstraint, WaterBudgetConstraint
HydroWaterFactorModelWaterSpillageVariable, HydroReservoirVolumeVariableReservoirInventoryConstraint, ReservoirLevelTargetConstraint. Contributes nothing to the objective

On PSY.HydroTurbine / PSY.HydroPumpTurbine

FormulationVariablesConstraints
HydroTurbineEnergyDispatch / …CommitmentActivePowerVariable (+ OnVariable for commitment)range limits (semicontinuous for commitment)
HydroTurbineWaterLinearDispatch / …CommitmentHydroTurbineFlowRateVariable (turbine × reservoir × t), ActivePowerVariablerange limits + TurbinePowerOutputConstraint (linear, shallow-reservoir head model)
HydroTurbineBilinearDispatchas aboveas above, with the flow × head product handled by the bilinear approximation API — exact NLP by default, MILP under a linearizing scheme
HydroWaterFactorModel (turbine side)HydroTurbineFlowRateVariable (turbine × t), ActivePowerVariablerange limits + HydroPowerConstraint
HydroPumpEnergyDispatch / …CommitmentActivePowerVariable, ActivePowerPumpVariable, optional ReservationVariablerange limits; commitment adds InputActivePowerVariableLimitsConstraint; ActivePowerPumpReservationConstraint when reserving

ActivePowerPumpVariable enters ActivePowerBalance with multiplier -1.0 — pumping is a withdrawal. The three water-flow turbine formulations are collected by the union alias HydroTurbineWaterFormulation.

All hydro constructors are bound <:AbstractNetworkModel as well as <:AbstractActivePowerModel, so every formulation in this section supports AC networks: HydroCommitmentRunOfRiver, HydroPumpEnergyDispatch, and HydroPumpEnergyCommitment add the reactive-power variable and constraint on their <:AbstractNetworkModel path, and the turbine water-flow formulations (HydroTurbineBilinearDispatch, HydroTurbineWaterLinearDispatch, and HydroWaterFactorModel on a turbine) have their own HydroTurbine-specific methods for both stages, so they no longer fall through to the generic HydroDispatchRunOfRiver methods under an AC template.

Storage and Hybrid Formulations

There is exactly one concrete storage formulation and one concrete hybrid formulation.

StorageDispatchWithReserves

Attributes: "reservation", "reserve_coverage" (default true; reserve_coverage = false DECOUPLES energy and AS for day-ahead-style clearing: no SOC deployment-coverage constraints, reserve bands bounded by capability instead of the reservation binary's dispatch side, and complete_coverage ignored with a warning - the binary still governs energy exclusivity), "cycling_limits", "energy_target", "complete_coverage", "regularization" (all default false).

StageEmits
ArgumentActivePowerInVariable, ActivePowerOutVariable, EnergyVariable, StorageEnergyOutput (aux), ReactivePowerVariable (AC only); ReservationVariable if reserving; energy-target and cycling slacks if enabled; InitialEnergyLevel initial condition; ActivePowerBalance ← ActivePowerOutVariable (+1) and ← ActivePowerInVariable (−1)
ModelOutput/InputActivePowerVariableLimitsConstraint, StateofChargeLimitsConstraint, EnergyBalanceConstraint; optional StateofChargeTargetConstraint, StorageCyclingCharge/Discharge, regularization constraints; with services, the reserve coverage / charge / discharge constraints

HybridDispatchWithReserves

A single PCC with optional thermal, renewable, storage and load subcomponents. Attributes: "reservation", "storage_reservation" (both default true), "energy_target", "regularization" (default false). One constructor pair covers all eleven network models; the reactive-power pieces are added conditionally.

The reserve trait axes

Storage and hybrid reserve expressions are parametrized on three axes rather than duplicated as sibling singletons:

  • Direction — PSY.ReserveUp / PSY.ReserveDown
  • Scale — UnscaledReserve (raw multiplier) / DeployedReserve (scaled by deployed_fraction). Attach a "deployed_fraction" profile to the reserve to make the fraction vary over the horizon; the scalar field then scales that profile, and the profile alone applies no scaling until the scalar is set. The profile is a left-hand-side parameter (DeployedFractionParameter), written into constraints as fixed coefficients, so a simulation rebuilds the model every step: rebuild_model is switched to true with a warning.
  • Side — DischargeSide / ChargeSide

giving eight instantiations each of StorageReserveBalanceExpression{D,S,Sd} and HybridPCCReserveExpression{D,S,Sd}. Unscaled is used for assignment constraints (what may be offered); Deployed for energy-affecting constraints (state-of-charge balance, cycling, regularization).

The load-bearing rule is that the sides swap: on the discharge side an up-reserve raises effective power, but on the charge side the roles reverse, because a charging battery's net consumption is increased by downward reserve.

Load, Source and Shunt Formulations

FormulationDeviceVariablesConstraints
StaticPowerLoadPSY.ElectricLoadnonenone — the time-series parameter goes straight into the balance (multiplier −1)
PowerLoadDispatchcontrollable loadActivePowerVariable, ReactivePowerVariableActivePowerVariableTimeSeriesLimitsConstraint; reactive limits are a constant-power-factor equality
PowerLoadInterruptioncontrollable loadas above + binary OnVariableas above + the binary linking constraint (meta = "binary")
PowerLoadShiftPSY.ShiftablePowerLoadShiftUp/ShiftDownActivePowerVariableShiftedActivePowerBalanceConstraint (optional "additional" meta), RealizedShiftedLoadMinimumBoundConstraint, shift limits, NonAnticipativityConstraint
ImportExportSourceModelPSY.SourceActivePowerIn/OutVariable, ReactivePowerVariable, optional ReservationVariablerange limits + ImportExportBudgetConstraint ("export"/"import")
SynchronousCondenserBasicDispatchPSY.SynchronousCondenserReactivePowerVariablenone
ShuntSusceptanceDispatchSwitchedAdmittance, FACTSControlDeviceShuntSusceptanceVariable, ReactivePowerVariableShuntReactivePowerConstraint ($q = b V^2$)
FixedShuntAdmittanceSwitchedAdmittance, FACTSControlDeviceReactivePowerVariableShuntReactivePowerConstraint ($q = b_\text{nominal} V^2$, fixed $b$)

ShuntSusceptanceDispatch is supported on ACPNetworkModel, ACRNetworkModel and IVRNetworkModel only: it is dropped from active-power templates by the models_reactive_power gate and rejected on LPACCNetworkModel by template validation. FixedShuntAdmittance injects a non-dispatched $q = b_\text{nominal} V^2$ (SwitchedAdmittance: $\operatorname{imag}(Y)$; FACTSControlDevice: the reactive-power setpoint) and builds on all four reactive networks — it is the only shunt formulation available under LPACCNetworkModel.

The ActivePowerBalance ← RealizedShiftedLoad wiring for PowerLoadShift is implemented for any <:AbstractNetworkModel, via the same _balance_expression_targets dispatch every other injection uses to reach its nodal, area, or system target.

Only PowerLoadDispatch and PowerLoadInterruption may contribute to reserves. When a load under either formulation carries a service model, its active-power limits move onto ActivePowerRangeExpressionLB/UB: an up award consumes shed headroom ($P - \sum r_\text{up} \ge 0$) and a down award consumes forecast headroom ($P + \sum r_\text{down} \le \bar{P}$), summed across every service at once. A template that wires a load into a reserve under any other formulation is rejected by name, because the award would otherwise be bounded only by the device's nameplate rating — uncoupled from its dispatch and uncoupled across services. StaticPowerLoad creates no variables and no cost expressions, so the capacity it sold would also be free in the objective; PowerLoadShift is priced and dispatchable, but its headroom is shift capability carrying an energy-recovery balance, which the range expression cannot express. The trait is supports_reserve_provision, and a load formulation added later must opt in.

SynchronousCondenserBasicDispatch is gated by models_reactive_power, so on an active-power network it is dropped from the template (with an @info message) rather than being built as a no-op.

Outage events

Attaching an EventModel for a PSY.Contingency supplemental attribute (e.g. FixedForcedOutage) to a DeviceModel in the template adds availability parameters and outage constraints to every device of that type carrying the attribute, on top of whatever variables and constraints its device formulation already contributes.

Parameters (per device and time step): AvailableStatusParameter (1 = available, initialized to 1) and AvailableStatusChangeCountdownParameter; loads and FixedOutput devices also get the balance offsets ActivePowerOffsetParameter / ReactivePowerOffsetParameter.

ActivePowerOutageConstraint bounds active power by available capacity, $p_t \le P^\text{max} \cdot \text{status}_t$, with the left-hand side depending on the device family: the range-expression upper bound for thermal and hydro generators, the active-power variable for loads and for renewables without a service model, the charge and discharge variables together for PSY.EnergyReservoirStorage, and, for PSY.HydroPumpTurbine, both the generation variable (ActivePowerOutageConstraint) and the pump variable (ActivePowerPumpOutageConstraint). Under reactive-power-capable networks, ReactivePowerOutageConstraint additionally bounds $q_t^2 \le \max\left((Q^\text{max})^2, (Q^\text{min})^2\right) \cdot \text{status}_t$.

The parameter values are constant within a single build; updating them across solves (outage sampling, countdown projection) is simulation-runtime functionality that lives outside this package.

Service Formulations

FormulationService typeArgument stageModel stage
RangeReservePSY.ReserveRequirementTimeSeriesParameter (omitted for static-requirement reserves), ActivePowerReserveVariableRequirementConstraint, ParticipationFractionConstraint
RampReservePSY.Reserveas aboveas above + RampConstraint
NonSpinningReservePSY.OfflineReserveas above, but no device-range expression wiringas above + ReservePowerConstraint
StepwiseCostReserve (operating reserve demand curve)PSY.ReserveServiceRequirementVariable + demand-curve slope/breakpoint parametersRequirementConstraint only — no participation constraint
GroupRangeReservePSY.GroupReserveno variablesRequirementConstraint across contributing services
GroupStepwiseCostReserve (elastic group)PSY.GroupReserveServiceRequirementVariable + group demand-curve slope/breakpoint parametersRequirementConstraint: member awards ≥ the group demand
ConstantMaxInterfaceFlowPSY.TransmissionInterfaceoptional slacks, InterfaceTotalFlow expressionInterfaceFlowLimit ("ub"/"lb")
VariableMaxInterfaceFlowPSY.TransmissionInterfaceas above + min/max flow-limit parametersas above, with parameterized limits

The group formulations are deliberately constructed last in both stages, because they aggregate the other services' award variables. A PSY.GroupReserve accepts only the group formulations (and vice versa); a mis-paired ServiceModel fails at declaration. A service whose demand driver is degenerate (zero requirement under the requirement formulations, no demand curve under the stepwise ones) is skipped as demand and built as supply only, so it can serve a group.

Group formulations do not support slacks

use_slacks = true on a group ServiceModel is ignored: reserve slacks attach to the requirement rows of the device-backed formulations, and no slack is added to a group's clearing constraint.

Reserve contributions reach a device through get_expression_type_for_reserve: for thermal, renewable and hydro an up-reserve enters ActivePowerRangeExpressionUB (+1) and a down-reserve ActivePowerRangeExpressionLB (−1); a controllable load is the inverse (an up-reserve is committed shed, entering ActivePowerRangeExpressionLB with −1, and a down-reserve is committed extra consumption, entering ActivePowerRangeExpressionUB with +1); storage and hybrid instead route everything into TotalReserveOffering. Any other device type hits an error — sources, condensers and shunts cannot contribute to a reserve.

Service meta strings are per-instance, not a fixed vocabulary: every reserve container is keyed by the service's own name (meta = get_service_name(model)). The only fixed metas here are "ub"/"lb" on InterfaceFlowLimit.

Offline reserves

For thermal unit commitment and HydroCommitmentRunOfRiver, OfflineReserve awards stay out of ActivePowerRangeExpressionUB and enter OfflineReserveBandConstraint, so a unit that is off supplies them up to the time step's available maximum $\bar{P}_t$: the ActivePowerTimeSeriesParameter when the DeviceModel maps it and the device has that series, the static $P^\text{max}$ otherwise. Two OfflineReserve ServiceModel attributes, both false by default, tighten an off-line award $r_t$:

AttributeRow
"offline_only" => trueOfflineReserveOffStateConstraint: $r_t \le P^\text{max} (1 - u_t)$
"exclude_shutdown_step" => trueOfflineReserveShutdownConstraint: $r_t \le P^\text{max} (1 - u_{t-1} + u_t)$, with $u_0$ from the DeviceStatus initial condition

Every right-hand side is non-negative, so zero off-line awards always satisfy the rows. Must-run thermal units never go off and get no shutdown-step row; HydroCommitmentRunOfRiver carries a DeviceStatus initial condition only under "exclude_shutdown_step". Other formulations book OfflineReserve awards against their headroom and get none of these rows.

AGC is not available

services_models/agc.jl is not included in the module and its construct_service! methods are commented out. AbstractAGCFormulation and PIDSmoothACE are still defined but have no constructor and cannot be used.

Feedforward Formulations

A feedforward binds a model's variables to a quantity recorded in the system state — it is a model-to-state relationship, not a link between two models. See Feedforwards for what the system state is, how the semicontinuous substitution works, and when to use each type.

POM builds them in two stages: add_feedforward_arguments! (ArgumentConstructStage) allocates the VariableValueParameter container that carries the state quantity plus any slack variables, and add_feedforward_constraints! (ModelConstructStage) builds the constraints that read it. Populating those parameters between executions is PowerSimulations' job — it needs simulation state, which POM does not have.

Attach one to a device model with attach_feedforward!:

device_model = DeviceModel(ThermalStandard, ThermalStandardDispatch)
attach_feedforward!(
    device_model,
    SemiContinuousFeedforward(;
        component_type = ThermalStandard,
        source = OnVariable,
        affected_values = [ActivePowerVariable],
    ),
)
FeedforwardParameterConstraintEffect
UpperBoundFeedforwardUpperBoundValueParameterFeedforwardUpperBoundConstraint$x_t \le \text{param}_t \cdot \text{mult}_t$
LowerBoundFeedforwardLowerBoundValueParameterFeedforwardLowerBoundConstraint$x_t \ge \text{param}_t \cdot \text{mult}_t$
SemiContinuousFeedforwardOnStatusParameterFeedforwardSemiContinuousConstraintcommitment status from the state bounds $x_t$ to 0 or its range
FixValueFeedforwardFixValueParameterFeedforwardFixValueConstraint$x_t = \text{param}_t \cdot \text{mult}_t$
EnergyTargetFeedforwardEnergyTargetParameterFeedforwardEnergyTargetConstraint$E_T + s_T \ge \text{param}_T \cdot \text{mult}_T$ at the horizon end $T$, with the StorageEnergyShortageVariable slack $s_T$ penalized in the objective at penalty_cost
ReservoirTargetFeedforwardReservoirTargetParameterFeedforwardEnergyTargetConstraint$x_T + s_T \ge \text{param}_T \cdot \text{mult}_T$ at target_period $T$, with the HydroEnergyShortageVariable slack $s_T$ penalized in the objective at penalty_cost
ReservoirLimitFeedforwardReservoirLimitParameterFeedforwardIntegralLimitConstraint$\sum_{t \in B} x_t \le \sum_{t \in B} \text{param}_t \cdot \text{mult}_t$ for each consecutive block $B$ of number_of_periods steps
EnergyLimitFeedforwardEnergyLimitParameterFeedforwardIntegralLimitConstraint$\sum_{t \in B} x_t \le \sum_{t \in B} \text{param}_t \cdot \text{mult}_t$ for each consecutive block $B$ of number_of_periods steps
WaterLevelBudgetFeedforwardWaterLevelBudgetParameterFeedForwardWaterLevelBudgetConstraint$\sum_t w_t \le \sum_t \text{param}_t$, where $w_t$ is the reservoir's TotalHydroFlowRateReservoirOutgoing expression, summed over the full horizon
HydroUsageLimitFeedforwardHydroUsageLimitParameterFeedForwardHydroUsageLimitConstraint$h \sum_t p_t \le \text{param}_T$ at the horizon end $T$, where $h$ is the resolution as a fraction of an hour and $p_t$ is ActivePowerVariable

UpperBoundFeedforward and LowerBoundFeedforward accept add_slacks = true, which relaxes the bound with a non-negative UpperBoundFeedForwardSlack / LowerBoundFeedForwardSlack penalized at BALANCE_SLACK_COST.

SemiContinuousFeedforward suppresses the affected formulation's own range constraints — has_semicontinuous_feedforward gates them — because the commitment status arrives as a parameter in the ActivePowerRangeExpressionUB / …LB expressions instead. Must-run thermal units are excluded throughout: they are never turned off, so they carry no OnStatusParameter entry and get no semicontinuous constraints.

ReservoirTargetFeedforward and EnergyTargetFeedforward build the same FeedforwardEnergyTargetConstraint container, distinguished by meta; unlike EnergyTargetFeedforward, ReservoirTargetFeedforward does not require target_period to be the horizon's last step. ReservoirLimitFeedforward and EnergyLimitFeedforward likewise share one FeedforwardIntegralLimitConstraint container, again separated by meta, and differ only in the parameter they read. ReservoirLimitFeedforward dispatches on any PSY.Component, not only hydro reservoirs, despite its name. HydroUsageLimitFeedforward reads ActivePowerVariable directly rather than an entry from affected_values; when the device model carries a service model, the sum nets in served regulation reserves (HydroServedReserveUpExpression / …DownExpression) before comparing to the limit.

Service feedforwards are not implemented

Feedforwards can only be attached to a DeviceModel. attach_feedforward! on a ServiceModel throws, and so does set_service_model! when a ServiceModel is constructed with a non-empty feedforwards kwarg — IOM's ServiceModel constructor still accepts it, so POM rejects it at template definition instead of letting it reach build!. Per-type service models key their reserve variables by (service_name, device_name, time), while the feedforward parameter path is keyed (device_name, time); the two are dimensionally inconsistent until the service VariableValueParameter path is re-keyed by (service, device).

Piecewise-linear cost

There are two piecewise-linear cost formulations, and which one you get is decided by the shape of the cost data, not by a flag:

Lambda (convex combination)Delta (incremental block offer)
Triggered byPiecewisePointCurve — absolute (power, cost) breakpointsPiecewiseIncrementalCurve / PiecewiseAverageCurve — per-segment slopes
VariablePiecewiseLinearCostVariable ($\lambda \in [0,1]$)PiecewiseLinearBlockIncrementalOffer / …DecrementalOffer ($\delta \ge 0$)
Linking constraintPiecewiseLinearCostConstraint: $p = \sum_i \lambda_i P_i$block-offer constraint: $p = \sum_k \delta_k + P_{min}$
NormalizationPiecewiseLinearCostNormalizationConstraint: $\sum_i \lambda_i = u$none
Segment boundsnone$\delta_k \le P_{k+1} - P_k$
SOS2only when the curve is non-convex — then the problem becomes a MILPnever — the segment-width bounds enforce ordering, so even a non-convex curve stays an LP

The lambda normalization couples to the commitment status: its right-hand side is 1.0 with no commitment, the OnStatusParameter for a compact dispatch, or the OnVariable for a unit commitment.

Offer direction selects the delta family: generation is an IncrementalOffer; the charging side of storage, the import side of a Source, and controllable loads are DecrementalOffer (negative objective sign). Operating reserve demand curves are also decremental, because a willingness-to-pay curve is concave.

When to use meta versus a new key type

Every container key (VariableKey, ConstraintKey, ExpressionKey, ParameterKey) carries a meta::String field, defaulting to IOM.CONTAINER_KEY_EMPTY_META (""). The choice between adding a meta and defining a new key type is a modelling decision, not a naming one:

  • Use meta when you are slicing one conceptual object along an orthogonal axis. The slices share a name because they are the same constraint viewed from different sides — for example a single FlowRateConstraint sliced into "lb" and "ub", or a single NetworkFlowConstraint sliced into "p_ft", "q_ft", "p_tf", "q_tf". Reserving a new type for each slice would multiply near-identical types and make it impossible to ask for "the flow-rate constraints" as a group.

  • Use a distinct key type when the constraint is semantically different — it expresses a different physical or economic relationship. NetworkFlowConstraint (Ohm's law) and FlowRateConstraint (a thermal rating) both act on branch flows, but they are different statements about the system and so are different types.

A useful test: if you would ever want to enable, disable, dualize, or report the two things independently as concepts, they are different types. If you would always want them together and are only separating them because a single container cannot hold both, that is a meta.

Two mechanical consequences worth knowing:

  • add_variable_container! has an extra method taking meta::String positionally as its fourth argument; add_constraints_container! accepts meta as a keyword only. The AC network slacks ("P" / "Q") use the positional form, so a grep for meta = will not find them.
  • meta must not contain the component-name delimiter; check_meta_chars enforces this.

Meta vocabulary

Most metas fall into a small number of families:

FamilyValuesUsed by
Bound direction"lb", "ub"FlowRateConstraint, AngleDifferenceConstraint, PostContingencyFlowRateConstraint, InterfaceFlowLimit, and the generic device range constraints (from IOM.constraint_meta)
Flow direction × bound"ft_ub", "ft_lb", "tf_ub", "tf_lb"FlowRateConstraint under DCPLLNetworkModel; HVDCPowerBalance
Power component × direction"p_ft", "q_ft", "p_tf", "q_tf" (+ "cr_fr", "ci_fr", "cr_to", "ci_to", "vr_to", "vi_to" under IVR)NetworkFlowConstraint on the AC models
Terminal"from", "to"HVDCDCControlConstraint, CurrentLimitConstraint, HVDCVSCApparentPowerLimitConstraint, RegulatedVoltageMagnitude on VSC/LCC
Voltage coordinate"va", "vm" (ACP); "vi", "vr" (ACR/IVR)ReferenceBusConstraint
Active vs reactive"P", "Q"SystemBalanceSlackUp / SystemBalanceSlackDown on the AC models (positional)
Per-instancemeta = get_service_name(model), "$(typeof(service))_$(name)"Services, and the storage/hybrid reserve families — these are not a fixed vocabulary
`HVDCPowerBalance` is two different types

POM defines HVDCPowerBalance <: ConstraintType, while IOM defines an unrelated HVDCPowerBalance <: ExpressionType. They share a name but are different types in different modules.