Benders Decomposition API
Planning Problem
GenX._benders_configure_solver — Method
_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.
GenX.configure_benders_planning_solver — Method
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).
GenX.generate_planning_problem — Method
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.
GenX.init_planning_problem — Method
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).
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.
Subproblems
GenX.configure_benders_subprob_solver — Method
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.
GenX.generate_operation_subproblem — Method
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.
GenX.get_local_planning_variables — Method
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.
GenX.init_dist_subproblems — Method
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.
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.
GenX.init_sequential_subproblems — Method
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.
GenX.init_subproblem — Method
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.
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.
GenX.benders_worker_exeflags — Method
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.
GenX.create_worker_process — Method
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).
GenX.generate_benders_inputs — Method
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 — aVector{Dict}when running with a single Julia process (nworkers() == 1), or aDArrayacross 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.
GenX.separate_inputs_subperiods — Method
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.
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 bystart_distributed_processes!, which detects an HPC allocation from the environment (Slurm or LSF) and otherwise falls back tomin(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.NWorkersof0or1: parallel solving is disabled and the subproblems are solved sequentially on the main process (a warning is emitted, sinceDistributed: truewas 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.
GenX.solver_available — Method
solver_available(solver_name::Symbol)Return true if solver_name has been loaded into Main on the current process.
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_NTASKSis set): workers are launched throughSlurmClusterManager.SlurmManager(), which starts one worker per Slurm task — request--ntasks=Nto getNworkers. Each worker getsSLURM_CPUS_PER_TASKJulia 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_NUMPROCis set):min(LSB_DJOB_NUMPROC ÷ lsf_cpus_per_task, number_of_subproblems)workers are launched, each withlsf_cpus_per_taskJulia 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.
Gurobi Optimizer Helper
GenX.benders_gurobi_optimizer — Function
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.