schedulr 0.7.2

Scheduling framework (activity/resource/interval DSL) built on unifier
Documentation

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.

pathwise → unifier → schedulr → (application: timetabling, appointment booking, ...)

Status

Version 0.7 is implemented. It provides:

  • 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 pools, and hierarchical/overlapping participant groups;
  • periodic slot calendars with absolute exceptions and lexicographic Strong/Medium/Weak score components;
  • compilesolve 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 compilesolveexplain 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.7"

License

MIT — see LICENSE.