Skip to main content

Crate schedulr

Crate schedulr 

Source
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 structured Conflict types 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 → solve plus analyze, evaluate_move, suggest, compare, explain, and baseline-aware repair paths.

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§

AcademicPeriod
Activity
Scheduling demand independent of any concrete solution assignment.
ActivityGoal
A distribution goal over a set of activities; every counted unit costs weight at level.
ActivityId
ActivityRelationConstraint
Ties two activities together with an ActivityRelation. Unlike ScoreRule, this is a hard constraint: the solver never produces a solution that violates it — unless it carries a SoftPenalty (see Self::soft), which makes a broken relation cost the penalty’s weight.
Analysis
Assignment
Concrete placement of one activity.
Bottleneck
BreakTemplate
A recurring break within a day (e.g. lunch, recess). window is day-relative, like SlotTemplate::offset. No activity may be placed so that it overlaps a break; see ScheduleTemplate::allowed_starts_for.
BucketLoadPattern
Allowed shapes of a group of activities’ teaching blocks, expressed over the schedule’s own buckets (plan 25, C1).
BucketWindow
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”.
CancellationToken
Thread-safe cancellation handle allowing external interruption of running solvers.
ChangeAspects
Which aspects of an assignment differ between two solutions.
ChangeEvaluation
The result of evaluating a whole batch of ChangeRequests at once.
CompileError
CompiledProblem
ConcurrentFreeGoal
More than limit of entities free at the same unit inside [start_offset, end_offset) of their active buckets costs weight per 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.
ConsecutiveRunLimit
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 (see Self::soft); then every unit it misses by costs the weight.
DayTemplate
GroupMembership
IdleBounds
Soft bounds on one entity’s free time between its assignments — “two to four free periods a week, and no double free periods”.
IsolatedBlockGoal
A lone block in one half of an active bucket costs weight — “nobody comes in for a single period in the afternoon”. split_offset divides each bucket window into two halves; a half holding more than nothing but at most max_units occupied units counts as lone.
JointPresenceGoal
Every bucket in which anchor is active but shares no activity with companion costs weight — “the class teacher sees the class every day”.
MaximumActiveBuckets
Hard cap on how many period buckets one entity is active in at all — “on at most four days”.
MaximumBucketStarts
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”.
MaximumDailyLoad
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”.
MaximumDistinctPerBucket
At most limit distinct 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 a SoftPenalty; then every group over the limit costs the weight.
MinimumBreak
Minimum distance between any two of an entity’s assignments, e.g. a person-specific minimum break between two of their activities.
MoveEvaluation
Participant
Person or group whose simultaneous activities can be detected.
ParticipantChoiceGroup
All activities in the group choose the same participant for their candidate requirement (plan 33.1).
ParticipantGroup
Domain-neutral participant group. Memberships are stored separately so groups can overlap.
ParticipantGroupId
ParticipantId
ParticipantPool
Named set of interchangeable participants.
ParticipantPoolId
ParticipantRequirement
One participant chosen from an exact id, a named pool, or an explicit candidate set.
ProposedActivity
Exact single-activity change checked against a crate::SchedulingState.
Recurrence
Blueprint for expanding one template Activity into several instances that repeat every step time units, each on the same relative position (plan 28, “Engine (schedulr, generisch)”).
RepairOptions
RepairResult
Resource
Capacity-constrained entity consumed by activities.
ResourceCostGoal
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 one default_cost, times weight.
ResourceId
ResourcePool
Named set of interchangeable resources.
ResourcePoolId
ResourceRequirement
Exact capacity demand on a resource.
RoomChangeGoal
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 different locations and less than travel_gap units separate the activities, that costs travel_weight on top — “time to walk to the annex”.
ScheduleTemplate
Periodic slot model (one week, A/B weeks, or block cycle) plus absolute exceptions.
SchedulingProblem
In-memory batch scheduling input.
SchedulingState
In-memory snapshot used for synchronous single-activity feasibility checks.
Score
Public score independent of the underlying solver representation.
ScoreComponent
ScoreRule
Named, inspectable scoring rule. Components are attached to each produced solution.
SlotTemplate
One reusable slot inside a periodic schedule template.
SoftGoal
One problem-level soft goal.
SoftPenalty
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.
SolutionCheck
The full verdict on one Solution, obtained without mutating it (plan 31, E0.1).
SolutionComparison
SolveOptions
Parameters controlling one CompiledProblem::solve_with run: 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 default unifier’s own SolverOptions uses.
SolveResult
SolveStatistics
Suggestion
TimePreference
A soft preference of one entity for or against time windows — “rather not on Monday morning”, “ideally the first two periods”.
TimeWindow
Half-open integer time interval [start, end).

Enums§

AbortReason
Why a solve run stopped without reaching a conclusive result — mirrors unifier::solver::AbortReason (except for AbortReason::RejectedSolution, which this crate decides on its own), kept as a distinct type here so callers never need to depend on unifier directly.
ActivityGoalKind
What an ActivityGoal measures. 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).
ActivityRelation
A hard temporal relation between two activities’ start times.
AssignmentChange
ChangeRequest
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.
ConflictSeverity
DayRuleKind
What a DayRule asks 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.
EntityRef
GroupMember
ParticipantConflictPolicy
How a conflict that names a Participant is reported (plan 26, §1).
ProximityMode
How the activities of different groups of an ActivityGoalKind::Proximity should relate inside one bucket.
RepairOutcome
Whether CompiledProblem::repair_with_outcome actually 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.
ScoreLevel
ScoreRuleKind
SoftGoalKind
A problem-level soft preference, contrasted with the activity-scoped ScoreRule.
SolveStatus
SolveStrategy
How CompiledProblem::solve_with spends its budget.

Constants§

DEFAULT_CAPACITY_DIMENSION

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.