● How to use
How to use optimaru
Install the package, build a problem, solve it and read the result. One page for each of the 22 use cases.
Install
pip install optimaru Requires Python 3.11 or newer. Wheels for CPython 3.11 to 3.14 are published for Linux, macOS and Windows, and pydantic comes with the package.
Each use case is a subpackage, optimaru.<name>, loaded on first use, so import optimaru stays cheap.
These pages describe optimaru 0.3. pip install -U optimaru updates an older install.
The common flow
- Build the problem from the models in
optimaru.<name>.models. School timetabling takes a plain dict. - Call the solver with a time limit and a seed. Without a time limit its budget is 30 seconds.
- Read the score and the decision fields. Each use case page lists them.
- Adjust the constraints, or restart from the result.
from optimaru.knapsack import solve
from optimaru.knapsack.models import ItemModel, KnapsackProblemModel
problem = KnapsackProblemModel(
capacity=10,
items=[
ItemModel(id="A", weight=6, value=9),
ItemModel(id="B", weight=4, value=7),
ItemModel(id="C", weight=5, value=8),
],
)
score = solve(problem, time_limit_seconds=1.0, random_seed=42)
print(score, [item.id for item in problem.items if item.selected]) Scores compare hard first, then medium where a use case has it, then soft. Hard 0 means every hard constraint holds. The search returns the best solution it found within the limit; it does not prove that solution optimal.
A score has a range. Weighted contributions are taken per unit, such as a vehicle or a day, or as the total of a constraint scored as one sum. Each must be finite, and a hard one must stay below 2^62 (about 4.6e18); otherwise solving or scoring raises OverflowError naming the constraint. The hard total never wraps around: it stops at −(2^63 − 1), so a larger penalty never looks better than a smaller one.
The call differs a little by use case:
| Call | Use cases |
|---|---|
solve_request(request) returns a response model. | Vehicle routing (VRP) , Pickup & delivery (PDPTW) , Order picking , Employee scheduling |
solve(model) returns a solved model. | Flight crew scheduling , Project scheduling (RCPSP) , Task assignment , Meeting scheduling , Tournament scheduling , Bed allocation |
solve(model.to_core()) works on an engine object in place. Read it back with from_core. | Facility location , Maintenance scheduling , Production line sequencing , Conference scheduling , Sports league scheduling , Bin packing |
solve(model) fills the model in place and returns the score. | Knapsack & selection , Assembly line balancing , Quadratic assignment , Max-cut & partitioning , Semiconductor fab scheduling |
solve(problem) takes a dict and returns a dict. | School timetabling |
Parameters
Every use case page lists its parameters in full: the input model fields, the solve parameters and every search option, each with its default.
The ones you will use most are the time limit, the seed and the parallel count.
Time limits, such as time_limit_seconds, spent_limit_seconds and unimproved_spent_limit_seconds, must be finite, 0 or more and at most 1e9 seconds (about 31.7 years). Anything else, infinity included, raises ValueError. To stop on step_count_limit alone, give a time limit long enough for every step.
If an on_new_best callback raises, solve raises the same exception. With parallel starts, the other starts stop at their next step, so the error reaches you at once.
Simulated annealing, the default on semiconductor fab scheduling and an acceptor_type on quadratic assignment, cools over the time limit, or over step_count_limit when that is further along. A seeded run with a step limit therefore gives the same result every time, as long as its steps stay ahead of the clock.
How constraints work
Every business rule and preference is a constraint plugin with a name, a level and a weight. Each name ends in _cost: a constraint is a cost, and 0 means it holds. list_constraints() on a use case shows the names, and constraint_info() the default level and weight of each.
Pick a constraint by calling its class, such as VehicleCapacityCost() from optimaru.vehicle_routing.constraints. Every class takes the same five knobs: weight=None, level=None, exponent=1.0 and, by keyword only, offset=0.0 and clip=True. None keeps the constraint's default.
- Switch on or off. Pass a list, and only what you list is scored. A constraint left out disappears from the score. An empty list is an error.
- Reweight.
TravelTimeCost(weight=10)multiplies the cost, in units of the level in effect.exponent=2.0squares it, so concentrated violations cost more. - Level.
level="hard"makes a constraint a must andlevel="soft"a preference;Level.HARDandLevel.SOFTfrom the same module work too. Left unset, it keeps its default level."medium"exists only on the 8 use cases with a medium level, where any constraint may move onto it: Vehicle routing (VRP), Pickup & delivery (PDPTW), Project scheduling (RCPSP), Production line sequencing, Task assignment, Meeting scheduling, Tournament scheduling, Bed allocation. Elsewhere it raisesValueError. A constraint with a fixed level refuses any change. - Rewards. A few constraints are rewards: their cost is 0 or below, so a higher weight raises the score. They refuse
level="hard"withValueError. Each page marks them. - Offset and clip.
offsetis subtracted from the raw costxbefore the exponent, per unit like the exponent, in the cost's own measure, such as seconds or a count. Withclip=True, the default, it is a threshold:TravelTimeCost(offset=10, exponent=3)chargesmax(0, x - 10)**3per vehicle, so a vehicle at 10 or less adds nothing. Withclip=Falsethe formula is literal:(x - 10)**3, sign kept, so a vehicle below 10, an idle one included, counts negative, as a reward. On a reward, a threshold rewards only the part beyond the offset.offset=0is the plain cost in both modes. A hard constraint refusesclip=Falsewith a non-zero offset (ValueError), because hard 0 must still mean no violation; use the threshold there, or move the constraint to a soft level. - Pick it again. A constraint may be listed several times. Each pick is its own instance with its own knobs and level, scored on its own, and the picks add up:
[TravelTimeCost(exponent=2), TravelTimeCost(exponent=3)]chargesx**2 + x**3.constraint_costs()keys the first pick by its name and the next onestravel_time_cost#2,travel_time_cost#3and so on. School timetabling'sAssemblykeeps one entry per name instead:add()replaces it. - A constraint with default weight 0 is off until you give it a weight.
from optimaru import vehicle_routing as vrp
from optimaru.vehicle_routing.constraints import (
MaxEndTimeCost, TravelTimeCost, UnassignedVisitsCost, VehicleCapacityCost, get_constraints,
)
from optimaru.vehicle_routing.models import (
RoutePlanRequest, RoutingInput, VehicleModel, VisitModel, solve_request,
)
print(vrp.list_constraints()) # every constraint name of this use case
request = RoutePlanRequest(
timelimit=2,
model_input=RoutingInput(
vehicles=[VehicleModel(id="V1", capacity=10, depot=(37.5665, 126.9780))],
visits=[VisitModel(id="A", location=(37.5700, 126.9820), demand=3, service_duration=600)],
),
)
# pick what to score: anything left out is off
solution = solve_request(request, constraints=get_constraints([
VehicleCapacityCost(), # on, as it is
UnassignedVisitsCost(), # on, as it is
TravelTimeCost(weight=10), # reweighted
MaxEndTimeCost(level="soft"), # hard turned into soft
TravelTimeCost(exponent=2, offset=3600), # picked again: (driving beyond 1 h per vehicle)**2
]))
print(solution.score, solution.routes) Every use case has get_constraints in its constraints module, and constraints= also takes a plain list. School timetabling also takes an Assembly, and employee scheduling takes its rules in model_input.rules. Each page shows the real call.
constraints= runs a single search, so the parallel count has no effect.
Starting from an existing solution
Every use case can start from a solution you already have instead of constructing one. Pass what a previous solve returned, as the result model or as its JSON. This needs the latest release:
pip install -U optimaru from optimaru.vehicle_routing.models import with_initial_solution
first = solve_request(request)
better = solve_request(with_initial_solution(request, first)) # or first.model_dump(mode="json")
print(better.score) - A complete solution skips construction: the search starts exactly from it.
- A partial solution keeps what it gives, and only what it leaves out is constructed.
- A solve's result is accepted back, even a hard-infeasible one. The exception is pickup and delivery, which refuses a route with a broken pickup and dropoff pair.
- Entities are matched by id, never by position. Unknown ids and out-of-range values raise
ValueError. with_initial_solutionreturns a new problem and leaves its input untouched.- Pinned parts are problem facts. Most use cases keep the problem's value and ignore what the solution says. Food packaging, pickup and delivery, max-cut, school timetabling and employee scheduling raise
ValueErrorwhen the solution contradicts them. - A seed is used once: a result edited and solved again starts from construction unless you pass it through
with_initial_solutionagain. - School timetabling takes dicts, so it uses a keyword:
solve(problem, initial_solution=previous). - A zero time limit stops at once. Use the time limit for this:
step_count_limit = 0means no limit.
Each page lists its exceptions. Two more matter here. Quadratic assignment takes only a complete solution. Food packaging returns before construction at 1 ms or less, so a partial solution stays partial.
Example notebooks
Every use case also has a Jupyter notebook in the optimaru repository. Each one builds a small problem, solves it, draws the result, checks it and then changes the rules. They are saved with their outputs, so you can read them on GitHub without running anything. They need optimaru 0.3, and the saved outputs come from it.
pip install "optimaru>=0.3" matplotlib jupyter