Skip to content

Commit 1b49668

Browse files
committed
[CBC interface] Support setting CBC hints
1 parent 16f8b6a commit 1b49668

2 files changed

Lines changed: 256 additions & 0 deletions

File tree

ortools/javatests/com/google/ortools/linearsolver/LinearSolverTest.java

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

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

ortools/linear_solver/cbc_interface.cc

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,37 @@ 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+
// Precompute the actual objective value for the hint solution, so CBC can
362+
// immediately compute a tighter optimality gap and prune more effectively.
363+
double hint_objective = solver_->Objective().offset();
364+
for (const auto& [hint_var, hint_val] : solver_->solution_hint_) {
365+
hint_objective +=
366+
solver_->Objective().GetCoefficient(hint_var) * hint_val;
367+
}
368+
369+
// setBestSolution registers the hint as the best known solution, which
370+
// branchAndBound uses to prune the search and guide heuristics.
371+
model.setBestSolution(hint_solution.data(), num_cols, hint_objective,
372+
false);
373+
}
374+
344375
// Set log level.
345376
CoinMessageHandler message_handler;
346377
model.passInMessageHandler(&message_handler);

0 commit comments

Comments
 (0)