schedulrv0.8.0

← All examples

Workshop plan

Five activities, three rooms and three trainers on a two-day slot calendar, solved with branch and bound. Rooms and trainers come from pools; the timeline shows the resulting intervals per resource and person. Several plans reach the best score, so a rerun can pick a different one with the same score.

Workshop plan

examples/workshop_plan.rs
//! A two-day workshop plan: rooms, trainers and five activities placed on a
//! weekly slot calendar. Uses only the API released in schedulr 0.8.0.
//!
//! One time unit is one hour; hour 0 is Monday 00:00.
//!
//! Run with `cargo run --example workshop_plan`.

use schedulr::{
    AcademicPeriod, Activity, ActivityId, Assignment, DayTemplate, GroupMembership, Participant,
    ParticipantGroup, ParticipantGroupId, ParticipantId, ParticipantPool, ParticipantPoolId,
    ParticipantRequirement, Resource, ResourceId, ResourcePool, ResourcePoolId,
    ResourceRequirement, ScheduleTemplate, SchedulingProblem, ScoreLevel, ScoreRule, SlotTemplate,
    TimeWindow, compile,
};

const DAYS: [&str; 2] = ["Mon", "Tue"];
const FIRST_HOUR: i64 = 9;
const LAST_HOUR: i64 = 16;

