Skip to content

Warm-starting a re-solve

How to start each solve in a loop from the work the solve before it did. It suits a loop whose solves differ by a small step: a rolling horizon, a myopic pathway, a search that inches. The reference is Model.solve.

Keep the solver's progress

Build once, then solve each update with keep='progress':

import specsolve as sps

model = sps.build('dispatch.yaml', sources)
model.solve()
for numbers in steps:
    result = model.update(numbers).solve(keep='progress')
    print(result.kept)  # progress

kept says what the solve actually kept. The first solve of a model keeps 'nothing', because no work came before it. An update that moves a mask or a coordinate set also gives 'nothing': the columns change, so the model is loaded again (Model.update).

The default, keep='solver', reuses the loaded model and discards the work. The answer is the same under every keep; only the time changes.

Check that it pays

Run the loop once with each keep= and read the clock the package keeps:

for keep in ('solver', 'progress'):
    model = sps.build('dispatch.yaml', sources)
    for numbers in steps:
        assert model.update(numbers).solve(keep=keep).kept in {keep, 'nothing'}
    print(keep, model.diagnostics().seconds['solve'])

Take the faster one. 'nothing' on every iteration means each update moved a mask and the model was rebuilt, so the loop is paying for the build, not the solve.

keep='progress' can lose by an order of magnitude and win by a factor of two, so measure rather than guess. Over six updates on HiGHS (#815), carrying the solver's work cost 76.6 s against 4.3 s on a dispatch model whose presolve cracks the problem outright, an 18× loss, and 111.2 s against 213.9 s on a storage model whose cyclic recurrence presolve cannot crack, a 1.9× win. It pays where the model is hard for its solver's preprocessing and consecutive solves differ by a small step. The answer does not change either way: across both models the objectives agreed to 2e-15 relative. No solver option reaches the same thing; on both solvers that ship, an option asking for it did not produce it (#815).

Warm-start a sweep

solve_over takes the same keep=, and carries from one slice to the next:

axis = sps.EachWindow('hour', steps=24, lookahead=24, into='t')
sweep = sps.solve_over('horizon.yaml', sources, axis, keep='progress')

Under executor=, every slice is a first solve and keeps 'nothing' (running slices in parallel).

Time a cold solve

Pass keep='nothing'. It discards the held solver before the load, so no basis, incumbent or solver-internal state survives. A benchmark needs that, and so does comparing two sets of solver_options.

Start from an earlier answer

keep='progress' carries the work only while the model's structure stays the same. A cutting-plane master re-solved after gaining a cut has gained a row, and a rebuild loads a fresh solver. Start an LP from an earlier answer instead:

B = {'variable_basis', 'constraint_basis'}
previous = master.solve(outputs=B)
for cut in cuts:
    previous = master.update(cut).solve(start=previous, outputs=B)

The engine lays the answer's basis onto the new build by coordinate. A coordinate both builds hold keeps its status. A variable only the new build holds starts at a bound, and a constraint only the new build holds starts not binding, so a cut enters without moving the vertex. The answer to start from must carry both basis outputs. It can be live, loaded with load_result or read from an archive, and it can come from another solver. A mixed-integer model is refused: it has no basis to start from.

It pays most where the model changes least. An unchanged model started from its own answer does no simplex work. Over a Benders run on the master of bench/warm_payoff.py, with HiGHS, a start from the previous master saved 66% to 73% of the simplex iterations at every size measured (#1877). Whether it saves time too depends on the model: on a small master, reading and matching the basis costs more than the iterations it saves. Measure before relying on it.