Expand description
§schedulr
Scheduling framework (activity/resource/interval DSL) for Rust, built
on unifier’s CSP/COP constraint
model and solvers, which in turn build on
pathwise’s generic search and
optimization traits.
Website and documentation: casoon.github.io/schedulr
pathwise → unifier → schedulr → (application: timetabling, appointment booking, ...)§Status
The current release is 0.14. It provides, among others:
- domain-neutral
Resource,Participant,Activity,Assignment,Score, and structuredConflicttypes without exposing solver internals; - synchronous create/move/cancel/participant-update checks through
SchedulingState, including self-exclusion when moving an activity; - resource matching by type, capacity and feature, named resource and participant pools, and hierarchical/overlapping participant groups;
- periodic slot calendars with absolute exceptions and lexicographic Strong/Medium/Weak score components;
compile→solveplusanalyze,evaluate_move,suggest,compare,explain, and baseline-awarerepairpaths.
The single-activity path evaluates only constraints affected by the proposed change and does not start a solver search. Persistence remains an application concern; solutions carry score components so applications can store them with their immutable schedule versions.
§Problem class
Scheduling / timetabling / appointment booking on top of unifier’s
CSP/COP model: activities placed against resources over time, subject
to hard constraints (no double-booking, capacity, precedence,
calendar/opening-hours exclusions) and soft preferences (e.g.
proximity to a requested time slot).
§Usage
Synchronous single-activity checks (e.g. a live booking desk), via
SchedulingState — no solver search, only the constraints touching the
proposed activity are evaluated:
use schedulr::{
Participant, ParticipantId, ProposedActivity, Resource, ResourceId,
ResourceRequirement, SchedulingState, TimeWindow,
};
let mut state = SchedulingState::new(
[Resource::new(ResourceId(1), "room", 1)],
[Participant::new(ParticipantId(1), "Alex")],
[],
);
let proposal = ProposedActivity::new("appointment", TimeWindow::new(10, 20))
.with_requirement(ResourceRequirement::new(ResourceId(1), 1))
.with_participant(ParticipantId(1));
// Blocking conflicts (e.g. room double-booked) prevent commit; advisory
// conflicts (e.g. a participant already booked elsewhere) do not.
let activity_id = state.commit(proposal).expect("room and participant are free");
// Moving an activity excludes its own prior booking from the check via
// `excluding`, so it does not conflict with itself:
let moved = ProposedActivity::new("appointment", TimeWindow::new(15, 25))
.with_requirement(ResourceRequirement::new(ResourceId(1), 1))
.with_participant(ParticipantId(1))
.excluding(activity_id);
state.commit(moved).expect("new slot is free");Batch scheduling with a minimal compile → solve → explain path for
hard resource conflicts:
use schedulr::{
Activity, ActivityId, Resource, ResourceId, ResourceRequirement,
SchedulingProblem, SolveStatus, TimeWindow, compile,
};
let room = Resource::new(ResourceId(1), "Physics lab", 1);
let first = Activity::new(ActivityId(1), "first", TimeWindow::new(10, 20), 10)
.with_requirement(ResourceRequirement::new(room.id(), 1));
let second = Activity::new(ActivityId(2), "second", TimeWindow::new(15, 25), 10)
.with_requirement(ResourceRequirement::new(room.id(), 1));
let compiled = compile(&SchedulingProblem::new(vec![room], vec![], vec![first, second]))
.expect("problem compiles");
let result = compiled.solve();
if result.status != SolveStatus::Feasible {
for conflict in compiled.explain(&result) {
println!("{}: {}", conflict.constraint_name, conflict.message);
}
}§Installation
schedulr = "0.14"§License
MIT — see LICENSE.
Structs§
- Academic
Period - Activity
- Scheduling demand independent of any concrete solution assignment.
- Activity
Goal - A distribution goal over a set of activities; every counted unit costs
weightatlevel. - Activity
Id - Activity
Relation Constraint - Ties two activities together with an
ActivityRelation. UnlikeScoreRule, this is a hard constraint: the solver never produces a solution that violates it — unless it carries aSoftPenalty(seeSelf::soft), which makes a broken relation cost the penalty’s weight. - Analysis
- Assignment
- Concrete placement of one activity.
- Bottleneck
- Break
Template - A recurring break within a day (e.g. lunch, recess).
windowis day-relative, likeSlotTemplate::offset. No activity may be placed so that it overlaps a break; seeScheduleTemplate::allowed_starts_for. - Bucket
Load Pattern - Allowed shapes of a group of activities’ teaching blocks, expressed over the schedule’s own buckets (plan 25, C1).
- Bucket
Window - One bucket a load rule is checked against: a half-open window plus the bucket it belongs to (plan 25, C1). The bucket index repeats across cycles — the same day of the cycle is the same bucket in every cycle, which is what makes “per day” mean “per day of the cycle”.
- Cancellation
Token - Thread-safe cancellation handle allowing external interruption of running solvers.
- Change
Aspects - Which aspects of an assignment differ between two solutions.
- Change
Evaluation - The result of evaluating a whole batch of
ChangeRequests at once. - Compile
Error - Compiled
Problem - Concurrent
Free Goal - More than
limitofentitiesfree at the same unit inside[start_offset, end_offset)of their active buckets costsweightper entity over the limit and unit — “not every class at lunch at once”. - Conflict
- Structured scheduling conflict suitable for direct display or app-side localization.
- Consecutive
RunLimit - Soft cap on how long a back-to-back run of a group’s activities may get inside one period bucket — “no more than two core-subject lessons in a row”.
- DayRule
- A rule on the shape of one entity’s occupancy inside each period bucket. Hard unless it
carries a
SoftPenalty(seeSelf::soft); then every unit it misses by costs the weight. - DayTemplate
- Group
Membership - Idle
Bounds - Soft bounds on one entity’s free time between its assignments — “two to four free periods a week, and no double free periods”.
- Isolated
Block Goal - A lone block in one half of an active bucket costs
weight— “nobody comes in for a single period in the afternoon”.split_offsetdivides each bucket window into two halves; a half holding more than nothing but at mostmax_unitsoccupied units counts as lone. - Joint
Presence Goal - Every bucket in which
anchoris active but shares no activity withcompanioncostsweight— “the class teacher sees the class every day”. - Maximum
Active Buckets - Hard cap on how many period buckets one entity is active in at all — “on at most four days”.
- Maximum
Bucket Starts - Hard cap on how many of a group of activities may start inside one period bucket — “at most one lesson of this course per day”.
- Maximum
Daily Load - Hard cap on the total occupied duration one entity may accumulate inside a single period bucket (e.g. one day): “sum of occupied duration per resource/participant and period bucket <= limit”.
- Maximum
Distinct PerBucket - At most
limitdistinct groups of activities start inside one period bucket — “at most five different subjects a day”. Each inner list is one group, e.g. all lessons of one course. Hard unless it carries aSoftPenalty; then every group over the limit costs the weight. - Minimum
Break - Minimum distance between any two of an entity’s assignments, e.g. a person-specific minimum break between two of their activities.
- Move
Evaluation - Participant
- Person or group whose simultaneous activities can be detected.
- Participant
Choice Group - All activities in the group choose the same participant for their candidate requirement (plan 33.1).
- Participant
Group - Domain-neutral participant group. Memberships are stored separately so groups can overlap.
- Participant
Group Id - Participant
Id - Participant
Pool - Named set of interchangeable participants.
- Participant
Pool Id - Participant
Requirement - One participant chosen from an exact id, a named pool, or an explicit candidate set.
- Proposed
Activity - Exact single-activity change checked against a
crate::SchedulingState. - Recurrence
- Blueprint for expanding one template
Activityinto several instances that repeat everysteptime units, each on the same relative position (plan 28, “Engine (schedulr, generisch)”). - Repair
Options - Repair
Result - Resource
- Capacity-constrained entity consumed by activities.
- Resource
Cost Goal - What the resources a set of activities uses cost — a ranking of fallback rooms, a home room,
a room that fits the group’s size. Every resource an activity uses (fixed or chosen from a
pool) costs its entry in
costs, an unlisted onedefault_cost, timesweight. - Resource
Id - Resource
Pool - Named set of interchangeable resources.
- Resource
Pool Id - Resource
Requirement - Exact capacity demand on a resource.
- Room
Change Goal - Resource changes of one entity inside each bucket: every two consecutive activities in
different resources cost
change_weight— “fewer room changes for a teacher”. When the two resources lie at differentlocationsand less thantravel_gapunits separate the activities, that coststravel_weighton top — “time to walk to the annex”. - Schedule
Template - Periodic slot model (one week, A/B weeks, or block cycle) plus absolute exceptions.
- Scheduling
Problem - In-memory batch scheduling input.
- Scheduling
State - In-memory snapshot used for synchronous single-activity feasibility checks.
- Score
- Public score independent of the underlying solver representation.
- Score
Component - Score
Rule - Named, inspectable scoring rule. Components are attached to each produced solution.
- Slot
Template - One reusable slot inside a periodic schedule template.
- Soft
Goal - One problem-level soft goal.
- Soft
Penalty - What breaking a rule costs when the caller makes it a preference instead of a hard rule.
- Solution
- A solved set of activity placements and its aggregate score.
- Solution
Check - The full verdict on one
Solution, obtained without mutating it (plan 31, E0.1). - Solution
Comparison - Solve
Options - Parameters controlling one
CompiledProblem::solve_withrun: how long to search and how to interrupt it early.solve()is a convenience default of this with no cancellation and a 10 second time limit — the same defaultunifier’s ownSolverOptionsuses. - Solve
Result - Solve
Statistics - Suggestion
- Time
Preference - A soft preference of one entity for or against time windows — “rather not on Monday morning”, “ideally the first two periods”.
- Time
Window - Half-open integer time interval
[start, end).
Enums§
- Abort
Reason - Why a solve run stopped without reaching a conclusive result — mirrors
unifier::solver::AbortReason(except forAbortReason::RejectedSolution, which this crate decides on its own), kept as a distinct type here so callers never need to depend onunifierdirectly. - Activity
Goal Kind - What an
ActivityGoalmeasures. Offsets count from the start of each bucket window; a bucket is a concrete window (Monday of week A and of week B are two buckets). - Activity
Relation - A hard temporal relation between two activities’ start times.
- Assignment
Change - Change
Request - One requested change in an atomic batch: a move or a swap. Every request in a batch is evaluated against the same starting solution, so a batch is all-or-nothing.
- Conflict
Severity - DayRule
Kind - What a
DayRuleasks of one entity’s occupancy, measured on the concrete bucket windows (Monday of week A and Monday of week B are two days). A bucket is active when the entity occupies anything in it. Offsets count from the start of each bucket window. - Entity
Ref - Group
Member - Participant
Conflict Policy - How a conflict that names a
Participantis reported (plan 26, §1). - Proximity
Mode - How the activities of different groups of an
ActivityGoalKind::Proximityshould relate inside one bucket. - Repair
Outcome - Whether
CompiledProblem::repair_with_outcomeactually repaired from the baseline or had to fall back to a fresh solve because the stability-augmented problem did not compile (plan 31, E0.2). A caller that shows a plan as a “repair” must check this first. - Score
Level - Score
Rule Kind - Soft
Goal Kind - A problem-level soft preference, contrasted with the activity-scoped
ScoreRule. - Solve
Status - Solve
Strategy - How
CompiledProblem::solve_withspends its budget.
Constants§
Functions§
- bucket_
load_ blocks - The consecutive block durations this group occupies per bucket, in bucket order (plan 25, C1). Two touching or overlapping assignments form one block, which is what makes “a double period” observable at all; every block is reported as an absolute end, so callers can compare block lengths without re-deriving the arithmetic.
- bucket_
load_ pattern - The group’s block durations across all buckets, largest first — the comparable form of a
teaching pattern.
[2, 1, 1]and[1, 2, 1]produce the same result (plan 25, C1). - bucket_
windows - The buckets load rules are checked against, derived from the schedule template: each day becomes a bucket, repeated once per cycle; without a valid template the whole horizon is one bucket (plan 25, C1).
- compare
- compile
- matches_
bucket_ load_ pattern - Whether the group’s teaching blocks match one of the allowed patterns (order irrelevant, plan 25, C1). A pattern that is not active is never violated.