● How to use · Timetabling
Conference scheduling
Place talks into timeslots and rooms, keeping speakers available, themes apart and popular talks in big rooms.
What it decides
For every talk, the solver picks a timeslot and a room.
Minimal example
from optimaru import conference_scheduling as cs
from optimaru.conference_scheduling.models import (
ConferenceScheduleModel, RoomModel, SpeakerModel, TalkModel, TimeslotModel,
)
problem = ConferenceScheduleModel(
name="demo",
talk_types=["Talk"],
timeslots=[
TimeslotModel(id="S1", start_minute=540, end_minute=585, talk_types=[0]),
TimeslotModel(id="S2", start_minute=630, end_minute=675, talk_types=[0]),
],
rooms=[
RoomModel(id="R1", capacity=200, talk_types=[0]),
RoomModel(id="R2", capacity=50, talk_types=[0]),
],
speakers=[SpeakerModel(id="Ana"), SpeakerModel(id="Ben", unavailable_timeslots=[0])],
talks=[
TalkModel(code="T1", speakers=[0], theme_track_tags=["AI"], favorite_count=120),
TalkModel(code="T2", speakers=[1], theme_track_tags=["AI"], favorite_count=40),
TalkModel(code="T3", speakers=[0], theme_track_tags=["Ops"], favorite_count=10),
],
)
core = problem.to_core()
cs.solve(core, time_limit_seconds=2.0, random_seed=42) # solves in place
solved = ConferenceScheduleModel.from_core(core)
print(solved.score_hard, solved.score_soft)
for talk in solved.talks:
slot, room = divmod(talk.slot_room, len(solved.rooms))
print(talk.code, solved.timeslots[slot].id, solved.rooms[room].id) The same use case as a Jupyter notebook, with charts and saved outputs: examples/conference-scheduling.ipynb ↗
Input parameters
TimeslotModel
| Field | Meaning |
|---|
id required start_ minute required end_ minute required talk_ types required talk_types this slot accepts.tags RoomModel
| Field | Meaning |
|---|
id required capacity talk_ types talk_types this room accepts.unavailable_ timeslots timeslots when the room is closed.tags SpeakerModel
| Field | Meaning |
|---|
id required unavailable_ timeslots timeslots the speaker cannot make.required_ timeslot_ tagsprohibited_ timeslot_ tagsrequired_ room_ tagsprohibited_ room_ tags preferred_ timeslot_ tagsundesired_ timeslot_ tagspreferred_ room_ tagsundesired_ room_ tags TalkModel
| Field | Meaning |
|---|
code required talk_ type talk_types.speakers speakers.theme_ track_ tags sector_ tags audience_ types audience_ level content_ tags language en by default.required_ timeslot_ tagsprohibited_ timeslot_ tagsrequired_ room_ tagsprohibited_ room_ tags preferred_ timeslot_ tagsundesired_ timeslot_ tagspreferred_ room_ tagsundesired_ room_ tags mutually_ exclusive_ talks_ tags prerequisite_ talks talks that must come first.favorite_ count crowd_ control_ risk pinned slot_room.ConferenceScheduleModel
| Field | Meaning |
|---|
name required talk_ types required timeslots required rooms required speakers talks minimum_ consecutive_ talks_ pause_ in_ minutes 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. |
compatible_ | 0.9 | Probability that a change move stays on cells that accept the talk's type. 0 disables the bias. |
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. |
hot_ | 0.0 | Probability of moving a talk that carries hard violations. 0 disables it. |
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. |
restart_ | 8 | Consecutive local-search runs sharing the time limit. 1 means a single run. |
ruin_ | True | Run adaptive ruin-and-recreate kicks when the search stagnates. |
ruin_ | 12 | Most entities that small ruin removes. |
ruin_ | -1.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. |
What comes back
solve takes the engine object, solves it in place and returns it. Convert with from_core to read the result.
TalkModel
| Field | Meaning |
|---|
slot_ room timeslot_index * room_count + room_index. -1 means unassigned.ConferenceScheduleModel
| Field | Meaning |
|---|
score_ hard score_ soft Constraints
Pick them by calling their classes from optimaru.conference_scheduling.constraints, such as TalkTypeIncompatibilityCost(). 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. |
audience_type_diversity_cost, audience_level_diversity_cost and language_diversity_cost are rewards with negative cost: raising their weight raises the score, and level="hard" raises ValueError. Everything else is a penalty.
This continues the minimal example above.
from optimaru.conference_scheduling.constraints import (
ContentConflictCost, RoomConflictCost, SpeakerConflictCost, get_constraints,
)
picked = get_constraints([RoomConflictCost(), SpeakerConflictCost(), ContentConflictCost(weight=500)])
custom = problem.to_core()
cs.solve(custom, time_limit_seconds=2.0, random_seed=42, constraints=picked)
print(custom.score) | Name | Level | Default weight | What it does |
|---|---|---|---|
talk_ | Hard | 100000 | A talk in a timeslot or room that does not accept its talk type. |
room_ | Hard | 100000 | A talk in a room that is closed in that timeslot. |
room_ | Hard | 1000 | Two talks in overlapping timeslots sharing a room, by overlapping minutes. |
speaker_ | Hard | 100 | A talk in a timeslot where one of its speakers is unavailable. |
speaker_ | Hard | 10 | Two overlapping talks sharing a speaker, by overlapping minutes per shared speaker. |
talk_ | Hard | 10 | A talk scheduled before, or overlapping, a talk marked as its prerequisite. |
talk_ | Hard | 1 | Two overlapping talks with the same mutually exclusive tag. |
consecutive_ | Hard | 1 | Two talks of one speaker back to back without the minimum pause. |
crowd_ | Hard | 1 | A crowd-risk talk overlapping two or more other risky talks in the same timeslot. |
speaker_ | Hard | 1 | Tags a speaker requires that the timeslot lacks. |
speaker_ | Hard | 1 | Tags a speaker prohibits that the timeslot carries. |
talk_ | Hard | 1 | Tags a talk requires that the timeslot lacks. |
talk_ | Hard | 1 | Tags a talk prohibits that the timeslot carries. |
speaker_ | Hard | 1 | Tags a speaker requires that the room lacks. |
speaker_ | Hard | 1 | Tags a speaker prohibits that the room carries. |
talk_ | Hard | 1 | Tags a talk requires that the room lacks. |
talk_ | Hard | 1 | Tags a talk prohibits that the room carries. |
theme_ | Soft | 10 | Two overlapping talks of the same theme track. |
theme_ | Soft | 10 | Two talks of one theme track on the same day in different rooms. |
sector_ | Soft | 10 | Two overlapping talks targeting the same sector. |
audience_ | Soft | 1 | Two overlapping talks sharing both a theme track and an audience type. |
content_ | Soft | 10 | An introductory talk scheduled after, or overlapping, an advanced talk of the same content. |
content_ | Soft | 100 | Two overlapping talks of the same content. The most expensive soft rule. |
different_ | Soft | 10 | Two talks sharing content or a theme track placed on different days. |
popular_ | Soft | 10 | A less popular talk getting a bigger room than a more popular one. |
speaker_ | Soft | 20 | Tags a speaker prefers that the timeslot lacks. |
speaker_ | Soft | 20 | Tags a speaker would rather avoid that the timeslot carries. |
talk_ | Soft | 20 | Tags a talk prefers that the timeslot lacks. |
talk_ | Soft | 20 | Tags a talk would rather avoid that the timeslot carries. |
speaker_ | Soft | 20 | Tags a speaker prefers that the room lacks. |
speaker_ | Soft | 20 | Tags a speaker would rather avoid that the room carries. |
talk_ | Soft | 20 | Tags a talk prefers that the room lacks. |
talk_ | Soft | 20 | Tags a talk would rather avoid that the room carries. |
speaker_ | Soft | 20 | Speakers whose talks spread over more than two days. |
audience_ | Soft Reward | 1 | Reward for a timeslot serving several audience types at once. |
audience_ | Soft Reward | 1 | Reward for a timeslot mixing audience levels. |
language_ | Soft Reward | 10 | Reward for a timeslot running talks in different languages. |
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 ConferenceScheduleModel or that model as a JSON dict. solve takes the engine object, so convert the result with to_core().
This continues the minimal example above.
restart = cs.with_initial_solution(problem, solved).to_core()
cs.solve(restart, time_limit_seconds=2.0, random_seed=43)
print(restart.score) - Talks are matched by
code. - A complete solution skips construction: the search starts exactly from it. A partial one keeps what it gives, and only the talks it leaves out are constructed.
- 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. - Pinned talks keep their value on
problem, whatever the solution says. slot_roomis a composite over the timeslot and room lists, so a solution that lists different ones, or the same ones in another order, raisesValueError. So do unknown codes and out-of-range values.
Exceptions and limits
- The search covers one timeslot-by-room grid.
talk_type_incompatibility_costis what keeps talks to compatible cells, so leave it on unless you mean to drop that rule. - Speakers, prerequisites, talk types and unavailable slots are positions in their lists, not ids.