Skip to content

Making a solve faster

How to solve a model that takes too long, and what each way costs. Try them in this order: the first ones keep the answer, the later ones change it.

Way Keeps the optimum What it costs
Keep the model loaded yes only data that moves no mask or label
Start from an earlier answer yes can make a solve slower
Build many small models faster yes only below 250,000 columns and rows
Accept a gap within the gap the answer is close, not optimal
Relax the integers no, a bound the schedule is fractional
Solve in windows no the windows cannot see past their end
Decompose within the gap you write and own the loop
Make the model smaller no it answers a different question

Keep the model loaded

Build once, then update and solve. The solver keeps the model, and the next solve skips the build:

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

An update that moves a mask or adds a label builds and loads again. Change it and solve again shows the loop and how to check that it stays loaded.

Start from an earlier answer

Each solve in a loop starts from the one before, by default. That is usually faster, and on some models much slower, because a solver that carries on skips its presolve. Starting from an earlier answer has the measurements and shows how to time both ways.

Build many small models faster

specsolve builds a model of at most 250,000 columns and 250,000 rows on polars' in-memory engine. To move the rest of a process onto it too, such as a sweep of many small models and your own queries:

import polars as pl

pl.Config.set_engine_affinity('in-memory')

POLARS_ENGINE_AFFINITY=in-memory in the environment does the same, and the workers of a parallel sweep inherit it.

Accept a gap

A MIP solver proves its answer optimal by closing a gap between the answer and a bound. Most of the time can go into the last part. Stop at 1%:

result = sps.solve('commitment.yaml', sources, solver_options={'mip_rel_gap': 0.01})

The option names are the solver's own; Choosing a solver shows how to pass them. The answer is within 1% of the optimum, and it can be a different schedule.

Relax the integers

Make the integer variables continuous. The model is then an LP, which solves faster and has prices:

from mathspec import to_spec

relaxed = to_spec('commitment.yaml').to_dict()
relaxed['variables']['on'] |= {'domain': 'continuous', 'bounds': {'lower': 0, 'upper': 1}}

The answer is a lower bound on the cost, not a schedule: a generator can be a third on. To get prices for the integer answer instead, see Prices from a MIP.

Solve in windows

Solve a long horizon one window at a time. Each window stays the same size, however long the horizon is:

sweep = sps.solve_over(
    'storage.yaml',
    sources,
    sps.EachWindow('hour', steps=24, lookahead=12, into='snapshot', carry={'soc_initial': 'soc'}),
)

A window cannot see past its end, so the schedule costs more than one solve. A lookahead buys that back, and makes each window larger. The windows run one after the other, because each one starts where the one before stopped. A horizon too large for one solve shows both.

Decompose

A model that is large because of one part, such as dispatch under an investment choice, can be split into a small master and a large subproblem. The answer has a gap that you choose, so stop at 1% and the answer is within 1%. specsolve gives no driver for it, so you write the loop. The Benders example is one, checked against the whole model.

Make the model smaller

Representative days instead of a year, fewer nodes or fewer weather years make a smaller model. That is a different model: it answers a different question, and nothing tells you how far its answer is from the one you asked for.