Skip to content

●  How to use · Scheduling

Task assignment

Build an ordered work queue for each person, matching skills and affinity and finishing critical work first.

What it decides

For every employee, the solver builds an ordered list of tasks, worked back to back.

Minimal example

from optimaru import task_assigning as ta
from optimaru.task_assigning.models import (
    CustomerModel, EmployeeModel, TaskAssigningSolutionModel, TaskModel, TaskTypeModel,
)

problem = TaskAssigningSolutionModel(
    task_types=[
        TaskTypeModel(code="DOC", base_duration=30, required_skills=["writing"]),
        TaskTypeModel(code="FIX", base_duration=60, required_skills=["python"]),
    ],
    customers=[CustomerModel(id="ACME")],
    tasks=[
        TaskModel(id="T1", task_type_code="DOC", customer_id="ACME", priority="CRITICAL"),
        TaskModel(id="T2", task_type_code="FIX", customer_id="ACME", priority="MAJOR"),
        TaskModel(id="T3", task_type_code="FIX", customer_id="ACME"),
    ],
    employees=[
        EmployeeModel(id="E1", skills=["writing"], customer_to_affinity={"ACME": "HIGH"}),
        EmployeeModel(id="E2", skills=["python"], customer_to_affinity={"ACME": "MEDIUM"}),
    ],
)

result = ta.solve(problem, time_limit_seconds=2.0, random_seed=42)
print(result.score_hard, result.score_soft0, result.score_soft1, result.score_soft2)
print({e.id: e.tasks for e in result.employees})
print({t.id: t.start_time for t in result.tasks})

The same use case as a Jupyter notebook, with charts and saved outputs: examples/task-assigning.ipynb ↗

Input parameters

TaskTypeModel

Field Meaning
code required Task type id. base_duration required Base duration in minutes, before the affinity multiplier. required_skills Skills this type needs.

CustomerModel

Field Meaning
id required Customer id.

TaskModel

Field Meaning
id required Task id. task_type_code required Its TaskTypeModel. customer_id required Its customer. min_start_time Earliest start in minutes. priority MINOR, MAJOR or CRITICAL. MINOR by default.

EmployeeModel

Field Meaning
id required Employee id. skills Skills the employee has. customer_to_affinity Customer id to HIGH, MEDIUM, LOW or NONE. A missing customer counts as NONE.

TaskAssigningSolutionModel

Field Meaning
task_types required Task types. customers required Customers. tasks required Tasks. employees required Employees.

Solve parameters

What you pass to the solve call. Time limit, seed and parallel count are here.

Parameter Default Meaning
time_limit_seconds None Search budget in seconds, from 0 to 1e9. Left unset, it is the config's spent_limit_seconds, 30 by default.
random_seed None Seed for the search. Left unset, it is the config's value, 0 by default.
parallel_solver_count 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_at_first_feasible False Stop as soon as hard 0 is reached.
full_assert False Check every incremental score against a full recompute. Slow; for debugging.
on_new_best 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 (23)

Set these on a SolverConfig and pass it as config=.

Parameter Default Meaning
best_score_limit — Stop early once the best score reaches this value. By default it never fires.
full_assert False Check every incremental score against a full recompute. Slow; for debugging.
late_acceptance_size 0 Late-acceptance history length. 0 picks it from the instance size.
list_swap_move_weight 0.3 Relative share of list swap moves. 0 disables them.
on_new_best None Callback run on each new best solution. If it raises, the solve stops and raises the same exception.
or_opt_move_weight 0.1 Relative share of Or-opt moves, which relocate a short segment. 0 disables them.
parallel_solver_count 1 Independent parallel starts, each with its own seed and the full budget. The best one wins.
random_seed 0 Seed for the search.
relocate_move_weight 0.4 Relative share of relocate moves. 0 disables them.
ruin_recreate True Run adaptive ruin-and-recreate kicks when the search stagnates.
ruin_recreate_max_ruin_count 60 Absolute cap on entities removed per kick.
ruin_recreate_max_ruin_fraction 0.3 Largest share of the solution one kick removes.
ruin_recreate_min_ruin_fraction 0.1 Smallest share of the solution one kick removes.
ruin_recreate_operator_decay 0.8 How fast operator weights adapt.
ruin_recreate_polish_steps 5000 Hill-climbing moves that polish each kick. Not counted against step_count_limit.
ruin_recreate_rewards [33.0, 9.0, 3.0, 0.0] Operator rewards for a new best, a better, an accepted and a rejected result.
ruin_recreate_unimproved_step_trigger 5000 Steps without a new best before a kick.
spent_limit_seconds 30.0 Time budget in seconds, from 0 to 1e9.
step_count_limit 0 Stop after this many steps. 0 means no limit.
tail_swap_move_weight 0.1 Relative share of tail swaps (2-opt*) between two lists. 0 disables them.
two_opt_move_weight 0.1 Relative share of 2-opt moves. 0 disables them.
unimproved_spent_limit_seconds 0.0 Stop after this many seconds without a new best. 0 means no limit.
unimproved_step_count_limit 0 Stop after this many steps without a new best. 0 means no limit.

