● How to use · Allocation & Packing
Assembly line balancing
Assign tasks to stations along a line so that precedence holds and no station exceeds the cycle time.
What it decides
For every task, the solver picks a station along the line.
Minimal example
from optimaru.assembly_line_balancing import solve
from optimaru.assembly_line_balancing.models import (
AssemblyLineBalancingProblemModel, PrecedenceModel, TaskModel,
)
problem = AssemblyLineBalancingProblemModel(
cycle_time=5,
tasks=[TaskModel(id="A", duration=3), TaskModel(id="B", duration=3), TaskModel(id="C", duration=2)],
precedences=[PrecedenceModel(before="A", after="B")],
)
solve(problem, time_limit_seconds=1.0, random_seed=42) # solves in place
print({task.id: task.station for task in problem.tasks}, problem.stations_used, problem.station_loads) The same use case as a Jupyter notebook, with charts and saved outputs: examples/assembly-line-balancing.ipynb ↗
Input parameters
TaskModel
| Field | Meaning |
|---|
id required duration required min_ station max_ station fixed_ station forbidden_ stations PrecedenceModel
| Field | Meaning |
|---|
before required after required TaskGroupModel
| Field | Meaning |
|---|
id required task_ ids required TaskPairModel
| Field | Meaning |
|---|
first required second required AssemblyLineBalancingProblemModel
| Field | Meaning |
|---|
cycle_ time required tasks required precedences max_ stations same_ station_ groups different_ station_ pairs Solve parameters
What you pass to the solve call. Time limit, seed and parallel count are here.
| Parameter | Default | Meaning |
|---|---|---|
time_ | None | Search budget in seconds, from 0 to 1e9. Left unset, it is the config's spent_limit_seconds, 30 by default. |
random_ | None | Seed for the search. Left unset, it is the config's value, 0 by default. |
parallel_ | None | Independent parallel starts. Each gets its own seed and the full budget, and the best result wins. Left unset, it is the config's value, 1 by default. |
stop_ | False | Stop as soon as hard 0 is reached. |
on_ | None | Callback run on each new best solution. If it raises, the solve stops and raises the same exception. |
config | None | A SolverConfig as the base configuration. Keyword arguments here override it for this solve only; the object you pass is not changed. |
constraints | None | Which constraints to score, and how. Left unset, the defaults apply. |
All search options (26)
Set these on a SolverConfig and pass it as config=.
| Parameter | Default | Meaning |
|---|---|---|
best_ | — | Stop early once the best score reaches this value. By default it never fires. |
change_ | 0.75 | Relative share of change moves. 0 disables them. |
close_ | True | Enable the move that empties one used station at once. It is the only way to cut the station count in one step. |
custom_ | 1.0 | Cap on how often a step may draw a use-case move. 1 means no cap. |
descent_ | 0.0 | Share of best-improvement descent. 0 turns it off. |
full_ | False | Check every incremental score against a full recompute. Slow; for debugging. |
late_ | 0 | Late-acceptance history length. 0 picks it from the instance size. |
on_ | None | Callback run on each new best solution. If it raises, the solve stops and raises the same exception. |
parallel_ | 1 | Independent parallel starts, each with its own seed and the full budget. The best one wins. |
random_ | 0 | Seed for the search. |
ruin_ | True | Run adaptive ruin-and-recreate kicks when the search stagnates. |
ruin_ | 12 | Most entities that small ruin removes. |
ruin_ | 0.0 | Probability that a step is a small ruin and repair, judged like any other move. Negative picks it automatically where the use case supports that; 0 turns it off. |
ruin_ | 60 | Absolute cap on entities removed per kick. |
ruin_ | 0.3 | Largest share of the solution one kick removes. |
ruin_ | 0.1 | Smallest share of the solution one kick removes. |
ruin_ | 0.8 | How fast operator weights adapt. |
ruin_ | 5000 | Hill-climbing moves that polish each kick. Not counted against step_count_limit. |
ruin_ | [33.0, 9.0, 3.0, 0.0] | Operator rewards for a new best, a better, an accepted and a rejected result. |
ruin_ | 5000 | Steps without a new best before a kick. |
spent_ | 30.0 | Time budget in seconds, from 0 to 1e9. |
step_ | 0 | Stop after this many steps. 0 means no limit. |
swap_ | 0.25 | Relative share of swap moves. 0 disables them. |
unimproved_ | 0.0 | Stop after this many seconds without a new best. 0 means no limit. |
unimproved_ | 0 | Stop after this many steps without a new best. 0 means no limit. |
value_ | 0.5 | Probability that a change move draws a task's new station from its precedence and station window instead of from every station. 0 means off. |
What comes back
solve fills problem in place and returns the engine Score, with .hard and .soft. It also takes the engine object from problem.to_core().
TaskModel
| Field | Meaning |
|---|
station AssemblyLineBalancingProblemModel
| Field | Meaning |
|---|
stations_ used station_ loads score_ hard score_ soft Constraints
Pick them by calling their classes from optimaru.assembly_line_balancing.constraints, such as StationCapacityCost(). Only what you pass is scored; pass nothing to keep the defaults. A list may name a constraint more than once, and each pick is scored on its own. Each one takes these parameters:
| Parameter | Default | Meaning |
|---|---|---|
weight | None | Multiplier on the constraint's cost, in units of the level in effect. Left unset, the default weight below applies. A constraint whose default weight is 0 is off until you give it a weight. |
level | None | "hard" makes the constraint a must and "soft" a preference, as do Level.HARD and Level.SOFT. Left unset, it keeps the level below. This use case has no medium level, so "medium" raises ValueError. A reward refuses "hard", and a fixed level refuses any change; both raise ValueError. |
exponent | 1.0 | Sign-preserving power on the cost, above 0, taken after offset. 2.0 squares it, so concentrated violations cost much more. It applies per unit, such as a vehicle or a day, or to the total of a constraint scored as one sum. |
offset | 0.0 | Subtracted from the cost x before the exponent, per unit or total like the exponent. It is in the cost's own measure: seconds, minutes, a count and so on. Any finite number of either sign, keyword only. clip decides what happens below it. |
clip | True | True makes offset a threshold: only the part beyond it counts, max(0, x - offset) ** exponent, so a cost at or below it adds nothing. False takes the literal (x - offset) ** exponent, sign kept: a unit below the offset counts negative, as a reward, even one whose cost is 0. On a level that counts toward hard, clip=False with a non-zero offset raises ValueError, since hard 0 must mean no violation. Keyword only. |
This continues the minimal example above.
from optimaru.assembly_line_balancing.constraints import (
StationCapacityCost, TaskPrecedenceCost, UsedStationsCost, WorkloadImbalanceCost,
)
custom = problem.model_copy(deep=True)
print(solve(custom, time_limit_seconds=1.0, random_seed=42, constraints=[
StationCapacityCost(), TaskPrecedenceCost(), UsedStationsCost(), WorkloadImbalanceCost(weight=1),
])) | Name | Level | Default weight | What it does |
|---|---|---|---|
station_ | Hard | 1 | Processing time above cycle_time, across stations. |
task_ | Hard | 1 | Precedence arcs assigned in reverse station order. |
max_ | Hard | 1 | Station indices beyond max_stations. |
task_ | Hard | 1 | Assignments outside a task's station bounds. |
fixed_ | Hard | 1 | Tasks not on their fixed_station. |
forbidden_ | Hard | 1 | Tasks on a forbidden station. |
same_ | Hard | 1 | Additional stations used by tasks that must share one. |
different_ | Hard | 1 | Task pairs on the same station that must be apart. |
used_ | Soft | 1 | Number of non-empty stations. |
workload_ | Soft | 0 | Range between the most and least loaded used stations. Weight 0 by default, so give it a weight to use it. |
Starting from an existing solution
This needs the latest release: pip install -U optimaru.
with_initial_solution(problem, solution) returns a copy of problem whose search starts from solution, a solved model or the same thing as a JSON dict.
This continues the minimal example above.
from optimaru.assembly_line_balancing import with_initial_solution
warm = with_initial_solution(problem, problem.model_dump(mode="json"))
solve(warm, time_limit_seconds=1.0, random_seed=42)
print({task.id: task.station for task in warm.tasks}, warm.stations_used) - Tasks are matched by id. Tasks left out or at station -1 are placed by the normal construction, and a complete seed skips it. While a task is unassigned it costs nothing.
- Any result of
solveis accepted, feasible or not:max_stations, station domains andfixed_stationstay hard penalties. - With
time_limit_seconds=0the solve stops at once, so a complete solution comes back as it went in. Use the time limit for this:step_count_limit = 0means no limit. - Unknown ids, duplicates, stations below -1 and non-integer stations raise
ValueError.
Exceptions and limits
workload_imbalance_costis off until you give it a weight.