fn main() {
    let resources = vec![
        Resource::new(ResourceId(1), "Room Aurora", 24)
            .with_type("room")
            .with_feature("projector"),
        Resource::new(ResourceId(2), "Room Birch", 12)
            .with_type("room")
            .with_feature("projector"),
        Resource::new(ResourceId(3), "Lab 1", 12)
            .with_type("lab")
            .with_feature("workstations"),
    ];
    let participants = vec![
        Participant::new(ParticipantId(1), "Ana"),
        Participant::new(ParticipantId(2), "Ben"),
        Participant::new(ParticipantId(3), "Chris"),
    ];
    let trainers = ParticipantGroupId(1);
    let seminar_rooms = ResourcePoolId(1);
    let rust_trainers = ParticipantPoolId(1);

    let activities = vec![
        Activity::new(ActivityId(1), "Kickoff", TimeWindow::new(0, 48), 1)
            .with_requirement(ResourceRequirement::matching("room", 1).with_minimum_capacity(20))
            .with_participant_group(trainers),
        Activity::new(
            ActivityId(2),
            "Rust fundamentals",
            TimeWindow::new(0, 48),
            3,
        )
        .with_requirement(
            ResourceRequirement::from_pool(seminar_rooms, 1).with_feature("projector"),
        )
        .with_participant_requirement(ParticipantRequirement::from_pool(rust_trainers)),
        Activity::new(ActivityId(3), "Hands-on lab", TimeWindow::new(0, 48), 2)
            .with_requirement(ResourceRequirement::matching("lab", 1).with_feature("workstations"))
            .with_participant_requirement(ParticipantRequirement::from_pool(rust_trainers)),
        Activity::new(ActivityId(4), "Solver deep dive", TimeWindow::new(0, 48), 2)
            .with_requirement(ResourceRequirement::from_pool(seminar_rooms, 1))
            .with_participant(ParticipantId(3)),
        Activity::new(ActivityId(5), "Q&A", TimeWindow::new(24, 48), 1)
            .with_requirement(ResourceRequirement::matching("room", 1).with_minimum_capacity(20))
            .with_participant_group(trainers),
    ];

    // Every day offers slots at 09, 10, 11 (ending by 12:00) and 13, 14, 15 (ending by 16:00).
    // Tuesday 13:00-14:59 is closed as a one-off exception.
    let mut day = DayTemplate::new(0);
    for (offset, length) in [(9, 3), (10, 2), (11, 1), (13, 3), (14, 2), (15, 1)] {
        day = day.with_slot(SlotTemplate::new(format!("{offset:02}:00"), offset, length));
    }
    let calendar = ScheduleTemplate::new(24)
        .with_day(day)
        .with_unavailable_range(37, 38);

    let problem = SchedulingProblem::new(resources, participants, activities)
        .with_resource_pool(ResourcePool::new(
            seminar_rooms,
            "Seminar rooms",
            [ResourceId(1), ResourceId(2)],
        ))
        .with_participant_pool(ParticipantPool::new(
            rust_trainers,
            "Rust trainers",
            [ParticipantId(1), ParticipantId(2)],
        ))
        .with_participant_group(ParticipantGroup::new(trainers, "Trainers"))
        .with_group_membership(GroupMembership::participant(trainers, ParticipantId(1)))
        .with_group_membership(GroupMembership::participant(trainers, ParticipantId(2)))
        .with_group_membership(GroupMembership::participant(trainers, ParticipantId(3)))
        .with_calendar(
            AcademicPeriod {
                window: TimeWindow::new(0, 48),
            },
            calendar,
        )
        .with_score_rule(ScoreRule::prefer_window(
            "kickoff on Monday at 09:00",
            ScoreLevel::Strong,
            ActivityId(1),
            TimeWindow::new(9, 10),
            1,
        ))
        .with_score_rule(ScoreRule::prefer_window(
            "Q&A on Tuesday afternoon",
            ScoreLevel::Medium,
            ActivityId(5),
            TimeWindow::new(37, 40),
            1,
        ))
        .with_score_rule(ScoreRule::prefer_window(
            "lab on Tuesday morning",
            ScoreLevel::Weak,
            ActivityId(3),
            TimeWindow::new(33, 36),
            1,
        ));

    println!(
        "Input: {} resources, {} participants, {} activities, 1 resource pool, 1 participant pool, 1 group",
        problem.resources.len(),
        problem.participants.len(),
        problem.activities.len(),
    );
    println!("Calendar: 24-hour cycle, starts at 09 10 11 13 14 15, Tue 13:00-14:59 closed");
    println!();

    let compiled = compile(&problem).expect("the workshop problem compiles");
    let result = compiled.solve();
    println!(
        "Status: {:?} (optimal: {})",
        result.status, result.statistics.optimal
    );
    let solution = result.solution.expect("the workshop problem is feasible");
    println!();

    println!(
        "{:<17} {:<21} {:<13} Participants",
        "When", "Activity", "Resource"
    );
    let mut ordered: Vec<&Assignment> = solution.assignments.iter().collect();
    ordered.sort_by_key(|assignment| (assignment.window.start, assignment.activity));
    for assignment in &ordered {
        let activity = problem
            .activities
            .iter()
            .find(|activity| activity.id() == assignment.activity)
            .expect("assigned activity exists");
        let resources = assignment
            .resources
            .iter()
            .map(|id| resource_name(&problem, *id))
            .collect::<Vec<_>>()
            .join(", ");
        let people = assignment
            .participants
            .iter()
            .map(|id| participant_name(&problem, *id))
            .collect::<Vec<_>>()
            .join(", ");
        println!(
            "{:<17} {:<21} {:<13} {}",
            format_window(assignment.window),
            format!("{} {}", assignment.activity, activity.name()),
            resources,
            people
        );
    }
    println!();

    println!("Timeline (one column per hour, 09-16)");
    let mut header = format!("{:<13}", "");
    for name in DAYS {
        header.push_str(&format!(" {:<21}", name));
    }
    println!("{}", header.trim_end());
    let mut hours = format!("{:<13}", "");
    for _ in DAYS {
        hours.push(' ');
        for hour in FIRST_HOUR..LAST_HOUR {
            hours.push_str(&format!("{hour:02} "));
        }
    }
    println!("{}", hours.trim_end());
    for resource in &problem.resources {
        let booked: Vec<&Assignment> = solution
            .assignments
            .iter()
            .filter(|assignment| assignment.resources.contains(&resource.id()))
            .collect();
        println!("{}", timeline_row(resource.name(), &booked));
    }
    for participant in &problem.participants {
        let booked: Vec<&Assignment> = solution
            .assignments
            .iter()
            .filter(|assignment| assignment.participants.contains(&participant.id()))
            .collect();
        println!("{}", timeline_row(participant.name(), &booked));
    }
    println!();

    println!(
        "Score: hard {}, strong {}, medium {}, weak {}",
        solution.score.hard, solution.score.strong, solution.score.medium, solution.score.weak
    );
    for component in &solution.score_components {
        let activity = component
            .activity
            .map_or_else(String::new, |id| id.to_string());
        println!(
            "  {:<7} {:<27} {:<3} {}",
            format!("{:?}", component.level),
            component.category,
            activity,
            component.value
        );
    }
}