What comes back

solve takes the model and returns the solved model.

EmployeeModel

Field Meaning
tasks Ordered task ids this employee performs.

TaskModel

Field Meaning
start_time Computed start in minutes. None when unassigned.

TaskAssigningSolutionModel

Field Meaning
score_hard Missing skills, negated. 0 means every task fits its employee. score_soft0 Unassigned tasks, negated. score_soft1 Makespan term, negated. score_soft2 Priority end-time terms, negated.

Constraints

Pick them by calling their classes from optimaru.task_assigning.constraints, such as RequiredSkillCost(). 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", "medium" or "soft" moves the constraint onto that level, as do Level.HARD, Level.MEDIUM and Level.SOFT. Left unset, it keeps the level below. This use case has a medium level, and any constraint may move onto it.
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.

Missing skills are hard, unassigned tasks medium and the rest soft. Medium folds in just below hard, so one missing skill outweighs any number of unassigned tasks below 1e9. squared_makespan_cost weighs 1e7 by default, which keeps it above the priority end times. A weight you pass counts in units of the level in effect, and level moves any constraint onto any of the three levels. The fold leaves the hard level room for about 9.2e9 weighted units; past that, hard scores saturate and the search can no longer tell them apart. So give squared_makespan_cost weight=1 when you make it hard: at 1e7 it passes that limit on most instances.

This continues the minimal example above.

from optimaru.task_assigning.constraints import (
    CriticalPriorityTaskEndTimeCost, RequiredSkillCost, UnassignedTasksCost, get_constraints,
)

picked = get_constraints([
    RequiredSkillCost(), UnassignedTasksCost(), CriticalPriorityTaskEndTimeCost(weight=20),
])
custom = ta.solve(problem, time_limit_seconds=2.0, random_seed=42, constraints=picked)
print(custom.score_hard, custom.score_soft2)
Name Level Default weight What it does
required_skill_cost Hard 1 Skills the task type requires that the assigned employee lacks.
squared_makespan_cost Soft 1e7 The square of each employee's end time, which takes work off the busiest person first.
critical_priority_task_end_time_cost Soft 4 End time of CRITICAL tasks, at four times the weight of MINOR.
major_priority_task_end_time_cost Soft 2 End time of MAJOR tasks, at twice the weight of MINOR.
minor_priority_task_end_time_cost Soft 1 End time of MINOR tasks.
unassigned_tasks_cost Medium 1 Tasks on no employee's list. It ranks below missing skills and is reported as score_soft0.

How constraints work →

Starting from an existing solution

This needs the latest release: pip install -U optimaru.

with_initial_solution(problem, solution) returns a new problem whose employees carry the solution's tasks, replacing the ones the problem had.

This continues the minimal example above.

seeded = ta.with_initial_solution(problem, result)  # or result.model_dump(mode="json")
better = ta.solve(seeded, time_limit_seconds=2.0, random_seed=42)
print(better.score_hard, better.score_soft1, better.score_soft2)
  • A complete solution skips construction: the search starts exactly from it. A partial one keeps what it gives, and only the tasks it leaves out are constructed.
  • With time_limit_seconds=0 the solve stops at once, so a complete solution comes back as it went in. Use the time limit for this: step_count_limit = 0 means no limit.
  • An unknown id or a task assigned twice raises ValueError.

Exceptions and limits

  • A task's duration is base_duration times the affinity multiplier: HIGH 1, MEDIUM 2, LOW 3, NONE 4.
  • Every task ends up assigned. A task no employee is qualified for gets assigned with a hard penalty instead of staying unassigned.
  • Times are integer minutes.