Skip to content

●  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

  1. Build the problem from the models in optimaru.<name>.models. School timetabling takes a plain dict.
  2. Call the solver with a time limit and a seed. Without a time limit its budget is 30 seconds.
  3. Read the score and the decision fields. Each use case page lists them.
  4. 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:

CallUse 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.0 squares it, so concentrated violations cost more.
  • Level. level="hard" makes a constraint a must and level="soft" a preference; Level.HARD and Level.SOFT from 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 raises ValueError. 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" with ValueError. Each page marks them.
  • Offset and clip. offset is subtracted from the raw cost x before the exponent, per unit like the exponent, in the cost's own measure, such as seconds or a count. With clip=True, the default, it is a threshold: TravelTimeCost(offset=10, exponent=3) charges max(0, x - 10)**3 per vehicle, so a vehicle at 10 or less adds nothing. With clip=False the 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=0 is the plain cost in both modes. A hard constraint refuses clip=False with 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)] charges x**2 + x**3. constraint_costs() keys the first pick by its name and the next ones travel_time_cost#2, travel_time_cost#3 and so on. School timetabling's Assembly keeps 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_solution returns 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 ValueError when 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_solution again.
  • 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 = 0 means 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

All notebooks on GitHub ↗

All use cases