Constraint Catalog
power_system.jl --- Operational Dispatch
Generator Constraints
| Label Pattern |
Family |
Description |
gen_zero_cap_g{g}_b{b}_t{t} |
GEN-0 |
Zero output for inactive generators (rated=0 and no investment) |
gen_output <= rated * availability * status |
GEN-1 |
Upper bound on generation |
gen_output >= min_power * status |
GEN-2 |
Minimum stable generation |
gen_output[t] - gen_output[t-1] <= ramp_up |
GEN-3 |
Ramp-up rate limit |
gen_output[t-1] - gen_output[t] <= ramp_down |
GEN-4 |
Ramp-down rate limit |
startup_detect_g{g}_b{b}_t{t} |
GEN-5a |
Startup detection: startup >= status[t] - status[t-1] |
min_up_g{g}_b{b}_t{t}_tau{t} |
GEN-5b |
Minimum up time |
min_down_g{g}_b{b}_t{t}_tau{t} |
GEN-5c |
Minimum down time |
gen_delay_ret_{g}_{b} |
GEN-RET |
Delay retirement penalty |
gen_forced_repl_{g}_{b} |
GEN-REPL |
Forced replacement penalty |
PWL Cost Curve Constraints (Generators)
Generators with multi-segment (PWL) cost curves have their output decomposed into segment variables linked to the aggregate output.
| Label Pattern |
Family |
Description |
gen_output[g,b,t] == sum(gseg_{g}_{b}[k,t] for k) |
PWL-G1 |
Segment output summation: aggregate output equals sum of all segment outputs |
gseg_{g}_{b}[k,t] <= fraction_k * rated * availability |
PWL-G2 |
Segment upper bound (renewable): each segment limited to its fraction of available capacity |
gseg_{g}_{b}[k,t] <= fraction_k * total_capacity |
PWL-G3 |
Segment upper bound (non-renewable): each segment limited to its fraction of total capacity |
The objective applies per-segment marginal costs: cost += marginal_cost_k * gseg[k,t], replacing the flat fuel_cost * gen_output term.
Reservoir Constraints
| Label Pattern |
Family |
Description |
reservoir_level[g,n,1] == initial_level * capacity |
RES-1 |
Initial reservoir level (or seasonal boundary, RES-9) |
level[t+1] = level[t]*(1-evap) + inflow + cascade_in - output/eta_t + pump*eta_p - spillage |
RES-2 |
Reservoir dynamics (water balance) |
min_level * total_cap <= level[g,n,t] <= max_level * total_cap |
RES-3 |
Reservoir level bounds |
pump[g,n,t] <= pump_capacity[n] |
RES-4 |
Pump-back power limit |
spillage[g,n,t] <= capacity (or 0 if not allowed) |
RES-5 |
Spillage limit |
level[g,n,end] ~ initial_level * capacity (within tolerance) |
RES-6 |
Cyclic end-of-horizon constraint (replaced by RES-9 when seasonal) |
output/eta_t + spillage >= min_release |
RES-7 |
Minimum environmental / ecological flow |
output <= rated*(hmf + (1-hmf)*(level-min*cap)/span) |
RES-8 |
Head-dependent power limit (low level → less peak power) |
cascade_in = sum_u (output_u/eta_u + spillage_u) @ t-delay |
RES-C |
Hydraulic cascade: upstream release feeds downstream inflow |
level[g,n,end] == next_period_boundary |
RES-9 |
Seasonal inter-period level chain (TSAM linking) |
reservoir_pump_term subtracted from power balance |
RES-PB |
Pump as demand-side load |
Battery Constraints
| Label Pattern |
Family |
Description |
bat_zero_charge_b{bi}_bus{b}_t{t} |
BAT-0a |
Zero charge for inactive batteries |
bat_zero_discharge_b{bi}_bus{b}_t{t} |
BAT-0b |
Zero discharge for inactive batteries |
bat_zero_soc_b{bi}_bus{b}_t{t} |
BAT-0c |
Zero SOC for inactive batteries |
charge <= MaxChargePower |
BAT-1 |
Maximum charge rate |
discharge <= MaxDischargePower |
BAT-2 |
Maximum discharge rate |
soc[t] = soc[t-1] + eta_c*charge - discharge/eta_d - self_discharge*soc[t-1] |
BAT-3 |
SOC dynamics |
soc >= max_DoD * capacity |
BAT-4 |
Minimum SOC (max depth of discharge) |
soc <= capacity |
BAT-5 |
Maximum SOC |
bat_soc_initial_{bi}_{b} |
BAT-6 |
Initial SOC from boundary conditions |
bat_soc_end_lower_{bi}_{b} |
BAT-7a |
End-of-horizon SOC lower bound |
bat_soc_end_upper_{bi}_{b} |
BAT-7b |
End-of-horizon SOC upper bound |
bat_min_cycling_{bi}_{b} |
BAT-8 |
Minimum cycling requirement over period |
bat_spillage_{bi}_{b}_{t} |
BAT-9 |
Spillage limit |
bat_delay_ret_{bi}_{b} |
BAT-RET |
Battery delay retirement penalty |
bat_forced_repl_{bi}_{b} |
BAT-REPL |
Battery forced replacement penalty |
PWL Cost Curve Constraints (Batteries)
Batteries with multi-segment discharge cost curves have discharge power decomposed into segment variables analogously to generators.
| Label Pattern |
Family |
Description |
bat_discharge[bi,b,t] == sum(bseg_{bi}_{b}[k,t] for k) |
PWL-B1 |
Segment discharge summation: aggregate discharge equals sum of segment outputs |
bseg_{bi}_{b}[k,t] <= fraction_k * MaxDischargePower |
PWL-B2 |
Segment upper bound: each discharge segment limited to its fraction of max discharge power |
The objective applies per-segment marginal costs: cost += marginal_cost_k * bseg[k,t], replacing the flat throughput degradation cost term.
Reserve Constraints
| Label Pattern |
Family |
Description |
reserve_static_{b}_{t} |
RES-1 |
Static reserve: reserve_supply + loss_of_reserve >= requirement |
reserve_dynamic_avail_{b}_{t} |
RES-2a |
Dynamic reserve availability |
reserve_dynamic_req_{b}_{t} |
RES-2b |
Dynamic reserve requirement |
Power Balance
| Label Pattern |
Family |
Description |
Single-bus: sum(gen) - sum(charge) + sum(discharge) + loss_load = demand |
PB-1 |
Power balance (single node) |
Multi-bus via DC power flow (see transmission_dc.jl) |
PB-2 |
Network power balance |
N-1 Security
| Label Pattern |
Family |
Description |
n1_gen_reserve_system_t{t} |
N1-1 |
Generation N-1: reserve >= largest unit |
n1_trans_reserve_{i}_{j}_t{t}_pos |
N1-2a |
Transmission N-1 positive direction |
n1_trans_reserve_{i}_{j}_t{t}_neg |
N1-2b |
Transmission N-1 negative direction |
Curtailment
| Label Pattern |
Family |
Description |
curtailment[g,b,t] <= gen_output[g,b,t] (renewables only) |
CUR-1 |
Curtailment definition |
sum(curtailment) <= max_curtailment_ratio * sum(renewable_gen) |
CUR-2 |
System curtailment limit |
rooftop_curt_limit_b{b}_t{t} |
CUR-3 |
Rooftop curtailment limit |
Renewable Energy Target
| Label Pattern |
Family |
Description |
re_penetration_target |
RE-1 |
sum(renewable_gen) / sum(total_gen) >= target - loss |
CO2 Emissions
| Label Pattern |
Family |
Description |
co2_emissions_def_b{b}_t{t} |
CO2-1 |
Emissions definition per bus/time |
CO2_budget_constraint |
CO2-2 |
sum(emissions) <= annual_budget + violation |
Inertia
| Label Pattern |
Family |
Description |
inertia_{t} |
INE-1 |
sum(inertia * status * rated) + loss_inertia >= threshold |
EV Constraints
| Label Pattern |
Family |
Description |
ev_soc_initial_{b} |
EV-1 |
Initial EV fleet SOC |
ev_soc_dynamics_{b}_{t} |
EV-2 |
SOC dynamics: soc[t] = soc[t-1] + eta_c*charge - v2g/eta_d |
ev_demand_{b}_{t} |
EV-3 |
EV charging demand requirement |
ev_max_charge_{b}_{t} |
EV-4 |
Max EV charging power |
ev_max_v2g_{b}_{t} |
EV-5 |
Max V2G discharge power |
ev_soc_min_{b}_{t} |
EV-6a |
Minimum EV SOC |
ev_soc_max_{b}_{t} |
EV-6b |
Maximum EV SOC |
ev_mutex_charge_{b}_{t} |
EV-7a |
Charge/discharge mutex (charge) |
ev_mutex_v2g_{b}_{t} |
EV-7b |
Charge/discharge mutex (V2G) |
Demand & Sectoral
| Label Pattern |
Family |
Description |
max_load_shed_b{b}_t{t} |
SEC-1 |
Maximum load shedding at bus |
sectoral_lol_sum_{b}_{t} |
SEC-2 |
Sectoral LOL aggregation |
sectoral_lol_cap_{sector}_{b}_{t} |
SEC-3 |
Per-sector LOL capacity |
flex_curt_cap_{sector}_{b}_{t} |
SHIFT-1a |
Flexible demand curtailment cap |
demand_shift_out_cap_{sector}_{b}_{t} |
SHIFT-1b |
Demand shift-out capacity |
Investment & Transfer
| Label Pattern |
Family |
Description |
max_node_inv_{b} |
INV-1 |
Max investment per node |
max_annual_system_cost |
INV-2 |
Annual system cost cap |
transfer_margin_{i}_{j} |
TRN-1 |
Transfer margin constraint |
transmission_dc.jl --- DC Power Flow
Network Balance (KCL)
| Label Pattern |
Family |
Description |
net_inj == flow_sum |
DC-1 |
Kirchhoff's Current Law at each bus |
voltage_drop == 0 |
DC-1s |
Single-node voltage (no network) |
PWL Transmission Losses
| Label Pattern |
Family |
Description |
pf == fpos - fneg |
LOSS-1 |
Flow direction decomposition |
fpos == sum_k(dp_k) |
LOSS-2a |
Positive direction segment sum |
fneg == sum_k(dn_k) |
LOSS-2b |
Negative direction segment sum |
dp_k <= delta_f_k |
LOSS-3a |
Segment width bound (positive) |
dn_k <= delta_f_k |
LOSS-3b |
Segment width bound (negative) |
ploss == sum_k(m_k * (dp_k + dn_k)) |
LOSS-4 |
PWL loss computation |
net_inj == sum_l(K * pf - 0.5 * abs(K) * ploss) |
LOSS-5 |
KCL with half-loss split |
Kirchhoff's Voltage Law (KVL)
| Label Pattern |
Family |
Description |
angle[from] - angle[to] == reactance * flow |
DC-2 |
Voltage angle / power flow relation |
voltage_angle[slack] == 0 |
DC-3 |
Slack bus reference angle |
Line Capacity
| Label Pattern |
Family |
Description |
flow[i,j,t] <= capacity + investment |
DC-4a |
Line capacity (positive direction) |
flow[i,j,t] >= -(capacity + investment) |
DC-4b |
Line capacity (negative direction) |
investment[i,j] == investment[j,i] |
DC-5 |
Bidirectional investment symmetry |
angle_diff <= max_angle_deg |
DC-6 |
Voltage angle difference limit |
Devices
| Label Pattern |
Family |
Description |
rectify + invert <= rated_power |
CONV-1 |
AC/DC converter power limit |
freq_ab + freq_ba <= rated_power |
CONV-2 |
Frequency converter power limit |
Load Shedding (DC mode)
| Label Pattern |
Family |
Description |
loss_load[b,t] <= demand[b,t] * threshold |
DC-LS |
Load shedding limit (if threshold < 1.0) |
master_problem.jl --- Capacity Expansion
Investment Variables & Constraints
| Label Pattern |
Family |
Description |
| Cumulative gen investment limit |
INV-1 |
sum_y(invest[g,n,y]) <= invest_max[g,n] |
| Cumulative battery power limit |
INV-2 |
sum_y(invest_pow[b,n,y]) <= invest_max_power[b,n] |
| Cumulative battery capacity limit |
INV-3 |
sum_y(invest_cap[b,n,y]) <= invest_max_capacity[b,n] |
| Cumulative reservoir capacity limit |
INV-3r |
sum_y(reservoir_invest[g,n,y]) <= reservoir_invest_max[g,n] |
min_duration_{bi}_{b} |
INV-4a |
Battery min E/P ratio |
max_duration_{bi}_{b} |
INV-4b |
Battery max E/P ratio |
Technology Investment Constraints
When per-technology investment is enabled (via technologies and battery_technologies), the master problem creates technology-level investment variables instead of per-generator variables.
| Label Pattern |
Family |
Description |
| Cumulative technology gen investment limit |
TECH-1 |
sum_y(tech_invest[t,n,y]) <= tech_invest_max[t,n] |
| Cumulative battery tech power limit |
TECH-2 |
sum_y(btech_invest_pow[t,n,y]) <= btech_invest_max_power[t,n] |
| Cumulative battery tech capacity limit |
TECH-3 |
sum_y(btech_invest_cap[t,n,y]) <= btech_invest_max_capacity[t,n] |
| Battery tech min E/P ratio |
TECH-4a |
btech_invest_cap[t,n,y] >= min_duration * btech_invest_pow[t,n,y] |
| Battery tech max E/P ratio |
TECH-4b |
btech_invest_cap[t,n,y] <= max_duration * btech_invest_pow[t,n,y] |
Budget
| Label Pattern |
Family |
Description |
annual_cost <= max_annual_investment + slack |
BUD-1 |
Annual investment budget with slack for feasibility |
Capacity Adequacy
| Label Pattern |
Family |
Description |
capacity_adequacy_{y_idx}_{n} |
CAP-1 |
Total capacity >= peak_demand * reserve_margin |
RE Targets
| Label Pattern |
Family |
Description |
re_penetration_target_{y_idx} |
RE-1 |
Annual RE penetration target |
re_penetration_min_increment |
RE-2 |
Minimum annual RE growth |
re_penetration_max_increment |
RE-3 |
Maximum annual RE growth |
Retirement
| Label Pattern |
Family |
Description |
Age-based: age_at_year >= lifetime -> capacity = 0 |
RET-1 |
Existing unit retirement |
Investment: year - invest_year >= lifetime -> capacity = 0 |
RET-2 |
Invested unit retirement |
Operational Validation (Representative Days)
| Label Pattern |
Family |
Description |
| Generator output bounds per day |
OP-1 |
gen[g,n,t] <= available_capacity |
| Battery SOC dynamics per day |
OP-2 |
SOC balance with cyclic constraint |
| Power balance per day |
OP-3 |
Supply meets demand |
| Curtailment limit per day |
OP-4 |
curtailment <= ratio * renewable |
sectoral_lol_cap_{sector}_{n}_{t} |
OP-5 |
Sectoral LOL per rep. day |
Inter-System Transmission (Multi-System DC-OPF)
| Label Pattern |
Family |
Description |
pf = fp - fn |
IS-1 |
Bidirectional flow decomposition |
pf <= cap_base + sum(invest) |
IS-2 |
Capacity limit (positive direction) |
pf >= -(cap_base + sum(invest)) |
IS-3 |
Capacity limit (negative direction) |
ploss = sum_k(m_k * (dp_k + dn_k)) |
IS-4 |
PWL loss approximation (or linear fallback) |
ext_inj_FROM = +pf - 0.5*ploss |
IS-5 |
KCL injection at FROM bus (half-loss split) |
ext_inj_TO = -pf - 0.5*ploss |
IS-6 |
KCL injection at TO bus (half-loss split) |
| Link investment limit |
MS-1 |
sum_y(link_invest[l,y]) <= max_investment |
| Border injection |
MS-3 |
Link power enters/exits at border nodes |
electrolyzer.jl --- Electrolysis
| Label Pattern |
Family |
Description |
power <= total_capacity |
ELZ-1 |
Electrolyzer power limit |
h2_prod == power * 1000 * eff / energy_per_kg |
ELZ-2 |
H2 production formula |
power[t] - power[t-1] <= ramp_up |
ELZ-3a |
Ramp-up limit |
power[t-1] - power[t] <= ramp_down |
ELZ-3b |
Ramp-down limit |
primary_energy.jl --- Fuel Supply Chain
Supply & Transport
| Label Pattern |
Family |
Description |
supply[f,n,p] <= max_availability[f,n] |
PE-1 |
Fuel supply limit |
transport[f,n,m,p] <= capacity + investment |
PE-2 |
Transport capacity |
received = sent * (1 - loss_rate * distance) |
PE-3 |
Transport losses |
Storage
| Label Pattern |
Family |
Description |
level[f,n,p] = level[p-1] + supply - consumption - transport_out |
PE-4 |
Storage balance |
min_level * capacity <= level <= capacity + investment |
PE-5 |
Storage bounds |
level[final] == level[initial] |
PE-6 |
Cyclic storage constraint |
Demand
| Label Pattern |
Family |
Description |
non_electric_consumption + loss >= demand |
PE-7 |
Non-electric demand satisfaction |
gen_consumption = gen_output / efficiency * energy_content |
PE-8 |
Generator fuel linkage |
Emissions
| Label Pattern |
Family |
Description |
emissions = consumption * emission_factor |
PE-EM |
Emission calculation |
mga.jl --- MGA and SPORES
Cost Slack Constraint
| Label Pattern |
Family |
Description |
total_cost <= (1 + slack) * optimal_cost |
MGA-1 |
Near-optimal cost slack: total system cost must be within slack_fraction of the cost-optimal objective. Shared by both methods |
MGA — Classical Hop-Skip-Jump diversity objective
The HSJ objective replaces the cost objective after the cost-optimal solution is found. Frequency-based scoring assigns scores based on how often each investment variable has been selected in previous alternatives.
| Label Pattern |
Family |
Description |
max sum(score_k * x_k / x_max_k) |
MGA-2 |
Maximize weighted diversity: score = 1 - 2 * frequency, where frequency is the fraction of previous alternatives that invested in variable k. Used by run_mga_spores and reusable inside a SPORES sweep when :hsj_diversity is in the objective list |
SPORES — Per-objective sweep
Each SPORES objective replaces the cost objective (and any previous SPORES objective's aux vars / constraints, via _clear_spores_aux!) under the same cost-slack envelope (MGA-1). All formulations are LP — the L1 distance in SPORES-4 is linearised with positive / negative deviation aux variables.
| Label Pattern |
Family |
Description |
min sum(I) |
SPORES-1 |
Minimum total build: \(\min \sum_{y,t,n} I^{tech} + \sum_{y,b,n} I^{bat,P} + \sum_{y,(i,j)} I^{tr}\). No auxiliary variables. Implemented by set_min_build_objective! |
min M, sum(I_t/I_max) <= M ∀ t |
SPORES-2 |
Technology equity (min-max over per-tech totals). Adds 1 auxiliary scalar \(M\) and $ |
min M, sum(I_n/I_max) <= M ∀ n |
SPORES-3 |
Regional equity (min-max over per-node totals). Adds 1 auxiliary scalar \(M\) and $ |
I - I_ref = d_pos - d_neg; max sum((d_pos + d_neg)/I_max) |
SPORES-4 |
Evolutionary distance (L1 from a reference solution, typically the cost-optimal). Adds 2 auxiliary variables and 1 constraint per investment variable. Implemented by set_evolutionary_distance_objective! |
transmission_acopf.jl --- AC Optimal Power Flow
Voltage Constraints
| Label Pattern |
Family |
Description |
soc_{l}_{t} |
AC-SOC |
SOC relaxation: w_i × w_j >= wr² + wi² per branch |
qc_wr_lb_{l}_{t}, qc_wr_ub_{l}_{t} |
AC-QC1 |
QC tighter bounds on wr using cos bounds |
qc_wi_lb_{l}_{t}, qc_wi_ub_{l}_{t} |
AC-QC2 |
QC tighter bounds on wi using sin bounds |
qc_cos_env_{l}_{t} |
AC-QC3 |
QC convex envelope for cos relaxation |
qc_angle_ub_{l}_{t}, qc_angle_lb_{l}_{t} |
AC-QC4 |
QC angle bounds via tan(θ_max) |
angle_ub_{l}_{t}, angle_lb_{l}_{t} |
AC-ANG |
Polar/Rect angle difference limits |
vm_lb_{i}_{t}, vm_ub_{i}_{t} |
AC-VM |
Rectangular voltage magnitude bounds: v_min² ≤ e² + f² ≤ v_max² |
Power Balance
| Label Pattern |
Family |
Description |
kcl_p_{i}_{t} |
AC-P |
Active power balance: net_injection_MW = base_mva × Σ P_flow_pu |
kcl_q_{i}_{t} |
AC-Q |
Reactive power balance: Q_gen - Q_load + Q_slack = base_mva × Σ Q_flow_pu |
Line Limits
| Label Pattern |
Family |
Description |
sline_from_{l}_{t} |
AC-SL1 |
Apparent power from-side: P_from² + Q_from² ≤ cap_pu² |
sline_to_{l}_{t} |
AC-SL2 |
Apparent power to-side: P_to² + Q_to² ≤ cap_pu² |
Constraint Count Summary
| Module |
Approximate Constraints |
Variables |
power_system.jl |
~37 families + PWL segments |
gen_output, bat_charge, bat_discharge, bat_soc, reservoir_level, reservoir_pump, reservoir_spillage, loss_load, curtailment, ev_charge, ev_v2g, ev_soc, gen_seg_output, bat_seg_discharge |
master_problem.jl |
~16 families + technology investment |
gen_invest, bat_invest_power, bat_invest_capacity, reservoir_invest_capacity, transfer_invest, tech_invest, btech_invest_power, btech_invest_capacity |
transmission_dc.jl |
~8 families |
power_flow, voltage_angle, transfer_investment |
electrolyzer.jl |
~4 families |
elz_power, h2_production |
primary_energy.jl |
~8 families |
fuel_supply, fuel_transport, fuel_storage, fuel_consumption |
mga.jl |
~6 families (MGA-1, MGA-2, SPORES-1..4) |
reuses master-problem variables; SPORES-2/3 add 1 scalar each, SPORES-4 adds 2 per investment var |
transmission_acopf.jl |
~7 families (SOC/QC/VM/ANG/KCL-P/KCL-Q/SL) |
w, wr, wi, vm, va, vr, vi_rect, q_gen, q_slack_pos, q_slack_neg |
Total: ~82+ constraint families across the optimization model.