Skip to content

Model formulation

Every constraint, variable and objective term of the FlexTool LP, taken from the EQ. / VAR. / OBJ. headers in the model code. Each entry names the source file and line it was generated from. Names in braces expand: maxOnline_{linear,integer} is maxOnline_linear and maxOnline_integer, {sfx} is _linear / _integer, and a part in square brackets is optional (co2_max_total[_path]). Capacities are in MW (MWh for storage) and costs in CUR, the default units of FlexTool (see Model parameters).

Capacity symbols used in the headers

For a unit, connection or storage node e in period d:

  • capacity[e,d] = existing[e,d] × virtual_unitsize[e] when virtual_unitsize is set, else existing[e,d] (MW, or MWh for storage). With virtual_unitsize, existing is the number of units.
  • unitsize[e] = virtual_unitsize[e], else existing (the largest period value), else 1000.
  • existing_count[e,d] = capacity[e,d] / unitsize[e]: equal to existing when virtual_unitsize is set; otherwise 1 (less in a period whose existing is below the largest, 0 without existing capacity).
  • invest_*, retire_*, cumulative_*, invest_forced and the capacity a construction lead time carries are MW (MWh) in every case. v_invest and v_divest are continuous, in units of unitsize.

Core model

FlexTool LP model: builds the FlexTool LP from FlexData d into a polar_high Problem m.

VAR. v_flow

v_flow[p,source,sink,d,t] ≥ 0   ∀ (p,source,sink,d,t) ∈ pss_dt

Flow on a process arc in units of unitsize: MW = v_flow · unitsize[p]. Input arcs of indirect units without invest/divest variables carry a per-element upper bound (their capacity is a constant); maxFlow caps every other arc.

Source: flextool/engine/model/core.py:911 (_declare_variables)

VAR. v_flow (coarse processes)

Names: v_flow

v_flow[p,s,k,d,b] ≥ 0   ∀ p ∈ processCoarse, (p,s,k) arcs, (d,b) the coarse steps of p (b = first fine step)

The flow of a coarse process: one rate per coarse step, the same on its fine steps. b.v_flow is indexed on every fine step (_coarse_flows.alias): a fine row of a coarse process reads its step's column.

Source: flextool/engine/model/core.py:918 (_declare_variables)

VAR. v_ramp_up / v_ramp_down

Names: v_ramp_up, v_ramp_down

v_ramp_up[p,s,k,d,t] ≥ 0, v_ramp_down[p,s,k,d,t] ≥ 0   ∀ (p,s,k,d,t) ∈ R
  R = the rows of ramp_change_eq (_ramp_cost_rows): (p,s,k) ∈ process_source_sink_ramp_cost with a ramp
  predecessor at (d,t) (dt_ramp_link or dt_ramp_carry; for a coarse process t = b, its coarse step)

The increase and the decrease of v_flow from the ramp predecessor, in units of unitsize (as v_flow). Priced equally by the ramp cost, so at most one of them is > 0 at an optimum (up + down = |Δv_flow|). Declared only when some arc has a ramp cost.

Source: flextool/engine/model/core.py:929 (_declare_variables)

VAR. vq_state_up / vq_state_down

Names: vq_state_up, vq_state_down

vq_state_up[n,d,t], vq_state_down[n,d,t] ≥ 0
  ∀ n ∈ nodeBalance ∪ nodeBalancePeriod, (d,t) ∈ dt

Balance slack in MW (shortfall / surplus); enters the balance times step_duration and is penalised in the objective. Nodes with penalty_method = off have none, so their balance is a hard equality.

Source: flextool/engine/model/core.py:944 (_declare_variables)

VAR. v_state

v_state[n,d,t] ≥ 0   ∀ (n,d,t) ∈ nodeState_dt

Storage state in units of the node's state unitsize: MWh = v_state · state_unitsize[n]. Its upper bound depends on invest/divest and is the maxState row.

Source: flextool/engine/model/core.py:955 (_declare_variables)

VAR. v_state_inter / v_state_rp_start

Names: v_state_inter, v_state_rp_start

v_state_inter[n,d,b] ≥ 0        ∀ n ∈ nodeState_rp, (d,b) ∈ rp_base_period
v_state_rp_start[n,d,t] ≥ 0     ∀ n ∈ nodeState_rp, (d,t) ∈ rp_block_first

Representative-period storage, in state units: the state at the end of base period b of period d, and the free starting state of each representative block. Every period has its own base periods (two periods may share base-period names). Upper bounds are the rows rp_inter_period_max_state and maxState_rp_start.

Source: flextool/engine/model/core.py:964 (_declare_variables)

VAR. v_invest_p / v_divest_p / v_invest_n / v_divest_n

Names: v_invest_p, v_divest_p, v_invest_n, v_divest_n

v_invest_p[p,d] ≥ 0 ∀ (p,d) ∈ pd_invest_set;  v_divest_p[p,d] ≥ 0 ∀ (p,d) ∈ pd_divest_set
v_invest_n[n,d] ≥ 0 ∀ (n,d) ∈ nd_invest_set;  v_divest_n[n,d] ≥ 0 ∀ (n,d) ∈ nd_divest_set

Capacity built / retired in period d, as a count of unitsize (process) or state unitsize (storage node). Only pre-existing capacity can be retired: pd/nd_divest_set hold the periods where the entity has some (before its end of life). Upper bounds are the maxInvest_var_bound / maxDivest_var_bound rows.

Source: flextool/engine/model/core.py:982 (_declare_variables)

VAR. v_flow_back

v_flow_back[p,source,sink,d,t] ≥ 0   ∀ (p,source,sink) ∈ twoway1var_arcs, (d,t) ∈ dt

Reverse flow (sink → source) of method_2way_1var_off and DC power-flow arcs, in units of unitsize; the signed flow is v_flow − v_flow_back. One variable serves both the balance and dc_flow_eq. A coarse process has one per coarse step, as v_flow.

Source: flextool/engine/model/core.py:1002 (_declare_variables)

EQ. rp_inter_period_balance

v_state_inter[n,d,b] − v_state_inter[n,d′,b′] = drift[n,d,b]
  drift[n,d,b] = Σ_{(d,b,r) ∈ rp_base__rep, (d,r) ∈ rp_block_first, d ∈ periods of dt}
                   rp_weight[d,b,r] · (v_state[n,d,last_step[d,r]] − v_state_rp_start[n,d,r])
  ∀ n ∈ nodeState_rp, (d,b,b′) ∈ rp_base_chain, d′ = d                         (within a period)
  ∀ n ∈ bind_within_solve_blended_weights ∪ bind_forward_only_blended_weights,
    (d,d′) ∈ rp_period_previous, b = first base period of d, b′ = last of d′    (period to period)

Chains the storage state across the base periods of each period: a base period is represented by weighted representative blocks r, and the boundary state moves by the weighted net change of those blocks. The across-solve variants continue from the last base period of the previous period in solve order, as bind_within_solve / bind_forward_only continue v_state across periods. All terms are in state units.

Source: flextool/engine/model/core.py:1218 (_eq_rp_storage_chain)

EQ. rp_inter_period_cyclic

v_state_inter[n,d,b_first(d)] − v_state_inter[n,d′,b_last(d′)] = drift[n,d,b_first(d)]
  n ∈ bind_within_period_blended_weights: every rp period d, d′ = d
  n ∈ bind_within_solve_blended_weights: (d,d′) ∈ d_first × d_last, d_first = the rp periods without a
    predecessor in rp_period_previous, d_last = the rp periods that are no period's predecessor

Closes the chain: the last boundary state connects back to the first. bind_within_period_blended_weights closes every period on itself, bind_within_solve_blended_weights closes once per path of the solve: deterministic, one pair (first period, last period). In a stochastic solve the rp periods form a tree (rp_period_previous): a fan after the trunk period (the shared period before the split) has one d_first and one d_last per forecast branch, so every branch's last period ends at the level the trunk period starts from; a fan in the first period has N roots and N leaves, and the N × N pairs are the loop of each path plus an equal chain start of all the roots. bind_forward_only_blended_weights has no closure.

Source: flextool/engine/model/core.py:1255 (_eq_rp_storage_chain)

EQ. rp_inter_period_fan_start

v_state_inter[n,d,b_first(d)] − drift[n,d,b_first(d)]
  − v_state_inter[n,d″,b_first(d″)] + drift[n,d″,b_first(d″)] = 0
  ∀ n ∈ bind_forward_only_blended_weights, d = the first root (sorted), d″ ∈ the other roots
  roots = the rp periods of period_first_of_solve (the forecast branch periods of a fan in the solve's
    first period; one root without a fan: no rows), read as the periods of nodeState_first_dt, which is
    built from period_first_of_solve

Every forecast of a fan in the solve's first period starts its base-period chain from the same level: the chain start v_state_inter − drift of the first base period, which no other row binds in a forward-only chain. A fix_start node is included: fix_start pins v_state in the first block, not the chain start. The bind_within_solve_blended_weights roots are already tied by rp_inter_period_cyclic; the base-period levels inside a tie window need no tie of their own (they follow from the tied net flows, the tied block starts and this shared chain start).

Source: flextool/engine/model/core.py:1301 (_eq_rp_storage_chain)

EQ. rp_inter_period_max_state / maxState_rp_start

Names: rp_inter_period_max_state, maxState_rp_start

v_state_inter[n,d,b] + Σ_{(n,d′,d) ∈ ndd_divest_active} v_divest_n[n,d′]
  − Σ_{(n,d′,d) ∈ edd_invest_set} v_invest_n[n,d′]  ≤  state_upper[n,d]
  ∀ n ∈ nodeState_rp, (d,b) ∈ rp_base_period, d ∈ periods of dt
v_state_rp_start[n,d,t] + (same invest/divest terms)  ≤  state_upper[n,d]
  ∀ (n,d,t) ∈ nodeState_rp × rp_block_first

Capacity bounds of the boundary and block-start states, as maxState bounds v_state: a base period of d is bounded by d's capacity. state_upper = capacity / state_unitsize, so every term is in state units. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/core.py:1342 (_eq_rp_storage_chain)

EQ. nodeBalance_eq

Σ_{(p,s,n) ∈ flow_to_n} v_flow[p,s,n,d,t]·unitsize[p]·Δt
  − Σ_{(p,n,k) ∈ flow_from_nodeBalance_eff}
      (v_flow[p,n,k,d,t]·unitsize[p]·slope[p,d,t]
       + v_online[p,d,t]·section[p,d,t]·unitsize[p])·Δt
  − Σ_{(p,n,k) ∈ flow_from_nodeBalance_noEff} v_flow[p,n,k,d,t]·unitsize[p]·Δt
  − Σ_{(p,n) ∈ startup_to_balance} v_startup[p,d,t]·startup_energy[p,n,d]·unitsize[p]   (no Δt: an event)
  + back-flow terms of two-way one-variable arcs
  + (vq_state_up[n,d,t] − vq_state_down[n,d,t])·Δt
  + (v_state[n,d,t⁻] − v_state[n,d,t])·state_unitsize[n]
  − (α[n,d,t]·v_state[n,d,t] + β[n,d,t]·S⁻[n,d,t])·state_unitsize[n]
  h = Δt·ln(1 + self_discharge_loss[n,d,t]),  α = h/(1 − e^{−h}) − 1,  β = 1 − h/(e^{h} − 1)
  S⁻ = the state the step starts from: v_state[n,d′,t⁻] by the binding method, v_state_rp_start at a
       representative block's first step, the fix_start start state / the roll carry at an open chain's
       first step, v_state[n,d,t] where the chain is open with neither
  + start terms of open-start nodes (fix_start in the first solve, the roll carry in continuation solves)
  = −inflow[n,d,t]
  ∀ (n,d,t) ∈ nodeBalance_dt, n ∉ nodeStateBlock ∪ nodeCoarse;   Δt = step_duration[d,t]
  section[p,d,t] = min_load·(1/eta_min − 1/eta) / (1 − min_load)
  slope[p,d,t] = 1/eta − section[p,d,t]   (= (1/eta − min_load/eta_min) / (1 − min_load))
    for min_load_efficiency units (eta = efficiency, default 1; min_load = 0 where not authored;
    eta_min = efficiency_at_min_load, else efficiency); slope = 1/eta and no section for the other units
  on a direct (one-input, one-output) unit, slope and section above are further scaled by
    k_sink/k_src (slope) and 1/k_src (section) — the unit's output/input conversion_flow_coeff
    (default 1 each; a zero coefficient keeps its uncapped-edge meaning instead, unscaled)

