Skip to content

Commit dd133b4

Browse files
committed
[CBC interface] Support setting CBC hints
1 parent 42858c9 commit dd133b4

2 files changed

Lines changed: 252 additions & 0 deletions

File tree

ortools/linear_solver/cbc_interface.cc

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,33 @@ MPSolver::ResultStatus CBCInterface::Solve(const MPSolverParameters& param) {
341341
// Solve
342342
CbcModel model(osi_);
343343

344+
// Use the solution hint if any.
345+
if (!solver_->solution_hint_.empty()) {
346+
const int num_cols = model.getNumCols();
347+
348+
// Build a full solution vector. Column 0 is the dummy variable for the
349+
// objective offset (fixed at 1.0). Real variables start at column 1.
350+
std::vector hint_solution(num_cols, 0.0);
351+
hint_solution[0] = 1.0; // Dummy variable is fixed at 1.0.
352+
353+
for (const auto&[hint_var, hint_val] : solver_->solution_hint_) {
354+
const int var_index = hint_var->index();
355+
const int cbc_col = MPSolverVarIndexToCbcVarIndex(var_index);
356+
if (cbc_col >= 0 && cbc_col < num_cols) {
357+
hint_solution[cbc_col] = hint_val;
358+
}
359+
}
360+
361+
// setBestSolution registers the hint as the best known solution, which
362+
// branchAndBound uses to prune the search and guide heuristics.
363+
model.setBestSolution(hint_solution.data(), num_cols, COIN_DBL_MAX, false);
364+
// setHotstartSolution sets a depth-first search preference, prioritizing
365+
// exploration near the hint. (The solution values themselves are only used
366+
// when CbcModel is compiled with HOTSTART > 0, which is never the case as it is hardocded to -1,
367+
// but the depth-first side effect seems to be always active.)
368+
model.setHotstartSolution(hint_solution.data());
369+
}
370+
344371
// Set log level.
345372
CoinMessageHandler message_handler;
346373
model.passInMessageHandler(&message_handler);

ortools/linear_solver/java/LinearSolverTest.java

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,4 +653,229 @@ public void testMPSolver_setHintAndSolverGetters() {
653653

654654
assertFalse(solver.setNumThreads(4));
655655
}
656+
657+
private void runSolveWithHint(MPSolver.OptimizationProblemType problemType) {
658+
if (!MPSolver.supportsProblemType(problemType)) {
659+
return;
660+
}
661+
final MPSolver solver = new MPSolver("testSolveWithHint", problemType);
662+
assertNotNull(solver);
663+
664+
final double infinity = MPSolver.infinity();
665+
final MPVariable x = solver.makeIntVar(0.0, infinity, "x");
666+
final MPVariable y = solver.makeIntVar(0.0, infinity, "y");
667+
668+
// Maximize x + 10 * y.
669+
final MPObjective objective = solver.objective();
670+
objective.setCoefficient(x, 1);
671+
objective.setCoefficient(y, 10);
672+
objective.setMaximization();
673+
674+
// x + 7 * y <= 17.5.
675+
final MPConstraint c0 = solver.makeConstraint(-infinity, 17.5, "c0");
676+
c0.setCoefficient(x, 1);
677+
c0.setCoefficient(y, 7);
678+
679+
// x <= 3.5.
680+
final MPConstraint c1 = solver.makeConstraint(-infinity, 3.5, "c1");
681+
c1.setCoefficient(x, 1);
682+
683+
// Provide a feasible hint to guide the solver.
684+
solver.setHint(new MPVariable[] {x, y}, new double[] {2.0, 1.0});
685+
686+
assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
687+
// Optimal: x = 3, y = 2, obj = 3 + 20 = 23.
688+
// With x <= 3.5 and integer, x max is 3. Then
689+
// x + 7*y <= 17.5 → 3 + 7*y <= 17.5 → 7*y <= 14.5 → y <= 2, so y = 2.
690+
assertThat(objective.value()).isWithin(NUM_TOLERANCE).of(23.0);
691+
assertThat(x.solutionValue()).isWithin(NUM_TOLERANCE).of(3.0);
692+
assertThat(y.solutionValue()).isWithin(NUM_TOLERANCE).of(2.0);
693+
}
694+
695+
@Test
696+
public void testMPSolver_solveWithHint() {
697+
runSolveWithHint(MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING);
698+
runSolveWithHint(MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING);
699+
runSolveWithHint(MPSolver.OptimizationProblemType.SAT_INTEGER_PROGRAMMING);
700+
runSolveWithHint(MPSolver.OptimizationProblemType.GUROBI_MIXED_INTEGER_PROGRAMMING);
701+
}
702+
703+
private void runSolveWithAndWithoutHint(
704+
MPSolver.OptimizationProblemType problemType, boolean useHint) {
705+
if (!MPSolver.supportsProblemType(problemType)) {
706+
return;
707+
}
708+
final MPSolver solver =
709+
new MPSolver("testSolveWithAndWithoutHint", problemType);
710+
assertNotNull(solver);
711+
712+
final double infinity = MPSolver.infinity();
713+
final MPVariable x = solver.makeIntVar(0.0, infinity, "x");
714+
final MPVariable y = solver.makeIntVar(0.0, infinity, "y");
715+
final MPVariable z = solver.makeIntVar(0.0, infinity, "z");
716+
717+
// Maximize 10*x + 6*y + 4*z.
718+
final MPObjective objective = solver.objective();
719+
objective.setCoefficient(x, 10);
720+
objective.setCoefficient(y, 6);
721+
objective.setCoefficient(z, 4);
722+
objective.setMaximization();
723+
724+
// x + y + z <= 100.
725+
final MPConstraint c0 = solver.makeConstraint(-infinity, 100.0);
726+
c0.setCoefficient(x, 1);
727+
c0.setCoefficient(y, 1);
728+
c0.setCoefficient(z, 1);
729+
730+
// 10*x + 4*y + 5*z <= 600.
731+
final MPConstraint c1 = solver.makeConstraint(-infinity, 600.0);
732+
c1.setCoefficient(x, 10);
733+
c1.setCoefficient(y, 4);
734+
c1.setCoefficient(z, 5);
735+
736+
// 2*x + 2*y + 6*z <= 300.
737+
final MPConstraint c2 = solver.makeConstraint(-infinity, 300.0);
738+
c2.setCoefficient(x, 2);
739+
c2.setCoefficient(y, 2);
740+
c2.setCoefficient(z, 6);
741+
742+
if (useHint) {
743+
// Provide a feasible hint: (x=30, y=70, z=0) satisfies all constraints:
744+
// 30 + 70 + 0 = 100 <= 100
745+
// 10*30 + 4*70 + 5*0 = 300 + 280 = 580 <= 600
746+
// 2*30 + 2*70 + 6*0 = 60 + 140 = 200 <= 300
747+
solver.setHint(new MPVariable[] {x, y, z}, new double[] {30.0, 70.0, 0.0});
748+
}
749+
750+
assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
751+
// Same problem as runLinearSolver with integer variables:
752+
// optimal: x=33, y=67, z=0, obj = 10*33 + 6*67 + 4*0 = 330 + 402 = 732.
753+
assertThat(objective.value()).isWithin(NUM_TOLERANCE).of(732.0);
754+
assertThat(x.solutionValue()).isWithin(NUM_TOLERANCE).of(33.0);
755+
assertThat(y.solutionValue()).isWithin(NUM_TOLERANCE).of(67.0);
756+
assertThat(z.solutionValue()).isWithin(NUM_TOLERANCE).of(0.0);
757+
}
758+
759+
@Test
760+
public void testMPSolver_solveWithAndWithoutHint() {
761+
for (MPSolver.OptimizationProblemType solverType :
762+
new MPSolver.OptimizationProblemType[] {
763+
MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING,
764+
MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING,
765+
MPSolver.OptimizationProblemType.SAT_INTEGER_PROGRAMMING,
766+
MPSolver.OptimizationProblemType.GUROBI_MIXED_INTEGER_PROGRAMMING,
767+
}) {
768+
// Solve without hint.
769+
runSolveWithAndWithoutHint(solverType, /*useHint=*/ false);
770+
// Solve with hint.
771+
runSolveWithAndWithoutHint(solverType, /*useHint=*/ true);
772+
}
773+
}
774+
775+
private void runSolveWithBadHint(MPSolver.OptimizationProblemType problemType) {
776+
if (!MPSolver.supportsProblemType(problemType)) {
777+
return;
778+
}
779+
final MPSolver solver =
780+
new MPSolver("testSolveWithBadHint", problemType);
781+
assertNotNull(solver);
782+
783+
final double infinity = MPSolver.infinity();
784+
final MPVariable x = solver.makeIntVar(0.0, infinity, "x");
785+
final MPVariable y = solver.makeIntVar(0.0, infinity, "y");
786+
787+
// Maximize x + 10 * y.
788+
final MPObjective objective = solver.objective();
789+
objective.setCoefficient(x, 1);
790+
objective.setCoefficient(y, 10);
791+
objective.setMaximization();
792+
793+
// x + 7 * y <= 17.5.
794+
final MPConstraint c0 = solver.makeConstraint(-infinity, 17.5, "c0");
795+
c0.setCoefficient(x, 1);
796+
c0.setCoefficient(y, 7);
797+
798+
// x <= 3.5.
799+
final MPConstraint c1 = solver.makeConstraint(-infinity, 3.5, "c1");
800+
c1.setCoefficient(x, 1);
801+
802+
// Provide a very suboptimal but still feasible hint:
803+
// (x=0, y=0) yields obj = 0 + 10*0 = 0, but the true optimum is 23.
804+
// If the solver blindly accepted the hint and stopped early, it would
805+
// return obj=0. Verifying obj=23 proves the solver continued searching
806+
// past the hint and found the true optimum.
807+
solver.setHint(new MPVariable[] {x, y}, new double[] {0.0, 0.0});
808+
809+
assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
810+
// Optimal: x = 3, y = 2, obj = 3 + 20 = 23.
811+
assertThat(objective.value()).isWithin(NUM_TOLERANCE).of(23.0);
812+
assertThat(x.solutionValue()).isWithin(NUM_TOLERANCE).of(3.0);
813+
assertThat(y.solutionValue()).isWithin(NUM_TOLERANCE).of(2.0);
814+
}
815+
816+
@Test
817+
public void testMPSolver_solveWithBadHint() {
818+
runSolveWithBadHint(
819+
MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING);
820+
runSolveWithBadHint(
821+
MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING);
822+
runSolveWithBadHint(
823+
MPSolver.OptimizationProblemType.SAT_INTEGER_PROGRAMMING);
824+
runSolveWithBadHint(
825+
MPSolver.OptimizationProblemType.GUROBI_MIXED_INTEGER_PROGRAMMING);
826+
}
827+
828+
private void runSolveWithInfeasibleHint(
829+
MPSolver.OptimizationProblemType problemType) {
830+
if (!MPSolver.supportsProblemType(problemType)) {
831+
return;
832+
}
833+
final MPSolver solver =
834+
new MPSolver("testSolveWithInfeasibleHint", problemType);
835+
assertNotNull(solver);
836+
837+
final double infinity = MPSolver.infinity();
838+
final MPVariable x = solver.makeIntVar(0.0, infinity, "x");
839+
final MPVariable y = solver.makeIntVar(0.0, infinity, "y");
840+
841+
// Maximize x + 10 * y.
842+
final MPObjective objective = solver.objective();
843+
objective.setCoefficient(x, 1);
844+
objective.setCoefficient(y, 10);
845+
objective.setMaximization();
846+
847+
// x + 7 * y <= 17.5.
848+
final MPConstraint c0 = solver.makeConstraint(-infinity, 17.5, "c0");
849+
c0.setCoefficient(x, 1);
850+
c0.setCoefficient(y, 7);
851+
852+
// x <= 3.5.
853+
final MPConstraint c1 = solver.makeConstraint(-infinity, 3.5, "c1");
854+
c1.setCoefficient(x, 1);
855+
856+
// Provide an infeasible hint: (x=10, y=10) violates both constraints:
857+
// x + 7*y = 10 + 70 = 80 > 17.5
858+
// x = 10 > 3.5
859+
// The solver should detect infeasibility and ignore the hint, solving
860+
// the problem normally and finding the true optimum.
861+
solver.setHint(new MPVariable[] {x, y}, new double[] {10.0, 10.0});
862+
863+
assertEquals(MPSolver.ResultStatus.OPTIMAL, solver.solve());
864+
// Optimal: x = 3, y = 2, obj = 3 + 20 = 23.
865+
assertThat(objective.value()).isWithin(NUM_TOLERANCE).of(23.0);
866+
assertThat(x.solutionValue()).isWithin(NUM_TOLERANCE).of(3.0);
867+
assertThat(y.solutionValue()).isWithin(NUM_TOLERANCE).of(2.0);
868+
}
869+
870+
@Test
871+
public void testMPSolver_solveWithInfeasibleHint() {
872+
runSolveWithInfeasibleHint(
873+
MPSolver.OptimizationProblemType.CBC_MIXED_INTEGER_PROGRAMMING);
874+
runSolveWithInfeasibleHint(
875+
MPSolver.OptimizationProblemType.SCIP_MIXED_INTEGER_PROGRAMMING);
876+
runSolveWithInfeasibleHint(
877+
MPSolver.OptimizationProblemType.SAT_INTEGER_PROGRAMMING);
878+
runSolveWithInfeasibleHint(
879+
MPSolver.OptimizationProblemType.GUROBI_MIXED_INTEGER_PROGRAMMING);
880+
}
656881
}

0 commit comments

Comments
 (0)