Create struct scheduler to encapsulate the search logic
This commit is contained in:
@@ -164,8 +164,8 @@ mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use crate::{
|
||||
bounds::WorkloadBounds, config::UserConfig, generator::backtracking, resident::Resident,
|
||||
schedule::MonthlySchedule, slot::Slot,
|
||||
bounds::WorkloadBounds, config::UserConfig, resident::Resident, schedule::MonthlySchedule,
|
||||
scheduler::Scheduler, slot::Slot,
|
||||
};
|
||||
|
||||
#[fixture]
|
||||
@@ -190,13 +190,14 @@ mod tests {
|
||||
WorkloadBounds::new_with_config(&config)
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn scheduler(config: UserConfig, bounds: WorkloadBounds) -> Scheduler {
|
||||
Scheduler::new(config, bounds)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
pub fn test_export_as_doc(
|
||||
mut schedule: MonthlySchedule,
|
||||
config: UserConfig,
|
||||
bounds: WorkloadBounds,
|
||||
) {
|
||||
backtracking(&mut schedule, Slot::default(), &config, &bounds);
|
||||
schedule.export_as_doc(&config);
|
||||
pub fn test_export_as_doc(mut schedule: MonthlySchedule, scheduler: Scheduler) {
|
||||
scheduler.search(&mut schedule, Slot::default());
|
||||
schedule.export_as_doc(&scheduler.config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
use rand::Rng;
|
||||
|
||||
use crate::{bounds::WorkloadBounds, config::UserConfig, schedule::MonthlySchedule, slot::Slot};
|
||||
|
||||
/// 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 backtracking(
|
||||
schedule: &mut MonthlySchedule,
|
||||
slot: Slot,
|
||||
config: &UserConfig,
|
||||
bounds: &WorkloadBounds,
|
||||
) -> bool {
|
||||
if !slot.is_first() && schedule.restrictions_violated(&slot.previous(), config, bounds) {
|
||||
log::trace!("Cutting branch due to restriction violation");
|
||||
return false;
|
||||
}
|
||||
|
||||
if slot.greater_than(config.total_days()) {
|
||||
if !schedule.is_per_shift_threshold_met(config, bounds) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log::trace!("Solution found, exiting recursive algorithm");
|
||||
return true;
|
||||
}
|
||||
|
||||
if schedule.is_slot_manually_assigned(&slot) {
|
||||
return backtracking(schedule, slot.next(), config, bounds);
|
||||
}
|
||||
|
||||
// sort candidates by current workload, add rng for tie breakers
|
||||
let mut candidates = config.candidates(slot, schedule);
|
||||
candidates.sort_unstable_by_key(|&(resident, _)| {
|
||||
let workload = schedule.current_workload(resident);
|
||||
let tie_breaker: f64 = rand::rng().random();
|
||||
(workload, (tie_breaker * 1000.0) as usize)
|
||||
});
|
||||
|
||||
for (resident, _) in candidates {
|
||||
schedule.insert(slot, resident);
|
||||
|
||||
if backtracking(schedule, slot.next(), config, bounds) {
|
||||
log::trace!("Solution found, exiting recursive algorithm");
|
||||
return true;
|
||||
}
|
||||
|
||||
schedule.remove(slot);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use crate::{
|
||||
bounds::WorkloadBounds,
|
||||
config::UserConfig,
|
||||
generator::backtracking,
|
||||
resident::Resident,
|
||||
schedule::MonthlySchedule,
|
||||
slot::{Day, ShiftPosition, Slot},
|
||||
};
|
||||
|
||||
#[fixture]
|
||||
fn schedule() -> MonthlySchedule {
|
||||
MonthlySchedule::new()
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn config() -> UserConfig {
|
||||
UserConfig::default().with_residents(vec![
|
||||
Resident::new("1", "Stefanos"),
|
||||
Resident::new("2", "Iordanis"),
|
||||
Resident::new("3", "Maria"),
|
||||
Resident::new("4", "Veatriki"),
|
||||
Resident::new("5", "Takis"),
|
||||
Resident::new("6", "Akis"),
|
||||
])
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn bounds(config: UserConfig) -> WorkloadBounds {
|
||||
WorkloadBounds::new_with_config(&config)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_backtracking(
|
||||
mut schedule: MonthlySchedule,
|
||||
config: UserConfig,
|
||||
bounds: WorkloadBounds,
|
||||
) {
|
||||
assert!(backtracking(
|
||||
&mut schedule,
|
||||
Slot::default(),
|
||||
&config,
|
||||
&bounds
|
||||
));
|
||||
|
||||
for d in 1..=config.total_days() {
|
||||
let day = Day(d);
|
||||
if day.is_open_shift() {
|
||||
let slot_first = Slot::new(day, ShiftPosition::First);
|
||||
assert!(schedule.get_resident_id(&slot_first).is_some());
|
||||
let slot_second = Slot::new(day, ShiftPosition::Second);
|
||||
assert!(schedule.get_resident_id(&slot_second).is_some());
|
||||
} else {
|
||||
let slot_first = Slot::new(day, ShiftPosition::First);
|
||||
assert!(schedule.get_resident_id(&slot_first).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
for r in &config.residents {
|
||||
let workload = schedule.current_workload(r);
|
||||
let limit = *bounds.max_workloads.get(&r.id).unwrap();
|
||||
assert!(workload <= limit as usize);
|
||||
}
|
||||
|
||||
println!("{}", schedule.pretty_print(&config));
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,19 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use log::info;
|
||||
|
||||
use crate::{
|
||||
bounds::WorkloadBounds,
|
||||
config::{UserConfig, UserConfigDTO},
|
||||
export::{Export, FileType},
|
||||
generator::backtracking,
|
||||
schedule::MonthlySchedule,
|
||||
slot::Slot,
|
||||
scheduler::Scheduler,
|
||||
};
|
||||
|
||||
mod bounds;
|
||||
mod config;
|
||||
mod export;
|
||||
mod generator;
|
||||
mod resident;
|
||||
mod schedule;
|
||||
mod scheduler;
|
||||
mod slot;
|
||||
|
||||
struct AppState {
|
||||
@@ -28,18 +25,16 @@ struct AppState {
|
||||
/// and the period of the schedule
|
||||
#[tauri::command]
|
||||
fn generate(config: UserConfigDTO, state: tauri::State<'_, AppState>) -> MonthlySchedule {
|
||||
info!("{:?}", config);
|
||||
let config = UserConfig::from(config);
|
||||
let bounds = WorkloadBounds::new_with_config(&config);
|
||||
info!("{:?}", config);
|
||||
let mut schedule = MonthlySchedule::new();
|
||||
schedule.prefill(&config);
|
||||
info!("{}", schedule.pretty_print(&config));
|
||||
|
||||
backtracking(&mut schedule, Slot::default(), &config, &bounds);
|
||||
let bounds = WorkloadBounds::new_with_config(&config);
|
||||
let scheduler = Scheduler::new(config, bounds);
|
||||
scheduler.run(&mut schedule);
|
||||
|
||||
let mut internal_schedule = state.schedule.lock().unwrap();
|
||||
*internal_schedule = schedule.clone();
|
||||
info!("{}", schedule.pretty_print(&config));
|
||||
|
||||
schedule
|
||||
}
|
||||
|
||||
131
src-tauri/src/scheduler.rs
Normal file
131
src-tauri/src/scheduler.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use crate::{bounds::WorkloadBounds, config::UserConfig, schedule::MonthlySchedule, slot::Slot};
|
||||
|
||||
use rand::Rng;
|
||||
|
||||
pub struct Scheduler {
|
||||
pub config: UserConfig,
|
||||
pub bounds: WorkloadBounds,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
pub fn new(config: UserConfig, bounds: WorkloadBounds) -> Self {
|
||||
Self { config, bounds }
|
||||
}
|
||||
|
||||
pub fn run(&self, schedule: &mut MonthlySchedule) -> bool {
|
||||
self.search(schedule, 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 {
|
||||
if !slot.is_first()
|
||||
&& schedule.restrictions_violated(&slot.previous(), &self.config, &self.bounds)
|
||||
{
|
||||
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 schedule.is_slot_manually_assigned(&slot) {
|
||||
return self.search(schedule, slot.next());
|
||||
}
|
||||
|
||||
// sort candidates by current workload, add rng for tie breakers
|
||||
let mut candidates = self.config.candidates(slot, schedule);
|
||||
candidates.sort_unstable_by_key(|&(resident, _)| {
|
||||
let workload = schedule.current_workload(resident);
|
||||
let tie_breaker: f64 = rand::rng().random();
|
||||
(workload, (tie_breaker * 1000.0) as usize)
|
||||
});
|
||||
|
||||
for (resident, _) in candidates {
|
||||
schedule.insert(slot, resident);
|
||||
|
||||
if self.search(schedule, slot.next()) {
|
||||
log::trace!("Solution found, exiting recursive algorithm");
|
||||
return true;
|
||||
}
|
||||
|
||||
schedule.remove(slot);
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use crate::{
|
||||
bounds::WorkloadBounds,
|
||||
config::UserConfig,
|
||||
resident::Resident,
|
||||
schedule::MonthlySchedule,
|
||||
scheduler::Scheduler,
|
||||
slot::{Day, ShiftPosition, Slot},
|
||||
};
|
||||
|
||||
#[fixture]
|
||||
fn schedule() -> MonthlySchedule {
|
||||
MonthlySchedule::new()
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn config() -> UserConfig {
|
||||
UserConfig::default().with_residents(vec![
|
||||
Resident::new("1", "Stefanos"),
|
||||
Resident::new("2", "Iordanis"),
|
||||
Resident::new("3", "Maria"),
|
||||
Resident::new("4", "Veatriki"),
|
||||
Resident::new("5", "Takis"),
|
||||
Resident::new("6", "Akis"),
|
||||
])
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn bounds(config: UserConfig) -> WorkloadBounds {
|
||||
WorkloadBounds::new_with_config(&config)
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn scheduler(config: UserConfig, bounds: WorkloadBounds) -> Scheduler {
|
||||
Scheduler::new(config, bounds)
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn test_search(mut schedule: MonthlySchedule, scheduler: Scheduler) {
|
||||
assert!(scheduler.search(&mut schedule, Slot::default()));
|
||||
|
||||
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);
|
||||
assert!(schedule.get_resident_id(&slot_first).is_some());
|
||||
let slot_second = Slot::new(day, ShiftPosition::Second);
|
||||
assert!(schedule.get_resident_id(&slot_second).is_some());
|
||||
} else {
|
||||
let slot_first = Slot::new(day, ShiftPosition::First);
|
||||
assert!(schedule.get_resident_id(&slot_first).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
for r in &scheduler.config.residents {
|
||||
let workload = schedule.current_workload(r);
|
||||
let limit = *scheduler.bounds.max_workloads.get(&r.id).unwrap();
|
||||
assert!(workload <= limit as usize);
|
||||
}
|
||||
|
||||
println!("{}", schedule.pretty_print(&scheduler.config));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user