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:
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%:
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.