Create struct scheduler to encapsulate the search logic
This commit is contained in:
@@ -164,8 +164,8 @@ mod tests {
|
|||||||
use rstest::{fixture, rstest};
|
use rstest::{fixture, rstest};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
bounds::WorkloadBounds, config::UserConfig, generator::backtracking, resident::Resident,
|
bounds::WorkloadBounds, config::UserConfig, resident::Resident, schedule::MonthlySchedule,
|
||||||
schedule::MonthlySchedule, slot::Slot,
|
scheduler::Scheduler, slot::Slot,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[fixture]
|
#[fixture]
|
||||||
@@ -190,13 +190,14 @@ mod tests {
|
|||||||
WorkloadBounds::new_with_config(&config)
|
WorkloadBounds::new_with_config(&config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[fixture]
|
||||||
|
fn scheduler(config: UserConfig, bounds: WorkloadBounds) -> Scheduler {
|
||||||
|
Scheduler::new(config, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
pub fn test_export_as_doc(
|
pub fn test_export_as_doc(mut schedule: MonthlySchedule, scheduler: Scheduler) {
|
||||||
mut schedule: MonthlySchedule,
|
scheduler.search(&mut schedule, Slot::default());
|
||||||
config: UserConfig,
|
schedule.export_as_doc(&scheduler.config);
|
||||||
bounds: WorkloadBounds,
|
|
||||||
) {
|
|
||||||
backtracking(&mut schedule, Slot::default(), &config, &bounds);
|
|
||||||
schedule.export_as_doc(&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 std::sync::Mutex;
|
||||||
|
|
||||||
use log::info;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
bounds::WorkloadBounds,
|
bounds::WorkloadBounds,
|
||||||
config::{UserConfig, UserConfigDTO},
|
config::{UserConfig, UserConfigDTO},
|
||||||
export::{Export, FileType},
|
export::{Export, FileType},
|
||||||
generator::backtracking,
|
|
||||||
schedule::MonthlySchedule,
|
schedule::MonthlySchedule,
|
||||||
slot::Slot,
|
scheduler::Scheduler,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod bounds;
|
mod bounds;
|
||||||
mod config;
|
mod config;
|
||||||
mod export;
|
mod export;
|
||||||
mod generator;
|
|
||||||
mod resident;
|
mod resident;
|
||||||
mod schedule;
|
mod schedule;
|
||||||
|
mod scheduler;
|
||||||
mod slot;
|
mod slot;
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
@@ -28,18 +25,16 @@ struct AppState {
|
|||||||
/// and the period of the schedule
|
/// and the period of the schedule
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn generate(config: UserConfigDTO, state: tauri::State<'_, AppState>) -> MonthlySchedule {
|
fn generate(config: UserConfigDTO, state: tauri::State<'_, AppState>) -> MonthlySchedule {
|
||||||
info!("{:?}", config);
|
|
||||||
let config = UserConfig::from(config);
|
let config = UserConfig::from(config);
|
||||||
let bounds = WorkloadBounds::new_with_config(&config);
|
|
||||||
info!("{:?}", config);
|
|
||||||
let mut schedule = MonthlySchedule::new();
|
let mut schedule = MonthlySchedule::new();
|
||||||
schedule.prefill(&config);
|
schedule.prefill(&config);
|
||||||
info!("{}", schedule.pretty_print(&config));
|
|
||||||
|
let bounds = WorkloadBounds::new_with_config(&config);
|
||||||
|
let scheduler = Scheduler::new(config, bounds);
|
||||||
|
scheduler.run(&mut schedule);
|
||||||
|
|
||||||
backtracking(&mut schedule, Slot::default(), &config, &bounds);
|
|
||||||
let mut internal_schedule = state.schedule.lock().unwrap();
|
let mut internal_schedule = state.schedule.lock().unwrap();
|
||||||
*internal_schedule = schedule.clone();
|
*internal_schedule = schedule.clone();
|
||||||
info!("{}", schedule.pretty_print(&config));
|
|
||||||
|
|
||||||
schedule
|
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