● How to use · Scheduling
Meeting scheduling
Place meetings in time and rooms so that required attendees can come, rooms are big enough and nothing overlaps.
What it decides
For every meeting, the solver picks a starting time grain and a room. A meeting then occupies duration_in_grains consecutive grains.
Minimal example
from optimaru import meeting_scheduling as ms
from optimaru.meeting_scheduling.models import (
MeetingAssignmentModel, MeetingModel, MeetingScheduleModel, PersonModel, RoomModel, TimeGrainModel,
)
problem = MeetingScheduleModel(
people=[PersonModel(id=f"P{i}") for i in range(4)],
time_grains=[ # 15-minute grains, 09:00 to 11:00 on one day
TimeGrainModel(id=f"G{i}", grain_index=i, day_of_year=200, starting_minute_of_day=540 + 15 * i)
for i in range(8)
],
rooms=[RoomModel(id="R1", capacity=4), RoomModel(id="R2", capacity=2)],
meetings=[
MeetingModel(id="M1", topic="Roadmap", duration_in_grains=2,
required_attendees=[0, 1], preferred_attendees=[2]),
MeetingModel(id="M2", topic="Budget", duration_in_grains=3, required_attendees=[1, 3]),
],
meeting_assignments=[
MeetingAssignmentModel(id="A1", meeting=0),
MeetingAssignmentModel(id="A2", meeting=1),
],
)
result = ms.solve(problem, time_limit_seconds=2.0, random_seed=42)
print(result.score_hard, result.score_soft)
for a in result.meeting_assignments:
grain, room = divmod(a.grain_room, len(result.rooms))
print(result.meetings[a.meeting].topic, result.time_grains[grain].starting_minute_of_day, result.rooms[room].id) The same use case as a Jupyter notebook, with charts and saved outputs: examples/meeting-scheduling.ipynb ↗
Input parameters
PersonModel
| Field | Meaning |
|---|
id required people.TimeGrainModel
| Field | Meaning |
|---|
id required grain_ index required day_ of_ year required starting_ minute_ of_ day required RoomModel
| Field | Meaning |
|---|
id required capacity MeetingModel
| Field | Meaning |
|---|
id required topic duration_ in_ grains required_ attendees people of those who must attend.preferred_ attendees people of those who should attend.MeetingAssignmentModel
| Field | Meaning |
|---|
id required meeting required meetings. Meetings and assignments are 1:1.pinned MeetingScheduleModel
| Field | Meaning |
|---|
people required time_ grains required rooms required meetings required meeting_ assignments 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 (23)
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. |
day_ | 0.8 | Probability that a change move draws a start whose whole meeting fits in one day. 0 disables it. |
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.0002 | 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 model and returns the solved model.
MeetingAssignmentModel
| Field | Meaning |
|---|
grain_ room time_grain_pos * room_count + room_index. None means unassigned.MeetingScheduleModel
| Field | Meaning |
|---|
score_ hard score_ soft -(medium * 1e6 + soft).Constraints
Pick them by calling their classes from optimaru.meeting_scheduling.constraints, such as OvertimeCost(). 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. |
A medium constraint weighs 1 in medium units by default. The search compares solutions on medium * 1e6 + soft, so medium outranks soft while the soft total stays below 1e6. With level, any constraint moves onto any of the three levels: level="medium" puts it on medium, and "hard" or "soft" takes a medium one off it. A weight counts in units of the level in effect.
This continues the minimal example above.
from optimaru.meeting_scheduling.constraints import (
ConsecutiveMeetingsCost, RequiredAttendanceConflictCost, RoomCapacityCost, RoomConflictCost,
get_constraints,
)
picked = get_constraints([
RoomConflictCost(), RequiredAttendanceConflictCost(), RoomCapacityCost(),
ConsecutiveMeetingsCost(weight=500),
])
custom = ms.solve(problem, time_limit_seconds=2.0, random_seed=42, constraints=picked)
print(custom.score_hard, custom.score_soft) | Name | Level | Default weight | What it does |
|---|---|---|---|
overtime_ | Hard | 1 | A meeting running past the end of the calendar. |
room_ | Hard | 1 | Seats missing in the assigned room for the attendees. |
day_ | Hard | 1 | A meeting whose last grain falls on a different day from its first. |
meeting_ | Soft | 1 | Each meeting's last grain index, which pulls the schedule toward the front of the calendar. |
larger_ | Soft | 1 | For every room larger than the assigned one, the capacity difference, which fills big rooms first. |
room_ | Hard | 1 | Grains in which two meetings use the same room. |
required_ | Hard | 1 | Overlapping grains of a person required in both meetings. |
required_ | Medium | 1 | Overlapping grains of a person required in one meeting and preferred in the other. |
preferred_ | Medium | 1 | Overlapping grains of a person preferred in both meetings. |
consecutive_ | Soft | 100 | Two meetings back to back without one grain between them. The heaviest soft term. |
meeting_ | Soft | 10 | Grains in which any two meetings run at once, regardless of room or attendees. |
room_ | Soft | 1 | Shared attendees whose next meeting starts within two grains in a different room. |
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, the model a solve returned or that model as a JSON dict.
This continues the minimal example above.
seeded = ms.with_initial_solution(problem, result.model_dump(mode="json"))
again = ms.solve(seeded, time_limit_seconds=2.0, random_seed=43)
print(again.score_hard, again.score_soft) - Assignments are matched by id.
- A complete solution skips construction: the search starts exactly from it. A partial one keeps what it gives, and only the assignments 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 assignments keep their value on
problem, whatever the solution says. grain_roomis a composite over the positions intime_grainsandrooms, so a solution that lists different ones, or the same ones in another order, raisesValueError. So do unknown ids and out-of-range values.
Exceptions and limits
- A grain is 15 minutes.
- Meetings and assignments must be 1:1, or
ValueErroris raised. score_softfolds medium into soft with a 1e6 offset, so medium always dominates while raw soft stays below 1e6.