Skip to content

●  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 Timeslot id. start_minute required Start, in minutes from a shared origin. Midnights are multiples of 1440. end_minute required End, in minutes. talk_types required Positions in the schedule's talk_types this slot accepts. tags Free-form tags that talks and speakers can require or avoid.

RoomModel

Field Meaning
id required Room id. capacity Seats. Popular talks should get the bigger rooms. talk_types Positions in talk_types this room accepts. unavailable_timeslots Positions in timeslots when the room is closed. tags Free-form tags.

SpeakerModel

Field Meaning
id required Speaker id. unavailable_timeslots Positions in timeslots the speaker cannot make. required_timeslot_tagsprohibited_timeslot_tagsrequired_room_tagsprohibited_room_tags Tags the slot or room must carry, or must not carry. Hard. preferred_timeslot_tagsundesired_timeslot_tagspreferred_room_tagsundesired_room_tags Tags the speaker would like, or would rather avoid. Soft.

TalkModel

Field Meaning
code required Talk id. talk_type Position in talk_types. speakers Positions in speakers. theme_track_tags Theme tracks. Talks of one track should not overlap. sector_tags Target sectors. Talks for one sector should not overlap. audience_types Audience types. audience_level Audience level, used for diversity and for flow within content. content_tags Content tags. Talks with the same content should not overlap. language Talk language. en by default. required_timeslot_tagsprohibited_timeslot_tagsrequired_room_tagsprohibited_room_tags Tags the slot or room must carry, or must not carry. Hard. preferred_timeslot_tagsundesired_timeslot_tagspreferred_room_tagsundesired_room_tags Tags the talk would like, or would rather avoid. Soft. mutually_exclusive_talks_tags Talks sharing one of these tags must not overlap. prerequisite_talks Positions in talks that must come first. favorite_count Popularity. Popular talks should get bigger rooms. crowd_control_risk Greater than 0 marks a talk that draws a crowd. pinned Keep the talk's slot_room.

ConferenceScheduleModel

Field Meaning
name required Schedule name. talk_types required Talk type names. timeslots required Timeslots. rooms required Rooms. speakers Speakers. talks Talks. minimum_consecutive_talks_pause_in_minutes Minimum pause between back-to-back talks of one speaker. 30 by default.

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.
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 (26)

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.
change_move_weight 0.75 Relative share of change moves. 0 disables them.
compatible_value_probability 0.9 Probability that a change move stays on cells that accept the talk's type. 0 disables the bias.
descent_weight 0.0 Share of best-improvement descent. 0 turns it off.
full_assert False Check every incremental score against a full recompute. Slow; for debugging.
hot_talk_probability 0.0 Probability of moving a talk that carries hard violations. 0 disables it.
late_acceptance_size 0 Late-acceptance history length. 0 picks it from the instance size.
on_new_best None Callback run on each new best solution. If it raises, the solve stops and raises the same exception.
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.
restart_segments 8 Consecutive local-search runs sharing the time limit. 1 means a single run.
ruin_recreate True Run adaptive ruin-and-recreate kicks when the search stagnates.
ruin_recreate_as_move_max_ruin 12 Most entities that small ruin removes.
ruin_recreate_as_move_weight -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_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.
swap_move_weight 0.25 Relative share of swap 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 engine object, solves it in place and returns it. Convert with from_core to read the result.

TalkModel

Field Meaning
slot_room Composite value, timeslot_index * room_count + room_index. -1 means unassigned.

ConferenceScheduleModel

Field Meaning
score_hard Hard score. 0 means feasible. score_soft Soft score: rewards minus penalties.

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_type_incompatibility_cost Hard 100000 A talk in a timeslot or room that does not accept its talk type.
room_unavailability_cost Hard 100000 A talk in a room that is closed in that timeslot.
room_conflict_cost Hard 1000 Two talks in overlapping timeslots sharing a room, by overlapping minutes.
speaker_unavailability_cost Hard 100 A talk in a timeslot where one of its speakers is unavailable.
speaker_conflict_cost Hard 10 Two overlapping talks sharing a speaker, by overlapping minutes per shared speaker.
talk_prerequisite_talks_cost Hard 10 A talk scheduled before, or overlapping, a talk marked as its prerequisite.
talk_mutually_exclusive_talks_tags_cost Hard 1 Two overlapping talks with the same mutually exclusive tag.
consecutive_talks_pause_cost Hard 1 Two talks of one speaker back to back without the minimum pause.
crowd_control_risk_cost Hard 1 A crowd-risk talk overlapping two or more other risky talks in the same timeslot.
speaker_required_timeslot_tags_cost Hard 1 Tags a speaker requires that the timeslot lacks.
speaker_prohibited_timeslot_tags_cost Hard 1 Tags a speaker prohibits that the timeslot carries.
talk_required_timeslot_tags_cost Hard 1 Tags a talk requires that the timeslot lacks.
talk_prohibited_timeslot_tags_cost Hard 1 Tags a talk prohibits that the timeslot carries.
speaker_required_room_tags_cost Hard 1 Tags a speaker requires that the room lacks.
speaker_prohibited_room_tags_cost Hard 1 Tags a speaker prohibits that the room carries.
talk_required_room_tags_cost Hard 1 Tags a talk requires that the room lacks.
talk_prohibited_room_tags_cost Hard 1 Tags a talk prohibits that the room carries.
theme_track_conflict_cost Soft 10 Two overlapping talks of the same theme track.
theme_track_room_change_cost Soft 10 Two talks of one theme track on the same day in different rooms.
sector_conflict_cost Soft 10 Two overlapping talks targeting the same sector.
audience_type_theme_track_conflict_cost Soft 1 Two overlapping talks sharing both a theme track and an audience type.
content_audience_level_flow_violation_cost Soft 10 An introductory talk scheduled after, or overlapping, an advanced talk of the same content.
content_conflict_cost Soft 100 Two overlapping talks of the same content. The most expensive soft rule.
different_day_talks_cost Soft 10 Two talks sharing content or a theme track placed on different days.
popular_talks_room_size_cost Soft 10 A less popular talk getting a bigger room than a more popular one.
speaker_preferred_timeslot_tags_cost Soft 20 Tags a speaker prefers that the timeslot lacks.
speaker_undesired_timeslot_tags_cost Soft 20 Tags a speaker would rather avoid that the timeslot carries.
talk_preferred_timeslot_tags_cost Soft 20 Tags a talk prefers that the timeslot lacks.
talk_undesired_timeslot_tags_cost Soft 20 Tags a talk would rather avoid that the timeslot carries.
speaker_preferred_room_tags_cost Soft 20 Tags a speaker prefers that the room lacks.
speaker_undesired_room_tags_cost Soft 20 Tags a speaker would rather avoid that the room carries.
talk_preferred_room_tags_cost Soft 20 Tags a talk prefers that the room lacks.
talk_undesired_room_tags_cost Soft 20 Tags a talk would rather avoid that the room carries.
speaker_makespan_cost Soft 20 Speakers whose talks spread over more than two days.
audience_type_diversity_cost Soft Reward 1 Reward for a timeslot serving several audience types at once.
audience_level_diversity_cost Soft Reward 1 Reward for a timeslot mixing audience levels.
language_diversity_cost Soft Reward 10 Reward for a timeslot running talks in different languages.

How constraints work →

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=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.
  • Pinned talks keep their value on problem, whatever the solution says.
  • slot_room is a composite over the timeslot and room lists, so a solution that lists different ones, or the same ones in another order, raises ValueError. So do unknown codes and out-of-range values.

Exceptions and limits

  • The search covers one timeslot-by-room grid. talk_type_incompatibility_cost is 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.