From 32a513dd8c07be5a870e614a4b26af967e62fb12 Mon Sep 17 00:00:00 2001 From: Comick Date: Fri, 5 Jun 2026 11:25:59 +0200 Subject: [PATCH] [CBC interface] Support setting CBC hints --- ortools/linear_solver/cbc_interface.cc | 27 +++ .../linear_solver/java/LinearSolverTest.java | 225 ++++++++++++++++++ 2 files changed, 252 insertions(+) diff --git a/ortools/linear_solver/cbc_interface.cc b/ortools/linear_solver/cbc_interface.cc index 0f02f9acac0..81501d12fa1 100644 --- a/ortools/linear_solver/cbc_interface.cc +++ b/ortools/linear_solver/cbc_interface.cc @@ -341,6 +341,33 @@ MPSolver::ResultStatus CBCInterface::Solve(const MPSolverParameters& param) { // Solve CbcModel model(osi_); + // Use the solution hint if any. + if (!solver_->solution_hint_.empty()) { + const int num_cols = model.getNumCols(); + + // Build a full solution vector. Column 0 is the dummy variable for the + // objective offset (fixed at 1.0). Real variables start at column 1. + std::vector hint_solution(num_cols, 0.0); + hint_solution[0] = 1.0; // Dummy variable is fixed at 1.0. + + for (const auto&[hint_var, hint_val] : solver_->solution_hint_) { + const int var_index = hint_var->index(); + const int cbc_col = MPSolverVarIndexToCbcVarIndex(var_index); + if (cbc_col >= 0 && cbc_col < num_cols) { + hint_solution[cbc_col] = hint_val; + } + } + + // setBestSolution registers the hint as the best known solution, which + // branchAndBound uses to prune the search and guide heuristics. + model.setBestSolution(hint_solution.data(), num_cols, COIN_DBL_MAX, false); + // setHotstartSolution sets a depth-first search preference, prioritizing + // exploration near the hint. (The solution values themselves are only used + // when CbcModel is compiled with HOTSTART > 0, which is never the case as it is hardocded to -1, + // but the depth-first side effect seems to be always active.) + model.setHotstartSolution(hint_solution.data()); + } + // Set log level. CoinMessageHandler message_handler; model.passInMessageHandler(&message_handler); diff --git a/ortools/linear_solver/java/LinearSolverTest.java b/ortools/linear_solver/java/LinearSolverTest.java index 09ac8cbcde2..a0d0e2befd4 100644 --- a/ortools/linear_solver/java/LinearSolverTest.java +++ b/ortools/linear_solver/java/LinearSolverTest.java @@ -657,4 +657,229 @@ public void testMPSolver_setHintAndSolverGetters() { assertFalse(solver.setNumThreads(4)); } + + private void runSolveWithHint(MPSolver.OptimizationProblemType problemType) { + if (!MPSolver.supportsProblemType(problemType)) { + return; + } + final MPSolver solver = new MPSolver("testSolveWithHint", problemType); + assertNotNull(solver); + + final double infinity = MPSolver.infinity(); + final MPVariable x = solver.makeIntVar(0.0, infinity, "x"); + final MPVariable y = solver.makeIntVar(0.0, infinity, "y"); + + // Maximize x + 10 * y. + final MPObjective objective = solver.objective(); + objective.setCoefficient(x, 1); + objective.setCoefficient(y, 10); + objective.setMaximization(); + + // x + 7 * y <= 17.5. + final MPConstraint c0 = solver.makeConstraint(-infinity, 17.5, "c0"); + c0.setCoefficient(x, 1); + c0.setCoefficient(y, 7); + + // x <= 3.5. + final MPConstraint c1 = solver.makeConstraint(-infinity, 3.5, "c1"); + c1.setCoefficient(x, 1); + + // Provide a feasible hint to guide the solver. + solver.setHint(new MPVariable[] {x, y}, new double[] {2.0, 1.0}); + + assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve()); + // Optimal: x = 3, y = 2, obj = 3 + 20 = 23. + // With x <= 3.5 and integer, x max is 3. Then + // x + 7*y <= 17.5 → 3 + 7*y <= 17.5 → 7*y <= 14.5 → y <= 2, so y = 2. + assertThat(objective.value()).isWithin(NUM_TOLERANCE).of(23.0); + assertThat(x.solutionValue()).isWithin(NUM_TOLERANCE).of(3.0); + assertThat(y.solutionValue()).isWithin(NUM_TOLERANCE).of(2.0); + } + + @Test + public void testMPSolver_solveWithHint() { + runSolveWithHint(MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING); + runSolveWithHint(MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING); + runSolveWithHint(MPSolver.OptimizationProblemType.SAT_INTEGER_PROGRAMMING); + runSolveWithHint(MPSolver.OptimizationProblemType.GUROBI_MIXED_INTEGER_PROGRAMMING); + } + + private void runSolveWithAndWithoutHint( + MPSolver.OptimizationProblemType problemType, boolean useHint) { + if (!MPSolver.supportsProblemType(problemType)) { + return; + } + final MPSolver solver = + new MPSolver("testSolveWithAndWithoutHint", problemType); + assertNotNull(solver); + + final double infinity = MPSolver.infinity(); + final MPVariable x = solver.makeIntVar(0.0, infinity, "x"); + final MPVariable y = solver.makeIntVar(0.0, infinity, "y"); + final MPVariable z = solver.makeIntVar(0.0, infinity, "z"); + + // Maximize 10*x + 6*y + 4*z. + final MPObjective objective = solver.objective(); + objective.setCoefficient(x, 10); + objective.setCoefficient(y, 6); + objective.setCoefficient(z, 4); + objective.setMaximization(); + + // x + y + z <= 100. + final MPConstraint c0 = solver.makeConstraint(-infinity, 100.0); + c0.setCoefficient(x, 1); + c0.setCoefficient(y, 1); + c0.setCoefficient(z, 1); + + // 10*x + 4*y + 5*z <= 600. + final MPConstraint c1 = solver.makeConstraint(-infinity, 600.0); + c1.setCoefficient(x, 10); + c1.setCoefficient(y, 4); + c1.setCoefficient(z, 5); + + // 2*x + 2*y + 6*z <= 300. + final MPConstraint c2 = solver.makeConstraint(-infinity, 300.0); + c2.setCoefficient(x, 2); + c2.setCoefficient(y, 2); + c2.setCoefficient(z, 6); + + if (useHint) { + // Provide a feasible hint: (x=30, y=70, z=0) satisfies all constraints: + // 30 + 70 + 0 = 100 <= 100 + // 10*30 + 4*70 + 5*0 = 300 + 280 = 580 <= 600 + // 2*30 + 2*70 + 6*0 = 60 + 140 = 200 <= 300 + solver.setHint(new MPVariable[] {x, y, z}, new double[] {30.0, 70.0, 0.0}); + } + + assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve()); + // Same problem as runLinearSolver with integer variables: + // optimal: x=33, y=67, z=0, obj = 10*33 + 6*67 + 4*0 = 330 + 402 = 732. + assertThat(objective.value()).isWithin(NUM_TOLERANCE).of(732.0); + assertThat(x.solutionValue()).isWithin(NUM_TOLERANCE).of(33.0); + assertThat(y.solutionValue()).isWithin(NUM_TOLERANCE).of(67.0); + assertThat(z.solutionValue()).isWithin(NUM_TOLERANCE).of(0.0); + } + + @Test + public void testMPSolver_solveWithAndWithoutHint() { + for (MPSolver.OptimizationProblemType solverType : + new MPSolver.OptimizationProblemType[] { + MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING, + MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING, + MPSolver.OptimizationProblemType.SAT_INTEGER_PROGRAMMING, + MPSolver.OptimizationProblemType.GUROBI_MIXED_INTEGER_PROGRAMMING, + }) { + // Solve without hint. + runSolveWithAndWithoutHint(solverType, /*useHint=*/ false); + // Solve with hint. + runSolveWithAndWithoutHint(solverType, /*useHint=*/ true); + } + } + + private void runSolveWithBadHint(MPSolver.OptimizationProblemType problemType) { + if (!MPSolver.supportsProblemType(problemType)) { + return; + } + final MPSolver solver = + new MPSolver("testSolveWithBadHint", problemType); + assertNotNull(solver); + + final double infinity = MPSolver.infinity(); + final MPVariable x = solver.makeIntVar(0.0, infinity, "x"); + final MPVariable y = solver.makeIntVar(0.0, infinity, "y"); + + // Maximize x + 10 * y. + final MPObjective objective = solver.objective(); + objective.setCoefficient(x, 1); + objective.setCoefficient(y, 10); + objective.setMaximization(); + + // x + 7 * y <= 17.5. + final MPConstraint c0 = solver.makeConstraint(-infinity, 17.5, "c0"); + c0.setCoefficient(x, 1); + c0.setCoefficient(y, 7); + + // x <= 3.5. + final MPConstraint c1 = solver.makeConstraint(-infinity, 3.5, "c1"); + c1.setCoefficient(x, 1); + + // Provide a very suboptimal but still feasible hint: + // (x=0, y=0) yields obj = 0 + 10*0 = 0, but the true optimum is 23. + // If the solver blindly accepted the hint and stopped early, it would + // return obj=0. Verifying obj=23 proves the solver continued searching + // past the hint and found the true optimum. + solver.setHint(new MPVariable[] {x, y}, new double[] {0.0, 0.0}); + + assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve()); + // Optimal: x = 3, y = 2, obj = 3 + 20 = 23. + assertThat(objective.value()).isWithin(NUM_TOLERANCE).of(23.0); + assertThat(x.solutionValue()).isWithin(NUM_TOLERANCE).of(3.0); + assertThat(y.solutionValue()).isWithin(NUM_TOLERANCE).of(2.0); + } + + @Test + public void testMPSolver_solveWithBadHint() { + runSolveWithBadHint( + MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING); + runSolveWithBadHint( + MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING); + runSolveWithBadHint( + MPSolver.OptimizationProblemType.SAT_INTEGER_PROGRAMMING); + runSolveWithBadHint( + MPSolver.OptimizationProblemType.GUROBI_MIXED_INTEGER_PROGRAMMING); + } + + private void runSolveWithInfeasibleHint( + MPSolver.OptimizationProblemType problemType) { + if (!MPSolver.supportsProblemType(problemType)) { + return; + } + final MPSolver solver = + new MPSolver("testSolveWithInfeasibleHint", problemType); + assertNotNull(solver); + + final double infinity = MPSolver.infinity(); + final MPVariable x = solver.makeIntVar(0.0, infinity, "x"); + final MPVariable y = solver.makeIntVar(0.0, infinity, "y"); + + // Maximize x + 10 * y. + final MPObjective objective = solver.objective(); + objective.setCoefficient(x, 1); + objective.setCoefficient(y, 10); + objective.setMaximization(); + + // x + 7 * y <= 17.5. + final MPConstraint c0 = solver.makeConstraint(-infinity, 17.5, "c0"); + c0.setCoefficient(x, 1); + c0.setCoefficient(y, 7); + + // x <= 3.5. + final MPConstraint c1 = solver.makeConstraint(-infinity, 3.5, "c1"); + c1.setCoefficient(x, 1); + + // Provide an infeasible hint: (x=10, y=10) violates both constraints: + // x + 7*y = 10 + 70 = 80 > 17.5 + // x = 10 > 3.5 + // The solver should detect infeasibility and ignore the hint, solving + // the problem normally and finding the true optimum. + solver.setHint(new MPVariable[] {x, y}, new double[] {10.0, 10.0}); + + assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve()); + // Optimal: x = 3, y = 2, obj = 3 + 20 = 23. + assertThat(objective.value()).isWithin(NUM_TOLERANCE).of(23.0); + assertThat(x.solutionValue()).isWithin(NUM_TOLERANCE).of(3.0); + assertThat(y.solutionValue()).isWithin(NUM_TOLERANCE).of(2.0); + } + + @Test + public void testMPSolver_solveWithInfeasibleHint() { + runSolveWithInfeasibleHint( + MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING); + runSolveWithInfeasibleHint( + MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING); + runSolveWithInfeasibleHint( + MPSolver.OptimizationProblemType.SAT_INTEGER_PROGRAMMING); + runSolveWithInfeasibleHint( + MPSolver.OptimizationProblemType.GUROBI_MIXED_INTEGER_PROGRAMMING); + } }