Energy conservation per node and time step, in MWh. Storage nodes add the state change; the previous step t⁻ depends on storage_binding_method. nodeStateBlock nodes are balanced per timeset block (nodeBalanceBlock_eq), nodeCoarse nodes per coarse step (nodeBalanceCoarse_eq). The section term exists for min_load_efficiency units only. The start-up term (core._startup_draw) is the start-up energy of the units with a start-up input node n (startup_input_method startup_and_input / startup_only), MWh per start-up event; a coarse process starts at the first fine step of its coarse step. The flow, start-up and slack terms are b.node_balance_flow_terms. Self-discharge: the content decays continuously at ln(1 + loss) per hour; flows within a step are spread evenly over the step. Exact for any step length while the loss is constant within the step; a coarse step / block with a time-varying loss uses h_b = Σ Δt·λ_t, exact for the idle decay only (#152). The decay is applied to a weighted average of the start and the end state (model/_self_discharge.py); the model folds it into the state change as (S⁻·(1 − β) − S·(1 + α))·state_unitsize.

Source: flextool/engine/model/core.py:1474 (_eq_node_balance)

EQ. nodeBalanceCoarse_eq

Σ_{t ∈ b} [ Σ_{(p,s,n) ∈ flow_to_n} v_flow[p,s,n,d,t]·unitsize[p]·Δt
  − Σ_{(p,n,k) ∈ flow_from_nodeBalance_eff}
      (v_flow[p,n,k,d,t]·unitsize[p]·slope[p,d,t] + v_online[p,d,t]·section[p,d,t]·unitsize[p])·Δt
  − Σ_{(p,n,k) ∈ flow_from_nodeBalance_noEff} v_flow[p,n,k,d,t]·unitsize[p]·Δt
  − Σ_{(p,n) ∈ startup_to_balance} v_startup[p,d,t]·startup_energy[p,n,d]·unitsize[p]
  + back-flow terms of two-way one-variable and DC arcs
  + (vq_state_up[n,d,t] − vq_state_down[n,d,t])·Δt
  + (v_state[n,d,t⁻] − v_state[n,d,t])·state_unitsize[n] − (α[n,d,t]·v_state[n,d,t] + β[n,d,t]·S⁻[n,d,t])·su[n]
  + start terms of open-start nodes ]
  = − Σ_{t ∈ b} inflow[n,d,t]
  ∀ n ∈ nodeCoarse, (n,d,b) ∈ node_coarse_step;   t ∈ b: (n,d,b,t) ∈ node_coarse_step;   Δt = step_duration[d,t]
  t⁻, start and roll terms per storage_binding_method exactly as in nodeBalance_eq
  a coarse process (processCoarse) has one v_flow per coarse step of its nodes: its fine terms read the same
  column, so its leg is v_flow[p,·,·,d,b]·unitsize·Σ_{t∈b} Δt (·slope_t on an efficiency source)
  α, β, S⁻ as in nodeBalance_eq with h_b = Σ_{t∈b} Δt·ln(1 + loss[n,d,t]), Δ_b = Σ_{t∈b} Δt: α spread over
  the fine steps by Δt/Δ_b, β at the first fine step only (the decay of the coarse step's own state over the
  coarse step; the flow weighting assumes the loss constant within the coarse step)
nodeBalance_eq summed over the fine steps of each coarse step of the node's group: every flow stays on the
solve's steps and each coarse leg sums its fine flows, so an arc between a fine and a coarse node, or
between two coarse nodes, conserves energy at both ends.  The node may shift energy freely within a coarse
step.  With the state constant within the coarse step (stateConstantCoarse_eq) the state terms telescope to
v_state[n,d,b⁻] − v_state[n,d,b] (b⁻ = the coarse step before b by the node's binding method), so storage
binding methods, fix_start and the roll carry apply to coarse nodes as to fine ones.  Same orientation and
units as nodeBalance_eq: its dual is the node's price (currency/MWh) over the coarse step.

Source: flextool/engine/model/core.py:1566 (_eq_node_balance_coarse)

EQ. stateConstantCoarse_eq

v_state[n,d,t] − v_state[n,d,t_previous] = 0
  ∀ (n,d,t,t_previous) ∈ node_coarse_interior   (n ∈ nodeCoarse ∩ nodeState; t_previous: the step before t
  within its coarse step)

A coarse storage node keeps one state per coarse step.

Source: flextool/engine/model/core.py:1603 (_eq_node_balance_coarse)

EQ. nodeBalancePeriod_eq

Σ_t w[d,t] · (flow, start-up and slack terms of nodeBalance_eq)[n,d,t]  =  −Σ_t w[d,t] · inflow[n,d,t]
  ∀ n ∈ nodeBalancePeriod, n ∉ nodeState, d ∈ periods of dt;   w = timestep_weight

Energy balance over a whole period instead of every step, in MWh. The terms are b.node_balance_flow_terms (Δt already applied); period nodes are never storage, so there are no state terms. Every term, slacks included, carries the timestep weight: a per-step balance needs no weight, but a period sum without it would let low-weight timeslices offset high-weight ones. The other op_factor parts (inflation_op, period_share) are per period and cancel in a row. Inflow is energy per step (no Δt), as in nodeBalance_eq.

Source: flextool/engine/model/core.py:1815 (_eq_node_balance_period)

EQ. node_balance_fix_quantity_eq_lower

v_state[n,d,t] · state_unitsize[n]
  = Σ_{(d_up,d) ∈ period_branch, (d,t,t_up) ∈ dtt_timeline_matching} fix_storage_quantity[n,d_up,t_up]
  ∀ n ∈ n_fix_storage_quantity, d ∈ period_last, (n,d,t) ∈ nodeState_last_dt

Pins the storage state (MWh) at the last step of each last period to the state an upper-level (coarser) solve reached at the matching time. Despite the name, the row is an equality.

Source: flextool/engine/model/core.py:1882 (_eq_node_balance_fix_quantity)

EQ. node_storage_usage_fix_le

− Σ_{(p,s,n) ∈ process_source_sink, t} v_flow[p,s,n,d,t]·unitsize[p]·Δt
+ Σ_{(p,n,k) ∈ process_source_sink_eff, t}
    (v_flow[p,n,k,d,t]·unitsize[p]·slope[p,d,t] + v_online[p,d,t]·section[p,d,t]·unitsize[p])·Δt
+ Σ_{(p,n,k) ∈ process_source_sink_noEff, t} v_flow[p,n,k,d,t]·unitsize[p]·Δt
  ≤ Σ_{(d_up,d) ∈ period_branch, t_up : ∃t (d,t,t_up) ∈ dtt_timeline_matching} fix_storage_usage[n,d_up,t_up]
  ∀ n ∈ n_fix_storage_usage, d ∈ period_last, (n,d,t_last) ∈ nodeState_last_dt;   Δt = step_duration[d,t]

Caps the net energy drawn from storage node n over a whole period (MWh) by the usage an upper-level (coarser) solve planned for the coarse windows overlapping that period. One row per period, at its last step; each coarse window counts once however many fine steps it covers. The section term exists for min_load_efficiency units only. The process sink/source coefficient ratio is not applied (nor in the source-side variable cost); it is 1 in every current dataset.

Source: flextool/engine/model/core.py:1985 (_eq_storage_usage_fix)

EQ. stateConstantWithinBlock_eq

v_state[n,d,t] − v_state[n,d,t_previous] = 0
  ∀ n ∈ nodeStateBlock, (d,t,t_previous) ∈ dtttdt_block_interior

Keeps the state of a nodeStateBlock node constant within each timeset block. Interior steps never cross a period boundary.

Source: flextool/engine/model/core.py:2055 (_eq_block_coupling)

EQ. nodeBalanceBlock_eq

Σ_{t ∈ b} [ Σ_{(p,s,n) ∈ flow_to_n} v_flow[p,s,n,d,t]·unitsize[p]·Δt
  − Σ_{(p,n,k) ∈ flow_from_nodeBalance_eff}
      (v_flow[p,n,k,d,t]·unitsize[p]·slope[p,d,t] + v_online[p,d,t]·section[p,d,t]·unitsize[p])·Δt
  − Σ_{(p,n,k) ∈ flow_from_nodeBalance_noEff} v_flow[p,n,k,d,t]·unitsize[p]·Δt
  − Σ_{(p,n) ∈ startup_to_balance} v_startup[p,d,t]·startup_energy[p,n,d]·unitsize[p]
  + back-flow terms of two-way one-variable and DC arcs
  + (vq_state_up[n,d,t] − vq_state_down[n,d,t])·Δt
  + (v_state[n,d′,t⁻] − v_state[n,d,t])·state_unitsize[n]   ((d′,t⁻) of (d,t) in dtttdt_block_lag; for a
    fix_start_end node in block_forward_lag)
  − (α[n,d,t]·v_state[n,d,t] + β[n,d,t]·S⁻[n,d,t])·state_unitsize[n]
  + start terms of open-start nodes (fix_start in the first solve, the roll carry in a continuation solve) ]
  = − Σ_{t ∈ b} inflow[n,d,t]
  ∀ n ∈ nodeStateBlock, (d,b) ∈ period_block;   t ∈ b by period_block_time;   Δt = step_duration[d,t]
  α, β, S⁻ as in nodeBalanceCoarse_eq with the block b (h_b = Σ_{t∈b} Δt·ln(1 + loss[n,d,t])); S⁻ at the
  block's first step is the previous block's state
nodeBalance_eq summed over each timeset block of a bind_intraperiod_blocks node.  The state is constant within
a block (stateConstantWithinBlock_eq), so the state terms telescope to v_state[b⁻] − v_state[b]: v_state of a
block is its state at the end of the block.  b⁻ is the previous block of the period, the first block of a
period continuing from the period's last block (the blocks of a period form a loop).  In a rolling solve the
blocks also end at the roll's realized end; a period the roll holds whole loops as above, while in a period
the roll holds only in part (period_block_chain) the blocks chain forward: the period's first block continues
from the previous step of the solve, and at the solve's first step the chain starts from fix_start (first
roll), the carried level (later rolls) or, with neither, with no state change.  A fix_start_end node ignores
the loop (its start and end are pinned separately): its blocks chain forward in every period like that
(_self_discharge.block_forward_nodes / block_forward_lag).  Same orientation and units as nodeBalance_eq: its
dual is the node's price (currency/MWh) over the block.

Source: flextool/engine/model/core.py:2072 (_eq_block_coupling)

EQ. maxFlow

v_flow[p,s,k,d,t] + Σ_r v_reserve_up[p,r,k,d,t] + Σ_r v_reserve_down[p,r,s,d,t] · φ[p,s,k,d,t]
  − M·Σ_{(p,d′,d) ∈ edd_invest_set} v_invest_p[p,d′]
  + M·Σ_{(p,d′,d) ∈ edd_divest_active} v_divest_p[p,d′]
  ≤ M · existing_count[p,d]
  ∀ (p,s,k,d,t) ∈ pss_dt, (p,s,k) ∉ flow_bound_arcs or (p,s,k) ∈ reserve_down_input_arcs
  M = capacity_max_coeff[p,s,k] · availability[p,d,t] on capped / uncapped arcs
  φ = 1 where v_flow is the input (k = p), 1/slope[p,d,t] on a direct unit's input→output arc
      (s ≠ p ≠ k); both reserve terms only for units (connections: connection_reserve_*, _reserve)

Caps every flow by the built capacity of its process (units of unitsize). On indirect-unit input arcs M = input_share_max · full-load input · a, a = x(availability) = (slope·W(a₀) + section)/(slope·W(1) + section) (a₀ = availability, W(y) the output-side fuel width filled to y, section only for a unit with an online variable); a ≥ availability (never tighter than the physical derated input), = availability when every output shares one conversion_flow_coeff (#102). Uncapped arcs (conversion_flow_coeff = 0) have M = 1 and RHS = p_flow_upper. Input arcs of units without invest/divest are bounded on v_flow instead (flow_bound_arcs). The reserve terms are the reserve that raises the flow: upward reserve held at the sink, and downward reserve held at the source of a unit (it consumes more), converted to the arc's flow. A bound input arc gets this row as well when it carries such a reserve (its v_flow bound stays, redundant). A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/core.py:2127 (_eq_max_flow)

EQ. maxFlow_negCap

(maxFlow LHS)  ≥  M · existing_count[p,d]
  ∀ (p,s,k,d,t) ∈ pss_dt, (p,d) ∈ pd_neg_cap

Negative-capacity processes (unitsize \< 0 and existing \< 0): dividing v_flow·unitsize ≤ capacity by the negative unitsize flips the sense, so the capacity becomes a minimum output. maxFlow keeps the ≤ side.

Source: flextool/engine/model/core.py:2173 (_eq_max_flow)

EQ. maxFlow_back

v_flow_back[p,s,k,d,t] − M·Σ v_invest_p[p,d′] + M·Σ v_divest_p[p,d′]
  ≤ M · existing_count[p,d]
  ∀ (p,s,k,d,t) ∈ pss_dt, (p,s,k) ∈ twoway1var_arcs

Caps the reverse flow of two-way one-variable arcs by the same built capacity, so |v_flow − v_flow_back| ≤ capacity. No reserve term. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/core.py:2192 (_eq_max_flow)

EQ. maxOutputSum / maxOutputSum_negCap

Names: maxOutputSum, maxOutputSum_negCap

Σ_k v_flow[p,p,k,d,t] + Σ_k Σ_r v_reserve_up[p,r,k,d,t]
  − av·Σ v_invest_p[p,d′] + av·Σ v_divest_p[p,d′]
  ≤ av · existing_count[p,d]
  ∀ p ∈ process_indirect with an output arc k, (d,t);   av = availability[p,d,t]

The capacity of an indirect (multi-flow) unit is the maximum sum of its outputs; maxFlow caps each output arc alone. capacity_max_coeff is not applied. Negative-capacity (p,d) rows get only the ≥ twin maxOutputSum_negCap: with both, a k-output unit would need Σ out = c while maxFlow pins every arc to c. existing_count[p,d] is the value maxFlow's output arcs of p carry (all equal; max collapses them); a unit without an existing row gets 0. Availability defaults to 1.0 for processes without availability data. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/core.py:2227 (_eq_max_output_sum)

EQ. ramp_{sink,source}_{up,down}_constraint

Names: ramp_sink_up_constraint, ramp_sink_down_constraint, ramp_source_up_constraint, ramp_source_down_constraint

Units without online variables (as c14a): v_flow[p,s,k,d,t] − v_flow[p,s,k,d′,t′] + R↑[p,s,k,d,t]

  ≤ +ρ̄ · c[p,s,k,d,t] · (existing_count[p,d] + Σ_{d″} v_invest_p[p,d″] − Σ_{d″} v_divest_p[p,d″])        (up)

v_flow[p,s,k,d,t] − v_flow[p,s,k,d′,t′] − R↓[p,s,k,d,t]

  ≥ −ρ̄ · c[p,s,k,d,t] · (existing_count[p,d] + Σ v_invest_p − Σ v_divest_p)                              (down)

Units with online variables (linear or integer class), room on the units online, split by unit state: v_flow[p,s,k,d,t] − v_flow[p,s,k,d′,t′] + R↑[p,s,k,d,t]

  ≤ c · ( ρ̄ · (v_online[p,d,t] − v_startup[p,d,t]) + SU[p,s,k,d,t] · v_startup[p,d,t] )
    − F[p,s,k,d′,t′] · v_shutdown[p,d,t]                                                                  (up)

v_flow[p,s,k,d,t] − v_flow[p,s,k,d′,t′] − R↓[p,s,k,d,t]

  ≥ −c · ( ρ̄ · (v_online[p,d,t] − v_startup[p,d,t]) + SD′[p,s,k,d′,t′] · v_shutdown[p,d,t] )
    + F[p,s,k,d,t] · v_startup[p,d,t]                                                                     (down)
  ρ̄ = ramp_speed_{dir}_{side}[p,node] · 60 · (step_duration[d′,t′] + step_duration[d,t]) / 2
  SU = min(1, ml[p,s,k,d,t] + ½ · ramp_speed_up_{side} · 60 · step_duration[d,t])
  SD′ = min(1, ml[p,s,k,d′,t′] + ½ · ramp_speed_down_{side} · 60 · step_duration[d′,t′])
  ml = min_load (output-measured arc) or x(min_load) / input_share_max (indirect unit's input arc), 0
    without min_load;  F = the arc's floor per unit online (arc_floor_share): min_load on the only arc of
    a unit's output measure, min_load · capacity_min_coeff on an output of a multi-output unit with
    capacity_min_coeff > 0, else 0
  ∀ (p,s,k) ∈ process_source_sink_ramp_limit_{side}_{dir}, (d,t,d′,t′) ∈ dt_ramp_link; on a link inside one
    period (d′ = d) only where ρ̄ < 1

On a continuation roll the first step of a period ramps from the previous roll's realized flow

  (dt_ramp_carry): v_flow[p,s,k,d′,t′] → f̂[p,s,k,d,t] (p_roll_continue_flow, a constant), step_duration
  [d′,t′] → its carried duration; for units with online variables F[d′,t′] → 0 (no shutdown term on the
  up row) and SD′ → max(1, f̂[p,s,k,d,t] / (c[p,s,k,d,t] · ĉ[p])) (1 where the carried online count
  ĉ = p_roll_continue_online is 0 — no stoppers then; exact and always feasible, unlike a fixed κ/c).

A coarse process (processCoarse, _coarse_flows.ramp_rows) ramps between its coarse steps: t = b (named by its

  first fine step), (d′,t′) = b⁻, the coarse step that holds the fine predecessor of b's first step, and
  step_duration → Δ_b = Σ_{t∈b} Δt, so ρ̄ = speed · 60 · (Δ_b⁻ + Δ_b)/2 (no row inside a coarse step; with
  24-h steps an in-period row exists only for a speed below 1/1440 per minute); a time-varying source factor
  c is its coarse-step mean; the carried step is the arc's last realized coarse step (its length carried).
  With online variables v_online is the step's value, v_startup / v_shutdown the events at b (start-ups and
  shut-downs relative to b⁻, online_change); c and F are their means over b (F[b⁻] over b⁻) and
  SU[b] = mean share over b of a unit started at b's start: ml + ½·ρ_b while ρ_b ≤ 1 − ml, else
  1 − (1 − ml)²/(2·ρ_b), ρ_b = ramp_speed_up · 60 · Δ_b, ml the mean over b (SD′[b⁻] likewise with the down
  speed over b⁻: the exact mean of a linear ramp that reaches full load within the step).

Steps without a predecessor, on arcs that hold reserve in that direction there (prundt):

  R↑ ≤ ρ·c·cap (up),  −R↓ ≥ −ρ·c·cap (down),  ρ = ramp_speed · 60 · step_duration[d,t] < 1;
  cap = v_online[p,d,t] for units with online variables, built units otherwise.

Source: flextool/engine/model/core.py:2312 (_eq_ramp)

EQ. ramp_{sink,source}_{up,down}_reserve_coarse

Names: ramp_sink_up_reserve_coarse, ramp_sink_down_reserve_coarse, ramp_source_up_reserve_coarse, ramp_source_down_reserve_coarse

R↑[p,s,k,d,b] ≤ room_b · cap (up),  −R↓[p,s,k,d,b] ≥ −room_b · cap (down)
  ∀ arc of a coarse process with the ramp limit, holding reserve in that direction on its coarse step (d,b)
  (b = its first fine step) with some ρ_t < 1;  room_b = min_{t∈b, ρ_t<1} ρ_t · c[p,s,k,d,t], ρ_t the room of
  the fine row at t: ramp_speed · 60 · (step_duration[d′,t′] + step_duration[d,t]) / 2 with t's ramp
  predecessor (d′,t′) (a carried one: the arc's carried duration p_roll_continue_flow_duration, as the
  carried ramp row), ramp_speed · 60 · step_duration[d,t] without one;  c the arc's capacity factor at t;
  cap = v_online (units online of the step) or the built units

A coarse process holds one reserve and one flow per coarse step: the fine rows inside the step (no flow change) would bound the reserve by each step's own room, so the smallest binds; the ramp between coarse steps above still holds the reserve together with the flow change. R↑ = Σ_r v_reserve[p,r,up,k,d,t] + φ·Σ_r v_reserve[p,r,down,s,d,t]

  (raises the arc: up at its sink, down at its source)

R↓ = Σ_r v_reserve[p,r,down,k,d,t] + φ·Σ_r v_reserve[p,r,up,s,d,t]

  (lowers it: down at its sink, up at its source)
  φ = 1 where v_flow is the input (indirect unit's input arc, sink-less unit), 1/slope on a direct arc;
  units only.

Limits the change of a flow between two consecutive steps, plus the reserve it holds in that direction, to its ramp room: ramp_speed is a share of capacity per minute and flows are step averages, so the room between two step averages is the speed × the distance between the two step centres (a valid bound for every trajectory that obeys the speed; it is the tight one for a linear ramp). For a unit with online variables the units online in both steps change by at most ρ̄ each, a unit starting in step t reaches at most SU (half a step's ramp above min load, the startup-step limit of maxFlow_online) and a stopping unit drops from at least its floor F; downwards a stopping unit drops at most from SD′ (the pre-shutdown limit) and a starting unit rises to at least F. v_startup / v_shutdown are relative to the same predecessor (online_change), so a start/stop pair with online unchanged gains c·(ml − ½·ρ·Δ) − F ≤ 0 of room on the only output of a unit (research_backbone_startup_ramp.md); on an input, or on one output of a multi-output unit, a pair can still gain up to the min-load share, paid by the startup cost. A step has a predecessor (dt_ramp_link) only where it is adjacent on the timeline in the un-rolled solve: no cyclic wrap at the first step of a solve or of a period that does not continue the previous one, no row across a gap or at the start of a representative block. Rows inside one period with ρ̄ ≥ 1 cannot bind and are not built (unchanged from c14a: every term of the online-capacity split is still implied there, design §1.4). Cross-period and carried rows are always built (the capacity, or the online predecessor, can differ). c scales the room as the arc's capacity is scaled: capacity_max_coeff[p,k] on the sink side; on the source side input_share_max[p,s], or on an indirect unit's input arc p_indirect_input_cap. v_flow and v_reserve are in units of unitsize, existing_count = capacity / unitsize. Arcs with conversion_flow_coeff = 0 get no row. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/core.py:2353 (_eq_ramp)

EQ. ramp_change_eq

v_flow[p,s,k,d,t] − v_flow[p,s,k,d′,t′] − v_ramp_up[p,s,k,d,t] + v_ramp_down[p,s,k,d,t] = 0
  ∀ (p,s,k) ∈ process_source_sink_ramp_cost, (d,t,d′,t′) ∈ dt_ramp_link   (every link, no room filter)
carried row (dt_ramp_carry): v_flow[p,s,k,d,t] − v_ramp_up + v_ramp_down = f̂[p,s,k,d,t] (p_roll_continue_flow)
coarse process: t = b (its coarse step, named by the first fine step), (d′,t′) = b⁻, as _coarse_flows.ramp_rows

Splits the change of a flow between a step and its ramp predecessor into an increase and a decrease, both priced by the ramp cost (OBJ. ramp_cost), in units of unitsize. The predecessors are those of the ramp limits: no row at the first step of a solve, after a gap in the timeline, at a period that does not continue the previous one or at a representative-block start; cross-period links are kept; a stochastic branch's first step links to the shared predecessor; a continuation roll's first step ramps from the flow the previous roll realized, so a change at a roll boundary is charged once, in the roll that realizes it. Arcs with conversion_flow_coeff = 0 have rows too (that parameter removes the ramp limits, not the cost). Reserves are not part of the change. An indirect unit's input and output arcs are priced separately, each by its own side.

Source: flextool/engine/model/core.py:2453 (_eq_ramp_cost)

EQ. max{Invest,Divest}_var_bound[_n]

Names: maxInvest_var_bound, maxInvest_var_bound_n, maxDivest_var_bound, maxDivest_var_bound_n

v_invest_p[p,d] ≤ max_units[p,d]   ∀ (p,d) ∈ pd_invest_set     (maxInvest_var_bound)
v_divest_p[p,d] ≤ max_units[p,d]   ∀ (p,d) ∈ pd_divest_set     (maxDivest_var_bound)
v_invest_n[n,d] ≤ max_units[n,d]   ∀ (n,d) ∈ nd_invest_set     (maxInvest_var_bound_n)
v_divest_n[n,d] ≤ max_units[n,d]   ∀ (n,d) ∈ nd_divest_set     (maxDivest_var_bound_n)

Caps the units invested or divested in one period (max_units = p_entity_max_units). Emitted as rows, not as variable bounds.

Source: flextool/engine/model/core.py:3012 (_eq_invest_max_units)

EQ. max{Invest,Divest}_entity_period_{p,n}

Names: maxInvest_entity_period_p, maxInvest_entity_period_n, maxDivest_entity_period_p, maxDivest_entity_period_n

Σ_{(p,o,d) ∈ edd_invest_commission} v_invest_p[p,o] · unitsize[p] + carried_commission[p,d]
                                     ≤ invest_max_period[p,d]   ∀ (p,d) with a decision coming online in d,
                                                                  p ∈ ed_invest_period_set
(n: v_invest_n with state_unitsize[n], n ∈ ed_invest_period_set ∩ nd_invest_set)
v_divest_p[p,d] · unitsize[p]        ≤ divest_max_period[p,d]   ∀ (p,d) ∈ ed_divest_period_set ∩ pd_divest_set
v_divest_n[n,d] · state_unitsize[n]  ≤ divest_max_period[n,d]   ∀ (n,d) ∈ ed_divest_period_set ∩ nd_divest_set

Caps the capacity coming online or retired in one period for entities with a per-period limit (e.g. invest_method = invest_period_total). An investment decided in o comes online in d = c(o) (construction lead time); without a lead time c(o) = o and the row is v_invest_p[p,d] · unitsize[p] ≤ invest_max_period[p,d] over ed_invest_period_set ∩ pd_invest_set, the form built when no decision of the solve has a lead time. carried_commission is the capacity earlier solves of a chain decided that comes online in d. The cap is raw capacity with unitsize on the LHS, as in the other invest caps, so the row dual is in objective units per unit of capacity.

Source: flextool/engine/model/core.py:3064 (_eq_invest_period_caps)

EQ. max{Invest,Divest}_entity_total[_path][_n]

Names: maxInvest_entity_total, maxInvest_entity_total_n, maxInvest_entity_total_path, maxInvest_entity_total_path_n, maxDivest_entity_total, maxDivest_entity_total_n, maxDivest_entity_total_path, maxDivest_entity_total_path_n

Σ_{d ∈ D(leaf)} v_invest_p[p,d] · unitsize[p]  ≤  max(0, invest_max_total[p] − prior_inv[p])
  ∀ p ∈ e_invest_total ∩ process_source_sink ∩ P(v_invest_p) (, leaf)
Σ_{d ∈ D(leaf)} v_divest_p[p,d] · unitsize[p]  ≤  max(0, divest_max_total[p] − prior_div[p])
  ∀ p ∈ e_divest_total ∩ process_source_sink ∩ P(v_divest_p) (, leaf)
  prior_inv[e] = p_entity_invested[e]  (capacity realized by all earlier solves of the
    chain, retired or not); prior_div[e] = p_entity_divested[e]  (capacity retired by all
    earlier solves of the chain, at realized non-branch periods only)
  _n: v_invest_n / v_divest_n with state_unitsize[n], n ∈ nodeState
  _path: one row per (entity, leaf), D(leaf) = periods of the leaf, only for (entity, leaf)
    pairs that have the variable

Caps the amount built (retired) over the chain; reinvestments count, retirements do not reduce it. Capacity in service is cumulative_max_capacity. Rows only for entities that have the variable in this solve (#43). If a user realizes a period in one solve and invests in it again in a later one, it is counted twice in prior_inv — same as the old (now-removed) max_d(p_entity_previously_invested_capacity) form; no fixture does this.

Source: flextool/engine/model/core.py:3130 (_eq_invest_total_caps)

EQ. maxRetired_existing_{p,n}

Names: maxRetired_existing_p, maxRetired_existing_n

Σ_{(p,d′,d) ∈ edd_divest_active} v_divest_p[p,d′] · unitsize[p]  ≤  pre_existing[p,d] − retired_carried[p,d]
  ∀ (p,d) with some (p,d′,d) ∈ edd_divest_active, p ∈ process_source_sink
Σ_{(n,d′,d) ∈ ndd_divest_active} v_divest_n[n,d′] · state_unitsize[n]  ≤  pre_existing[n,d] − retired_carried[n,d]
  ∀ (n,d) with some (n,d′,d) ∈ ndd_divest_active, n ∈ nodeState
  edd_divest_active = {(e,d′,d) : (e,d′) ∈ ed_divest_set, yr[d′] ≤ yr[d] < EOL[e]}, EOL[e] the end of
    life of the entity's pre-existing capacity (from the start of the run; none for reinvest_automatic)
  pre_existing = p_entity_pre_existing (the user's existing × unitsize, lifetime-gated: MW, MWh)
  retired_carried = p_entity_retired_carried (retirements of earlier solves of a chain still counting in d)

Only capacity that existed before the run can be retired: the capacity retired so far and still counting in d stays within the pre-existing capacity of d (a declining existing map, or an entity that also invests, cannot retire more than was there). A retirement option exists only where pre_existing > 0.

Source: flextool/engine/model/core.py:3195 (_eq_retired_existing)

EQ. conversion_indirect

Σ_{s ∈ in(p)} v_flow[p,s,p,d,t] · unitsize[p] · conv_in[p,s]
  + Σ_s Σ_{(t_s,t,δ) ∈ delay} v_flow[p,s,p,d,t_s] · unitsize[p] · delay_weight[p,δ] · conv_in[p,s]
  = Σ_{k ∈ out(p)} v_flow[p,p,k,d,t] · unitsize[p] · slope[p,d,t] · conv_out[p,k]
    + v_online[p,d,t] · section[p,d,t] · unitsize[p]
  ∀ (p,d,t) ∈ process_indirect_dt
  section[p,d,t] = min_load·(1/eta_min − 1/eta) / (1 − min_load)
  slope[p,d,t] = 1/eta − section[p,d,t]   (= (1/eta − min_load/eta_min) / (1 − min_load))
    for min_load_efficiency units (eta = efficiency, default 1; min_load = 0 where not authored;
    eta_min = efficiency_at_min_load, else efficiency); slope = 1/eta and no section for the other units
    (indirect units never fold conv_in/conv_out into slope/section themselves — see conv_in/conv_out below)

Energy balance of an indirect (multi-flow) unit: weighted input equals weighted output times the slope. conv_in / conv_out = source / sink conversion_flow_coeff (default 1); unequal sink coefficients express e.g. the heat/electricity trade-off of an extraction CHP. The first input sum covers undelayed units; delayed units take their inputs at the earlier source-side steps t_s (dtt__delay_duration, _delay.delayed_input_expr), t being the sink-side time. Section term (indirect min_load_efficiency units, linear and integer v_online): the input of an indirect unit is its own v_flow, so this is the only place the section fuel enters; node balance, commodity cost, CO2 and the reported inputs all read that v_flow. It carries no conversion_flow_coeff; for a delayed unit it sits on the output side at time t. Rows without a v_online column lack the term.

Source: flextool/engine/model/core.py:3251 (_eq_conversion)

EQ. minInputShare

conv[p,s] · v_flow[p,s,p,d,t]  ≥  share_min[p,s] · Σ_{s′ ∈ in(p)} conv[p,s′] · v_flow[p,s′,p,d,t]
  ∀ (p,s) with share_min[p,s] > 0, p ∈ process_indirect with ≥ 2 inputs, (p,d,t) ∈ process_indirect_dt

While the unit runs, input s supplies at least the share input_share_min of its current input energy (conv = source conversion_flow_coeff, default 1). The row is homogeneous: with no input it reads 0 ≥ 0, so it needs no online variable. unitsize cancels (v_flow is per unitsize on every input of p).

Source: flextool/engine/model/core.py:3314 (_eq_min_input_share)

EQ. co2_max_period

Σ_{(p,s,k,c,g) ∈ flow_from_co2_capped, t}
    (v_flow[p,s,k,d,t]·unitsize[p]·slope[p,d,t] + v_online[p,d,t]·section[p,d,t]·unitsize[p])
    · co2_content[c]·w[d,t]
  + Σ_{(p,s,k,c,g) ∈ flow_from_co2_capped_noEff, t} v_flow[p,s,k,d,t]·unitsize[p]·co2_content[c]·w[d,t]
  − Σ_{(p,s,k,c,g) ∈ flow_to_co2_capped, t} v_flow[p,s,k,d,t]·unitsize[p]·co2_content[c]·w[d,t]
  + Σ_{(p,n,c,g) ∈ startup_from_co2_capped, t} v_startup[p,d,t]·startup_energy[p,n,d]·unitsize[p]
      · co2_content[c]·w[d,t]/step_duration[d,t]   (start-up energy: an event, no step_duration)
  ≤ co2_max_period[g,d] · f_d_k[d]  −  co2_cum_realized_tonnes[g,α(d)] / period_share[d]
  ∀ (g,d) ∈ group_d_co2_capped;   w = step_duration · timestep_weight / period_share
  f_d_k[d] = (realized hours of α(d) from earlier solves + this solve's hours of d) / complete hours of α(d)
  α(d) = the period label of a forecast branch period d, d itself otherwise; period_share, f_d_k and the
  realized carry of a forecast come from α(d) unless it has its own row: every forecast continues from
  its period's realized history (the realization solve's carry)

Caps the annualised CO2 emissions of a group in each period (tonnes per year): dividing by period_share, the LHS is the emissions of the solve's own steps of d annualised over their hours. The RHS (_co2_period_cap_with_carry) is this period's remaining budget as of this roll: f_d_k[d] (1 outside rolling / chained solves) is the cumulative share of d's hours reached by the end of this roll's horizon, and the realized carrier (0 on the first solve of a chain) is what earlier rolls / solves already emitted for this exact period, net of removal credits (#19) — never negative by construction (#38, Q22; see the helper's own docstring). Emissions come from the fuel that processes draw from the group's nodes when it carries a CO2-emitting commodity c, measured as in co2_price_eff (_eff_fuel_input): on eff processes the output flow times slope plus, for min_load_efficiency units (linear and integer v_online), the section fuel of the units online. noEff processes take neither slope nor section (applying them would over-count). The removal term (#19, Q14) subtracts flows into the same group's node(s) — e.g. DAC/BECCS — unsplit, no slope, same measure as flow_to_co2_priced; it only loosens the cap, never tightens it.

Source: flextool/engine/model/core.py:3513 (_eq_co2_max_period)

EQ. co2_max_total[_path]

Names: co2_max_total, co2_max_total_path

Σ_{(p,s,k,c,g) ∈ flow_from_co2_capped_total, d∈D(leaf),t}
    (v_flow[p,s,k,d,t]·unitsize[p]·slope[p,d,t] + v_online[p,d,t]·section[p,d,t]·unitsize[p])
    · co2_content[c]·w_y[d,t]
  + Σ_{(p,s,k,c,g) ∈ flow_from_co2_capped_total_noEff, d∈D(leaf),t}
    v_flow[p,s,k,d,t]·unitsize[p]·co2_content[c]·w_y[d,t]
  − Σ_{(p,s,k,c,g) ∈ flow_to_co2_capped_total, d∈D(leaf),t} v_flow[p,s,k,d,t]·unitsize[p]·co2_content[c]·w_y[d,t]
  + Σ_{(p,n,c,g) ∈ startup_from_co2_capped_total, d∈D(leaf),t}
    v_startup[p,d,t]·startup_energy[p,n,d]·unitsize[p]·co2_content[c]·w_y[d,t]/step_duration[d,t]
  ≤ co2_max_total[g] · horizon_fraction_reached  −  Σ_d R_rep[g,d]
  ∀ g ∈ group_co2_max_total (, leaf);   w_y = step_duration · timestep_weight / period_share · years_represented[d]
  R_rep[g,d] = realized tonnes of d from earlier rolls / solves · years_represented[d] / period_share[d],
  with the share and years of the solve that realized them (the w_y its own row gave them)

Caps the CO2 emissions of a group over the horizon the solve represents (tonnes): each period's annualised emissions (as in co2_max_period, section fuel included) times the years the period represents, summed over the periods — the measure of the model CO2 output. years_represented is not discounted. The RHS (_co2_total_cap_with_carry) is the remaining budget as of this roll: without it, each solve of a chain and each roll of a rolling solve got the FULL cap again for only its own periods / window, over-allowing roughly by the number of rolls / chained solves (#38) — fixed the same way the commodity ladder's cumulative tier cap was (#89/#143): horizon_fraction_reached scales the fixed whole-horizon budget down to what's allocated so far, and the realized carrier (0 on the first solve of a chain) — net of removal credits (#19), weighted like the LHS (R_rep, not physical tonnes: a realized step counts years_represented / period_share times its tonnes, as it did in the row of the solve that realized it) — is subtracted; never negative by construction. The removal term (#19, Q14) mirrors co2_max_period's: flows into the same group's node(s), unsplit, no slope; it only loosens the cap. _path (Q16, #52): under recourse every forecast branch got its own copy of the whole-horizon budget summed into ONE row, letting N branches spend N× the cap between them (the stochastic fan is parallel futures, not N consecutive years). One row per (g, leaf) fixes it: the LHS sums only the leaf's own periods (shared pre-reveal trunk periods belong to every leaf, same as the invest/divest _path families); the RHS is the SAME remaining whole-horizon budget repeated per leaf (horizon_fraction_reached and the realized carry are chain-wide, not per-branch quantities — the carry is kept exactly as _co2_total_cap_with_carry computes it, same as the entity/group total-cap _path families reuse their own bare cap per leaf).

Source: flextool/engine/model/core.py:3654 (_eq_co2_max_total)

EQ. process_constraint_{equal,less_than,greater_than}[_period]

Names: process_constraint_equal, process_constraint_equal_period, process_constraint_less_than, process_constraint_less_than_period, process_constraint_greater_than, process_constraint_greater_than_period

Σ_{(p,s,k) ∈ flow_constraint[cn]} v_flow[p,s,k,d,t]·unitsize[p]·flow_coef[p,s,k,cn]
  + Σ_n v_state[n,d,t]·state_unitsize[n]·node_state_coef[n,cn]
  + Σ_e inv[e,d]·invested_coef[e,cn]
  + Σ_e con[e,d]·in_construction_coef[e,cn]
  + Σ_e v_divest[e,d]·us[e]·retired_coef[e,cn]
  + Σ_e pre[e,d]·prebuilt_coef[e,cn]
  + Σ_e cap[e,d]·capacity_coef[e,cn]
  + Σ_{p ∈ process_online} v_online[p,d,t]·online_coef[p,cn]
  = / ≤ / ≥  constant[cn] (or constant[cn,d] for a period map)
  ∀ (cn,d,t) ∈ cdt_{eq,le,ge} with a flow, state or online coefficient   (process_constraint_*)
  ∀ (cn,d) of the other constraints of cdt_{eq,le,ge}             (process_constraint_*_period)
where, for e a process p (us = unitsize, existing = capacity = existing_count·unitsize) or a storage node n
(us = state_unitsize, existing = capacity, the storage capacity), MW (MWh):
  inv[e,d] = Σ_{(e,o,d) ∈ edd_invest_commission} v_invest[e,o]·us[e] + carried_commission[e,d]
  con[e,d] = Σ_{(e,o,d) ∈ edd_invest_in_construction} v_invest[e,o]·us[e] + carried_in_construction[e,d]
  pre[e,d] = existing[e,d] − carried_commission[e,d] + Σ_{(e,d′,d) ∈ edd_invest_lookback} v_invest[e,d′]·us[e]
                           − Σ_{(e,d′,d) ∈ edd/ndd_divest_active, d′ ≠ d} v_divest[e,d′]·us[e]
  cap[e,d] = existing[e,d] + Σ_{(e,d′,d) ∈ edd_invest_set} v_invest[e,d′]·us[e]
                           − Σ_{(e,d′,d) ∈ edd/ndd_divest_active} v_divest[e,d′]·us[e]

A linear constraint authored by the user over flows (MW), storage states (MWh) and capacities. The invested term is the capacity that comes online in d (an investment decided in o with a construction lead time comes online in c(o); without one c(o) = o and inv[e,d] = v_invest[e,d]·us[e]); the in-construction term is the capacity decided but not yet online (zero without a lead time); the retired term counts the retirements of d; pre is the capacity in service at the start of d (existing plus earlier investments still alive, minus earlier retirements, also those of the same solve: a single solve and a chain of solves give the same row); cap is the capacity in service in d, the measure of maxFlow and the cumulative-capacity rows. cap = pre + inv − retired, with or without a lead time. carried_* are the investments earlier solves of a chain decided with a lead time (constants; existing holds them from their start of operation, so pre subtracts the part coming online in d). The online term counts the units online (of unitsize) in each step; integer with binary startup, continuous with linear. v_online is the variable of the process's UC class (v_online_linear / v_online_integer). Terms without data are absent. The existing-capacity terms are constants and enter as Param terms. A constraint without flow, state and online terms is the same in every step of a period, so it gets one row per period (the _period family) instead of one per step. The constraint axis is cn (c is the commodity axis). Dims: flow, state and online terms (cn, d, t); every capacity term (cn, d), broadcast over t on the per-step rows. A row whose terms all vanish in the solve (its entities have no flow, state, investment or retirement there) is left out: a configuration error when the remaining existing-capacity constant sense constant cannot hold, else a warning naming the constraint. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Row rule (coarse steps): a per-step constraint whose flow and online terms are all on coarse processes and whose state terms are on coarse nodes gets one row per coarse step, process_constraint_*_coarse = the row's Δt-weighted mean over the step (_coarse_flows.add_cstr); its coefficients are constant, so it is the per-step row itself on one flow per step (the units online of a coarse process are one level per coarse step). A constraint with a fine flow, online or state term keeps its rows per solve step and reads the coarse flows and units online at their constant value.

Source: flextool/engine/model/core.py:3744 (_eq_user_constraints)

EQ. process_constraint_{equal,less_than,greater_than}_period_total

Names: process_constraint_equal_period_total, process_constraint_less_than_period_total, process_constraint_greater_than_period_total

Σ_t w[d,t]·( Σ_{(p,s,k) ∈ flow_constraint[cn]} v_flow[p,s,k,d,t]·unitsize[p]·flow_coef[p,s,k,cn]
             + Σ_{p ∈ process_online} v_online[p,d,t]·online_coef[p,cn] )
  + H[d]·( Σ_e inv[e,d]·invested_coef[e,cn] + Σ_e con[e,d]·in_construction_coef[e,cn]
           + Σ_e v_divest[e,d]·us[e]·retired_coef[e,cn] + Σ_e pre[e,d]·prebuilt_coef[e,cn]
           + Σ_e cap[e,d]·capacity_coef[e,cn] )
  = / ≤ / ≥  constant[cn] (or constant[cn,d] for a period map)
  ∀ (cn,d) ∈ cdt_{eq,le,ge}, cn ∈ constraint_period_total, window_fraction[d] = 1
w[d,t] = step_duration[d,t]·timestep_weight[d,t] / period_share[d];   H[d] = Σ_t w[d,t]
inv, con, pre, cap, us and the existing / carried constants as in process_constraint_* (_eq_user_constraints).

One row per period over the period's time-integrated, annualised terms (per year: flows in MWh, units online in unit-hours, the constant in the constraint's unit per year), the weighting of co2_max_period. H is the number of hours the period's modelled steps stand for in a year (8760 for a full year, also with representative periods, whose weights are normalised); computed, not 8760, so a roll window or a varying step duration gets its own. Every capacity term, and the existing / carried capacity constants, are multiplied by H: each term is its per-step quantity integrated over the period, so a coefficient keeps its per-step meaning (flow − 0.3·capacity ≤ 0 caps the annual capacity factor at 0.3). State terms are rejected by the reader (a state summed over steps is not an energy). A row needs the whole period in the solve: a period that the solve holds in part (window_fraction \< 1, the look-ahead tail of a roll) has no row; a rolling solve whose rolls do not each realize whole periods has none at all (user_constraint_period_total_off, the run's pre-solve check names the constraints and the solve). Stochastic branch periods are their own d (no probability weight: each forecast meets the limit). Coarse flows and units online enter through their alias on the fine steps (exact: Σ_t v·Δt). Dims: flow and online terms (cn, d, t) summed over t; capacity terms and constants (cn, d).

Source: flextool/engine/model/core.py:4030 (_eq_user_constraints_period_total)

EQ. maxState

v_state[n,d,t] + Σ_{(n,d′,d) ∈ ndd_divest_active} v_divest_n[n,d′]
  − Σ_{(n,d′,d) ∈ edd_invest_set} v_invest_n[n,d′]  ≤  state_upper[n,d]
  ∀ (n,d,t) ∈ nodeState_dt

Caps the storage state by the built storage capacity. state_upper = capacity / state_unitsize, so every term is in state units. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/core.py:4255 (_eq_max_state)

EQ. profile_state_{upper_limit,lower_limit,fixed}

Names: profile_state_upper_limit, profile_state_lower_limit, profile_state_fixed

v_state[n,d,t] − profile[f,d,t]·av[n,d,t]·Σ_{(n,d′,d) ∈ edd_invest_set} v_invest_n[n,d′]
  + profile[f,d,t]·av[n,d,t]·Σ_{(n,d′,d) ∈ ndd_divest_active} v_divest_n[n,d′]
  ≤ / ≥ / =  profile[f,d,t] · av[n,d,t] · capacity[n,d] / state_unitsize[n]
  ∀ (n,f) ∈ node_profile_{upper,lower,fixed}, (d,t) ∈ dt

Bounds the storage state by a profile times the built storage capacity, in state units. av = node availability, 1 for nodes without availability data. The invest/divest terms are restricted to the (n,f) pairs of the method's set. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/core.py:4287 (_eq_state_profiles)

EQ. storage_state_start_binding

v_state[n,d,t] − state_start[n]·Σ_{(n,d′,d) ∈ edd_invest_set} v_invest_n[n,d′]
  + state_start[n]·Σ_{(n,d′,d) ∈ ndd_divest_active} v_divest_n[n,d′]
  = state_start[n] · capacity[n,d] / state_unitsize[n]
  ∀ (n,d,t) ∈ nodeState_first_dt, n ∈ storage_fix_start, n ∉ carried,
    n ∉ bind_forward_only ∪ bind_within_timeblock ∪ bind_within_solve ∪ bind_within_period ∪ nodeStateBlock

Pins the state at the first step to the share state_start of the built storage capacity (state units). Only where no state of the node was realised before this solve (carried: the nodes of roll_continue_state), i.e. the first solve of the model and of each nested level. The nodes left are the representative-period (blended weights) nodes, whose first step starts from v_state_rp_start. bind_forward_only nodes get their start through nodeBalance_eq (_fix_start_terms); the cyclic nodes (bind_within_timeblock, bind_within_solve, bind_within_period, bind_intraperiod_blocks) are pinned by storage_state_start_binding_cyclic_period. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/core.py:4343 (_eq_storage_start_binding)

EQ. storage_state_start_binding_cyclic_period

v_state[n,d′,t′] − state_start[n]·Σ_{(n,d″,d′) ∈ edd_invest_set} v_invest_n[n,d″]
  + state_start[n]·Σ_{(n,d″,d′) ∈ ndd_divest_active} v_divest_n[n,d″]
  = state_start[n] · capacity[n,d′] / state_unitsize[n]
  ∀ n ∈ storage_fix_start ∩ (bind_within_timeblock ∪ bind_within_solve ∪ bind_within_period
    ∪ looping nodeStateBlock), n ∉ carried, (n,d,t) ∈ nodeState_first_dt, (d′,t′) = the step (d,t) starts
    from (the node's wrap), (n,d′,t′) ∉ the steps node_balance_fix_quantity_eq_lower fixes

The first step of a cyclic node starts from its wrap target, so the start state is pinned there, to the share state_start of the built storage capacity (state units): the last step of the first timeblock (bind_within_timeblock; in a roll, of the timeblock's part in the roll), of the solve (bind_within_solve) or of the first period (bind_within_period, bind_intraperiod_blocks). Only where no state of the node was realised before this solve (the first solve of the model and of each nested level); a later solve neither pins nor carries a cyclic node (it cycles within the solve). Where an upper-level solve fixes the state at the wrap target the upper value wins (the two equalities would sit on one variable). A bind_intraperiod_blocks node that chains forward (fix_start_end, or a roll that holds its first period only in part) has its start in nodeBalanceBlock_eq (_fix_start_terms); fix_start_end nodes of the other cyclic methods are forward-only in the solve (_run_model._fix_start_end_chains_forward). A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/core.py:4439 (_eq_storage_start_binding_cyclic)

EQ. storage_within_solve_path_end

v_state[n,d,t_last(d)] − v_state[n,d*,t_last(d*)] = 0
  ∀ n ∈ bind_within_solve, d ∈ period_last, d ≠ d*, (n,d,t_last(d)) and (n,d*,t_last(d*)) ∈ nodeState_last_dt,
    unless both ends are already set (each is a step storage_state_start_binding_cyclic_period pins or
    node_balance_fix_quantity_eq_lower fixes)
  d* = the last period the solve's first steps wrap to (the state-change lag of bind_within_solve,
    _self_discharge.state_links): the last period key of the solve, the trunk's wrap target
bind_within_solve closes one loop per path of a stochastic solve.  The first step of the solve continues from
the last step of d* only, so this row ends every other path (period_last: the solve's last period and its
forecast branch siblings) at the same level: every branch's end is the level the trunk starts from.  With a
fan in the solve's first period each root forecast's first step continues from the last step of its own path
(dtttdt_within_solve), so equal ends also give all the forecasts the same start.  A deterministic solve has
one last period and no rows.  A fix_start node keeps the rows where only one end is pinned (a fan after the
trunk period: the pin sits at d*'s end); with both ends pinned or fixed the row would only repeat (or
contradict) those values.

Source: flextool/engine/model/core.py:4507 (_eq_storage_within_solve_path_end)

EQ. storage_state_fan_start

v_state[n,d,t_first(d)] − v_state[n,d″,t_first(d″)] = 0
  ∀ n ∈ bind_forward_only, (n,d,t_first(d)) and (n,d″,t_first(d″)) ∈ nodeState_first_dt with no start term
    (n ∉ storage_fix_start ∪ carried), d = the first root (sorted), d″ ∈ the other roots, one row
    per group of roots that non_anticipativity_storage_state_first does not already tie to d's group
  roots = the periods of nodeState_first_dt (built from period_first_of_solve): the forecast branch periods
    of a fan in the solve's first period; one root without a fan: no rows

The first step of a bind_forward_only node has no state change (dtttdt_forward_only): without fix_start or a carried state it is free, so each forecast of a fan in the first period could pick its own start level from what it later sees. The fan has only forecasts (no realized member), so this row is what makes their start the same. A fix_start or carried node starts every root from the same value already (_fix_start_terms, the roll carry). For a node of an include_stochastics group, non_anticipativity_storage_state_first may tie the first step of a root to its reference forecast already (a tie window shorter than the root period): those pairs are left out, read from that row's domain.

Source: flextool/engine/model/core.py:4575 (_eq_storage_state_fan_start)

EQ. storage_state_end_binding

v_state[n,d,t] − state_end[n]·Σ_{(n,d′,d) ∈ edd_invest_set} v_invest_n[n,d′]
  + state_end[n]·Σ_{(n,d′,d) ∈ ndd_divest_active} v_divest_n[n,d′]
  = state_end[n] · capacity[n,d] / state_unitsize[n]
  ∀ n ∈ storage_fix_end, (n,d,t) ∈ nodeState_last_dt, d ∈ period_last, storage_solve_at_model_end,
    (n,d,t) ∉ the steps node_balance_fix_quantity_eq_lower / node_storage_usage_fix_le fix

Pins the state at the end of the last step of the entire model to the share state_end of the built storage capacity (state units), in every sub-solve whose horizon holds that step (storage_solve_at_model_end, _carry_routing.reaches_model_end: e.g. both an investment solve and the last dispatch roll; each nested level's solve that holds it; a roll whose look-ahead runs to it); each last period (each terminal branch of a stochastic solve) ends there. storage_fix_end = fix_end ∪ fix_start_end. Where an upper-level solve fixes the quantity or caps the usage at that step the upper value wins; a nested fix_price steers the end through the objective and keeps the pin. A fix_start_end node chains forward whatever its storage_binding_method (_run_model._fix_start_end_chains_forward, _self_discharge.block_forward_nodes), so its start is in the first step's balance and never on this state; a cyclic node is pinned here by fix_end only. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/core.py:4660 (_eq_storage_end_binding)

EQ. storage_state_solve_horizon_reference_value

v_state[n,d,t] − reference_value[n,d,t]·Σ_{(n,d′,d) ∈ edd_invest_set} v_invest_n[n,d′]
  + reference_value[n,d,t]·Σ_{(n,d′,d) ∈ ndd_divest_active} v_divest_n[n,d′]
  = reference_value[n,d,t] · capacity[n,d] / state_unitsize[n]
  ∀ n ∈ storage_use_reference_value, (n,d,t) ∈ horizon_end,
    (n ∈ nodeStateBlock ⇒ d ∈ period_block_chain), (n,d,t) ∉ the steps an upper-level solve fixes
  horizon_end = nodeState_last_dt ∩ period_last  ∪  block_tail_end

Pins the state at the end of the solve horizon to the reference share of the built storage capacity (state units), as maxState and the start pins count it. period_last is the solve's last period and its stochastic branch siblings only: an earlier period of a multi-period solve is not a horizon end. Nodes with a competing end condition are not in storage_use_reference_value; a bind_intraperiod_blocks node is, in a rolling solve, and is pinned where the roll holds the period only in part (there its blocks chain forward instead of looping): at the roll end and at block_tail_end, the last step of a chained part-period tail that a period held whole follows (nothing continues from it, _self_discharge.block_tail_ends). Where an upper-level solve fixes the node at the step (nested fix_quantity, fix_usage, fix_price) the upper value overrides the reference value: two equalities on one state (fix_quantity), a usage cap the pin could make infeasible (fix_usage), or a price meant to steer that end (fix_price). reference_value on the pinned rows only: as a coefficient of the invest / divest sums it would otherwise span every (n, d_invest, d, t) of the solve. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/core.py:4743 (_eq_storage_reference_value)

EQ. profile_flow_{upper_limit,lower_limit,fixed}

Names: profile_flow_upper_limit, profile_flow_lower_limit, profile_flow_fixed

v_flow[p,s,k,d,t] ± (Σ_r v_reserve[p,r,k,d,t] + Σ_r v_reserve[p,r,s,d,t] · φ[p,s,k,d,t])
  − profile[f,d,t]·av[p,d,t]·Σ_{(p,d′,d) ∈ edd_invest_set} v_invest_p[p,d′]
  + profile[f,d,t]·av[p,d,t]·Σ_{(p,d′,d) ∈ edd_divest_active} v_divest_p[p,d′]
  ≤ / ≥ / =  profile[f,d,t] · av[p,d,t] · existing_count[p,d]
  ∀ (p,s,k,f) ∈ process_profile_{upper,lower,fixed}, (d,t) ∈ dt

Bounds a flow by a profile times the built capacity, in units of unitsize. av = availability, 1 for processes without availability data. The upper limit adds the reserve that raises the arc's flow (up at the sink; down at a unit's source), the lower limit subtracts the reserve that lowers it (down at the sink; up at a unit's source); a source-side reserve is converted to the arc's flow by φ (1 where v_flow is the input, 1/slope on a direct unit's input→output arc). The fixed profile has no reserve term. The invest/divest terms are restricted to the (p,f) pairs of the method's set. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/core.py:4798 (_eq_profiles)

OBJ. balance_slack_penalty

Σ_{(n,d,t)} vq_state_up[n,d,t] · penalty_up[n,d,t] · ncs[n,d] · w_op[d,t]
  + Σ_{(n,d,t)} vq_state_down[n,d,t] · penalty_down[n,d,t] · ncs[n,d] · w_op[d,t]
  ∀ (n,d,t) with balance slacks (nodeBalance ∪ nodeBalancePeriod, penalty_method ≠ off)
  penalty is defined on every slack cell: the node's authored value, else the schema default
  ncs = node_capacity_for_scaling, factor 1 when the parameter is absent;   currency

Prices the energy a node balance cannot supply (up) or absorb (down); penalty in currency/MWh. The slacks enter the balance without ncs, so with node_capacity_for_scaling ≠ 1 the effective penalty per MWh is penalty · ncs (unreachable today; known inconsistency).

Source: flextool/engine/model/core.py:4900 (_obj_balance_slack)

OBJ. commodity_cost_eff

Σ_{(p,s,k,c) ∈ flow_from_commodity_eff, (d,t)}
    (v_flow[p,s,k,d,t]·unitsize[p]·slope[p,d,t] + v_online[p,d,t]·section[p,d,t]·unitsize[p])
    · commodity_price[c,d,t] · w_op[d,t]

Cost of the commodity c bought from node s by processes with an efficiency: the input is the output flow times slope, plus the section input per online unit (min_load_efficiency units only, linear and integer online alike). Price in currency/MWh.

Source: flextool/engine/model/core.py:4940 (_obj_commodity)

OBJ. commodity_cost_noEff

Σ_{(p,s,k,c) ∈ flow_from_commodity_noEff, (d,t)} v_flow[p,s,k,d,t]·unitsize[p] · commodity_price[c,d,t] · w_op[d,t]

Cost of the commodity bought by processes without an efficiency: the input equals the flow.

Source: flextool/engine/model/core.py:4951 (_obj_commodity)

OBJ. commodity_cost_startup

Σ_{(p,n,c) ∈ startup_from_commodity, (d,t)}
    v_startup[p,d,t]·startup_energy[p,n,d]·unitsize[p] · commodity_price[c,d,t] · w_ev[d,t]
  w_ev = timestep_weight · inflation_op / period_share · branch_weight   (as startup_cost)

Cost of the start-up energy that units draw from the commodity node n of commodity c (startup_input_method startup_and_input / startup_only; core._startup_draw). The draw is MWh per start-up event, so the weight has no step_duration. Commodities with a price ladder are priced by _commodity_ladder.

Source: flextool/engine/model/core.py:4960 (_obj_commodity)

OBJ. commodity_sales

− Σ_{(p,s,k,c) ∈ flow_to_commodity, (d,t)} v_flow[p,s,k,d,t]·unitsize[p] · commodity_price[c,d,t] · w_op[d,t]

Revenue from flows into a commodity node k, sold at the commodity price (negative cost). Every process arc counts, with or without efficiency: the sink-side flow is the delivered energy.

Source: flextool/engine/model/core.py:4972 (_obj_commodity)

OBJ. co2_price_eff

Σ_{(p,s,k,c,g) ∈ flow_from_co2_priced, (d,t)}
    (v_flow[p,s,k,d,t]·unitsize[p]·slope[p,d,t] + v_online[p,d,t]·section[p,d,t]·unitsize[p])
    · co2_content[c] · co2_price[g,d,t] · w_op[d,t]

Cost of the CO2 emitted when processes with an efficiency draw a CO2-bearing commodity c from a node of a CO2-priced group g; content in t/MWh, price in currency/t. The section term exists for min_load_efficiency units only.

Source: flextool/engine/model/core.py:4991 (_obj_co2_price)

OBJ. co2_removal_credit

− Σ_{(p,s,k,c,g) ∈ flow_to_co2_priced, (d,t)}
    v_flow[p,s,k,d,t]·unitsize[p] · co2_content[c] · co2_price[g,d,t] · w_op[d,t]

Credit for flows into the group's priced commodity node(s) — removal (e.g. DAC/BECCS modelled as a flow into a CO2-content commodity node of the priced group g). No slope or section term: the delivered flow is the physical quantity removed, not a fuel input (mirrors flow_to_commodity, the commodity-sale term). Ported from GMPL's "Receiving credits for removing CO2" (#19, Q14).

Source: flextool/engine/model/core.py:5003 (_obj_co2_price)

OBJ. co2_price_startup

Σ_{(p,n,c,g) ∈ startup_from_co2_priced, (d,t)}
    v_startup[p,d,t]·startup_energy[p,n,d]·unitsize[p] · co2_content[c] · co2_price[g,d,t] · w_ev[d,t]

Cost of the CO2 of the start-up energy drawn from a node of a CO2-priced group g that carries the CO2-bearing commodity c (core._startup_draw); an event, so the weight w_ev has no step_duration.

Source: flextool/engine/model/core.py:5016 (_obj_co2_price)

OBJ. co2_price_noEff

Σ_{(p,s,k,c,g) ∈ flow_from_co2_priced_noEff, (d,t)}
    v_flow[p,s,k,d,t]·unitsize[p] · co2_content[c] · co2_price[g,d,t] · w_op[d,t]

As co2_price_eff for processes without an efficiency: the input equals the flow, no slope and no section term.

Source: flextool/engine/model/core.py:5035 (_obj_co2_price_noEff)

OBJ. variable_cost_noEff

Σ_{(p,s,k,d,t) ∈ pssdt_varCost_noEff} v_flow[p,s,k,d,t]·unitsize[p] · varCost[p,s,k,d,t] · w_op[d,t]
pssdt_varCost_noEff = {(p,s,k,d,t) : (p,s,k) ∈ process_source_sink_noEff, varCost[p,s,k,d,t] ≠ 0}
O&M cost of the flows of processes without an efficiency; varCost (currency/MWh) is the arc's
combined process, source-side and sink-side other_operational_cost (every authored shape: constant,
period, time, period × time, stochastic time).

Source: flextool/engine/model/core.py:5057 (_obj_variable_cost)

OBJ. variable_cost_eff_source

Σ_{(p,s,k,d,t) ∈ pssdt_varCost_eff_unit_source}
    (v_flow[p,s,k,d,t]·unitsize[p]·slope[p,d,t] + v_online[p,d,t]·section[p,d,t]·unitsize[p])
    · other_operational_cost_source[p,s,d,t] · w_op[d,t]
pssdt_varCost_eff_unit_source = {(p,s,k) ∈ process_source_sink_eff} × dt where
    other_operational_cost_source[p,s,d,t] ≠ 0 (every authored shape: constant, period, time,
    period × time, stochastic time).
O&M cost on the input side of units with an efficiency, priced on the input energy (flow times
slope, plus the section input of min_load_efficiency units).  Added like the other O&M terms
(GMPL subtracted it).  The sink/source coefficient ratio of the input is not applied (refactor
bug 10).

Source: flextool/engine/model/core.py:5070 (_obj_variable_cost)

OBJ. variable_cost_eff_sink

Σ_{(p,s,k,d,t) ∈ pssdt_varCost_eff_unit_sink} v_flow[p,s,k,d,t]·unitsize[p] · other_operational_cost_sink[p,k,d,t] · w_op[d,t]
pssdt_varCost_eff_unit_sink = {(p,s,k) ∈ process_source_sink_eff} × dt where
    other_operational_cost_sink[p,k,d,t] ≠ 0 (every authored shape, as above).
O&M cost on the output side of units with an efficiency, priced on the output flow.

Source: flextool/engine/model/core.py:5104 (_obj_variable_cost)

OBJ. variable_cost_eff_connection

Σ_{(p,s,k,d,t) ∈ pssdt_varCost_eff_connection} v_flow[p,s,k,d,t]·unitsize[p] · other_operational_cost[p,d,t] · w_op[d,t]
pssdt_varCost_eff_connection = {(p,s,k) ∈ process_source_sink_eff} × dt where
    other_operational_cost[p,d,t] ≠ 0 (constant, period, time or period × time).
O&M cost of connections with an efficiency, priced on the flow of each direction.

Source: flextool/engine/model/core.py:5116 (_obj_variable_cost)

OBJ. startup_cost

Σ_{(p,d,t) ∈ pdt_online_{linear,integer}} v_startup_{linear,integer}[p,d,t] · startup_cost[p,d] · unitsize[p] · w_ev[d,t]
  w_ev = timestep_weight · inflation_op / period_share · branch_weight

Cost of starting up units, startup_cost in currency/MW started. A startup is an event, so the weight has no step_duration.

Source: flextool/engine/model/core.py:5137 (_obj_startup_cost)

OBJ. ramp_cost

Σ_{(p,s,k,d,t) ∈ R} (v_ramp_up + v_ramp_down)[p,s,k,d,t] · unitsize[p] · rc[p,s,k] · w_ev[d,t]
  rc[p,s,k] = p_ramp_cost_arc = Σ_{side ∈ {source s, sink k}, ramp_method[p,side] ∈ {ramp_cost, both}}
              ramp_cost[p,side]   (> 0; a direct unit's one arc takes both sides)
  w_ev = timestep_weight · inflation_op / period_share · branch_weight   (as startup_cost)

Cost of changing a flow, ramp_cost in currency per MW of change of the step-average flow, up and down alike. A change is an event, as a start-up, so the weight has no step_duration: ramping 0 → X → 0 costs 2·rc·X whatever the step lengths. A start-up from 0 pays both its startup cost and this cost.

Source: flextool/engine/model/core.py:5161 (_obj_ramp_cost)

OBJ. storage_reference_price

− Σ_{(n,d,t) ∈ horizon_end, (n,d) ∈ reference_price}
    v_state[n,d,t]·state_unitsize[n] · reference_price[n,d] · w_ev[d,t]
  horizon_end = nodeState_last_dt ∩ period_last  ∪  block_tail_end   (as storage_state_solve_horizon_
                reference_value: the solve's last period and its branch siblings, and in a rolling solve the
                end of a chained bind_intraperiod_blocks tail that a looping period follows)
  w_ev = timestep_weight · inflation_op / period_share · branch_weight

Values the energy left in storage at the end of the solve horizon (revenue, hence the minus), reference_price in currency/MWh. The price comes from use_reference_price nodes or from the storage-price handoff of a previous solve. An end-of-horizon valuation is a point event, so the weight has no step_duration. Rows with a zero price are kept.

Source: flextool/engine/model/core.py:5191 (_obj_storage_reference_price)

OBJ. invest_cost_p

Σ_{(p,d) ∈ pd_invest_set} v_invest_p[p,d]·unitsize[p] · annual_discounted[p,d] · bw[d]
  + Σ_{(p,d) ∈ pd_invest_set} v_invest_p[p,d]·unitsize[p] · lifetime_fixed_cost[p,d] · bw[d]

Cost of process capacity decided in period d, currency/MW: the investment annuity summed over the discounted years it operates within the horizon, and the inflation-adjusted fixed cost over the same years of its lifetime. With a construction lead time the capacity operates from its start of operation c(d): both windows start there, and the invest cost, interest rate, lifetime and fixed cost are those of c(d) (invest_cost is an overnight cost); without one c(d) = d. No w_op: both factors are already per period.

Source: flextool/engine/model/core.py:5236 (_obj_invest_divest)

OBJ. divest_cost_p

− Σ_{(p,d) ∈ pd_divest_set} v_divest_p[p,d]·unitsize[p] · lifetime_fixed_cost_divest[p,d] · bw[d]
  − Σ_{(p,d) ∈ pd_divest_set} v_divest_p[p,d]·unitsize[p] · annual_divest_discounted[p,d] · bw[d]
  lifetime_fixed_cost_divest[p,d] = Σ_{d′ ∈ W(p,d)} fixed_cost[p,d′]·1000·inflation_op[d′]
  annual_divest_discounted[p,d] = ann(salvage_value[p,d], discount_rate[p,d], lifetime[p,d])
                                  · Σ_{d′ ∈ S(p,d)} inflation_inv[d′]
  W(p,d) = the solve's periods d′ with yr[d] ≤ yr[d′] < EOL[p] (the window of edd_divest_active: until the
    end of life of p's pre-existing capacity, the end of the horizon for reinvest_automatic)
  S(p,d) = the solve's periods d′ with yr[d] ≤ yr[d′] < yr[d] + lifetime[p,d] (raw lifetime)

Saving from process capacity retired in period d, currency/MW: the fixed cost of the existing capacity it no longer pays (each period's, with the factor the existing fixed cost has), and the salvage-value annuity over the discounted years of lifetime inside the horizon, the basis of an investment's annuity (so retiring existing capacity and building identical new capacity gains nothing).

Source: flextool/engine/model/core.py:5254 (_obj_invest_divest)

OBJ. invest_cost_n

Σ_{(n,d) ∈ nd_invest_set} v_invest_n[n,d]·state_unitsize[n] · annual_discounted[n,d] · bw[d]
  + Σ_{(n,d) ∈ nd_invest_set} v_invest_n[n,d]·state_unitsize[n] · lifetime_fixed_cost[n,d] · bw[d]

As invest_cost_p for storage capacity, currency/MWh of storage.

Source: flextool/engine/model/core.py:5276 (_obj_invest_divest)

OBJ. divest_cost_n

− Σ_{(n,d) ∈ nd_divest_set} v_divest_n[n,d]·state_unitsize[n] · lifetime_fixed_cost_divest[n,d] · bw[d]
  − Σ_{(n,d) ∈ nd_divest_set} v_divest_n[n,d]·state_unitsize[n] · annual_divest_discounted[n,d] · bw[d]

As divest_cost_p for storage capacity (W over ndd_divest_active's window).

Source: flextool/engine/model/core.py:5290 (_obj_invest_divest)

OBJ. existing_fixed_cost

Σ_{(e,d)} existing[e,d] · fixed_cost[e,d] · inflation_op[d] · bw[d]
  bw = pd_branch_weight, factor 1 when absent

Fixed cost of the capacity that exists in each period, currency (fixed_cost in currency/MW/year). A constant: it moves the objective value but not the solution. Added only with include_existing_fixed_cost; when pd_branch_weight is present, periods without a weight row drop out. Multiplied by scale_the_objective like every other objective term.

Source: flextool/engine/model/core.py:5314 (_obj_existing_fixed_cost)

DC power flow

DC power flow: linearised AC flow on the connections of a DC power-flow network.

VAR. v_angle

−π ≤ v_angle[n,d,t] ≤ π   ∀ n ∈ node_dc_power_flow, (d,t) ∈ dt

Voltage angle of node n, radians (π = 3.14159265). Variable bounds are scalar, so the reference nodes are pinned to 0 by dc_reference_angle_eq.

Source: flextool/engine/model/_dc_power_flow.py:184 (add_variables)

VAR. v_angle (coarse-angle nodes)

Names: v_angle

−π ≤ v_angle[n,d,b] ≤ π   ∀ n ∈ coarse-angle nodes, (d,b) the coarse steps of n (b = its first fine step)
  coarse-angle node = a node of a DC network component whose every line is a coarse process and whose
  nodes all have the same coarse steps (flex_data.coarse_angle_steps)

One angle per coarse step, held over its fine steps (a level: b.v_angle is indexed on every fine step, _coarse_flows.alias). Every node of a component with a DC line at the solve's step keeps an angle per solve step, so the fine line keeps its per-step physics.

Source: flextool/engine/model/_dc_power_flow.py:189 (add_variables)

EQ. dc_reference_angle_eq

v_angle[n,d,t] = 0   ∀ n ∈ node_reference_angle, (d,t) ∈ dt
  (a coarse-angle node: (d,b) its coarse steps, b = the step's first fine step)

Fixes the angle reference of each connected DC network.

Source: flextool/engine/model/_dc_power_flow.py:233 (_eq_reference_angle)

EQ. dc_flow_eq

(v_flow[p,s,k,d,t] − v_flow_back[p,s,k,d,t]) · unitsize[p]
  = susceptance[p] · (v_angle[s,d,t] − v_angle[k,d,t])
  ∀ (p,s,k) ∈ DC arcs (dc_arcs_frame), (d,t) ∈ dt

Linearised AC flow: the signed flow (MW) on a DC arc follows the angle difference of its end nodes. v_flow_back is absent when the model has no reverse-flow variable. The rows run over dt. Row rule (coarse steps): a coarse line whose both end nodes are coarse-angle nodes gets one row per coarse step, dc_flow_eq_coarse = the row's mean over the step (_coarse_flows.add_cstr; the susceptance is constant, so it is the row itself on one flow and two angles per step). Any other line keeps a row per solve step and reads a coarse flow at its constant value (a coarse line in a component with a fine DC line holds the angle difference of its per-step angles constant over the coarse step).

Source: flextool/engine/model/_dc_power_flow.py:258 (_eq_dc_flow)

Unit commitment

Unit commitment: units (and connections) online, startups, shutdowns and minimum up/down times.

VAR. v_online_linear / v_startup_linear / v_shutdown_linear

Names: v_online_linear, v_startup_linear, v_shutdown_linear

v_online_linear[p,d,t], v_startup_linear[p,d,t], v_shutdown_linear[p,d,t] ≥ 0
  ∀ (p,d,t) ∈ p_online_dt, p ∈ process_online_linear

Units online, and units started / shut down: the change of units online, online_change (count of unitsize), continuous.

Source: flextool/engine/model/_online.py:46 (add_variables)

VAR. v_online{sfx} / v_startup{sfx} / v_shutdown{sfx} (coarse processes)

Names: v_online_linear, v_online_integer, v_startup_linear, v_startup_integer, v_shutdown_linear, v_shutdown_integer

on[p,d,b], su[p,d,b], sd[p,d,b]   ∀ p ∈ processCoarse ∩ process_online, (d,b) the coarse steps of p
  (b = its first fine step b_first)

Units online once per coarse step, held over its fine steps (a level: b.v_online_* is indexed on every fine step, _coarse_flows.alias); the units started / shut down at the step's start (events: registered at b only, not aliased, so a sum over t counts each once and a fine row of an interior step has no event term).

Source: flextool/engine/model/_online.py:52 (add_variables)

VAR. v_online_integer / v_startup_integer / v_shutdown_integer

Names: v_online_integer, v_startup_integer, v_shutdown_integer

v_online_integer[p,d,t] ∈ ℤ≥0, v_startup_integer[p,d,t], v_shutdown_integer[p,d,t] ≥ 0
  ∀ (p,d,t) ∈ p_online_dt, p ∈ process_online_integer

As the linear family, but units online are integer; startups and shutdowns stay continuous. Continuous when the solve's reduce_unit_detail_method relaxes integers (d.relax_integrality; same names, rows and _integer class).

Source: flextool/engine/model/_online.py:64 (add_variables)

EQ. maxOnline_{linear,integer}

Names: maxOnline_linear, maxOnline_integer

v_online[p,d,t] − Σ v_invest_p[p,d′] + Σ v_divest_p[p,d′]  ≤ existing_count[p,d]
  ∀ (p,d,t) ∈ p_idx

Units online cannot exceed the units built. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/_online.py:141 (add_constraints)

EQ. online_change_{linear,integer}

Names: online_change_linear, online_change_integer

v_online[p,d,t] − v_online_prev[p,d,t] − v_startup[p,d,t] + v_shutdown[p,d,t]  =  0
  ∀ (p,d,t) ∈ p_idx;   v_online_prev as derived above (three pieces)

The change in units online is the units started minus the units shut down (Backbone's q_startshut). With maxStartup / maxShutdown below, a startup without a unit coming online, or a shutdown without one going offline, is impossible; a fleet (unitsize \< built) can still start and stop units in the same step (both cost nothing extra on the ramp rows of a single-output arc, part C) — a single integer unit cannot (maxStartup / maxShutdown forbid it there).

Source: flextool/engine/model/_online.py:216 (add_constraints)

EQ. maxStartup_{linear,integer}

Names: maxStartup_linear, maxStartup_integer

v_startup[p,d,t] − v_online[p,d,t]  ≤  0                                 ∀ (p,d,t) ∈ p_idx

Units started in a step are online in it.

Source: flextool/engine/model/_online.py:229 (add_constraints)

EQ. maxShutdown_{linear,integer}

Names: maxShutdown_linear, maxShutdown_integer

v_shutdown[p,d,t] + v_online[p,d,t] − Σ v_invest_p[p,d″] + Σ v_divest_p[p,d″]  ≤  existing_count[p,d]
  ∀ (p,d,t) ∈ p_idx whose online predecessor is v_online of the SAME period d (a dt_ramp_link with
  d_previous = d, or a dt_online_wrap; the same-period predecessor keeps the shutdown bound)

Units shut down in a step are offline in it. Not built where the predecessor is in another period or carried from an earlier roll (capacity may differ there — a unit online at the end of the previous period / roll must still be able to "shut down" even if this step's capacity is lower); sd ≤ v_online_prev follows there from online_change + maxStartup instead (su ≤ on_t ⇒ sd = su − on_t + on_prev ≤ on_prev). A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/_online.py:236 (add_constraints)

EQ. maxFlow_online_{linear,integer}

Names: maxFlow_online_linear, maxFlow_online_integer

v_flow[p,s,k,d,t] + Σ_r v_reserve_up[p,r,k,d,t] + Σ_r v_reserve_down[p,r,s,d,t] · φ[p,s,k,d,t]
  ≤  κ[p,s,k] · ( v_online[p,d,t] · availability[p,d,t] − red_su[p,s,k,d,t] · v_startup[p,d,t]
                  − max(0, red_sd − red_su)[p,s,k,d,t] · v_shutdown[p,d,t⁺] )
  ∀ (p,s,k) capped arc of an online process, (d,t) ∈ dt;   φ as in maxFlow, units only;
  κ = capacity_max_coeff[p,s,k]

Source: flextool/engine/model/_online.py:266 (add_constraints)

EQ. maxFlow_online_shutdown_{linear,integer}

Names: maxFlow_online_shutdown_linear, maxFlow_online_shutdown_integer

(maxFlow_online LHS)
  ≤  κ · ( v_online · availability − red_sd · v_shutdown[p,d,t⁺]
           − max(0, red_su − red_sd) · v_startup[p,d,t] )
  ∀ rows of maxFlow_online with red_sd > 0 and a UNIQUE successor t⁺

Source: flextool/engine/model/_online.py:273 (add_constraints)

EQ. maxFlow_online_shutdown_branch_{linear,integer}

Names: maxFlow_online_shutdown_branch_linear, maxFlow_online_shutdown_branch_integer

(maxFlow_online LHS)
  ≤  κ · ( v_online · availability − red_sd · v_shutdown[p,d⁺,t⁺]
           − max(0, red_su − red_sd) · v_startup[p,d,t] )
  ∀ (row of maxFlow_online, successor (d⁺,t⁺) of (d,t)) with red_sd > 0, at a (d,t) with SEVERAL
    successors (a branch point: several mid-horizon branches continuing from the same trunk step)
red_su = max(0, availability − SU),
  SU = min(1, min_load[p,d,t] + ½ · ramp_speed_up · 60 · step_duration[d,t])
  over the sides of the arc with an up ramp limit (process_source_sink_ramp_limit_{sink,source}_up; the
  smaller SU when both); red_sd likewise with ramp_speed_down at the pre-shutdown step (d,t); 0 without a
  ramp limit.  (d⁺,t⁺) = a successor whose ramp predecessor (dt_ramp_link) is (d,t) — the same period
  (today's only case) or the next continuing period (#98).

A coarse process (processCoarse): the averaged twins maxFlow_online{sfx}_coarse,

  maxFlow_online_shutdown{sfx}_coarse and maxFlow_online_shutdown_branch{sfx}_coarse (_coarse_flows.add_cstr)
  with the reduction on the coarse grid: red_su[p,s,k,d,b] = max(0, ā_b − SU_b) at the start-ups of b and
  red_sd = max(0, ā_b − SD_b) at the shut-downs of the coarse successor b⁺ (the coarse step whose first
  step's ramp predecessor lies in b, _coarse_flows.coarse_links); ā_b, m̄l_b the Δt-weighted means over b,
  SU_b = m̄l_b + ½·ρ_b while ρ_b ≤ 1 − m̄l_b, else 1 − (1 − m̄l_b)²/(2·ρ_b), ρ_b = ramp_speed_up · 60 · Δ_b
  (the mean share over the whole step of a unit started at its start that ramps up to full load within
  it; SD_b with the down speed: a unit stopping at the end of b); κ its step mean.

A unit's flow and the reserve that raises it — upward reserve at the arc's sink and "consume more" downward reserve at its source — are limited by the units online (spinning reserve: an offline unit holds neither). The reserve terms are maxFlow's (units only; an online connection's reserve is in connection_reserve_*_online_*). Units starting in step t reach at most SU of their capacity on average over the step (synchronised at min load, then ramping as fast as they may: half a step's ramp on average; owner question Q-c14a-A), units shutting down after step t at most SD in t. Two rows, so capacity online for a single step gets the smaller of the two limits instead of both reductions (exact for any mix of starts and stops; GMPL's one row with both reductions forbids one-step runs). At a branch point (several successors of one (d,t): several mid-horizon branches continuing from the same trunk step) the shutdown cross-term cannot be split across the single maxFlow_online row (one v_flow[t] variable, one row); it is dropped there (maxFlow_online keeps only its startup term, a valid — looser — bound) and maxFlow_online_shutdown_branch gives the tight, correctly-isolated bound per branch instead: summing the branches' v_shutdown into one row would let several mutually-exclusive branches each buy back a whole unit of ramp room from the trunk step. Only capped arcs (direct arcs and indirect output arcs, see _capped_arcs): indirect input arcs are tied to the outputs by conversion_indirect, and conversion_flow_coeff = 0 arcs are uncapped. Availability and capacity_max_coeff are densified to their 1.0 defaults so an unauthored unit keeps its row. The reduction is per arc (a multi-output unit's maxOutputSum_online is not reduced).

Source: flextool/engine/model/_online.py:278 (add_constraints)

EQ. maxFlow_back_online_{linear,integer}

Names: maxFlow_back_online_linear, maxFlow_back_online_integer

v_flow_back[p,s,k,d,t]  ≤  κ[p,s,k] · availability[p,d,t] · v_online[p,d,t]
  ∀ (p,s,k) ∈ twoway1var_arcs with p online, (d,t) ∈ dt;   κ = capacity_max_coeff (1.0 defaults)

The reverse flow of a one-variable connection is limited by the units online, as its v_flow is by maxFlow_online: one online status covers both directions (the mirror of maxFlow_back). No reserve term (connection reserve has its own rows, connection_reserve_*). DC power-flow lines never get here (a startup_method on them raises in core._prepare).

Source: flextool/engine/model/_online.py:380 (add_constraints)

EQ. maxInputFlow_online_{linear,integer}

Names: maxInputFlow_online_linear, maxInputFlow_online_integer

v_flow[p,s,p,d,t] + Σ_r v_reserve_down[p,r,s,d,t]
  ≤ input_cap[p,s,p,d,t] · ( v_online[p,d,t] · x(availability)[p,d,t] − red_su · v_startup[p,d,t]
                             − max(0, red_sd − red_su) · v_shutdown[p,d,t⁺] )
  ∀ (p,s,p) input arc of an online indirect unit that is in reserve_down_input_arcs or has a source-side
    ramp limit with a reduction, (d,t) ∈ dt
  input_cap = p_indirect_input_cap (input_share_max × full-load input per unit of capacity)

Source: flextool/engine/model/_online.py:404 (add_constraints)

EQ. maxInputFlow_online_shutdown_{linear,integer}

Names: maxInputFlow_online_shutdown_linear, maxInputFlow_online_shutdown_integer

(maxInputFlow_online LHS) ≤ input_cap · ( v_online · x(availability) − red_sd · v_shutdown[p,d,t⁺]
                                          − max(0, red_su − red_sd) · v_startup[p,d,t] )
  ∀ rows of maxInputFlow_online with red_sd > 0 and a UNIQUE successor t⁺

Source: flextool/engine/model/_online.py:412 (add_constraints)

EQ. maxInputFlow_online_shutdown_branch_{linear,integer}

Names: maxInputFlow_online_shutdown_branch_linear, maxInputFlow_online_shutdown_branch_integer

(maxInputFlow_online LHS) ≤ input_cap · ( v_online · x(availability) − red_sd · v_shutdown[p,d⁺,t⁺]
                                          − max(0, red_su − red_sd) · v_startup[p,d,t] )
  ∀ (row of maxInputFlow_online, successor (d⁺,t⁺) of (d,t)) with red_sd > 0, at a (d,t) with several
    successors (a branch point) — split per successor exactly as maxFlow_online_shutdown_branch,
    never summed across mutually-exclusive branches.
x(y) = (slope · W(y) + section) / (slope · W(1) + section): the input drawn at output level y per unit of
  full-load input (W(y): the output-side fuel width filled up to y, as in p_indirect_input_cap; section
  only for min_load_efficiency units).  x(availability) replaces availability: the section is drawn per
  unit online, so availability × full-load input would cap the derated full output (x = availability
  without a section).
red_su = max(0, x(availability) − SU_in), SU_in = min(1, x(min_load) / input_share_max[p,s]
  + ½ · ramp_speed_up · 60 · step_duration) from the input node's ramp speeds (red_sd likewise), so a
  started unit can always draw what it needs at min load.  (d⁺,t⁺) = a successor whose ramp
  predecessor (dt_ramp_link) is (d,t) — the same period (today's only case) or the next continuing
  period (#98).

A coarse process: the averaged twins maxInputFlow_online{sfx}_coarse (+ _shutdown, _shutdown_branch), with

  input_cap and x(availability) as step means and the reduction on the coarse grid as in maxFlow_online
  (SU_in_b, SD_in_b from the step-mean x(min_load) / input_share_max and ρ_b).

The online analogue of maxFlow on an indirect unit's input arc: consuming more needs online capacity, and a starting (stopping) unit draws at most its startup (pre-shutdown) share of the input. The other inputs and the outputs are not adjusted (the per-arc simplification of the input-side floors).

Source: flextool/engine/model/_online.py:416 (add_constraints)

EQ. maxOutputSum_online_{linear,integer}

Names: maxOutputSum_online_linear, maxOutputSum_online_integer

Σ_k v_flow[p,p,k,d,t] + Σ_k Σ_r v_reserve_up[p,r,k,d,t]  ≤  v_online[p,d,t] · availability[p,d,t]
  ∀ p ∈ process_indirect online with an output arc k, (p,d,t) ∈ p_idx

The online analogue of maxOutputSum: the sum of an indirect unit's outputs and of the upward reserve it holds at those outputs is limited by the units online. No capacity_max_coeff term; availability defaults to 1.0.

Source: flextool/engine/model/_online.py:440 (add_constraints)

EQ. minimum_uptime_{linear,integer}

Names: minimum_uptime_linear, minimum_uptime_integer

v_online[p,d,t]  ≥  Σ_{(p,d′,t′,d,t) ∈ uptime_lookback} v_startup[p,d′,t′] + carry_up[p,d,t]
  ∀ (p,d,t) ∈ pdt_uptime_set ∩ this class ∩ online_rows

A unit started within the minimum uptime stays online. uptime_lookback = {(p,d′,t′,d,t) : (d′,t′) = (d,t), or (d′,t′) an ancestor of (d,t) along dt_ramp_link

  with elapsed(d′,t′ → d,t) = Σ step_duration over the chain from (d′,t′) up to, not including, (d,t)
  < min_uptime[p]} — the chain stops at a step without a ramp predecessor (solve start, a period that
  does not continue the previous one, a gap in the timeline, a representative-block start, a forecast
  branch that starts the solve); no cyclic wrap.

carry_up[p,d,t] = Σ_{h ∈ online_history} startup[p,h] over the root r ∈ dt_ramp_carry that is (d,t) or an

  ancestor of it along dt_ramp_link, with elapsed(r → d,t) + hours_to_end[h] < min_uptime[p]
  (d.uptime_carry, 0-filled; absent where no step continues the previous solve's last realized step).

A coarse process: rows and look-back only at the first fine steps of its coarse steps (its only start-ups),

  so elapsed(b′ → b) = Σ of the coarse durations from b′ up to b: a start-up counts while the steps since
  its start last less than min_uptime, i.e. min_uptime in whole coarse steps, rounded up.  A row whose
  look-back holds only its own step and no carry (the previous coarse step lasts ≥ min_uptime) is maxStartup
  and is not built.

Source: flextool/engine/model/_online.py:471 (add_constraints)

EQ. minimum_downtime_{linear,integer}

Names: minimum_downtime_linear, minimum_downtime_integer

v_online[p,d,t] + Σ_{(p,d′,t′,d,t) ∈ downtime_lookback} v_shutdown[p,d′,t′] + carry_dn[p,d,t]
  − Σ v_invest_p[p,d″] + Σ v_divest_p[p,d″]  ≤  existing_count[p,d]
  ∀ (p,d,t) ∈ pdt_downtime_set ∩ this class

A unit shut down within the minimum downtime stays offline: existing − v_online ≥ Σ v_shutdown, with the invest/divest terms of maxOnline (without them an invest-only unit with existing = 0 could never be online). downtime_lookback: as uptime_lookback with min_downtime[p] (a coarse process as in minimum_uptime; a row

  whose look-back holds only its own step and no carry is implied by maxShutdown / online_change).

carry_dn: as carry_up with shutdown[p,h] and min_downtime[p] (d.downtime_carry, 0-filled). A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/_online.py:513 (add_constraints)

EQ. online_fix_eq_{linear,integer}

Names: online_fix_eq_linear, online_fix_eq_integer

v_online[p,d,t] = p_fix_online[p,d,t]   ∀ (p,d,t) ∈ online_rows with a p_fix_online value
  (a coarse process: at the first fine step of each coarse step; its pin is the same on every fine step of
  the step, or the process keeps the solve's step, flex_data.coarse_pin_varies)

In a separate-dispatch solve, pins units online to the commitment of the parent solve. Only declared v_online cells are pinned. p_fix_online is Utf8-keyed: it is joined to the native-typed index through Utf8 copies of (p, d, t).

Source: flextool/engine/model/_online.py:554 (add_constraints)

The exact transfer method

The exact transfer method: a connection flows in one direction per time step.

VAR. v_exact_forward

v_exact_forward[p,d,t] ∈ {0, 1}   ∀ p ∈ process_exact with both arcs, (d,t) ∈ dt
1: the exact connection is set to its forward direction (node_1 → node_2 of connection__node__node) in the
step, 0: to the reverse direction.  One binary per connection: the whole connection transfers in one
direction.  No cost, no inter-temporal link.  Continuous in [0, 1] when the solve's reduce_unit_detail_method
relaxes integers (d.relax_integrality); every such level also clears process_exact, so the variable is then
not declared at all.

Source: flextool/engine/model/_exact.py:109 (add_variables)

VAR. v_exact_forward (coarse connections)

Names: v_exact_forward

v_exact_forward[p,d,b] ∈ {0, 1}   ∀ p ∈ processCoarse ∩ process_exact, (d,b) the coarse steps of p
  (b = its first fine step b_first)

One direction per coarse step, held over its fine steps (a level: b.v_exact_forward is indexed on every fine step, _coarse_flows.alias), as the connection's flows are one value per coarse step.

Source: flextool/engine/model/_exact.py:117 (add_variables)

EQ. maxFlow_exact_forward / maxFlow_exact_reverse

Names: maxFlow_exact_forward, maxFlow_exact_reverse

v_flow[p,a,b,d,t] − Ū[p,a,b,d,t] · v_exact_forward[p,d,t]  ≤  0                         (forward)
v_flow[p,b,a,d,t] + Ū[p,b,a,d,t] · v_exact_forward[p,d,t]  ≤  Ū[p,b,a,d,t]              (reverse)
  ∀ p ∈ process_exact, (a,b) = (node_1, node_2), (d,t) ∈ dt
  Ū = p_flow_upper · capacity_max_coeff · availability (structural upper bound, units of unitsize)

An exact connection does not flow both ways at once (docs: the exact method needs a binary variable): in each step it carries flow in one direction only. maxFlow and maxFlow_online still bound each direction by the built and the online capacity; with investment Ū is a valid big-M (GMPL used v_online_integer and the unit count as the switch, which fails for a non-integer number of units). A coarse connection: maxFlow_exact_forward_coarse / maxFlow_exact_reverse_coarse, the rows' mean over each coarse step (_coarse_flows.add_cstr): v_flow ≤ Ū_b · v_exact_forward[p,d,b] with Ū_b the step mean of Ū — the flow and the direction are one value per step.

Source: flextool/engine/model/_exact.py:138 (add_constraints)

Unit output floors

Unit output floors: minimum load, per-output floors and downward-reserve headroom.

EQ. minFlow_minload_{linear,integer}

Names: minFlow_minload_linear, minFlow_minload_integer

Σ_{(p,s,k) ∈ A_p} v_flow[p,s,k,d,t]  ≥  v_online[p,d,t] · min_load[p] + Rdn_p[p,d,t]
  ∀ p online with min_load, (p,d,t) ∈ p_idx
  process_minload = units with min_load > 0 on some (d,t) of this solve; not authored = 0 (no floor)

Minimum load, once per unit on the sum of its output measure. Rdn_p sums the reducing reserves over A_p, so downward reserve cannot take the unit below it. The floor is not scaled by availability.

Source: flextool/engine/model/_unit_floors.py:94 (add_constraints)

EQ. minFlow_output_floor_{linear,integer}

Names: minFlow_output_floor_linear, minFlow_output_floor_integer

v_flow[p,p,k,d,t]  ≥  v_online[p,d,t] · min_load[p] · capacity_min_coeff[p,k] + Rdn_k[p,k,d,t]
  ∀ (p,p,k) ∈ A_p of a multi-output unit with capacity_min_coeff[p,k] > 0, (p,d,t) ∈ p_idx

Per-output floor of a running unit; capacity_min_coeff must lie in [0, 1].

Source: flextool/engine/model/_unit_floors.py:114 (add_constraints)

EQ. minFlow_reserve

Σ_{(p,s,k) ∈ A_p} v_flow[p,s,k,d,t]  ≥  Rdn_p[p,d,t]
  ∀ unit p with a reducing reserve on A_p, (p,d,t) ∈ prundt without a minFlow_minload row

Zero floor of units without a minimum-load row: a unit cannot offer more downward reserve than its output. A coarse unit: minFlow_reserve_coarse, the row's mean over each coarse step.

Source: flextool/engine/model/_unit_floors.py:144 (add_constraints)

EQ. minFlow_reserve_arc

v_flow[p,s,k,d,t]  ≥  Rdn_a[p,s,k,d,t]
  ∀ unit arc (p,s,k) with a reducing reserve, outside A_p or one of several arcs in A_p,
    (p,d,t) ∈ prundt, without a minFlow_output_floor row

Per-arc zero floor where the per-unit rows cannot see the arc. A coarse unit: minFlow_reserve_arc_coarse.

Source: flextool/engine/model/_unit_floors.py:163 (add_constraints)

Group soft floors

Group soft floors: capacity margin, inertia and the non-synchronous share limit.

VAR. vq_capacity_margin

vq_capacity_margin[g,d] ≥ 0   ∀ g ∈ groupCapacityMargin, d ∈ D_inv
  D_inv = period_invest ∩ periods of dt: the solve's investment periods and, in a stochastic fan, the
  forecast branch periods of those anchors (a period label has no dt); one slack per forecast

Capacity margin shortfall of group g in period d, as a fraction of group_capacity_for_scaling: MW = vq_capacity_margin · group_capacity_for_scaling[g,d].

Source: flextool/engine/model/_group_slack.py:240 (add_variables)

VAR. v_capacity_margin_slack_energy

v_capacity_margin_slack_energy[s,d] ≥ 0   ∀ s ∈ ∪_g S(g), d ∈ D_inv
  S(g) = the storage nodes with an upward balance slack that feed N(g) (capacity_margin_storage_slack_nodes)

The energy the upward balance slack of storage node s makes in period d, MWh (capacityMargin_slack_energy). The capacity margin takes it back, so slack energy cannot pose as storage delivery.

Source: flextool/engine/model/_group_slack.py:252 (add_variables)

VAR. vq_inertia

vq_inertia[g,d,t] ≥ 0   ∀ g ∈ groupInertia, (d,t) ∈ dt

Inertia shortfall of group g, as a fraction of its inertia limit: MWs = vq_inertia · inertia_limit[g,d,t].

Source: flextool/engine/model/_group_slack.py:264 (add_variables)

VAR. vq_inertia, vq_non_synchronous (coarse rows)

Names: vq_inertia, vq_non_synchronous

vq[g,d,b] ≥ 0   ∀ g whose inertia / non-synchronous row is averaged over coarse steps (the row rule), (d,b)
  the coarse steps of its row (b = the step's first fine step)

One shortfall per coarse step, held over its fine steps (a level, _coarse_flows.alias), as the row it relaxes.

Source: flextool/engine/model/_group_slack.py:269 (add_variables)

VAR. vq_non_synchronous

vq_non_synchronous[g,d,t] ≥ 0   ∀ g ∈ groupNonSync, (d,t) ∈ dt

Non-synchronous infeed of group g above its allowed share, as a fraction of group_capacity_for_scaling: MW = vq_non_synchronous · group_capacity_for_scaling[g,d].

Source: flextool/engine/model/_group_slack.py:279 (add_variables)

EQ. capacityMargin

Σ_{(p,s,k) ∈ K(g)} (Σ_{d′} v_invest_p[p,d′] − Σ_{d′} v_divest_p[p,d′]) · unitsize[p] · κ[g,p,s,k,d,t] · icap[g,d]
  + Σ_{n ∈ N(g)} B¬FV[g,n,d,t] · icap[g,d]
  − Σ_{n ∈ N(g)} vq_state_down[n,d,t] · icap[g,d]
  − Σ_{s ∈ S(g)} v_capacity_margin_slack_energy[s,d] / Δt[d,t] · icap[g,d]
  + vq_capacity_margin[g,d]
  ≥ (M[g,d] + Σ_{n ∈ N(g)} demand[n,d,t]) · icap[g,d]
    − Σ_{(p,s,k) ∈ K(g)} existing_count[p,d] · unitsize[p] · κ[g,p,s,k,d,t] · icap[g,d]
  ∀ g ∈ groupCapacityMargin, (d,t) ∈ dt : d ∈ D_inv
  groupCapacityMargin = groups with capacity_margin_method manual or share_of_peak
  D_inv = period_invest ∩ periods of dt (investment periods of the solve; a period label has no dt, so a
  fan contributes its forecast branch periods, one row per forecast); a solve without investment periods
  has no capacity margin
  N(g) = nodes of g in nodeBalance without a storage state;  icap = inv_group_cap = 1 / group_capacity_for_scaling
  F(g), V(g) = the unit output arcs into N(g) that count at capacity (capacity_margin_counted_arcs): not of a
  storage-backed unit (an input from a storage-state node, Σ input_share_max of its other inputs < 1), not of
  a unit with an input from N(g), not conversion_flow_coeff 0; V(g) those with an upper_limit or fixed profile
  K(g) = F(g) ∪ V(g), except that a unit of maxOutputSum (process_indirect) keeps only its first arc into N(g)
  κ = c · av (firm arc);  min(c, profile) · av (VRE arc);  min(av, Σ_{its arcs into N(g)} κ[arc]) (a unit of
  maxOutputSum: its outputs share one capacity);  c = capacity_max_coeff (p_arc_max_cap_coef, default 1),
  av = availability (default 1), profile = the smallest value of the arc's upper_limit and fixed profiles
  B¬FV[g,n,d,t] = the flow terms of nodeBalance_eq at n (balance_flow_terms, power: no Δt) without the arcs
  of F(g) ∪ V(g) and without the arcs left out (capacity_margin_left_out_arcs: the connections with both ends
  in N(g), the lossy converters inside g); the other units' output and input (slope, section), connections at
  N(g) (v_flow, v_flow_back), Benders half-flows; only at the (g, n) pairs of N(g).  The start-up input
  draw (startup_input_method, core._startup_draw: energy per start-up event, not capacity) is not a term, so
  with a draw at a node of N(g) (a unit started on grid electricity) the substituted balance below differs
  from the row by that draw
  S(g) = the storage nodes with an upward balance slack that feed N(g) through a storage-backed unit or a
  connection (capacity_margin_storage_slack_nodes);  v_capacity_margin_slack_energy = Σ_t vq_state_up · Δt
  (capacityMargin_slack_energy)
  demand = −inflow_real / Δt (pdtNodeInflow_per_step: the inflow before any energy margin)
  M[g,d] = capacity_margin[g,d] (manual)  |  capacity_margin_share[g,d] · P[g,d] (share_of_peak), 0 where not
  authored;  P[g,d] = max(0, max_t Σ_{n ∈ N(g)} demand[n,d,t]) over the period's original time series
  (before representative periods and new_stepduration; _capacity_margin_peak, resolve_capacity_margin)
  Σ_{d′} over edd_invest_set / edd_divest_active

Substituting the balance of each n ∈ N(g) (flow_FV + B¬FV + q_up − q_down = demand) gives

  Σ_{K} (counted capacity − flow into N(g)) + vq ≥ M + Σ_n q_up − (slack energy of S(g)) / Δt:

only firm and VRE headroom holds the margin. Each counted capacity bounds the unit's dispatch into N(g) (maxFlow, profile_flow_*, maxOutputSum), so the headroom is never negative. Storage-backed units, imports, exports and the group's own consumption count by their flow: imports and storage help by displacing firm output, exports add to the requirement. Energy dumped with vq_state_down does not count, and neither does energy that a storage node's balance slack makes (it could be stored and sent at the binding step). Lines and lossy converters inside g are left out, so their losses are not part of the requirement and shedding the demand behind them cannot lower it: unserved energy (q_up) leaves the row unchanged where counted capacity served it, and is priced once, by penalty_up. Units fed from fuel or commodity nodes count at capacity: the fuel is assumed to last through the peak. Existing capacity is a constant and moves to the right-hand side. The row is scaled by icap, which makes the slack a fraction of group_capacity_for_scaling. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Row rule (coarse steps): a group whose flow members (the processes of B¬FV) are all coarse processes gets one row per coarse step, capacityMargin_coarse = the row's Δt-weighted mean over the step (_coarse_flows.add_cstr): availability, the profile, the inflow and the slope act through their step means, so the margin covers the step-mean demand, not its peak (named in the coarse-flow behaviour notice). Otherwise the rows stay per solve step.

Source: flextool/engine/model/_group_slack.py:672 (_eq_capacity_margin)

EQ. capacityMargin_slack_energy

v_capacity_margin_slack_energy[s,d] = Σ_{t : (d,t) ∈ dt} vq_state_up[s,d,t] · Δt[d,t]
  ∀ (s,d) of v_capacity_margin_slack_energy

The energy made by storage node s's upward balance slack over the period's steps. Energy made at any step can be stored and sent into the group at the binding step, so the capacityMargin row takes back the whole period's slack energy, in MW at its own step (÷ Δt): one variable per (s,d) keeps the rows sparse.

Source: flextool/engine/model/_group_slack.py:850 (_eq_slack_energy)

EQ. inertia_constraint

Σ_{side ∈ {source, sink}} Σ_{(p,s,k) ∈ arcs_side(g)} x[p,·,d,t] · inertia_constant[p,side] · unitsize[p]
  + vq_inertia[g,d,t] · inertia_limit[g,d,t]
  ≥ inertia_limit[g,d,t]
  ∀ g ∈ groupInertia, (d,t) ∈ dt
  groupInertia = groups with has_inertia = yes;  inertia_limit[g,d,t] = the authored constant, period, time
  or period + time value (a time map covers every time step of the solve: a gap is an input error,
  _direct.check_group_limit_time_coverage), 0 for a group without any value or an authored 0 (no requirement:
  the row reads Σ … ≥ 0 and its slack is in no row)
  arcs_side(g) = arcs whose side node is in g and that carry an inertia constant on that side
  x = v_online[p,d,t] for p ∈ process_online (linear or integer), else v_flow[p,s,k,d,t]

Synchronous machines connected to the group's nodes supply inertia (MWs) in proportion to the units online, or to their flow when they have no online variable; the shortfall is penalised. The slack is a fraction of the limit. Row rule (coarse steps): a group whose inertia providers are all coarse processes gets one row per coarse step, inertia_constraint_coarse = the row's mean over the step (_coarse_flows.add_cstr), on one slack per step; the limit enters as its Δt-weighted step mean, and a limit that varies within a coarse step is named in the coarse-flow behaviour notice. Otherwise per solve step.

Source: flextool/engine/model/_group_slack.py:1221 (_eq_inertia)

EQ. non_sync_constraint

Σ_{(p,s,k) ∈ in_ns(g)} v_flow[p,s,k,d,t] · unitsize[p] · Δt[d,t] · icap[g,d]
  + Σ_{(p,s,k) ∈ back_in_ns(g)} v_flow_back[p,s,k,d,t] · unitsize[p] · Δt[d,t] · icap[g,d]
  + Σ_{n ∈ N(g)} positive_inflow[n,d,t] · icap[g,d]
  − vq_non_synchronous[g,d,t] · Δt[d,t]
  ≤ ns_limit[g,d,t] · icap[g,d] · ( Σ_{(p,s,k) ∈ out_noEff(g)} v_flow[p,s,k,d,t] · unitsize[p] · Δt[d,t]
                                + Σ_{(p,s,k) ∈ out_eff(g)} v_flow[p,s,k,d,t] · slope[p,d,t] · unitsize[p]
                                                           · Δt[d,t]
                                + Σ_{(p,s,k) ∈ out_eff(g), p ∈ process_min_load_eff}
                                      v_online[p,d,t] · section[p] · unitsize[p] · Δt[d,t]
                                + Σ_{(p,s,k) ∈ back_out(g)} v_flow_back[p,s,k,d,t] · unitsize[p] · Δt[d,t]
                                − Σ_{n ∈ N(g)} negative_inflow[n,d,t] )
  ∀ g ∈ groupNonSync, (d,t) ∈ dt
  groupNonSync = groups with has_non_synchronous = yes
  N(g) = nodes of g;  icap = inv_group_cap;  ns_limit = non_synchronous_limit (constant, period, time or
  period + time); a group without any value, and an authored 0, read as 0 (the right-hand side then
  vanishes: no non-synchronous infeed without the penalised slack); a time map covers every time step of
  the solve (a gap is an input error)
  in_ns(g)      = arcs (p,s,k), k ∈ N(g), (p,k) ∈ process_sink_nonSync, (p,g) ∉ inside
  back_in_ns(g) = twoway1var_arcs (p,s,k), s ∈ N(g), (p,s) ∈ process_sink_nonSync, (p,g) ∉ inside
  out_*(g)      = arcs (p,s,k), s ∈ N(g), (p,g) ∉ inside
  back_out(g)   = twoway1var_arcs (p,s,k), k ∈ N(g), (p,g) ∉ inside
  process_sink_nonSync = unit arcs with is_non_synchronous ∪ (c,n) for is_DC connections c and their nodes n
  inside = process_group_inside_nonSync: processes with a source and a different sink node in g
  positive_inflow / negative_inflow = max(inflow_real, 0) / min(inflow_real, 0);  inflow_real = the node's
  inflow before the invest-solve energy margin

Non-synchronous infeed into the group (flagged unit arcs, DC connections, positive exogenous inflow) is at most ns_limit times the group's supply, measured as the energy its nodes deliver to processes, exports and the real demand (an energy margin does not count); the excess is penalised. Flows between nodes of the group are neither (inside). The reverse flow of a one-variable / DC arc arrives at the arc's source and leaves its sink, lossless, as in nodeBalance_eq. The slack has no icap, which makes it a fraction of group_capacity_for_scaling. slope already holds the sink/source coefficient ratio of multi-arc processes. Row rule (coarse steps): a group whose processes are all coarse gets one row per coarse step, non_sync_constraint_coarse = the row's Δt-weighted mean over the step (_coarse_flows.add_cstr), on one slack per step: the non-synchronous share then holds for the step's energy, with the inflow, the slope and the limit through their step means (named in the coarse-flow behaviour notice). Otherwise per solve step.

Source: flextool/engine/model/_group_slack.py:1291 (_eq_non_sync)

OBJ. inertia / non-synchronous / capacity margin slack

Names: inertia_penalty, non_synchronous_penalty, capacity_margin_penalty

+ Σ_{(g,d,t)} vq_inertia[g,d,t] · inertia_limit[g,d,t] · penalty_inertia[g,d] · w_op[d,t]
+ Σ_{(g,d,t)} vq_non_synchronous[g,d,t] · gcs[g,d] · penalty_non_synchronous[g,d] · w_op[d,t]
+ Σ_{g ∈ groupCapacityMargin, d ∈ D_inv} vq_capacity_margin[g,d] · gcs[g,d] · penalty_capacity_margin[g,d]
                                                        · inflation_op[d] · bw[d] · 1000
  gcs = group_capacity_for_scaling;  w_op = b.op_factor
  bw = pd_branch_weight[d], 1 where it has no row (deterministic and pre-reveal periods)

Each slack is scaled back to physical units (MWs, MW) and priced per hour of operation. The capacity margin is a planning quantity: it is priced once per period, weighted by pd_branch_weight, without step weights, and its penalty is authored in CUR/kW (× 1000 to CUR/MW). In a stochastic fan each forecast branch period keeps its own slack and pays it with its probability. The output layer (calc_slacks) reports the same costs for the realized periods (weight 1).

Source: flextool/engine/model/_group_slack.py:1429 (objective_terms)

Reserves

Reserves: capacity held back from the energy market to cover a reserve requirement of a node group.

VAR. v_reserve

v_reserve[p,r,ud,n,d,t] ≥ 0   ∀ (p,r,ud,n,d,t) ∈ prundt

Reserve r in direction ud (up / down) that process p holds for node n, in units of unitsize like v_flow: MW = v_reserve · unitsize[p].

Source: flextool/engine/model/_reserve.py:527 (add_variables)

VAR. v_reserve (coarse processes)

Names: v_reserve

v_reserve[p,r,ud,n,d,b] ≥ 0   ∀ p ∈ processCoarse, (p,r,ud,n) of prundt, (d,b) the coarse steps of p
  (b = its first fine step b_first)

One reserve per coarse step, held over its fine steps (a level: b.v_reserve is indexed on every fine step, _coarse_flows.alias); the requirement rows still read it at every solve step.

Source: flextool/engine/model/_reserve.py:532 (add_variables)

VAR. vq_reserve

0 ≤ vq_reserve[r,ud,g,d,t] ≤ 1   ∀ (r,ud,g) ∈ reserve_upDown_group, (d,t) ∈ dt

Shortfall of the reserve requirement of group g, as a fraction of the largest requirement: MW = vq_reserve · scale[r,ud,g,d,t] (reserve_shortfall_scale).

Source: flextool/engine/model/_reserve.py:541 (add_variables)

VAR. v_reserve_total

v_reserve_total[r,ud,g,d,t] ≥ 0   ∀ (r,ud,g) ∈ rug_total, (d,t) ∈ dt
  rug_total = (r,ud,g) with an n-1 row (in fail_g) and at least one provider in pruna_g

Reserve held for group g by all its providers, MW: at most reserve_sum (reserve_total_upper). Lets each n-1 row subtract the failing process's own reserve instead of repeating every other provider, and replaces reserve_sum in the timeseries / dynamic rows of the same group. No cost.

Source: flextool/engine/model/_reserve.py:550 (add_variables)

EQ. reserveBalance_timeseries_eq

reserve_sum[r,ud,g,d,t] + vq_term[r,ud,g,d,t]  ≥ reservation[r,ud,g,d,t]
  ∀ (r,ud,g) ∈ reserve_upDown_group_method_timeseries, (d,t) ∈ dt
  reserve_sum is replaced by v_reserve_total for (r,ud,g) ∈ rug_total

Reserve held for a group covers its authored reservation timeseries, or the shortfall is penalised. RHS 0 when no reservation is authored. Reserve of every provider counts at full MW (no efficiency slope). A group that also has n-1 rows uses its total v_reserve_total: it is at most reserve_sum, enters only ≥ rows and has no cost, so the rows are equivalent (same optimal value for every right-hand side).

Source: flextool/engine/model/_reserve.py:684 (add_constraints)

EQ. reserveBalance_dynamic_eq

reserve_sum[r,ud,g,d,t] + vq_term[r,ud,g,d,t]
  ≥ Σ_{(p,s,n) ∈ process_source_sink, (p,r,ud,n) ∈ irr_g} v_flow[p,s,n,d,t] · unitsize[p] · ratio[p,r,ud,n]
  + Σ_{(p,n,k) ∈ eff_out, (p,r,ud,n) ∈ irr_g}
      (v_flow[p,n,k,d,t] · unitsize[p] · slope[p,d,t] + v_online[p,d,t] · section[p,d,t] · unitsize[p])
      · ratio[p,r,ud,n]
  + Σ_{(p,n,k) ∈ process_source_sink \ eff_out, (p,r,ud,n) ∈ irr_g} v_flow[p,n,k,d,t] · unitsize[p]
      · ratio[p,r,ud,n]
  + Σ_{(p,n,k) ∈ twoway1var_arcs, (p,r,ud,n) ∈ irr_g} v_flow_back[p,n,k,d,t] · unitsize[p] · ratio[p,r,ud,n]
  + Σ_{(p,s,n) ∈ twoway1var_arcs, (p,r,ud,n) ∈ irr_g} v_flow_back[p,s,n,d,t] · unitsize[p] · ratio[p,r,ud,n]
  + group_demand[r,ud,g,d,t]
  ∀ (r,ud,g) ∈ reserve_upDown_group_method_dynamic, (d,t) ∈ dt
  group_demand = ratio_g[r,ud,g] · Σ_{n : (g,n) ∈ group_node} max(−inflow[n,d,t], 0) / Δt[d,t]
  ratio_g = reserve__upDown__group.increase_reserve_ratio;  inflow in MWh per step without the
            energy margin (p_negative_inflow, the real demand), so group_demand is MW in the step
  irr_g   = process_reserve_upDown_node_increase_reserve_ratio with n ∈ group g
  ratio   = increase_reserve_ratio;   eff_out = process_source_sink_eff
  section term only for min_load_efficiency units with online variables (v_online_linear / _integer)
  reserve_sum is replaced by v_reserve_total for (r,ud,g) ∈ rug_total

The requirement grows with the flows into and out of the group's nodes (e.g. variable generation and consumption), in the upward and the downward rows alike: the ratio stands for the flow's variability, which can go either way (unlike large_failure, which is directional). A flow out of a node counts as drawn from it, as in nodeBalance_eq: on an efficiency arc the input is v_flow · slope plus the min-load section (_drawn_terms). The reverse flow of a one-variable (and DC) arc arrives at the arc's source and leaves its sink, as in nodeBalance_eq (lossless, no slope). The group term adds the demand of the group's nodes (negative inflow only; a node with positive inflow does not count) times the group's ratio (group_demand_requirement). RHS 0 when nothing drives it. A group that also has n-1 rows uses v_reserve_total, as in the timeseries rows.

Source: flextool/engine/model/_reserve.py:707 (add_constraints)

EQ. reserveBalance_up_n_1_eq / reserveBalance_down_n_1_eq

Names: reserveBalance_up_n_1_eq, reserveBalance_down_n_1_eq

v_reserve_total[r,ud,g,d,t]
  − Σ_{n : (f,r,ud,n) ∈ pruna_g} v_reserve[f,r,ud,n,d,t] · unitsize[f] · reliability[f,r,ud,n]
  + vq_term[r,ud,g,d,t]
  ≥ up:   Σ_{(f,s,n) ∈ process_source_sink, (f,r,ud,n) ∈ lfr_g} v_flow[f,s,n,d,t] · unitsize[f] · ratio[f,r,ud,n]
    down: Σ_{(f,n,k) ∈ eff_out, (f,r,ud,n) ∈ lfr_g} (v_flow[f,n,k,d,t] · unitsize[f] · slope[f,d,t]
                                                   + v_online[f,d,t] · section[f,d,t] · unitsize[f])
                                                  · ratio[f,r,ud,n]
          + Σ_{(f,n,k) ∈ process_source_sink \ eff_out, (f,r,ud,n) ∈ lfr_g} v_flow[f,n,k,d,t] · unitsize[f]
              · ratio[f,r,ud,n]
  + up:   Σ_{(f,n,k) ∈ twoway1var_arcs, (f,r,ud,n) ∈ lfr_g} v_flow_back[f,n,k,d,t] · unitsize[f] · ratio[f,r,ud,n]
    down: Σ_{(f,s,n) ∈ twoway1var_arcs, (f,r,ud,n) ∈ lfr_g} v_flow_back[f,s,n,d,t] · unitsize[f] · ratio[f,r,ud,n]
  ∀ (r,ud,g,f) ∈ fail_g, (d,t) ∈ dt
  pruna_g = the providers of the reserve_sum above, per group
  lfr_g   = process_reserve_upDown_node_large_failure_ratio with n ∈ group g,
            (r,ud,g) ∈ reserve_upDown_group_method_n_1 with ud = up / down
  fail_g  = (r,ud,g,f): f in lfr_g with a flow (v_flow or v_flow_back) into (up) / out of (down) a node of g
  ratio   = large_failure_ratio;   f is the row dim p_fail;   eff_out, section as in the dynamic row

Reserve covers the loss of each large process f separately (the largest failure binds): upward reserve the loss of its output into the group, downward reserve the loss of its consumption from the group. The failing process's own reserve, at every node of the group, does not count: it is subtracted from the group total. All rows of a group share vq_term, so the priced shortfall is the largest deficit. The lost consumption is the flow the process draws from the node, as in nodeBalance_eq (output · slope plus the min-load section on an efficiency arc); a lost output counts at unitsize. A one-variable (or DC) connection also fails while flowing backwards: its v_flow_back arrives at the arc's source and leaves its sink (lossless). No row for a group without a failing process; a group without providers (or without reliability data) has no v_reserve_total term. Nonzeros grow with providers + failing processes per group.

Source: flextool/engine/model/_reserve.py:789 (add_constraints)

EQ. reserve_total_upper

v_reserve_total[r,ud,g,d,t] − reserve_sum[r,ud,g,d,t]  ≤  0   ∀ (r,ud,g) ∈ rug_total, (d,t) ∈ dt
v_reserve_total enters only ≥ requirement rows, with a positive sign,
and has no cost, so an upper bound is enough: the n-1 rows above are
exactly the per-provider form (one row per failing process, its own
reserve not counted) with v_reserve_total = reserve_sum, and the
timeseries / dynamic rows of the group are those with reserve_sum.

This needs reserve_sum ≥ 0, which holds because providers have reliability > 0 (process_reserve_upDown_node_active) and unitsize > 0; otherwise v_reserve_total ≥ 0 would add a constraint. When v_reserve_total > 0, the magnitude of this row's dual is the sum of the duals of the group-step's rows that use v_reserve_total (opposite sign convention: ≤ row against ≥ rows).

Source: flextool/engine/model/_reserve.py:863 (add_constraints)

EQ. reserve_process_upward / reserve_process_downward

Names: reserve_process_upward, reserve_process_downward

v_reserve[p,r,ud,n,d,t] − max_share[p,r,ud,n] · c[p,n,d,t] · Σ_{(p,d′,d) ∈ edd_invest_set} v_invest_p[p,d′]
  + max_share[p,r,ud,n] · c[p,n,d,t] · Σ_{(p,d′,d) ∈ edd_divest_active} v_divest_p[p,d′]
  ≤ max_share[p,r,ud,n] · c[p,n,d,t] · existing_count[p,d]
  ∀ (p,r,ud,n,d,t) ∈ prundt with ud = up / down (a coarse process: reserve_process_{upward,downward}_coarse,
    the row's mean over each coarse step, _coarse_flows.add_cstr: one reserve per step under the step-mean c)
  c = 1 at a node the process feeds, and for connections;
  at an input node n of a unit (n the source of one of its arcs and not a sink):
    c = input_cap[p,n,p,d,t]           indirect unit (input_share_max × full-load input per unit)
    c = slope[p,d,t] + section[p,d,t]  direct unit's input→output arc (full-load input = capacity /
                                       efficiency; section only for min_load_efficiency units with
                                       online variables)
    c = 1                              sink-less unit (its capacity is the input), and
                                       conversion_flow_coeff = 0 inputs

A process holds at most max_share of the built capacity of its flow at that node as reserve (units of unitsize; built = existing + invested − retired, also for units with online variables). At an input node the capacity is the input's (_input_capacity_factor). max_share not authored → no reserve (RHS 0). A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/_reserve.py:882 (add_constraints)

EQ. reserve_fix_eq

v_reserve[p,r,ud,n,d,t] = p_fix_reserve[p,r,ud,n,d,t]   ∀ (p,r,ud,n,d,t) ∈ prundt with a p_fix_reserve value
  (a coarse process: at the first fine step of each coarse step; its pin is the same on every fine step of
  the step, or the process keeps the solve's step, flex_data.coarse_pin_varies)

In a realization solve, pins the reserve each process holds to the non-anticipativity-tied reserve of the stochastic solve it realizes (that solve's reference forecast on its tied steps). Only declared v_reserve cells are pinned. p_fix_reserve is Utf8-keyed: it is joined to the native-typed index through Utf8 copies.

Source: flextool/engine/model/_reserve.py:940 (_add_reserve_fix)

EQ. connection_reserve_up / connection_reserve_down

Names: connection_reserve_up, connection_reserve_down

x_in[p,n,d,t] − s_out[p,n,d,t] · x_out[p,n,d,t] + Σ_r v_reserve[p,r,up,n,d,t]
  − M_in · Σ_{(p,d′,d) ∈ edd_invest_set} v_invest_p[p,d′]
  + M_in · Σ_{(p,d′,d) ∈ edd_divest_active} v_divest_p[p,d′]  ≤  C_in[p,n,d,t]   (up)
x_out[p,n,d,t] − x_in[p,n,d,t] / s_out[p,n,d,t] + Σ_r v_reserve[p,r,down,n,d,t] / s_out[p,n,d,t]
  − M_out · Σ v_invest_p[p,d′] + M_out · Σ v_divest_p[p,d′]  ≤  C_out[p,n,d,t]                         (down)
  ∀ connection p (process_source_sink \ process_unit) with (p,r,ud,n,d,t) ∈ prundt, ud = up (down)
  x_in  = flow of p arriving at n:  v_flow[p,m,n,d,t], or v_flow_back[p,n,m,d,t] of a one-variable / DC arc
  x_out = flow of p leaving n (measured where it arrives):  v_flow[p,n,m,d,t], or v_flow_back[p,m,n,d,t]
  s_out = slope[p,d,t] when x_out is v_flow on an arc of process_source_sink_eff, else 1 (power drawn at n per
          unit of x_out)
  C, M  = the channel arc's maxFlow (maxFlow_back) RHS and invest multiplier (b.flow_upper_rhs,
          b.flow_cap_mult: capacity_max_coeff · availability · existing_count and coef · availability)
  a channel p does not have contributes nothing (x = 0, C = 0, M = 0, s_out = 1)

Source: flextool/engine/model/_reserve.py:1091 (_add_connection_reserve_constraints)

EQ. connection_reserve_{up,down}_online_{linear,integer}

Names: connection_reserve_up_online_linear, connection_reserve_up_online_integer, connection_reserve_down_online_linear, connection_reserve_down_online_integer

  same LHS without invest terms  ≤  κ · availability · v_online[p,d,t]   (κ, availability of the up row's
  in-channel arc / the down row's out-channel arc)   ∀ rows above with p online and that channel present

Source: flextool/engine/model/_reserve.py:1106 (_add_connection_reserve_constraints)

EQ. connection_reserve_{up,down}_exact

Names: connection_reserve_up_exact, connection_reserve_down_exact

  same LHS − Ū · v_exact_forward[p,d,t]  ≤  0     when that channel is the exact connection's forward arc
  same LHS + Ū · v_exact_forward[p,d,t]  ≤  Ū     when it is the reverse arc
  ∀ rows above with p ∈ process_exact;  Ū = p_flow_upper · capacity_max_coeff · availability of the arc

A coarse connection: each family as \<family>_coarse, the row's mean over each coarse step

  (_coarse_flows.add_cstr): one reserve and one flow per step under the step-mean capacity and availability.

A connection's reserve at a node is limited by its net position there: upward reserve by the free capacity into n plus the flow out of n it can stop, downward reserve by the flow into n it can stop plus the free capacity out of n (converted to power at n by the efficiency). One row per capacity bound (built, online, exact direction) of the channel whose capacity the reserve uses, so each row holds one channel's capacity. A one-way connection gets a floor at its sending node (upward reserve ≤ what it draws there) and its old capacity row at the receiving node; a one-variable connection's rows bound the reserve by its signed flow (C ∓ f). Reserve at a node the connection does not link is 0. maxFlow / maxFlow_online carry the upward reserve of units only (b.reserve_up_to_sink_units); the profile cap profile_flow_upper_limit keeps the connection's in-arc term (conservative: it does not count stopping the out-flow). A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/_reserve.py:1109 (_add_connection_reserve_constraints)

EQ. reserve_storage_up_floor

v_state[n,d,t] · state_unitsize[n]
  − Σ_{(p,r,ud,nb,g) ∈ providers_up(n)} v_reserve[p,r,ud,nb,d,t] · unitsize[p] · duration[r,ud,g] · slope[p,d,t]
  ≥ 0
  ∀ n ∈ storage nodes of upward providers, (d,t) ∈ dt
  providers_up: upward reserve for nb by a process with source n ∈ nodeState and sink nb, or by a
    one-variable / DC connection whose arc runs nb → n (its reverse flow v_flow_back runs n → nb; slope 1)

Upward reserve from storage is backed by stored energy (MWh) for the reserve duration (hours); slope = 1/η converts output to energy drawn. Reliability is not applied: the energy matches the full committed MW. No slack: with v_reserve = 0 the row is v_state ≥ 0, so a provider that cannot sustain the duration commits less and the group pays the shortfall penalty. Couples to the intra-period state v_state only, not to the representative-period state v_state_inter. Row rule: a storage node n whose state is coarse (constant within its coarse step, stateConstantCoarse_eq) and whose providers are all coarse processes gets one row per coarse step, reserve_storage_up_floor_coarse (the row's mean over the step of the providers' partition, _coarse_flows.add_cstr): the reserve, the state and the step-mean slope are then all one value per step. Otherwise the row stays per solve step and reads a coarse provider's reserve at its constant value. The same for reserve_storage_down_headroom.

Source: flextool/engine/model/_reserve.py:1443 (_add_storage_energy_constraints)

EQ. reserve_storage_down_headroom

v_state[n,d,t] · state_unitsize[n]
  + Σ_{(p,r,ud,nb,g) ∈ providers_down(n)} v_reserve[p,r,ud,nb,d,t] · unitsize[p] · duration[r,ud,g] / slope[p,d,t]
  + Σ_{(n,d′,d) ∈ ndd_divest_active} v_divest_n[n,d′] · state_unitsize[n]
  − Σ_{(n,d′,d) ∈ edd_invest_set} v_invest_n[n,d′] · state_unitsize[n]
  ≤ state_upper[n,d,t] · state_unitsize[n]
  ∀ n ∈ storage nodes of downward providers, (d,t) ∈ dt
  providers_down: downward reserve for nb by a process with source nb and sink n ∈ nodeState, or by a
    one-variable / DC connection whose arc runs n → nb (its reverse flow runs nb → n; slope 1)

Downward reserve into storage needs free storage capacity (MWh) for the reserve duration; 1/slope = η converts input to energy stored. The capacity terms are those of maxState, in MWh. Same notes as the floor. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/_reserve.py:1476 (_add_storage_energy_constraints)

OBJ. reserve shortfall

Names: reserve_shortfall_penalty

+ Σ_{(r,ud,g,d,t)} vq_reserve[r,ud,g,d,t] · scale[r,ud,g,d,t] · penalty_reserve[r,ud,g] · w_op[d,t]

Prices the shortfall in MW (vq_reserve · scale, the multiplier of the reserveBalance rows) per hour of operation. penalty_reserve is the authored value, else the default (reserve_penalty). w_op = b.op_factor.

Source: flextool/engine/model/_reserve.py:1526 (objective_terms)

Investment floors, cumulative capacity limits, group investment limits and flow-group limits

Investment floors, cumulative capacity limits, group investment limits and flow-group limits.

EQ. fix_v_invest_no_investment_eq_{p,n}

Names: fix_v_invest_no_investment_eq_p, fix_v_invest_no_investment_eq_n

v_invest_p[p,d] = 0   ∀ (p,d) ∈ ed_invest_forbidden_no_investment ∩ pd_invest_set
v_invest_n[n,d] = 0   ∀ (n,d) ∈ ed_invest_forbidden_no_investment ∩ nd_invest_set

Pins investment to zero where the entity's invest method forbids it. Never emits rows: ed_invest_set (the v_invest_* domain) already excludes the forbidden cells, so the row domain is empty.

Source: flextool/engine/model/_cumulative_invest.py:209 (_eq_fix_no_investment)

EQ. min{Invest,Divest}_entity_period_{p,n}

Names: minInvest_entity_period_p, minInvest_entity_period_n, minDivest_entity_period_p, minDivest_entity_period_n

Σ_{(p,o,d) ∈ edd_invest_commission} v_invest_p[p,o] · unitsize[p] + carried_commission[p,d]
                                     ≥ invest_min_period[p,d]   ∀ (p,d) with a decision coming online in d,
                                                                  p ∈ ed_invest_period_set
(n: v_invest_n with state_unitsize[n], n ∈ ed_invest_period_set ∩ nd_invest_set)
v_divest_p[p,d] · unitsize[p]        ≥ divest_min_period[p,d]   ∀ (p,d) ∈ ed_divest_period_set ∩ pd_divest_set
v_divest_n[n,d] · state_unitsize[n]  ≥ divest_min_period[n,d]   ∀ (n,d) ∈ ed_divest_period_set ∩ nd_divest_set

Forces a minimum capacity to come online or be retired in a period; the floor twin of max{Invest,Divest}_entity_period in model/core.py. Capacity in MW (MWh for storage nodes). Without a construction lead time c(o) = o: v_invest_p[p,d] · unitsize[p] ≥ invest_min_period[p,d] over ed_invest_period_set ∩ pd_invest_set. With one, a period of an entity with a lead time where nothing can come online (no decision early enough in the solve) and the carried capacity is below the floor is a configuration error.

Source: flextool/engine/model/_cumulative_invest.py:239 (_eq_min_entity_period)

EQ. minInvest_entity_total_path_{p,n}

Names: minInvest_entity_total_path_p, minInvest_entity_total_path_n

Σ_{d ∈ D(leaf)} v_invest_p[p,d] · unitsize[p]  ≥  max(0, invest_min_total[p] − prior_inv[p])
  ∀ p ∈ e_invest_total ∩ P(v_invest_p) with invest_min_total (, leaf)
  prior_inv[p] = p_entity_invested[p]  (capacity realized by all earlier solves of the chain)
  _n: the node analogue with v_invest_n and state_unitsize[n]

The part of the minimum not met by earlier solves of the chain must be met in this solve (Q3). One row per entity over the chain horizon, replacing the old per-period "invested and still alive" row; edd_invest_set is no longer read. Always named _path, even with one scenario leaf: a single row summed over alternative branches would let one branch meet the minimum for all, so the per-leaf row shape is load-bearing, not cosmetic.

Source: flextool/engine/model/_cumulative_invest.py:287 (_eq_min_invest_total)

EQ. minDivest_entity_total_path_{p,n}

Names: minDivest_entity_total_path_p, minDivest_entity_total_path_n

Σ_{d ∈ D(leaf)} v_divest_p[p,d] · unitsize[p]  ≥  max(0, divest_min_total[p] − prior_div[p])
  ∀ p ∈ e_divest_total ∩ P(v_divest_p) with divest_min_total (, leaf)
  prior_div[p] = p_entity_divested[p]  (capacity retired by earlier solves of the chain, at
    realized non-branch periods only)
  _n: the node analogue with v_divest_n and state_unitsize[n]

Forces the part of the minimum not retired by earlier solves of the chain to be retired in this solve (Q3). Always named _path, for the same reason as minInvest.

Source: flextool/engine/model/_cumulative_invest.py:330 (_eq_min_divest_total)

EQ. {max,min}Cumulative_capacity_{p,n}

Names: maxCumulative_capacity_p, maxCumulative_capacity_n, minCumulative_capacity_p, minCumulative_capacity_n

Σ_{d′ : (p,d′,d) ∈ edd_invest_set} v_invest_p[p,d′] · unitsize[p]
  − Σ_{d′ : (p,d′,d) ∈ edd_divest_active} v_divest_p[p,d′] · unitsize[p]
  ≤ cumulative_max_capacity[p,d] − existing[p,d]        (maxCumulative_capacity_p)
  ≥ cumulative_min_capacity[p,d] − existing[p,d]        (minCumulative_capacity_p)
  max: ∀ (p,d) ∈ ed_invest_cumulative ∩ v_invest_p entities, p ∈ authors(cumulative_max_capacity) —
       a period the author's own map leaves out is a configuration error (FA-12), not "no limit"

  min: ∀ (p,d) ∈ ed_invest_cumulative ∩ v_invest_p entities ∩ cumulative_min_capacity.frame
  _n: the node analogue with v_invest_n / v_divest_n, state_unitsize[n] and ``ndd_divest_active``.

Keeps the capacity of an entity in a period (existing + invested and still alive − every retirement up to and including d, #29, processes and storage nodes) within its cumulative limits, MW (MWh for storage nodes). Existing capacity is a constant on the right-hand side. A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/_cumulative_invest.py:370 (_eq_cumulative_capacity)

EQ. {max,min}{Invest,Divest}Group_entity_period_{p,n}

Names: maxInvestGroup_entity_period_p, maxInvestGroup_entity_period_n, maxDivestGroup_entity_period_p, maxDivestGroup_entity_period_n, minInvestGroup_entity_period_p, minInvestGroup_entity_period_n, minDivestGroup_entity_period_p, minDivestGroup_entity_period_n

Σ_{p : (g,p) ∈ group_entity} v_invest_p[p,d] · unitsize[p]  ≤ group_invest_max_period[g,d]   (max…_p)
                                                            ≥ group_invest_min_period[g,d]   (min…_p)
  ∀ (g,d) ∈ gd_invest_period, restricted to the (g,d) some member actually has the variable for
  Divest: v_divest_p with group_retire_{max,min}_period over gd_divest_period (same restriction)
  _n: the node analogues with v_invest_n / v_divest_n and state_unitsize[n]

With a construction lead time the invest rows count capacity coming online in d:

  Σ_{p : (g,p) ∈ group_entity} (Σ_{(p,o,d) ∈ edd_invest_commission} v_invest_p[p,o] · unitsize[p]
                                + carried_commission[p,d])  ≤ / ≥ group_invest_{max,min}_period[g,d]
  over the (g,d) where a member's decision comes online, g with a per-period limit (c(o) = o without a lead
  time: the form above).

Bounds the capacity a group of entities builds or retires in one period, MW (MWh for storage nodes); processes and storage nodes of a group get separate rows. Group retire_{max,min}_period is not a schema parameter, so the divest rows are reachable only with off-schema data. Under recourse ``gd_invest_period`` / ``gd_divest_period`` list a group's own invest/divest period's branch periods too (#67); a branch period with no member actually invest/divest-eligible there (e.g. a branch's own 0 beating the anchor's cost, #82) gets no row, not a vacuous 0 ≥ a positive floor.

Source: flextool/engine/model/_cumulative_invest.py:452 (_eq_group_entity_period)

EQ. maxInvestGroup_entity_total[_path]_{p,n} / minInvestGroup_entity_total_path_{p,n}

Names: maxInvestGroup_entity_total_p, maxInvestGroup_entity_total_n, maxInvestGroup_entity_total_path_p, maxInvestGroup_entity_total_path_n, minInvestGroup_entity_total_path_p, minInvestGroup_entity_total_path_n

Σ_{p:(g,p)∈group_entity} Σ_{d∈D(leaf)} v_invest_p[p,d]·unitsize[p]
  ≤ max(0, group_invest_max_total[g] − Σ_{p∈g} prior_inv[p])   (maxInvestGroup_entity_total[_path]_p)
  ≥ max(0, group_invest_min_total[g] − Σ_{p∈g} prior_inv[p])   (minInvestGroup_entity_total_path_p)
  ∀ g ∈ g_invest_total (, leaf)
  prior_inv[p] = p_entity_invested[p]  (capacity realized by all earlier solves of the
    chain); the group's prior sums every member of the axis kind, not only those with the
    variable in THIS solve — their earlier investment still counts
  _n: storage-node members, state_unitsize[n]
  max: _path (one row per (g, leaf)) only under recourse, like the entity max side.
  min: always _path — a plain per-group row, summed across alternative branches, would let
    one branch meet the floor for every branch (Q3, same reason as the entity min side)

Changed from per-(g,d) "invested and still alive" rows to one row per group (per leaf); processes and storage nodes of a group get separate rows.

Source: flextool/engine/model/_cumulative_invest.py:567 (_eq_group_invest_total)

EQ. maxDivestGroup_entity_total[_path]_{p,n} / minDivestGroup_entity_total_path_{p,n}

Names: maxDivestGroup_entity_total_p, maxDivestGroup_entity_total_n, maxDivestGroup_entity_total_path_p, maxDivestGroup_entity_total_path_n, minDivestGroup_entity_total_path_p, minDivestGroup_entity_total_path_n

Σ_{p:(g,p)∈group_entity} Σ_{d∈D(leaf)} v_divest_p[p,d]·unitsize[p]
  ≤ max(0, group_retire_max_total[g] − Σ_{p∈g} prior_div[p])   (maxDivestGroup_entity_total[_path]_p)
  ≥ max(0, group_retire_min_total[g] − Σ_{p∈g} prior_div[p])   (minDivestGroup_entity_total_path_p)
  ∀ g ∈ g_divest_total (, leaf)
  prior_div[p] = p_entity_divested[p]  (capacity retired by earlier solves of the chain, at
    realized non-branch periods only); the group's prior sums every member of the axis kind
  _n: storage-node members, state_unitsize[n]
  max: _path (one row per (g, leaf)) only under recourse, as before — the cap applies to
    each scenario path instead of adding up divestments of alternative branches.
  min: always _path (new — Q3, same reason as every other min-total family: a plain
    per-group row, summed across alternative branches, would let one branch meet the floor
    for every branch)

Bounds the capacity a group retires over the chain; processes and storage nodes of a group get separate rows. Group retire_{max,min}_total is not a schema parameter, so these rows are reachable only with off-schema data.

Source: flextool/engine/model/_cumulative_invest.py:629 (_eq_group_divest_total)

EQ. {max,min}InvestGroup_entity_cumulative

Names: maxInvestGroup_entity_cumulative, minInvestGroup_entity_cumulative

Σ_{p : (g,p) ∈ group_entity} Σ_{d′ : (p,d′,d) ∈ edd_invest_set} v_invest_p[p,d′] · unitsize[p]
  + Σ_{n : (g,n) ∈ group_entity} Σ_{d′ : (n,d′,d) ∈ edd_invest_set} v_invest_n[n,d′] · state_unitsize[n]
  − Σ_{p : (g,p) ∈ group_entity} Σ_{d′ : (p,d′,d) ∈ edd_divest_active} v_divest_p[p,d′] · unitsize[p]
  − Σ_{n : (g,n) ∈ group_entity} Σ_{d′ : (n,d′,d) ∈ ndd_divest_active} v_divest_n[n,d′] · state_unitsize[n]
  ≤ cumulative_max_capacity[g,d] − Σ_{e : (g,e) ∈ group_entity} existing[e,d]   (max…)
  ≥ cumulative_min_capacity[g,d] − Σ_{e : (g,e) ∈ group_entity} existing[e,d]   (min…)
  ∀ g ∈ g_invest_cumulative (invest_method = cumulative_limits, #5/B-Q3), d with a cap value
  and some member alive (invest) or retirable (divest) in d

Keeps the capacity of a group in a period (existing + invested and still alive − every retirement up to and including d, #30b) within its cumulative limits (schema "[MW or MWh]"); processes and storage nodes share one row. Max rows: groups that author the cap at all (any period), restricted to periods the group actually has a candidate (g, d) row for; FA-12 (check_max_cap) then catches a period that same group's own map leaves out — mirrors the entity-level cumulative row (previously this family had no FA-12 check at all, unlike the entity-level row). Min rows: the periods with a value other than 0 (a min of 0 is no floor). A period map that leaves out an investment period of the solve cannot reach here: pass 1b refuses it (``require_period_coverage``). Capacity invested by earlier solves of a chain is not counted (pre-existing, separate item). A retirement v_divest[e,d′] counts in d iff (e,d′,d) ∈ edd_divest_active (ndd_divest_active):

  d′ ≤ d < EOL[e], the end of life of e's pre-existing capacity (none for reinvest_automatic).

Source: flextool/engine/model/_cumulative_invest.py:697 (_eq_group_invest_cumulative)

EQ. {max,min}Cumulative_flow_solve[_path]

Names: maxCumulative_flow_solve, maxCumulative_flow_solve_path, minCumulative_flow_solve, minCumulative_flow_solve_path

Σ_{d,t} F[g,d,t]·w_y[d,t]  ≤ max_cumulative_flow[g] · Σ_{d,t} w_y[d,t]   (max…)
                           ≥ min_cumulative_flow[g] · Σ_{d,t} w_y[d,t]   (min…)
  ∀ g with a max / min_cumulative_flow (, per leaf under recourse)
  F = net flow into the group (_group_flow)
  w_y[d,t] = step_duration[d,t]·timestep_weight[d,t]·r[d]
  r[d] = (years_represented[d]/period_share[d]) / max_d′(years_represented[d′]/period_share[d′])

Limits the energy flowing into a flow group over the solve to an average power times the weighted hours of the solve — the same weighting every operational cost term already carries (op_factor), so a representative period or an unequal roll window does not under/over-count its hours. Under recourse each scenario path gets its own row (_path, one per (g, leaf), leaf_rows): a plain per-group row, summed across branches, would let one branch's surplus cover another's deficit. Constraints carry no stochastic branch weight — each branch must respect the limit on its own, unlike the objective (owner decision).

Source: flextool/engine/model/_cumulative_invest.py:813 (_eq_cumulative_flow_solve)

EQ. {max,min}Cumulative_flow_period

Names: maxCumulative_flow_period, minCumulative_flow_period

Σ_t F[g,d,t]·sd[d,t]·tw[d,t]  ≤ max_cumulative_flow[g,d] · Σ_t sd[d,t]·tw[d,t]   (max…)
                              ≥ min_cumulative_flow[g,d] · Σ_t sd[d,t]·tw[d,t]   (min…)
  ∀ (g,d) with a max / min_cumulative_flow
  F = net flow into the group (_group_flow); sd = step_duration, tw = timestep_weight

Limits the energy flowing into a flow group in each period to an average power times the weighted hours of the period — the same timestep_weight every operational cost term already carries (op_factor), so a representative-period step does not count as a full unweighted hour. A branch period that authors no value of its own reads its anchor's value (broadcast_to_period's branch-period inheritance); it still gets its own (g,d) row here, not merged with the anchor's — the continuation fan always starts at a solve's first step (_branch.py, d_leaf_lf), so no period is ever split into an anchor fragment and a branch fragment within one solve, and there is nothing to merge.

Source: flextool/engine/model/_cumulative_invest.py:871 (_eq_cumulative_flow_period)

EQ. {max,min}Instant_flow

Names: maxInstant_flow, minInstant_flow

F[g,d,t]  ≤ max_instant_flow[g,d,t]   (maxInstant_flow)
          ≥ min_instant_flow[g,d,t]   (minInstant_flow)
  ∀ (g,d,t) ∈ _instant_flow_support(cap, dt)
  F = net flow into the group (_group_flow), MW

Limits the net power flowing into a flow group in every step. The cap may be authored as a scalar, period map, time map or period-time map; the rows cover every active step of its shape. Row rule (coarse steps): a flow group whose processes are all coarse gets one row per coarse step, {max,min}Instant_flow_coarse = the row's Δt-weighted mean over the step (_coarse_flows.add_cstr): the step's constant net flow against the step-mean cap (named in the coarse-flow behaviour notice when the cap varies within a step). A group with a fine process keeps its rows per solve step.

Source: flextool/engine/model/_cumulative_invest.py:909 (_eq_instant_flow)

Commodity price ladder

Commodity price ladder: a commodity bought in tiers, each with its own price and quantity.

VAR. v_trade

v_trade[c,n,d,i] ≥ 0   ∀ (c,n,d,i) ∈ cndi_ladder = cndi_ladder_ann ∪ cndi_ladder_cum

Purchase of commodity c at node n in period d in price tier i, in units of the commodity unitsize: MWh = v_trade · commodity_unitsize[c].

Source: flextool/engine/model/_commodity_ladder.py:295 (add_variables)

EQ. commodity_ladder_balance

Σ_i v_trade[c,n,d,i] · commodity_unitsize[c]
  = Σ_{(p,n,k,c) ∈ flow_from_commodity_eff, t}
      (v_flow[p,n,k,d,t] · unitsize[p] · slope[p,d,t] + v_online[p,d,t] · section[p,d,t] · unitsize[p]) · w[d,t]
  + Σ_{(p,n,k,c) ∈ flow_from_commodity_noEff, t} v_flow[p,n,k,d,t] · unitsize[p] · w[d,t]
  − Σ_{(p,s,n,c) ∈ flow_to_commodity, t}         v_flow[p,s,n,d,t] · unitsize[p] · w[d,t]
  + Σ_{(p,n,c) ∈ startup_from_commodity, t} v_startup[p,d,t] · startup_energy[p,n,d] · unitsize[p]
      · timestep_weight[d,t]   (start-up energy, MWh per event: no step_duration; core._startup_draw)
  ∀ (c,n,d) ∈ cnd_ladder;   w = step_duration · timestep_weight

The period's net purchase of commodity c at node n (buy − sell, MWh) is split into price tiers. The efficiency-arc term uses the shared fuel measure _eff_fuel_input (core.py), so min_load_efficiency units' part-load section fuel counts toward the tiers too, same as commodity_cost_eff / co2_price_eff / the CO2 caps (#56) — before this fix a ladder commodity bought by such a unit was under-counted by the part-load fuel. A term is absent when its flow set is empty or there are no processes.

Source: flextool/engine/model/_commodity_ladder.py:328 (_eq_ladder_balance)

EQ. ladder_tier_cap_annual_roll

Σ_n v_trade[c,n,d,i] · commodity_unitsize[c]
  ≤  quantity_ann[c,i,d] · period_share[d] · f_d_k[d] − realized_ann[c,i,α(d)]
  ∀ (c,i,d) with (c,n,d,i) ∈ cndi_ladder_ann and quantity_ann[c,i,d] < 1e29
  f_d_k[d] = (realized hours of α(d) from earlier solves + this solve's hours of d) / complete hours of α(d)
  α(d) = the period label of a forecast branch period d, d itself otherwise; period_share, f_d_k and the
  realized carry of a forecast come from α(d) unless it has its own row: every forecast continues from
  its period's realized history (the realization solve's carry)
quantity_ann is a per-YEAR amount (GMPL "per-year MWh"): period_share[d] scales it down to this
period's own share of the modelled year, matching the price term's inflation_op/period_share
annualisation (#89) — a 48h solve of a 48h period gets 48h/8760 of the annual tier, not the whole
year's worth.  f_d_k[d] (1 outside rolling) further scales it to the share of the period's own
hours reached by the end of this roll's horizon (f_d_k already measures exactly that — see
p_f_d_k_from_source) — the quantity allocated so far, as of this roll.  realized_ann[c,i,d] (the
same per-(c,i,d) carried-purchases Param the cumulative ladder uses, 0 when absent) is what
earlier rolls already bought against that allocation, so the remaining budget can never go
negative: an earlier roll could buy at most up to its own (earlier) horizon end and realized
only part of that (#143's fix applies here too — no clamp needed, the row is feasible by
construction instead of approximately-compliant). Unlimited tiers (quantity ≥ 1e29) get no row.

Branch periods of a stochastic solve use their anchor period's quantity (and price) unless they have their own (inherit_branch_periods). share, f_d_k and the realized carry of a forecast come from α(d) unless it has its own row (branch_periods_inherit): every forecast continues from its period's realized history (the realization solve's carry); without it a null share/fdk would clamp the tier to 0, and a realized 0 would re-open the period's already-spent budget (#59).

Source: flextool/engine/model/_commodity_ladder.py:382 (_eq_tier_cap_annual)

EQ. ladder_tier_cap_cumulative_roll

Σ_{n,d} v_trade[c,n,d,i] · commodity_unitsize[c]
  ≤  quantity_cum[c,i] · Σ_d (years_represented[d] · realized_hours[d] / 8760) − Σ_d realized[c,i,d]
  ∀ (c,i) ∈ ci_ladder_cumulative with quantity_cum[c,i] < 1e29;  (c,n,d,i) ∈ cndi_ladder_cum

Caps what tier i supplies over the whole modelled horizon. quantity_cum, like quantity_ann, is a per-modelled-YEAR amount: Σ_d (years_represented[d] · realized_hours[d] / 8760) is the real- calendar-years-equivalent of representative dispatch simulated so far — realized_hours[d]/8760 annualises each period's own realized representative-sample hours up to a full calendar year (the hours-form of period_share[d]·f_d_k[d], #89's annualisation), and years_represented[d] then multiplies that by the number of real calendar years period d stands for (co2_max_total scales its emissions cap the same way, docs: "each period's annualised emissions times the years it represents"); without it a period block representing several years was charged for only one (p_ladder_cum_weight, load_data). Sums over every period the chain has ever touched — not just the ones still active this roll — the same reason f_d_k itself is read from a provider CSV that folds finished periods back in (#143): without that, this sum would reset to the new period's own local progress each time the chain moved on, while realized kept climbing, eventually driving this row negative. "quantity allocated up to the end of this roll's horizon" is monotone non- decreasing across rolls — the same requirement #89's fix gives the annual cap — and realized (what earlier rolls actually bought, always ≤ what their own, earlier horizon end allocated) can never exceed it, so the row is feasible by construction; no clamp. Without p_ladder_cum_weight (load_data couldn't build the years_represented/realized_hours Param — e.g. a synthetic fixture that skips the derived-param pass) the weight is Σ f_d_k over the path's periods, fan labels left out (a member's f_d_k holds its period's realized hours), the pre-years_represented-fix formula; realized = tier MWh bought by earlier solves (0 when absent). Unlimited tiers get no row.

Source: flextool/engine/model/_commodity_ladder.py:491 (_eq_tier_cap_cumulative)

EQ. ladder_tier_cap_cumulative_roll_path

Σ_{n, d∈D(leaf)} v_trade[c,n,d,i] · commodity_unitsize[c]
  ≤ quantity_cum[c,i] · ( Σ_{d∈D(leaf)} w[d] + Σ_{d∉period_in_use} w[d] ) − Σ_d realized[c,i,d]
  ∀ (c,i) ∈ ci_ladder_cumulative with quantity_cum[c,i] < 1e29, leaf ∈ d_leaf, when v_trade spans >1 leaf

One row per scenario path (#58): the pooled row's weight summed every forecast into one budget, letting the forecasts buy N× the tier between them. w[d] = years_represented[d]·realized_hours[d]/8760 (p_ladder_cum_weight; fallback f_d_k[d] with this solve's fan labels left out). D(leaf) = the leaf's branch periods, its fan labels and the shared pre-reveal periods (the (d, leaf) join of d_leaf, as in the invest/divest _path families' leaf_rows); a shared period's weight counts once on each path. The second sum is the periods earlier solves and rolls finished: one realized past shared by every path, matching realized, which is the chain-wide carry of what that past bought. Every period of period_in_use with a weight lies on some leaf (raises otherwise), so the second sum holds only the finished history; it is added only when non-zero. A chained earlier solve's period missing from this solve's p_years_represented.csv counts with years_represented 1 (as in the pooled row). No branch weight on the row: constraints are per path, the branch weights belong to the objective.

Source: flextool/engine/model/_commodity_ladder.py:535 (_eq_tier_cap_cumulative)

OBJ. commodity ladder

Names: commodity_ladder_cost

+ Σ_{(c,n,d,i) ∈ cndi_ladder_cum} v_trade[c,n,d,i] · cu[c] · price_cum[c,i]
                                  · inflation_op[d] / period_share[d] · bw[d]
+ Σ_{(c,n,d,i) ∈ cndi_ladder_ann} v_trade[c,n,d,i] · cu[c] · price_ann[c,i,d]
                                  · inflation_op[d] / period_share[d] · bw[d]
  cu = commodity_unitsize;  bw[d] = branch_weight[d,t], the same for every t of d (factor 1 outside
  stochastic solves)

Each tier's purchase at its price, annualised per period and weighted by the probability of the period's branch, as w_op weights the other operational costs. v_trade is already a period sum, so no step duration or timestep weight enters; a period whose branch weight varies over t raises.

Source: flextool/engine/model/_commodity_ladder.py:632 (objective_terms)

Non-anticipativity

Non-anticipativity: decisions taken before a forecast is revealed are equal in every forecast branch.

EQ. non_anticipativity_storage_use

net[n,d,t] − net[n,b,t] = 0
  net[n,d,t] = Σ_{(p,s,n) ∈ flow_to_n} v_flow[p,s,n,d,t] · unitsize[p] · Δt[d,t]
             − Σ_{(p,n,k) ∈ flow_from_nodeBalance_eff}
                 (v_flow[p,n,k,d,t] · slope[p,d,t] + v_online[p,d,t] · section[p,d,t]) · unitsize[p] · Δt[d,t]
             − Σ_{(p,n,k) ∈ flow_from_nodeBalance_noEff} v_flow[p,n,k,d,t] · unitsize[p] · Δt[d,t]
             − Σ_{(p,s,n) ∈ twoway1var_arcs} v_flow_back[p,s,n,d,t] · unitsize[p] · Δt[d,t]
             + Σ_{(p,n,k) ∈ twoway1var_arcs} v_flow_back[p,n,k,d,t] · unitsize[p] · Δt[d,t]
             − Σ_{(p,n) ∈ startup_to_balance} v_startup[p,d,t] · startup_energy[p,n,d] · unitsize[p]
  ∀ n ∈ nodes of groupStochastic ∩ nodeBalance, (d,b) ∈ forecast star, (d,t) ∈ tied steps
  section only for p ∈ process_min_load_eff (v_online = v_online_linear or v_online_integer)

The net flow into a stochastic node is the same in every forecast over the tied steps: exactly the flow terms of nodeBalance_eq, built by the same helper (_balance_terms.balance_flow_terms). It ties the flows, not v_state, and leaves out the balance slacks: they could otherwise absorb a flow difference. The section input counts because efficiency and min_load may differ between forecasts. A node of the group without an energy balance (e.g. a commodity node) has no balance flow terms to tie. A coarse process has one flow column per coarse step, so a tie at one of its steps holds the whole step; it is coarse only where each of its coarse steps is wholly tied or wholly untied (flex_data.coarse_tie_straddles). Row rule (coarse steps): a node whose processes are all coarse gets one tie per wholly tied coarse step, non_anticipativity_storage_use_coarse = the row's Δt-weighted mean over the step (_coarse_flows.add_cstr, the weights of forecast d's coarse step, which the reference forecast b shares over the tied steps): the forecasts' net energy into n over the step is the same, with a forecast-specific slope or section through its step mean. A node with a fine process keeps its ties per solve step, reading the coarse flows constant.

Source: flextool/engine/model/_non_anticipativity.py:208 (add_dispatch_constraints)

EQ. non_anticipativity_storage_state_first

v_state[n,d,t_first(d)] − v_state[n,b,t_first(d)] = 0
  ∀ n ∈ nodes of groupStochastic, (d,b) ∈ forecast star, t_first(d) = first tied step of d,
    only where the tied steps of d are fewer than the v_state steps of (n,d)

The net-charge tie above fixes the shape of the state over the tied steps but leaves a constant offset between forecasts; this row removes it. Where the tied steps cover the whole horizon the state is already bound at both ends (start and reference value) and the row is redundant, so it is emitted only when untied steps follow (rolling). The offset is pinned at the optimum, not by construction: interior drift stays feasible where balance slack or forecast-specific inflow absorbs it. v_online and v_reserve need no analogue: their ties pin the variable itself at every tied step.

Source: flextool/engine/model/_non_anticipativity.py:254 (add_dispatch_constraints)

EQ. non_anticipativity_rp_block_start

v_state_rp_start[n,d,t] − v_state_rp_start[n,b,t] = 0
  ∀ n ∈ nodes of groupStochastic ∩ nodeState_rp, (d,b) ∈ forecast star, (d,t) ∈ tied steps,
    (d,t) and (b,t) ∈ rp_block_first, (n,d,b,t) ∉ the rows of non_anticipativity_storage_state_first

A representative block starts from a free level; inside the tie window it is a decision taken before the forecast is known, so it is the same in every forecast (the net-flow tie fixes only the shape of the state within the block). A block whose first step already has a non_anticipativity_storage_state_first row is left out: there v_state at t_first (the end of the block's first step) is tied, so its start is tied up to the forecasts' difference in balance slack (and inflow) over that step; the net flows of the step are tied already. The base-period levels v_state_inter need no tie of their own: inside the window they follow from the tied net flows, these block starts and the shared chain start (the trunk period's end level, or rp_inter_period_fan_start).

Source: flextool/engine/model/_non_anticipativity.py:279 (add_dispatch_constraints)

EQ. non_anticipativity_online_{integer,linear}

Names: non_anticipativity_online_integer, non_anticipativity_online_linear

v_online[p,d,t] − v_online[p,b,t] = 0
  ∀ p ∈ process_online_{integer,linear}, (d,b) ∈ forecast star, (d,t) ∈ tied steps,
    (p,d,t) and (p,b,t) ∈ p_online_dt

Units online are the same in every forecast over the tied steps, the committed section decided before the forecast is known (inside a fan: the committed cells of its period label, solve_data/committed_in_fan). With no tied step (shared_operation_periods = [], or a rolling fan only in the look-ahead) they are free. A coarse process (processCoarse) is tied once per coarse step, at its first fine step: it is coarse only where each of its coarse steps is wholly tied or wholly untied and both forecasts have the same coarse steps (flex_data.coarse_tie_straddles).

Source: flextool/engine/model/_non_anticipativity.py:326 (add_dispatch_constraints)

EQ. non_anticipativity_reserve

v_reserve[p,r,ud,n,d,t] − v_reserve[p,r,ud,n,b,t] = 0
  ∀ (p,r,ud,n) ∈ process_reserve_upDown_node_active, (d,b) ∈ forecast star, (d,t) ∈ tied steps

Reserve offered is the same in every forecast over the tied steps. A coarse process is tied once per coarse step, at its first fine step (coarse only where each coarse step is wholly tied or untied, as the units online).

Source: flextool/engine/model/_non_anticipativity.py:365 (add_dispatch_constraints)

EQ. non_anticipativity_{invest,divest}_{p,n}

Names: non_anticipativity_invest_p, non_anticipativity_invest_n, non_anticipativity_divest_p, non_anticipativity_divest_n

v_invest[e,d] − v_invest[e,b] = 0,   v_divest[e,d] − v_divest[e,b] = 0
  ∀ (d,b) ∈ pd_non_anticipativity, (e,d) and (e,b) ∈ {pd,nd}_{invest,divest}_set,
    e = process (p) or node (n); the divest sets hold the periods with pre-existing capacity only

Investment and divestment inside the window are the same in every branch. Divestment must be tied too: under recourse it has a column per branch like investment, and a free divestment would let the net capacity existing + invest − divest differ across branches. pd_non_anticipativity.b holds period labels typed on the branch axis; it is re-typed on the period axis, the axis of the invest sets' d and of the variable view's b. The (e,b) check drops every pair when the branches have no invest column of their own.

Source: flextool/engine/model/_non_anticipativity.py:411 (add_invest_constraints)

Coarse processes

Coarse processes: one value per coarse step of their nodes (FlexData.processCoarse).

EQ. <family>_coarse: maxFlow_coarse, maxFlow_negCap_coarse, maxFlow_back_coarse, maxOutputSum_coarse, maxOutputSum_negCap_coarse, conversion_indirect_coarse, minInputShare_coarse, profile_flow_{upper_limit,lower_limit,fixed}_coarse, maxFlow_online{sfx}_coarse, maxFlow_online_shutdown{sfx}_coarse, maxFlow_online_shutdown_branch{sfx}_coarse, maxFlow_back_online{sfx}_coarse, maxInputFlow_online{sfx}_coarse, maxInputFlow_online_shutdown{sfx}_coarse, maxInputFlow_online_shutdown_branch{sfx}_coarse, maxOutputSum_online{sfx}_coarse, minFlow_minload{sfx}_coarse, minFlow_output_floor{sfx}_coarse, minFlow_reserve_coarse, minFlow_reserve_arc_coarse, reserve_process_{upward,downward}_coarse, connection_reserve_{up,down}_coarse, connection_reserve_{up,down}_online{sfx}_coarse, connection_reserve_{up,down}_exact_coarse, reserve_storage_up_floor_coarse, reserve_storage_down_headroom_coarse, maxFlow_exact_{forward,reverse}_coarse; by the row rule (add_row_rule_cstr): dc_flow_eq_coarse, capacityMargin_coarse, inertia_constraint_coarse, non_sync_constraint_coarse, {max,min}Instant_flow_coarse, process_constraint_{equal,less_than,greater_than}_coarse, non_anticipativity_storage_use_coarse ({sfx} = _linear / _integer)

Names: maxFlow_coarse, maxFlow_negCap_coarse, maxFlow_back_coarse, maxOutputSum_coarse, maxOutputSum_negCap_coarse, conversion_indirect_coarse, minInputShare_coarse, profile_flow_upper_limit_coarse, profile_flow_lower_limit_coarse, profile_flow_fixed_coarse, maxFlow_online_linear_coarse, maxFlow_online_integer_coarse, maxFlow_online_shutdown_linear_coarse, maxFlow_online_shutdown_integer_coarse, maxFlow_online_shutdown_branch_linear_coarse, maxFlow_online_shutdown_branch_integer_coarse, maxFlow_back_online_linear_coarse, maxFlow_back_online_integer_coarse, maxInputFlow_online_linear_coarse, maxInputFlow_online_integer_coarse, maxInputFlow_online_shutdown_linear_coarse, maxInputFlow_online_shutdown_integer_coarse, maxInputFlow_online_shutdown_branch_linear_coarse, maxInputFlow_online_shutdown_branch_integer_coarse, maxOutputSum_online_linear_coarse, maxOutputSum_online_integer_coarse, minFlow_minload_linear_coarse, minFlow_minload_integer_coarse, minFlow_output_floor_linear_coarse, minFlow_output_floor_integer_coarse, minFlow_reserve_coarse, minFlow_reserve_arc_coarse, reserve_process_upward_coarse, reserve_process_downward_coarse, connection_reserve_up_coarse, connection_reserve_down_coarse, connection_reserve_up_online_linear_coarse, connection_reserve_up_online_integer_coarse, connection_reserve_down_online_linear_coarse, connection_reserve_down_online_integer_coarse, connection_reserve_up_exact_coarse, connection_reserve_down_exact_coarse, reserve_storage_up_floor_coarse, reserve_storage_down_headroom_coarse, maxFlow_exact_forward_coarse, maxFlow_exact_reverse_coarse, dc_flow_eq_coarse, capacityMargin_coarse, inertia_constraint_coarse, non_sync_constraint_coarse, maxInstant_flow_coarse, minInstant_flow_coarse, process_constraint_equal_coarse, process_constraint_less_than_coarse, process_constraint_greater_than_coarse, non_anticipativity_storage_use_coarse

Σ_{t∈b} w[K,d,b,t] · ( LHS_t − RHS_t )  {≤,=,≥}  E[K,d,b]      w = Δt / Δ_b,  Δ_b = Σ_{t∈b} Δt
  ∀ coarse row key K (a coarse process p, or a row key whose every member is coarse: row_partition — a group
  g, a user constraint cn, a stochastic node n, a DC arc between coarse-angle nodes), (…, d, b)
  its coarse steps (b = its first fine step b_first);  LHS_t / RHS_t = the level terms of the fine row of the
  family at (d, t);  E = the row's event terms on the coarse grid (start-ups at b, shut-downs at the coarse
  successor b⁺: the capacity reduction of maxFlow_online / maxInputFlow_online and their _shutdown rows);
  only where the fine family has a row of K at every fine step of b — a key whose rows cover part of b (a
  limit authored on part of the step) keeps its fine rows there, reading the coarse terms at their constant
  value (_split)

The fine row's Δt-weighted average over the coarse step. The flow is one column per coarse step, so its own term averages to itself; time-varying availability, profile, efficiency (slope) and capacity coefficients act through their step means (the mean of a product, not the product of means); so do the units online and the reserve (one value per coarse step) times a time-varying availability, profile or minimum load. With one time-varying limit per arc the averaged row allows exactly the energy the fine rows allow over the step; with several it is their convex relaxation (it can be looser).

Source: flextool/engine/model/_coarse_flows.py:236 (add_cstr)

Benders decomposition master

Benders (Option C) regional decomposition — master + multi-cut loop.

VAR. C

C[conn] ≥ 0   ∀ conn ∈ the connections of the cross-region arcs

Capacity invested in a trade connection, in units of its unitsize (as v_invest_p): one column per connection, summed over the periods it may invest in (the hand master, master="hand").

Source: flextool/engine/solve/_benders.py:1123 (_build)

VAR. f

f[p,source,sink,d,t] ≥ 0   ∀ cross-region arcs (p,source,sink), (d,t) on the arc's grid

Trade flow on a cross-region arc, in units of the connection's unitsize, keyed by the same (p,source,sink) triple the region half-flows carry so the pin values map 1:1.

Source: flextool/engine/solve/_benders.py:1131 (_build)

VAR. eta

eta[r] ≥ eta_floor   ∀ regions r

The recourse cost of region r that the optimality cuts bound from below (scaled objective units). The large-negative finite floor (_ETA_FLOOR_MULT) keeps the cut-less iter-0 master optimal without cutting off the optimum; the f̄ = 0 bootstrap then seeds the real cuts.

Source: flextool/engine/solve/_benders.py:1143 (_build)

EQ. trade_capacity

C[conn] − f[p,source,sink,d,t] ≥ 0   ∀ arcs (p,source,sink) of conn, (d,t) on the arc's grid

A trade flow stays within the capacity invested in its connection. Both sides are in units of the connection's unitsize, so it cancels. One row per (arc, d, t); the connection key is a join column so Where aligns f with its connection's C. Existing capacity of the connection is not in the row: the hand master treats every trade connection as greenfield (the flextool master path counts it).

Source: flextool/engine/solve/_benders.py:1152 (_build)

EQ. maxInvest

C[conn] ≤ Σ_{d ∈ invest periods of conn} max_units[conn,d]   ∀ conn

Caps the invested capacity (max_units = p_entity_max_units, units of unitsize), mirroring the monolith's maxInvest_var_bound; it also bounds the master before enough cuts accumulate.

Source: flextool/engine/solve/_benders.py:1175 (_build)

OBJ. master_objective

Σ_conn C_cost[conn] · C[conn] + Σ_r eta[r]
  C_cost[conn] = s · unitsize[conn] · Σ_{d ∈ invest periods of conn} (annual_discounted[conn,d]
                                                                  + lifetime_fixed_cost[conn,d])
  s = obj_scale (scale_the_objective), so the master lives in the scaled space of the regions

The invest cost of the trade capacity, as OBJ. invest_cost_p of the monolith, plus the regions' recourse costs.

Source: flextool/engine/solve/_benders.py:1194 (_build)