Benders Decomposition API

Planning Problem

GenX._benders_configure_solverMethod
_benders_configure_solver(settings_file, optimizer, solver_name)

Private helper: look up solver_name in _SOLVER_CONFIGURE_FUNCTIONS and call the matching solver-specific configure function with settings_file and optimizer. Raises an error listing supported solvers if solver_name is not recognised.

source
GenX.configure_benders_planning_solverMethod
configure_benders_planning_solver(solver_settings_path, optimizer)

Return a solver OptimizerWithAttributes for the Benders planning (master) problem.

Looks first for {solver}_benders_planning_settings.yml in solver_settings_path, falling back to {solver}_settings.yml. Supports any solver that GenX's configure_solver infrastructure supports (HiGHS, Gurobi, CPLEX, Clp, Cbc, SCIP).

source
GenX.generate_planning_problemMethod
generate_planning_problem(setup, inputs, OPTIMIZER)

Build and return the Benders master (planning) JuMP model.

Calls planning_model! to add capacity expansion variables and constraints, then adds one recourse-value variable vTHETA[w] per representative period and minimizes total fixed cost plus the sum of those recourse approximations. Also constructs the dictionary eAvailableCapacity (mapping of all new capacity variables) used by the Benders algorithm in MacroEnergySolvers.jl.

Raises an error if retrofits or multi-stage planning are enabled, as these are not yet compatible with Benders decomposition.

source
GenX.init_planning_problemMethod
init_planning_problem(setup, inputs, optimizer)

Initialize the Benders master problem and return (EP, varnames).

Configures the planning solver via configure_benders_planning_solver, builds the model with generate_planning_problem, and returns the JuMP model together with the names of all decision variables except vZERO and vTHETA (i.e., the linking/planning variables that will be fixed in subproblems).

source
GenX.update_with_planning_solution!Method
update_with_planning_solution!(planning_problem, planning_variable_values)

Fix every planning variable in planning_problem to the value supplied in planning_variable_values (a Dict mapping variable name → value), then re-solve the model. Used after Benders convergence to realign the planning problem with a specific first-stage solution (e.g. the best incumbent) so that capacity and dual-based outputs are read from that solution rather than the last master solve.

If the fixed re-solve does not yield a primal solution (e.g. a numerically infeasible fixing), the fixings are reverted and the model is re-solved unfixed so that the caller is left with a usable solution instead of an empty one. Returns true when the planning problem holds a primal solution at the requested values, false otherwise.

source

Subproblems

GenX.configure_benders_subprob_solverMethod
configure_benders_subprob_solver(solver_settings_path, optimizer)

Return a solver OptimizerWithAttributes for Benders operational subproblems.

Looks first for {solver}_benders_subprob_settings.yml in solver_settings_path, falling back to {solver}_settings.yml.

source
GenX.generate_operation_subproblemMethod
generate_operation_subproblem(setup, inputs, OPTIMIZER)

Build and return a single Benders operational (subproblem) JuMP model for one representative period.

Calls operation_model! to add all operational variables and constraints, then sets the objective to minimize operational cost scaled by setup["ObjScale"]. The planning/linking variables are left free at this stage; bounds and objective coefficients are removed in init_subproblem.

source
GenX.get_local_planning_variablesMethod
get_local_planning_variables(subproblems_local)

Return a Dict mapping each subperiod index to its linking variable name vector.

Iterates over the local subproblem entries on a worker and collects the :linking_variables_sub field stored by init_local_subproblems!, keyed by :subproblem_index.

source
GenX.init_dist_subproblemsMethod
init_dist_subproblems(setup, inputs_decomp, planning_variables, optimizer)

Initialize all Benders subproblems as a DistributedArrays.DArray across available workers.

Distributes subperiod inputs across workers, spawning init_local_subproblems! on each worker with only that worker's slice of inputs_decomp (avoiding serialisation of the full dataset to every worker). After initialisation, collects the per-subperiod linking variable names into a merged Dict and returns (subproblems_all, planning_variables_sub) where planning_variables_sub maps subperiod index to the linking variable names present in that subproblem.

source
GenX.init_local_subproblems!Method
init_local_subproblems!(setup, inputs_local, subproblems_local, planning_variables, OPTIMIZER)

Initialize a set of Benders subproblems in-place on a single worker.

Iterates over inputs_local (one entry per representative subperiod assigned to this worker), calls init_subproblem for each, and stores the resulting JuMP model, linking variable names, and subperiod index into the corresponding entry of subproblems_local. Mutates subproblems_local directly; returns nothing.

source
GenX.init_sequential_subproblemsMethod
init_sequential_subproblems(setup, inputs_decomp, planning_variables, optimizer)

Initialize all Benders subproblems as a plain Vector{Dict} on the current process.

Used when nworkers() == 1 (no extra Julia workers are available). Avoids routing every subproblem solve through Julia's distributed message-passing infrastructure (@fetchfrom 1 / @spawnat 1), which can deadlock when the LP solver (e.g. HiGHS IPM) spawns OpenMP threads that interfere with Julia's cooperative task scheduler.

Returns (subproblems, planning_variables_sub) with the same semantics as init_dist_subproblems: subproblems is a Vector{Dict} accepted by the solve_subproblems(::Vector{Dict}, ...) method in MacroEnergySolvers, and planning_variables_sub is a Dict mapping subperiod index to its linking variable names.

source
GenX.init_subproblemMethod
init_subproblem(setup, inputs, OPTIMIZER, planning_variables)

