Use maps to track workload progress instead of recalculating them at every step of the search, calculate total days/slots once, add integration tests, add log folder
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
use crate::{
|
||||
bounds::WorkloadBounds, config::UserConfig, resident::ResidentId, schedule::MonthlySchedule,
|
||||
config::UserConfig,
|
||||
resident::ResidentId,
|
||||
schedule::MonthlySchedule,
|
||||
slot::Slot,
|
||||
workload::{WorkloadBounds, WorkloadTracker},
|
||||
};
|
||||
|
||||
use rand::Rng;
|
||||
@@ -15,52 +18,62 @@ impl Scheduler {
|
||||
Self { config, bounds }
|
||||
}
|
||||
|
||||
pub fn run(&self, schedule: &mut MonthlySchedule) -> bool {
|
||||
pub fn new_with_config(config: UserConfig) -> Self {
|
||||
let bounds = WorkloadBounds::new_with_config(&config);
|
||||
|
||||
Self { config, bounds }
|
||||
}
|
||||
|
||||
pub fn run(&self, schedule: &mut MonthlySchedule, tracker: &mut WorkloadTracker) -> bool {
|
||||
schedule.prefill(&self.config);
|
||||
self.search(schedule, Slot::default())
|
||||
for (slot, res_id) in schedule.0.iter() {
|
||||
tracker.insert(res_id, &self.config, *slot);
|
||||
}
|
||||
|
||||
self.search(schedule, tracker, Slot::default())
|
||||
}
|
||||
|
||||
/// DFS where maximum depth is calculated by total_days_of_month + odd_days_of_month each node is called a slot
|
||||
/// Starts with schedule partially completed from the user interface
|
||||
/// Ends with a full schedule following restrictions and fairness
|
||||
pub fn search(&self, schedule: &mut MonthlySchedule, slot: Slot) -> bool {
|
||||
pub fn search(
|
||||
&self,
|
||||
schedule: &mut MonthlySchedule,
|
||||
tracker: &mut WorkloadTracker,
|
||||
slot: Slot,
|
||||
) -> bool {
|
||||
if !slot.is_first()
|
||||
&& schedule.restrictions_violated(&slot.previous(), &self.config, &self.bounds)
|
||||
&& schedule.restrictions_violated(&slot.previous(), &self.config, &self.bounds, tracker)
|
||||
{
|
||||
log::trace!("Cutting branch due to restriction violation");
|
||||
return false;
|
||||
}
|
||||
|
||||
if slot.greater_than(self.config.total_days()) {
|
||||
if !schedule.is_per_shift_threshold_met(&self.config, &self.bounds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log::trace!("Solution found, exiting recursive algorithm");
|
||||
return true;
|
||||
if slot.greater_than(self.config.total_days) {
|
||||
return tracker.are_all_thresholds_met(&self.config, &self.bounds);
|
||||
}
|
||||
|
||||
if schedule.is_slot_manually_assigned(&slot) {
|
||||
return self.search(schedule, slot.next());
|
||||
return self.search(schedule, tracker, slot.next());
|
||||
}
|
||||
|
||||
// sort candidates by current workload, add rng for tie breakers
|
||||
let mut valid_resident_ids = self.valid_residents(slot, schedule);
|
||||
valid_resident_ids.sort_unstable_by_key(|res_id| {
|
||||
let workload = schedule.current_workload(res_id);
|
||||
let workload = tracker.current_workload(res_id);
|
||||
let tie_breaker: f64 = rand::rng().random();
|
||||
(workload, (tie_breaker * 1000.0) as usize)
|
||||
});
|
||||
|
||||
for id in &valid_resident_ids {
|
||||
schedule.insert(slot, id);
|
||||
tracker.insert(id, &self.config, slot);
|
||||
|
||||
if self.search(schedule, slot.next()) {
|
||||
log::trace!("Solution found, exiting recursive algorithm");
|
||||
if self.search(schedule, tracker, slot.next()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
schedule.remove(slot);
|
||||
tracker.remove(id, &self.config, slot);
|
||||
}
|
||||
|
||||
false
|
||||
@@ -68,15 +81,19 @@ impl Scheduler {
|
||||
|
||||
/// Return all valid residents for the current slot
|
||||
pub fn valid_residents(&self, slot: Slot, schedule: &MonthlySchedule) -> Vec<&ResidentId> {
|
||||
let required_type = slot.shift_type(); // Calculate once here
|
||||
let other_resident = schedule.get_resident_id(&slot.other_position());
|
||||
let required_type = slot.shift_type();
|
||||
let other_resident = slot
|
||||
.other_position()
|
||||
.and_then(|partner_slot| schedule.get_resident_id(&partner_slot));
|
||||
|
||||
self.config
|
||||
.residents
|
||||
.iter()
|
||||
.filter(|r| Some(&r.id) != other_resident)
|
||||
.filter(|r| !r.negative_shifts.contains(&slot.day))
|
||||
.filter(|r| r.allowed_types.contains(&required_type))
|
||||
.filter(|r| {
|
||||
Some(&r.id) != other_resident
|
||||
&& !r.negative_shifts.contains(&slot.day)
|
||||
&& r.allowed_types.contains(&required_type)
|
||||
})
|
||||
.map(|r| &r.id)
|
||||
.collect()
|
||||
}
|
||||
@@ -87,12 +104,12 @@ mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use crate::{
|
||||
bounds::WorkloadBounds,
|
||||
config::UserConfig,
|
||||
resident::Resident,
|
||||
schedule::MonthlySchedule,
|
||||
scheduler::Scheduler,
|
||||
slot::{Day, ShiftPosition, Slot},
|
||||
workload::{WorkloadBounds, WorkloadTracker},
|
||||
};
|
||||
|
||||
#[fixture]
|
||||
@@ -122,11 +139,20 @@ mod tests {
|
||||
Scheduler::new(config, bounds)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_search(mut schedule: MonthlySchedule, scheduler: Scheduler) {
|
||||
assert!(scheduler.search(&mut schedule, Slot::default()));
|
||||
#[fixture]
|
||||
fn tracker() -> WorkloadTracker {
|
||||
WorkloadTracker::default()
|
||||
}
|
||||
|
||||
for d in 1..=scheduler.config.total_days() {
|
||||
#[rstest]
|
||||
fn test_search(
|
||||
mut schedule: MonthlySchedule,
|
||||
mut tracker: WorkloadTracker,
|
||||
scheduler: Scheduler,
|
||||
) {
|
||||
assert!(scheduler.run(&mut schedule, &mut tracker));
|
||||
|
||||
for d in 1..=scheduler.config.total_days {
|
||||
let day = Day(d);
|
||||
if day.is_open_shift() {
|
||||
let slot_first = Slot::new(day, ShiftPosition::First);
|
||||
@@ -140,9 +166,9 @@ mod tests {
|
||||
}
|
||||
|
||||
for r in &scheduler.config.residents {
|
||||
let workload = schedule.current_workload(&r.id);
|
||||
let workload = tracker.current_workload(&r.id);
|
||||
let limit = *scheduler.bounds.max_workloads.get(&r.id).unwrap();
|
||||
assert!(workload <= limit as usize);
|
||||
assert!(workload <= limit);
|
||||
}
|
||||
|
||||
println!("{}", schedule.pretty_print(&scheduler.config));
|
||||
|
||||
Reference in New Issue
Block a user