jijzepttools.modeling.algorithm.benders_decomposition#
Benders Decomposition algorithm implementation.
This module provides tools for decomposing mixed-integer programming problems using the Benders decomposition method, which separates binary and continuous variables to solve large-scale optimization problems more efficiently.
The main components are: - extract: Functions to extract variables and constraints - replace: Functions to replace expressions with fixed values or placeholders - generate: Functions to generate master problem, subproblems, and cuts - benders_decomposition: Main algorithm implementation
Submodules#
Classes#
Benders Decomposition algorithm for mixed-integer programming. |
|
Result of Benders decomposition iteration. |
|
Metadata for a Benders cut. |
|
Type of Benders cut. |
Package Contents#
- class BendersDecomposition(problem: jijmodeling.Problem, instance_data: dict, binary_vars: List[jijmodeling.BinaryVar] = None, slack_M: float = 1000.0, alpha_bounds: Tuple[float, float] = (-100000, 100000), use_pareto_optimal_cuts: bool = False, initial_core_point: Dict[str, Any] | None = None, use_two_phase: bool = False, phase1_tolerance: float = 1e-06, phase1_slack_M: float | None = None, max_cuts: int | None = 100, max_samples: int | None = None, round_cut_coefficients: bool = False)#
Benders Decomposition algorithm for mixed-integer programming.
This class implements the Benders decomposition algorithm that separates binary and continuous variables to solve large-scale mixed-integer problems more efficiently.
- original_problem#
- instance_data#
- slack_M = 1000.0#
- phase1_slack_M = None#
- round_cut_coefficients = False#
- continuous_vars#
- coupling_constraints = []#
- alpha_var#
- lower_bound#
- upper_bound#
- iteration_count = 0#
- max_cuts = 100#
- max_samples = None#
- sub_problem#
- use_pareto_optimal_cuts = False#
- core_point: Dict[str, Any] | None = None#
- use_two_phase = False#
- phase: int = 2#
- phase1_tolerance = 1e-06#
- phase1_converged: bool = False#
- phase1_lower_bound: float#
- phase1_upper_bound: float#
- master_problem#
- property cuts: List[jijmodeling.Constraint]#
Get list of cut constraints (for backward compatibility).
- solve_master_problem(solver_callback: Callable[[ommx.v1.Instance], ommx.v1.Solution | ommx.v1.SampleSet]) Tuple[float, List[Dict[str, Any]], List[float], List[bool], ommx.v1.Solution | ommx.v1.SampleSet]#
Solve the master problem using the provided callback.
Supports callbacks that return either Solution or SampleSet. When SampleSet is returned, multiple binary solutions are extracted (limited by max_samples, sorted by objective value).
For SampleSet results (heuristic solvers), each solution is checked for feasibility against master problem constraints. Infeasible solutions are still returned but marked as such, since they can still generate valid Benders cuts.
- Parameters:
solver_callback – Callback function that takes ommx Instance and returns Solution or SampleSet.
- Returns:
- Tuple of (best_objective, binary_solutions_list, alpha_values_list,
feasibility_list, raw_result).
best_objective: Best objective value from feasible solutions (or all if none feasible)
binary_solutions_list: List of binary variable dictionaries
alpha_values_list: List of alpha values corresponding to each solution
feasibility_list: List of booleans indicating master feasibility for each solution
raw_result: Original Solution or SampleSet from callback
- solve_original_problem(binary_solution: Dict[str, Any]) ommx.v1.Solution#
Solve the original problem with fixed binary variables. :param binary_solution: Fixed binary variable values :type binary_solution: tp.Dict[str, tp.Any]
- Returns:
OMMX solution object
- Return type:
Solution
- solve_sub_problem(binary_solution: Dict[str, Any]) Tuple[float, Dict[str, Any], Dict[str, numpy.ndarray], bool, ommx.v1.Solution]#
Solve the subproblem with fixed binary variables.
- Parameters:
binary_solution (tp.Dict[str, tp.Any]) – Fixed binary variable values
- Returns:
Subproblem objective, continuous solution, dual variables, feasibility, solution
- Return type:
tp.Tuple[float, tp.Dict, tp.Dict, bool, Solution]
- solve_phase1_sub_problem(binary_solution: Dict[str, Any]) Tuple[float, Dict[str, Any], Dict[str, numpy.ndarray], bool]#
Solve Phase 1 subproblem with slack-only objective.
Phase 1 objective: minimize bigM * sum(slack_vars) This focuses purely on feasibility, not optimality.
- Parameters:
binary_solution (tp.Dict[str, tp.Any]) – Fixed binary variable values
- Returns:
Phase 1 objective (slack penalty), continuous solution, dual variables, feasibility
- Return type:
tp.Tuple[float, tp.Dict, tp.Dict, bool]
- solve_phase1_pareto_sub_problem(core_point: Dict[str, Any]) Tuple[float, Dict[str, numpy.ndarray]]#
Solve Phase 1 Pareto-optimal subproblem with core point.
Similar to solve_pareto_sub_problem but for Phase 1 (slack-only objective).
- Parameters:
core_point (tp.Dict[str, tp.Any]) – Core point values for binary variables
- Returns:
Phase 1 Pareto subproblem objective and dual variables
- Return type:
tp.Tuple[float, tp.Dict[str, np.ndarray]]
- solve_pareto_sub_problem(core_point: Dict[str, Any]) Tuple[float, Dict[str, numpy.ndarray]]#
Solve Pareto-optimal subproblem with core point.
This solves: max (b - y0)^T u s.t. A^T u <= c
The key difference from solve_sub_problem is that we fix binary variables to the CORE POINT values, not the current master solution. This generates Pareto-optimal dual variables for stronger cuts.
Uses self.pareto_sub_problem which has binary variables relaxed to continuous (0-1), allowing fractional core point values.
- Parameters:
core_point (tp.Dict[str, tp.Any]) – Core point values for binary variables
- Returns:
Pareto subproblem objective and dual variables
- Return type:
tp.Tuple[float, tp.Dict[str, np.ndarray]]
- add_cuts(dual_variables: Dict[str, numpy.ndarray], is_subproblem_feasible: bool, binary_key: tuple | None = None) int#
Add Benders cuts to the master problem.
- Parameters:
dual_variables (tp.Dict[str, np.ndarray]) – Dual variables from subproblem
is_subproblem_feasible (bool) – Whether subproblem was feasible
binary_key (tuple, optional) – Hashable key identifying the binary solution
- Returns:
Number of cuts added
- Return type:
int
- update_bounds(master_objective: float, subproblem_objective: float, alpha_value: float, binary_solution: Dict[str, Any])#
Update lower and upper bounds.
- Parameters:
master_objective (float) – Master problem objective value (from feasible solution only)
subproblem_objective (float) – Subproblem objective value
alpha_value (float) – Alpha variable value from master solution
binary_solution (tp.Dict[str, tp.Any]) – Current binary solution
- is_converged(tolerance: float = 1e-06) bool#
Check if algorithm has converged.
Convergence is achieved when: 1. Gap (UB - LB) <= tolerance, OR 2. Gap < 0 (LB > UB due to integer rounding of alpha variable)
The second condition handles the case where alpha is an integer variable and gets rounded up, causing LB to slightly exceed UB. This is effectively convergence since we’ve found an optimal solution.
- Parameters:
tolerance (float) – Convergence tolerance
- Returns:
True if converged
- Return type:
bool
- iterate(solver_callback: Callable[[ommx.v1.Instance], ommx.v1.Solution | ommx.v1.SampleSet]) BendersResult#
Perform one iteration of Benders decomposition.
Supports multi-cut generation when solver returns SampleSet. Each sample generates its own cut, potentially accelerating convergence.
In two-phase mode: - Phase 1: Solve Phase 1 subproblem (slack-only objective) - Phase 2: Solve regular subproblem (full objective)
- Parameters:
solver_callback – Callback to solve master problem. Can return Solution or SampleSet.
- Returns:
Result of current iteration
- Return type:
- solve(solver_callback: Callable[[ommx.v1.Instance], ommx.v1.Solution | ommx.v1.SampleSet], max_iterations: int = 100, tolerance: float = 1e-06, verbose: bool = False) BendersResult#
Solve the problem using Benders decomposition.
Supports multi-cut generation when solver_callback returns SampleSet. In two-phase mode, this will run Phase 1 until convergence (LB=UB=0), then automatically transition to Phase 2.
- Parameters:
solver_callback – Callback to solve master problem. Can return Solution or SampleSet. When SampleSet is returned, multiple cuts are generated per iteration.
max_iterations (int) – Maximum number of iterations (total for both phases)
tolerance (float) – Convergence tolerance for Phase 2
verbose (bool) – Whether to print iteration information
- Returns:
Final result
- Return type:
Example
>>> benders = BendersDecomposition(problem, instance_data, use_two_phase=True) >>> result = benders.solve(OMMXHighsAdapter.solve, max_iterations=50, tolerance=1e-6, verbose=True) >>> print(f"Converged: {result.converged}") >>> print(f"Optimal value: {result.upper_bound}")
- get_solution_summary(result: BendersResult) str#
Get a summary string of the solution.
- Parameters:
result (BendersResult) – Solution result
- Returns:
Summary string
- Return type:
str
- class BendersResult#
Result of Benders decomposition iteration.
- solution#
OMMX solution object
- Type:
Solution
- master_objective#
Master problem objective value
- Type:
float
- subproblem_objective#
Subproblem objective value
- Type:
float
- lower_bound#
Current lower bound
- Type:
float
- upper_bound#
Current upper bound
- Type:
float
- is_feasible#
Whether subproblem was feasible
- Type:
bool
- binary_solution#
Solution values for binary variables
- Type:
tp.Dict
- continuous_solution#
Solution values for continuous variables
- Type:
tp.Dict
- iteration#
Iteration number
- Type:
int
- converged#
Whether algorithm has converged
- Type:
bool
- pareto_subproblem_objective#
Pareto subproblem objective value
- Type:
float, optional
- pareto_dual_variables#
Dual variables from Pareto subproblem
- Type:
tp.Dict, optional
- core_point#
Current core point for Pareto-optimal cuts
- Type:
tp.Dict, optional
- phase#
Current phase (1 = feasibility, 2 = optimality)
- Type:
int
- phase1_converged#
Whether Phase 1 has converged
- Type:
bool
- phase1_lower_bound#
Phase 1 lower bound
- Type:
float, optional
- phase1_upper_bound#
Phase 1 upper bound
- Type:
float, optional
- num_cuts_added#
Number of cuts added in this iteration
- Type:
int
- num_samples_processed#
Number of samples processed in this iteration
- Type:
int
- num_master_feasible#
Number of master-feasible samples (for SampleSet callbacks)
- Type:
int
- solution: ommx.v1.Solution | None#
- master_objective: float#
- subproblem_objective: float#
- lower_bound: float#
- upper_bound: float#
- is_feasible: bool#
- binary_solution: Dict[str, Any]#
- continuous_solution: Dict[str, Any]#
- iteration: int#
- converged: bool = False#
- pareto_subproblem_objective: float | None = None#
- pareto_dual_variables: Dict[str, numpy.ndarray] | None = None#
- core_point: Dict[str, Any] | None = None#
- phase: int = 2#
- phase1_converged: bool = False#
- phase1_lower_bound: float | None = None#
- phase1_upper_bound: float | None = None#
- num_cuts_added: int = 0#
- num_samples_processed: int = 1#
- num_master_feasible: int = 1#
- class CutInfo#
Metadata for a Benders cut.
- constraint#
The jijmodeling constraint representing the cut
- Type:
jm.Constraint
- iteration#
Iteration when the cut was generated
- Type:
int
- slack#
Current slack value in master problem (0 = tight/active)
- Type:
float
- binary_key#
Hashable key identifying the binary solution that generated this cut
- Type:
tuple
- constraint: jijmodeling.Constraint#
- iteration: int#
- slack: float = 0.0#
- binary_key: tuple | None = None#