Initialize a single Benders subproblem and return (EP, planning_variables_sub).

Builds the operational model, then strips bounds and zeros out the objective coefficient for every variable whose name appears in planning_variables. This makes those variables pure parameters (fixed by the master each iteration) rather than degrees of freedom of the subproblem. Returns the modified model and the subset of planning_variables that are actually present in this subproblem.

source

Utilities

GenX.add_benders_workers!Method
add_benders_workers!(n::Int)

Add n Benders worker processes and prepare them, returning the new worker ids.

Used on the explicit NWorkers > 1 path. Workers inherit the currently active project.

source
GenX.benders_worker_exeflagsMethod
benders_worker_exeflags()
benders_worker_exeflags(cpus_per_task::Int)

Julia command-line flags for a Benders worker.

Always sets the active project, so that using GenX on the worker resolves to the same environment as the main process. The second method additionally gives the worker cpus_per_task Julia threads, and is used on the HPC paths where the scheduler has allocated that many CPUs to each task.

source
GenX.create_worker_processMethod
create_worker_process(pid)

Load GenX on worker pid, along with any optional solver package that is already loaded on the main process (loading it on the main process only is not enough: subproblems are built and solved on the workers).

source
GenX.generate_benders_inputsMethod
generate_benders_inputs(setup, inputs, inputs_decomp, optimizer)

Build and return the complete set of Benders decomposition inputs as a Dict.

Initializes the planning (master) problem and all operational subproblems, then assembles them into a single benders_inputs dictionary with fields:

  • "planning_problem": the master JuMP model
  • "planning_variables": names of first-stage decision variables
  • "subproblems": operational subproblem dicts — a Vector{Dict} when running with a single Julia process (nworkers() == 1), or a DArray across multiple workers otherwise
  • "planning_variables_sub": per-subperiod mapping of linking variable names

The "subproblems" entry of the dictionary uses a Vector{Dict} when only one process is available to avoid routing every subproblem solve through Julia's distributed message-passing infrastructure (@fetchfrom 1 / @spawnat 1), which can deadlock when the solver (e.g. HiGHS IPM) spawns OpenMP threads that interfere with Julia's cooperative task scheduler on the single OS thread.

source
GenX.separate_inputs_subperiodsMethod
separate_inputs_subperiods(inputs)

Decompose the full-year inputs dictionary into per-representative-period sub-dictionaries.

Returns a Dict keyed by subperiod index w = 1:REP_PERIOD, where each entry is a deep copy of inputs with time-indexed arrays (demand pD, capacity factors pP_Max, fuel costs, start costs, time weights omega, etc.) sliced to the hours belonging to subperiod w. Each sub-dictionary also carries REP_PERIOD = 1 and a SubPeriod field with its index, making it self-contained for building a single operational subproblem.

source
GenX.setup_benders_workers!Method
setup_benders_workers!(setup::Dict, number_of_subproblems::Int; lsf_cpus_per_task::Int = 1)

Launch the Julia worker processes used to solve Benders subproblems in parallel, and return the resulting nworkers().

Does nothing (returns the current nworkers()) unless setup[:Distributed] is true. The number of workers is controlled by setup[:NWorkers]:

  • NWorkers = -1 (the default): the worker count is chosen automatically by start_distributed_processes!, which detects an HPC allocation from the environment (Slurm or LSF) and otherwise falls back to min(number_of_subproblems, Sys.CPU_THREADS).
  • NWorkers > 1: that many workers are requested explicitly; workers are added only if fewer are already running. If more are already running, the existing ones are used as-is.
  • NWorkers of 0 or 1: parallel solving is disabled and the subproblems are solved sequentially on the main process (a warning is emitted, since Distributed: true was asked for).

number_of_subproblems is the number of operational subproblems (representative periods); there is no benefit to launching more workers than that. lsf_cpus_per_task is the number of CPUs assigned to each worker under LSF and is only used on that path.

source
GenX.solver_availableMethod
solver_available(solver_name::Symbol)

Return true if solver_name has been loaded into Main on the current process.

source
GenX.start_distributed_processes!Method
start_distributed_processes!(number_of_subproblems::Int; lsf_cpus_per_task::Int = 1)

Automatically size and launch the pool of Benders worker processes. Used when NWorkers = -1.

The worker count is taken from the surrounding HPC allocation when one is detected:

  • Slurm (SLURM_NTASKS is set): workers are launched through SlurmClusterManager.SlurmManager(), which starts one worker per Slurm task — request --ntasks=N to get N workers. Each worker gets SLURM_CPUS_PER_TASK Julia threads. A warning is issued if the allocation has more tasks than there are subproblems, since the surplus workers will sit idle.
  • LSF (LSB_DJOB_NUMPROC is set): min(LSB_DJOB_NUMPROC ÷ lsf_cpus_per_task, number_of_subproblems) workers are launched, each with lsf_cpus_per_task Julia threads.
  • Otherwise (laptop / workstation): min(number_of_subproblems, Sys.CPU_THREADS) single-threaded workers.

GenX (and any optional solver already loaded on the main process) is loaded on each new worker.

source

Gurobi Optimizer Helper

GenX.benders_gurobi_optimizerFunction
benders_gurobi_optimizer(attributes::Dict)

Return a Gurobi OptimizerWithAttributes for use in Benders sub/planning problems. Requires the Gurobi package to be loaded (triggers the GenXGurobiExt extension). Throws an informative error if Gurobi has not been loaded.

source