Relations and breaks
One training day with a lunch break, a trainer who is unavailable in the morning, and SameStart, Consecutive, Precedence and NoOverlap relations. These features are on master and not yet in the 0.8.0 release.
Relations and breaks
examples/relations_and_breaks.rs//! One training day with a lunch break, a trainer who is only available in the
//! afternoon, and hard relations between activities.
//!
//! Breaks, availability ranges and activity relations were added after the
//! 0.8.0 release; this example needs the current master branch.
//!
//! One time unit is one hour; hour 0 is Monday 00:00.
//!
//! Run with `cargo run --example relations_and_breaks`.
use schedulr::{
AcademicPeriod, Activity, ActivityId, ActivityRelation, ActivityRelationConstraint,
BreakTemplate, DayTemplate, Participant, ParticipantId, Resource, ResourceId,
ResourceRequirement, ScheduleTemplate, SchedulingProblem, SlotTemplate, Solution, TimeWindow,
compile,
};
fn main() {
let aurora = ResourceId(1);
let birch = ResourceId(2);
let lab = ResourceId(3);
let library = ResourceId(4);
let resources = vec![
Resource::new(aurora, "Room Aurora", 24),
Resource::new(birch, "Room Birch", 12),
Resource::new(lab, "Lab 1", 12),
Resource::new(library, "Library", 6),
];
let ana = ParticipantId(1);
let ben = ParticipantId(2);
let chris = ParticipantId(3);
let participants = vec![
Participant::new(ana, "Ana"),
// Ben cannot start anything between 09:00 and 13:00 (inclusive).
Participant::new(ben, "Ben").with_unavailable_range(9, 13),
Participant::new(chris, "Chris"),
];
let day = TimeWindow::new(0, 24);
let activities = vec![
Activity::new(ActivityId(1), "Welcome", day, 1)
.with_requirement(ResourceRequirement::new(aurora, 1)),
Activity::new(ActivityId(2), "Theory", day, 2)
.with_requirement(ResourceRequirement::new(aurora, 1))
.with_participant(ana),
Activity::new(ActivityId(3), "Lab", day, 1)
.with_requirement(ResourceRequirement::new(lab, 1))
.with_participant(ana),
Activity::new(ActivityId(4), "Review", day, 1)
.with_requirement(ResourceRequirement::new(birch, 1))
.with_participant(ben),
Activity::new(ActivityId(5), "Parallel track", day, 2)
.with_requirement(ResourceRequirement::new(birch, 1))
.with_participant(chris),
Activity::new(ActivityId(6), "Office hour", day, 1)
.with_requirement(ResourceRequirement::new(library, 1))
.with_participant(ben),
];
// Slots start every hour from 09 to 16 and end by 17:00; lunch is 12:00-13:00.
let mut template = DayTemplate::new(0).with_break(BreakTemplate {
name: "Lunch".to_string(),
window: TimeWindow::new(12, 13),
});
for hour in 9..17 {
template = template.with_slot(SlotTemplate::new(
format!("{hour:02}:00"),
hour,
(17 - hour) as u64,
));
}
let relations = [
(1, 2, ActivityRelation::Precedence { min_gap: 1 }),
(2, 3, ActivityRelation::Consecutive),
(3, 4, ActivityRelation::Precedence { min_gap: 0 }),
(2, 5, ActivityRelation::SameStart),
(1, 6, ActivityRelation::NoOverlap),
];
let mut problem = SchedulingProblem::new(resources, participants, activities).with_calendar(
AcademicPeriod { window: day },
ScheduleTemplate::new(24).with_day(template),
);
for (first, second, relation) in relations {
problem = problem.with_relation(ActivityRelationConstraint::new(
ActivityId(first),
ActivityId(second),
relation,
));
}
let compiled = compile(&problem).expect("the training day compiles");
let result = compiled.solve();
println!("Status: {:?}", result.status);
let solution = result.solution.expect("the training day is feasible");
println!();
println!(
"{:<13} {:<19} {:<13} Participants",
"When", "Activity", "Resource"
);
let mut ordered = solution.assignments.clone();
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 resource = assignment
.resources
.iter()
.map(|id| {
problem
.resources
.iter()
.find(|resource| resource.id() == *id)
.map_or("?", Resource::name)
})
.collect::<Vec<_>>()
.join(", ");
let people = assignment
.participants
.iter()
.map(|id| {
problem
.participants
.iter()
.find(|participant| participant.id() == *id)
.map_or("?", Participant::name)
})
.collect::<Vec<_>>()
.join(", ");
let line = format!(
"{:<13} {:<19} {:<13} {}",
format!(
"{}-{}",
clock(assignment.window.start),
clock(assignment.window.end)
),
format!("{} {}", assignment.activity, activity.name()),
resource,
people
);
println!("{}", line.trim_end());
}
println!();
println!("Checks against the solution");
for relation in &problem.relations {
let first = window_of(&solution, relation.first);
let second = window_of(&solution, relation.second);
let (name, detail) = match relation.relation {
ActivityRelation::SameStart => (
"SameStart".to_string(),
format!(
"{} and {} both start at {}",
relation.first,
relation.second,
clock(second.start)
),
),
ActivityRelation::Consecutive => (
"Consecutive".to_string(),
format!(
"{} ends {}, {} starts {}",
relation.first,
clock(first.end),
relation.second,
clock(second.start)
),
),
ActivityRelation::Precedence { min_gap } => (
format!("Precedence (gap {min_gap}h)"),
format!(
"{} ends {}, {} starts {}",
relation.first,
clock(first.end),
relation.second,
clock(second.start)
),
),
ActivityRelation::NoOverlap => (
"NoOverlap".to_string(),
format!(
"{} {}-{}, {} {}-{}",
relation.first,
clock(first.start),
clock(first.end),
relation.second,
clock(second.start),
clock(second.end)
),
),
};
println!(" {name:<22} {detail}");
}
let lunch = TimeWindow::new(12, 13);
let during_lunch = solution
.assignments
.iter()
.filter(|assignment| {
assignment.window.start < lunch.end && lunch.start < assignment.window.end
})
.count();
println!(
" {:<22} {during_lunch} activities overlap the break",
"Lunch 12:00-13:00"
);
println!(
" {:<22} a6 starts {}, a4 starts {}",
"Ben unavailable 09-13",
clock(window_of(&solution, ActivityId(6)).start),
clock(window_of(&solution, ActivityId(4)).start)
);
}
fn window_of(solution: &Solution, activity: ActivityId) -> TimeWindow {
solution
.assignments
.iter()
.find(|assignment| assignment.activity == activity)
.expect("every activity is assigned")
.window
}
fn clock(hour: i64) -> String {
format!("{:02}:00", hour % 24)
}Status: Feasible When Activity Resource Participants 09:00-10:00 a1 Welcome Room Aurora 13:00-15:00 a2 Theory Room Aurora Ana 13:00-15:00 a5 Parallel track Room Birch Chris 14:00-15:00 a6 Office hour Library Ben 15:00-16:00 a3 Lab Lab 1 Ana 16:00-17:00 a4 Review Room Birch Ben Checks against the solution Precedence (gap 1h) a1 ends 10:00, a2 starts 13:00 Consecutive a2 ends 15:00, a3 starts 15:00 Precedence (gap 0h) a3 ends 16:00, a4 starts 16:00 SameStart a2 and a5 both start at 13:00 NoOverlap a1 09:00-10:00, a6 14:00-15:00 Lunch 12:00-13:00 0 activities overlap the break Ben unavailable 09-13 a6 starts 14:00, a4 starts 16:00