Repository navigation
Add MIP start support to the SCIP interface - #3483
Conversation
SCIP's solve_via_data accepted a warm_start flag and ignored it, so there was no way to hand SCIP an incumbent from Python. GUROBI already supports this via data['init_value']; this brings SCIP in line. apply() now collects Variable.value through utilities.stack_vals with NaN marking entries the user did not set, and solve_via_data registers them before the constraints are added. Two design points: - The start is registered with createPartialSol() rather than createSol(). Users typically set values on a subset of their variables, and SCIP's completesol heuristic is built to fill in the rest; NaN is what lets the interface tell 'unset' apart from a genuine zero. - addSol() is used rather than trySol(). At the point where CVXPY builds the model SCIP is still in the problem creation stage, and trySol raises 'method cannot be called at this time in solution process' there. Verified through SCIP's own statistics that the values reach the solver: the completesol heuristic is never called without a start and is called once with one, and the resulting incumbent is attributed to it. Partial fix for cvxpy#849
| def test_scip_mip_start(self) -> None: | ||
| """A MIP start does not change the optimum.""" | ||
| prob, x, y = self.get_mip_problem() | ||
| expected = prob.solve(solver=cp.SCIP, warm_start=False) | ||
|
|
||
| x.value = np.array([2.0, 3.0]) | ||
| assert prob.solve(solver=cp.SCIP, warm_start=True) == expected |
There was a problem hiding this comment.
maybe you could test that the number of iterations decreases? I am not sure if there is an easy way to get the number of iterations for SCIP though.. it differs from solver to solver.
| def test_scip_mip_start_infeasible_values(self) -> None: | ||
| """An unusable start is discarded by SCIP, not propagated.""" | ||
| prob, x, y = self.get_mip_problem() | ||
| x.value = np.array([99.0, 99.0]) | ||
| assert prob.solve(solver=cp.SCIP, warm_start=True) == 9.0 |
There was a problem hiding this comment.
this test is not really showing that the warm-start is being used or disregarded. Could we maybe try to show a log output? I guess this is not trivial, but could be worth giving it a try?
|
Benchmarks that have stayed the same:
|
Review feedback: the tests showed that the optimum was unchanged, but not that the start was used rather than discarded. SCIP's model is reachable from Python through solver_stats.extra_stats, so its statistics can be read back. The completesol heuristic exists to complete a partial start, and its call count separates the two cases: both solves pass warm_start=True and differ only in whether the user set any variable values, so it runs in one and not the other. The two robustness tests now assert the same counter, which also pins down what happens to an unusable start: SCIP is offered it, tries to complete it, and the result is unaffected.
|
I tried this and I don't think it can carry a test. I went with the |
|
Agreed, and it turned out to be doable without parsing logs. The SCIP model is reachable from Python via
I added the same assertion to the two robustness tests, which also pins down what happens to an unusable start: SCIP is offered it, tries to complete it, and the result is unaffected. |
Description
SCIP.solve_via_dataaccepts awarm_startflag and ignores it, so there is currently no way to hand SCIP an incumbent from Python. GUROBI already supports this throughdata['init_value']; this brings SCIP in line.apply()collectsVariable.valuethroughutilities.stack_vals— the same helper GUROBI uses — andsolve_via_dataregisters the values before the constraints are added.Two things differ from the GUROBI path, both forced by SCIP's API:
createPartialSol()rather thancreateSol(). Users typically set values on a subset of their variables, and SCIP'scompletesolheuristic is built to fill in the rest. GUROBI usesGRB.UNDEFINEDas its "not provided" sentinel; SCIP has no equivalent, so NaN is what lets the interface tell an unset entry apart from a genuine zero.addSol()rather thantrySol(). At the point where CVXPY builds the model SCIP is still in the problem creation stage, andtrySolraisesmethod cannot be called at this time in solution processthere.Verification that the values reach the solver, rather than being silently discarded — from SCIP's own statistics output:
The heuristic is never called without a start, is called once with one, and the resulting incumbent is attributed to it.
Caveat, stated plainly: I could not measure a speedup on the instances I tried — SCIP's own heuristics found better incumbents than the greedy ones I supplied. This exposes a capability that is currently not expressible at all; it is not a performance improvement by default.
Compatibility: behaviour changes only when
warm_start=Trueand variable values are set, which previously did nothing for SCIP. The optimum is unaffected (covered by tests); the search path may differ.Scope note: this covers the
Variable.valuepath from #849. Re-using a previous solve viasolver_cache, which GUROBI also implements, is not addressed here — the SCIP interface does not populatesolver_cacheat all.Partial fix for #849
Type of change
Contribution checklist
ruff0.8.1 (the pre-commit pin) reports no findingsTestSCIP:init_valuecontents, that a start does not change the optimum,warm_startwith no values set, and an unusable starttest_conic_solvers.py, no regressions