Decomposition
Decomposition in FlexTool means splitting a monolithic LP into smaller
solvable pieces, then recombining their solutions. FlexTool supports
three independent flavours, all implemented natively in
engine:
- Spatial / Benders — partition the network into regions joined
only by the inter-regional trade flows; a coordinating master holds
those trade flows plus the trade-connection investment, and each
region is an operational subproblem whose duals become optimality
cuts the master accumulates until the optimality gap closes. Selected
per solve from the database (
solve.decomposition = benders); see §2.8. - Flex-temporal — balance some nodes at a coarser step than the
solve (hourly power, daily hydrogen, …) in one LP; every flow stays on
the solve's steps. Configured per node via
group.new_stepduration. - Sequential / rolling / nested — split a long horizon into
overlapping windows (rolling) or into an outer long-horizon solve
whose decisions feed an inner short-horizon solve (nested). The user
configuration is in
solve.solve_mode/solve.contains_solves; the engine wiring is insolve/_recursive_solve.py+solve/_handoff.py.
Spatial and flex-temporal are LP-level decompositions (one solve, many sub-problems). Rolling / nested is a meta-level decomposition (one chain of solves, each with its own LP). They compose, with one exception noted in §6.
The engine-side overview of the per-solve pipeline and warm-start / Benders / cascade solve modes lives in engine.md; this page focuses on the decomposition mechanics themselves.
1. Picking a decomposition
| Symptom | Use |
|---|---|
| Monolithic LP slow, but the system has clear regional structure (multi-country grid, sparse interconnections) | Spatial / Benders |
| Power demand needs hourly resolution but some commodities (long-duration storage, hydrogen, district heat) only need daily / weekly resolution | Flex-temporal |
| Long horizon (decades, full reanalysis years) that does not fit one solve | Rolling |
| Long-term storage needs a multi-year view, but dispatch needs intra-week resolution | Nested |
| Capacity-expansion at coarse resolution + intra-week dispatch at fine resolution | Nested |
| Multiple operational scenarios sharing the same realised path | Nested + stochastic branches |
The three flavours are orthogonal in intent (a decomposition along the geography axis, the time-resolution axis, and the horizon axis). §6 covers which combinations are actually wired today.
2. Spatial / Benders decomposition
2.1 Concept
Benders decomposition splits the LP into a coordinating master and a
set of operational subproblems, one per region. The complicating
decisions — the inter-regional trade flows and the trade-connection
investment — live in the master; everything inside a region (its own
network, generators, storage, demand) is a subproblem solved with the
master's trade schedule pinned. Each subproblem returns the duals of
its pinned trade columns; the master accumulates those as optimality
cuts and re-solves. The master objective is a valid lower bound on
the true optimum; the incumbent feasible cost is the upper bound; the loop
stops when the optimality gap (UB − LB) / |UB| drops below tolerance.
This replaces the earlier dual-subgradient (Lagrangian) scheme, which severed each cross-region arc into invest-less half-flows bounded near zero and collapsed greenfield trade to an autarkic solution with an invalid bound above the true optimum. Benders puts the trade investment and the trade flow / capacity coupling in the master, feeds each region the master's chosen flow as a pinned boundary injection, and returns a certified lower bound and the true optimum.
The master is built by build_flextool over a network-only reduced
FlexData (_region_filter.master_network_data) — the SAME emit the
monolith uses — so the master picks up the trade maxFlow capacity
coupling, the invest annuity, and the connection flow cost natively and
RP-weight-consistently: the master's trade flow cost and the regions'
recourse costs are both weighted by the same representative-period /
timestep weights build_flextool applies everywhere, so summing them is
arithmetically the monolithic objective (no hand-rolled annuity, no
weight mismatch). One recourse variable η_r per region carries that
region's operational cost in the master objective.
flowchart TD
A[FlexData whole system] --> B[_region_filter.split
regions + half-flows]
A --> M0[_region_filter.master_network_data
network-only reduced FlexData]
M0 --> M[Master = build_flextool + WarmProblem
trade flow f, invest C, recourse η_r]
B --> C[Per-region Problem
build_flextool, trade pinned to f̄]
M --> C
C --> D[subproblem duals
col_dual of pinned trade cols]
D --> E[master.add_cut_row
optimality cut per region]
E --> M
M --> F{gap ≤ tol?}
F -- iterate --> C
F -- stop --> G[BendersResult
region_objectives, LB/UB, log]
2.2 Region declaration
Regions are group entities whose decomposition_method parameter
equals the string benders_regional. The group's
group__node / group__unit / group__connection memberships list
the entities belonging to that region. The coordinator requires at
least two such groups in the active scenario; one is treated as an
error (use the monolithic path instead).
# Spine input DB fixture sketch.
entities += [("group", "region_A"), ("group", "region_B")]
parameter_values += [
("group", "region_A", "decomposition_method", "benders_regional", ALT),
("group", "region_B", "decomposition_method", "benders_regional", ALT),
]
for r in ("A", "B"):
for node in [f"elec_{r}", f"h2_{r}", f"battery_{r}"]:
entities.append(("group__node", (f"region_{r}", node)))
The reference catalogue entry is group.decomposition_method in
Groups; the only accepted value is
benders_regional (the _region suffix makes the geographic flavour
explicit, leaving naming room for any future temporal variant). The
source of truth is
flextool/engine/solve/_region_filter.py:load_decomposition_method, which
reads the sub-solve provider's input/p_group_decomposition frame (a
provider is required; the work folder is never read).
2.3 Half-flow rewriting
Every connection whose endpoints straddle two region groups is rewritten into a pair of import / export half-flows through a virtual commodity node, one per region:
hf_<pipe>__export__<region_A> (in region A, virtual sink node)
hf_<pipe>__import__<region_B> (in region B, virtual source node)
Inside a region's standalone LP each half-flow is an ordinary v_flow
column. The export and import sides of an arc carry the same flow at
optimality. In the Benders scheme that consensus is enforced by the
master: each iteration pins every region's forward cross-region
half-flow to the master's current f̄ per (d, t) (reverse pinned to 0)
before solving the region. The cut slope for (arc, d, t) is then the
reduced cost of the pinned forward column
(Solution.col_dual[pin_col_id] = ∂cost_r / ∂f̄, basis-correct, no
monolith reference). The rewriter / half-flow metadata structure is in
_region_filter (HalfFlow, Coupling, split); the arc / pin
bookkeeping for the master lives in _benders._build_arcs.
Star topologies (more than two regions sharing one pipeline) are not supported; the rewriter asserts bilateral arcs only.
2.4 Algorithm — master + multi-cut Benders loop
The master is a single polar_high.Problem wrapped in a
polar_high.WarmProblem, built ONCE and grown by appended optimality cut
rows. The loop:
- Bootstrap
f̄ = 0, solve every region, harvest the first cuts. - Master solve → new trade schedule
f̄and investC; the master objective is the lower boundLB(a valid global under-estimate — the whole point versus the old scheme). - Subproblems: pin each region's forward trade columns to
f̄, solve, readcol_dualof the pinned columns. - Cuts: append one optimality cut per region via
WarmProblem.add_cut_row, bounding that region's recourseη_r. - Upper bound:
UB = master invest cost(C) + Σ_r cost_r(f̄); incumbent = best (min)UB. - Repeat from 2 until
gap = (best_UB − LB) / |best_UB| ≤ tolor the iteration cap is hit.
The recourse vars and cut rows are appended through the polar-high
primitives WarmProblem.add_recourse_col and WarmProblem.add_cut_row;
each region subproblem is solved with WarmProblem.solve(retry_on_unknown=
…) so a transient HiGHS kUnknown is retried rather than aborting the
run. Each η_r is lower-bounded by a large-NEGATIVE finite floor (NOT a
hard η ≥ 0): FlexTool region costs can be negative, so a blind zero floor
could cut off the optimum; the finite floor is a provably valid global
under-estimate that keeps the cut-less iter-0 master optimal.
The two knobs:
DB parameter (solve entity) |
Default | Meaning |
|---|---|---|
benders_max_iter |
50 |
Outer-loop iteration cap |
benders_tolerance |
1e-3 |
Relative optimality-gap threshold (gap normalised by the incumbent UB) |
They are resolved per solve by SolveConfig.benders_config_for; a solve
that authors neither falls back to these defaults.
2.5 Existing vs investable trade capacity
The master handles existing and investable trade connections in ONE
build. An existing-only arc (existing > 0, no invest_method) carries
no invest variable; its flow is bounded by the existing unit count (existing_count = capacity / unitsize) through
the FlexTool maxFlow RHS (p_flow_upper_existing). An investable arc
gets a v_invest var and its capacity is bounded by existing +
invested. A greenfield arc (existing = 0) is unchanged — all its trade
capacity is invested C. Because the master is the SAME emit as the
monolith, the existing-capacity term and the invest annuity drop in with
no rescale, and the per-region half-flows are left uncapped
(benders_uncap_cross_region=True) so a positive master pin is always
feasible in the region.
2.6 Validity guard
A monolith-free sandwich guard is always on: LB ≤ optimum ≤ best_UB
must hold (best_UB is itself an upper bound on the optimum). An
LB > best_UB is the genuine invalid-lower-bound pathology — exactly the
bug this scheme fixes — and raises RuntimeError rather than reporting a
wrong-but-plausible optimum. No external big-M is needed.
2.7 Diagnostics
Every outer iteration logs the iteration index, the master LB, the
incumbent UB, and the current gap; the orchestrator also prints a final
convergence + LB/UB sandwich summary. The same data is available on the
returned BendersResult:
BendersResult.iteration_log— per-iteration dicts (LB / UB / gap trajectory)BendersResult.lower_bound/upper_bound/gap— final valuesBendersResult.region_objectives— per-region recourse cost at the incumbentBendersResult.converged— True iff the gap dropped belowtolbefore the iteration cap
Look for:
- the gap closing monotonically (LB rising, UB falling toward each
other); a stalled gap usually means
benders_max_iteris too low or the cut slopes are degenerate (a region whose pinned trade column is basic returns a zero reduced cost) - region objectives broadly similar magnitude — one region dominating often means the cut placement is wrong (a major load centre ended up in a region with too little generation)
2.8 Selecting the scheme per solve
Decomposition is chosen per solve, from the database — there is no
CLI flag. Set solve.decomposition = benders on the solve(s) you
want decomposed, optionally overriding the two benders_* knobs:
solve parameter |
Value list / type | Default | Effect |
|---|---|---|---|
decomposition |
decomposition_schemes = {none, benders} |
none |
benders routes this solve through solve_benders; none solves it monolithically |
benders_max_iter |
float | 50 |
Outer-loop iteration cap |
benders_tolerance |
float | 1e-3 |
Relative optimality-gap threshold |
Because the decision is per solve, a single chain can mix schemes — for
example a Benders investment solve followed by a monolithic
full-resolution dispatch solve. The orchestrator
(engine.solve._orchestration) reads each solve's resolved
decomposition value as it iterates model.solves. This is distinct
from the group-level group.decomposition_method
(none / benders_regional), which declares which groups are
regions; solve.decomposition declares whether a given solve
decomposes. Benders still requires at least two benders_regional
groups.
Invest → dispatch handoff. A Benders investment solve assembles the
owner-selected invested trade + in-region capacity into a whole-system
SolveHandoff (the TIER-1 invest→dispatch chain) so a downstream rolling
or monolithic dispatch solve consumes the realised capacity exactly as it
would after any other invest solve. The orchestrator records which Benders
solves produced an invest handoff and threads it forward; a downstream
solve that would consume a Benders solve which produced none is rejected
loudly rather than silently dropping the link.
The --region <GROUP_NAME> flag is a separate, filter-only entry
point that emits a per-region input directory and exits without
solving — useful for inspecting how the rewriter sees a given region.
2.9 Reference implementation
| Symbol | Module |
|---|---|
solve_benders(data, *, ...) — top-level entry |
flextool/engine/solve/_benders.py |
BendersResult — return type |
same |
Coupling, _build_arcs, the master + cut loop |
same |
master_network_data (network-only reduced FlexData for the master) |
flextool/engine/solve/_region_filter.py |
RegionSplit, HalfFlow, split |
same |
load_decomposition_method (reads decomposition_method from the provider's input/p_group_decomposition) |
same |
WarmProblem.add_cut_row / add_recourse_col / solve(retry_on_unknown=…) |
polar_high (external) |
The per-solve routing lives in flextool/engine/solve/_orchestration.py
(_PolarHighCascadeSolver._run_benders_solve, driven by
SolveConfig.decomposition_for). The Benders path is also surfaced
as a "Solve modes" entry in
engine.md § Solve modes.
3. Flex-temporal decomposition
3.1 Concept
Mixed-resolution scheduling: some nodes (power demand, VRE, batteries)
need fine timesteps; others (long-duration storage, hydrogen, district
heat) only need a coarser energy balance. A node of a group whose
new_stepduration is longer than the solve's step is a coarse node:
it is balanced once per coarse step by summing its fine balance terms
(nodeBalanceCoarse_eq), and a coarse storage node keeps one state per
coarse step (stateConstantCoarse_eq). An arc between a fine and a
coarse node stays on the solve's steps and enters both balances, so it
conserves energy at both ends without any per-arc weights or filters.
A unit or connection whose every balance node is coarse is a coarse
process (params._coarse.coarse_process_frames → processCoarse,
process_coarse_step): its flows, units online, start-ups / shut-downs,
reserve and the direction of an exact connection have one column per
coarse step (model/_coarse_flows, §3.6). Only a process with a delay
keeps the solve's steps (flex_data.coarse_process_exclusions), plus
Benders cross-region arcs and master flows (§3.5), stochastic processes
whose coarse step a forecast tie covers in part
(flex_data.coarse_tie_straddles) and realization-solve processes whose
pin varies within a step (flex_data.coarse_pin_varies); an info line
names each with its reason.
This is structurally different from the Benders path: there is one LP, one HiGHS run, no outer loop.
3.2 Schema surface
| Parameter | Entity | Effect |
|---|---|---|
solve.new_stepduration |
solve |
Solve-wide step (aggregates the solve's timeline) |
group.new_stepduration |
group |
The group's balance / storage nodes are balanced per coarse step of this length (when longer than the solve's step) |
group.decomposition_method |
group |
Benders region groups |
group__node |
(relation) | The coarse nodes |
group__unit / group__connection |
(relation) | No effect on time resolution (a warning names a listed process that touches a node outside the group — Q-c15-A) |
Validation in validate_group_membership:
- every entity may belong to at most one resolution-group
- every entity may belong to at most one decomposition-group (the Benders region groups counted here too)
- reserve participants may sit in a resolution-group (a coarse provider holds one reserve per coarse step; the requirement stays per solve step)
Validation in params._coarse.coarse_node_frames: a group step that is
not a multiple of the solve's step, a fine step crossing the end of a
coarse step, a coarse step that would straddle a representative period,
and a bind_intraperiod_blocks node in a coarse group raise
FlexToolConfigError; a group step shorter than the solve's step is
ignored with a warning.
3.3 Block layout and coarse steps
emit/_blocks.py (production) and its port BlockLayout.build (in
_block_layout.py) emit the per-solve block frames:
entity_block—(entity, block): each node's group, elsedefaultblock_step_duration—(block, period, step, step_duration)overlap_set,block_step_previous,block_period_time_first/_last
The model reads the node → group map and the group step durations from
these frames and the default block's period boundaries
(nodeState_last_dt). The per-block aggregated timelines of the coarse
blocks are a plain chunking of each period and do not feed the model.
The coarse steps come from one place, params._coarse.coarse_node_frames,
called at the end of load_flextool for every solve, roll, nested and
stochastic sub-solve. It takes the solve's own steps (dt in order, step
durations, dtttdt), splits each period into runs of consecutive steps — a
run ends where t_previous_within_timeset is not the step before (a gap, a
timeset block start, a period or roll start), at every representative-block
start (rp_block_first) and after the period's last realized step (a roll's
realized end, so the state a roll hands over excludes look-ahead flows) — and
chunks each run from its start: a coarse step closes once its fine durations
add up to the group step (a run that is not a multiple ends with a shorter
step). Result: nodeCoarse (n), node_coarse_step (n, d, b_first, t) and
node_coarse_interior (n, d, t, t_previous).
3.4 The coarse balance
nodeBalanceCoarse_eq[n, d, b] is nodeBalance_eq summed over the fine
steps t ∈ b: flows, slacks, the state change of the node's binding
method, the fix_start and roll-continuation start terms and
self-discharge, with the summed −inflow as RHS. With the state constant
inside the coarse step the state terms telescope to
v_state[b⁻] − v_state[b], so every storage binding method applies.
Same orientation and units as nodeBalance_eq: its dual is the node's
price over the coarse step. bind_intraperiod_blocks nodes have the
same summed row per timeset block (nodeBalanceBlock_eq) with the block
lag dtttdt_block_lag (the blocks of a period form a loop; in a rolling
solve the blocks also end at the roll's realized end, and in a period the
roll holds only in part — period_block_chain, from
solve_data/roll_period_whole — the lag is the bind_forward_only lag,
so the blocks chain forward and the roll carry starts the chain).
3.5 Paths
- Rolling / nested / stochastic: every sub-solve builds its own
coarse frames from its own steps; nested
fix_pricereads the coarse row's dual for a coarse parent node. - Benders: region subproblems and the master filter the coarse frames
by node; half-flows enter coarse rows like any arc, so a region↔master
boundary may join nodes of different resolutions. Cross-region arcs and
master flows stay on the solve's steps (
_region_filter); a process inside one region may be coarse in its subproblem. - Warm start: the coarse frames are in the fingerprint.
3.6 Coarse processes and the row rule
model/_coarse_flows (module docstring is the reference):
- Levels (
v_flow(_back),v_online_*,v_reserve,v_exact_forward;v_angleof a coarse-angle node,vq_inertia/vq_non_synchronousof an averaged row) are registered at each coarse step's first fine stepb_firstand read throughalias, which maps every fine row to its step's column. - Events (
v_startup_*,v_shutdown_*) live atb_firstonly and are not aliased; minimum up / down times count whole coarse steps. - Arc-local rows (capacity, conversion, profiles, online capacity,
minimum load, start-up / shut-down reduction, reserve limits) are written
once per coarse step as the twin family
<family>_coarse, the Δt-weighted average of the fine row (add_cstr). - Row rule (
add_row_rule_cstr,row_partition;flex_data.coarse_row_members/COARSE_ROW_FAMILIES): a row coupling several entities (capacity margin, inertia, non-synchronous, flow-group instant flow, user constraint, forecast storage-use tie, DC power flow) whose every member is coarse is one averaged<family>_coarserow per coarse step; a row with a fine member stays per solve step and reads the coarse terms at their constant value. The reserve requirement (reserveBalance_*) is always per solve step. - Ramps run between coarse steps (
ramp_rows,reserve_room_rows). - The behaviour notice
coarse_process_flows(upgraded databases only) names coarse processes whose result can differ from the fine model (flex_data.coarse_process_reasons).
3.7 Outputs
Every reader of a coarse variable expands it to the solve's steps through
engine/output/_coarse_flows (COARSE_VARS: copy for levels, first
for events — the whole start-up at the step's first fine step, 0 at the
others, so totals and costs are the LP's); a grep gate in the tests keeps
direct reads out. A coarse storage state is constant within its coarse
step. Coarse-node prices are the nodeBalanceCoarse_eq duals broadcast
to the fine steps of each coarse step
(read_highs_solution._broadcast_block_node_duals).
3.8 Caveats
- A process with a delay keeps the solve's steps (its flow is shifted by solve steps).
- An averaged row sees step means: a capacity margin covers the mean net demand of the step, not its peak.
The engine-side overview of flex-temporal lives in engine.md § Flex-temporal decomposition.
4. Rolling / nested solves
These are user-facing patterns documented in Solve sequences, rolling and nested solves; this section covers the engine wiring.
4.1 Rolling
A long horizon is split into overlapping optimisation windows. Each window ("roll") is its own LP; the next window starts at the previous window's "jump" point with the storage state at that point fixed.
Schema:
solve.solve_mode = rolling_windowsolve.rolling_solve_jump— interval between roll start points (also the output interval per roll)solve.rolling_solve_horizon— length of the roll's optimisation horizon (> jump; the overlap ishorizon - jump)solve.rolling_duration— optional total horizon length;-1(or unset) means "until the active time list is exhausted"
Engine wiring:
RecursiveSolveBuilder.create_rolling_solvesin_recursive_solve.pyexpands the parent solve into a sequencesolve_roll_0, solve_roll_1, …with per-roll active-time and realized-time lists.- Per-roll storage carry-over rides
SolveHandoff.roll_end_state(solve/_handoff.py), routed to the sub-solve that continues from it bysolve/_carry_routing.py. - Every roll builds its own LP: the in-process
polar_high.WarmProblemis reused only for an identical sub-solve (everyFlexDatafield, labels included, and the solver configuration), and a roll'sdtlabels always shift. See engine.md § Warm-start cascade.
4.2 Nested
An outer long-horizon solve fixes long-term decisions (capacity, long- term storage state at chosen anchor periods); an inner short-horizon solve dispatches within those decisions.
Schema:
solve.contains_solves— array of child solves run after the parent using the parent's realised datasolve.fix_storage_periods— periods where the parent's last storage value becomes a target for the childnode.storage_nested_fix_method— how the child handles the fix-storage target (fix_quantity/fix_price/ etc.;fix_pricerequiresstorage_state_reference_price)
Engine wiring:
- The
RecursiveSolveBuilderwalks the solve tree atdefine_solve_recursivetime and threadsParentSolveInfointo nested children. - The parent's realised capacity / storage state rides
SolveHandoff.realized_invest/realized_existing/fix_storage/fix_storage_timesteps. - The child's preprocessing reads from
state.handoffs[parent_name](in memory; there is no disk fallback).
The single-matching-period rename quirk: when a child solve's period
set intersects its parent's by exactly one period, the child is
renamed solve + "_" + period and SolveConfig.derive_solve gives
the new name a copy of every per-solve setting of the child
(PER_SOLVE_FIELDS, solver settings included). Preserved verbatim
from the legacy preprocessor.
4.3 Stochastic branches
Each branch is itself a sub-solve in the chain. solve.stochastic_branches
is a 4-D map that decorates affected periods with branch-suffixed
clones (y2025 → y2025, y2025_low, y2025_high). Only the
realised branch contributes to realized_time_lists and
fix_storage_time_lists; the other branches' LP variables exist for
the optimisation but do not enter the realised outputs.
The wiring lives in _stochastic.StochasticSolver (called from
RecursiveSolveBuilder); non-anticipativity constraints are emitted
by _non_anticipativity.py (add_dispatch_constraints, add_invest_constraints). See
engine.md § Stochastic branches.
5. RELEASE notes context
The 3.29.0–3.33.0 release cycle is the relevant window for these
features. Quick map (full text in
release.md or CHANGELOG.md):
- 3.29.0 — Flex-temporal decomposition lands.
v50migration movesnew_stepdurationfromtimesettosolve.v51adds group-levelnew_stepduration+decomposition_method. - 3.30.0–3.32.0 — Block-aware emitters complete (then known as writer ports); output expansion stabilises.
- 4.0.0 (
v60migration) — spatial decomposition becomes DB-driven and per-solve:solve.decompositionplus per-solve knobs. The global--decompositionCLI flags are removed; the orchestrator routes each solve from its DB value. - 4.0.0 (
v62migration) — the spatial scheme switches from the dual-subgradient (Lagrangian) coordinator to Benders. Thedecomposition_schemesvaluelagrangian→bendersand the group methodlagrangian_region→benders_regional; the oldlagrangian_alpha/lagrangian_max_iter/lagrangian_toleranceknobs are dropped and replaced bybenders_max_iter(50) /benders_tolerance(1e-3). Existing DBs are auto-migrated (breaking schema change, DB version 62). The Benders invest solve threads its realised capacity into a downstream dispatch solve (TIER-1 invest→dispatch handoff).
6. Where they compose
| Combination | Supported | Notes |
|---|---|---|
| Benders + flex-temporal | Yes | Region subproblems and the master filter the coarse-step frames by node; a region↔master boundary may join nodes of different resolutions (the half-flows are hourly on both sides). |
| Benders + rolling | No | One Benders solve is the unit of work for the outer rolling loop. Not currently wired; would require per-roll cut warm-start. The TIER-1 invest→dispatch handoff threads a Benders invest solve into a downstream rolling dispatch solve, which is the supported pattern. |
| Benders + nested | Partial | The outer (parent) solve can be Benders, but the parent's handoff to children propagates only the invest→dispatch capacity, not per-region operational state. Treat as untested beyond the invest→dispatch chain. |
| Flex-temporal + rolling | Yes | Each roll cuts its own coarse steps; a coarse step ends at the roll's realized end, and the roll carry reaches the first coarse row. |
| Flex-temporal + nested | Yes | Each parent / child solve builds its own coarse steps; fix_price reads a coarse parent node's coarse-row dual. |
| Flex-temporal + stochastic | Yes | Every branch period gets its own coarse steps; a branch's first coarse row starts from the trunk's state. |
| Rolling + nested | Yes | The legacy "more than one solve in a rolling solve" raise still trips for unsupported sibling-rolling-children patterns. |
| Rolling + stochastic | Yes | Each roll can carry branches; the realised branch chains forward. |
7. See also
- engine.md — the engine guide; Solve modes section covers monolithic / warm / Benders and Flex-temporal decomposition section covers the coarse-group frames.
- architecture.md — top-level architecture; how the decomposition pieces fit into the larger pipeline.
- scaling.md — auto-scaling pipeline; runs per sub-problem, so each region / each roll / each block gets its own scaling pass.
- Model parameters — the parameters touched here:
solve.solve_mode,solve.rolling_solve_jump,solve.rolling_solve_horizon,solve.contains_solves,solve.fix_storage_periods,solve.stochastic_branches,solve.new_stepduration,group.new_stepduration,group.decomposition_method,node.storage_nested_fix_method. - How-to recipes — practical recipes for setting up Benders regions, rolling windows, nested solves, and stochastic branches.