fn format_window(window: TimeWindow) -> String {
    let day = DAYS[usize::try_from(window.start / 24).expect("non-negative day")];
    format!(
        "{day} {:02}:00-{:02}:00",
        window.start % 24,
        window.end - window.start / 24 * 24
    )
}

fn timeline_row(name: &str, booked: &[&Assignment]) -> String {
    let mut row = format!("{name:<13}");
    for day in 0..DAYS.len() as i64 {
        row.push(' ');
        for hour in FIRST_HOUR..LAST_HOUR {
            let time = day * 24 + hour;
            let cell = booked
                .iter()
                .find(|assignment| assignment.window.start <= time && time < assignment.window.end)
                .map_or_else(
                    || "..".to_string(),
                    |assignment| assignment.activity.to_string(),
                );
            row.push_str(&format!("{cell} "));
        }
    }
    row.trim_end().to_string()
}

fn resource_name(problem: &SchedulingProblem, id: ResourceId) -> &str {
    problem
        .resources
        .iter()
        .find(|resource| resource.id() == id)
        .map_or("?", Resource::name)
}

fn participant_name(problem: &SchedulingProblem, id: ParticipantId) -> &str {
    problem
        .participants
        .iter()
        .find(|participant| participant.id() == id)
        .map_or("?", Participant::name)
}
Input: 3 resources, 3 participants, 5 activities, 1 resource pool, 1 participant pool, 1 group
Calendar: 24-hour cycle, starts at 09 10 11 13 14 15, Tue 13:00-14:59 closed

Status: Feasible (optimal: true)

When              Activity              Resource      Participants
Mon 09:00-10:00   a1 Kickoff            Room Aurora   Ana, Ben, Chris
Mon 10:00-12:00   a4 Solver deep dive   Room Aurora   Chris
Mon 13:00-16:00   a2 Rust fundamentals  Room Aurora   Ben
Tue 09:00-11:00   a3 Hands-on lab       Lab 1         Ben
Tue 15:00-16:00   a5 Q&A                Room Aurora   Ana, Ben, Chris

Timeline (one column per hour, 09-16)
              Mon                   Tue
              09 10 11 12 13 14 15  09 10 11 12 13 14 15
Room Aurora   a1 a4 a4 .. a2 a2 a2  .. .. .. .. .. .. a5
Room Birch    .. .. .. .. .. .. ..  .. .. .. .. .. .. ..
Lab 1         .. .. .. .. .. .. ..  a3 a3 .. .. .. .. ..
Ana           a1 .. .. .. .. .. ..  .. .. .. .. .. .. a5
Ben           a1 .. .. .. a2 a2 a2  a3 a3 .. .. .. .. a5
Chris         a1 a4 a4 .. .. .. ..  .. .. .. .. .. .. a5

Score: hard 0, strong 0, medium 0, weak 0
  Strong  kickoff on Monday at 09:00  a1  0
  Medium  Q&A on Tuesday afternoon    a5  0
  Weak    lab on Tuesday morning      a3  0
  • batch solve
  • pools
  • groups
  • calendar
  • scores