Remove min by shift type boundaries, improve logging, add more tests, add logs in tests, move fixtures in separate file, move restrictions_violated logic into valid_residents of next slot

This commit is contained in:
2026-02-21 23:46:26 +02:00
parent c291328bfa
commit a41d1cd469
15 changed files with 856 additions and 453 deletions

View File

@@ -1,7 +1,7 @@
use std::sync::atomic::{AtomicBool, Ordering};
use crate::{
config::UserConfig,
config::{ToxicPair, UserConfig},
errors::SearchError,
resident::ResidentId,
schedule::MonthlySchedule,
@@ -10,7 +10,6 @@ use crate::{
workload::{WorkloadBounds, WorkloadTracker},
};
use log::info;
use rand::{rngs::SmallRng, seq::SliceRandom, SeedableRng};
use rayon::{
current_thread_index,
@@ -60,7 +59,7 @@ impl Scheduler {
.map(Slot::from)
.ok_or(SearchError::ScheduleFull)?;
let resident_ids = self.valid_residents(slot, schedule);
let resident_ids = self.valid_residents(slot, schedule, tracker);
let solved_in_thread = AtomicBool::new(false);
let sovled_state = resident_ids.par_iter().find_map_any(|&id| {
@@ -81,7 +80,8 @@ impl Scheduler {
Ok(true) => Some((local_schedule, local_tracker)),
Ok(false) => None,
Err(e) => {
info!("Thread Id: [{}] {}", current_thread_index().unwrap(), e);
let thread_id = current_thread_index().unwrap();
log::log!(e.log_level(), "Thread Id: [{}] {}", thread_id, e);
None
}
}
@@ -114,18 +114,13 @@ impl Scheduler {
return Err(SearchError::Timeout);
}
if !slot.is_first()
&& schedule.restrictions_violated(&slot.previous(), &self.config, &self.bounds, tracker)
{
if schedule.has_resident_in_consecutive_days(&slot.previous()) {
return Ok(false);
}
if slot.greater_than(self.config.total_days) {
if tracker.are_all_thresholds_met(&self.config, &self.bounds) {
solved_in_thread.store(true, Ordering::Relaxed);
return Ok(true);
}
return Ok(false);
if self.found_solution(slot) {
solved_in_thread.store(true, Ordering::Relaxed);
return Ok(true);
}
if schedule.is_slot_manually_assigned(&slot) {
@@ -133,7 +128,7 @@ impl Scheduler {
}
let mut rng = SmallRng::from_rng(&mut rand::rng());
let mut valid_resident_ids = self.valid_residents(slot, schedule);
let mut valid_resident_ids = self.valid_residents(slot, schedule, tracker);
valid_resident_ids.shuffle(&mut rng);
valid_resident_ids.sort_by_key(|res_id| {
let type_count = tracker.get_type_count(res_id, slot.shift_type());
@@ -156,10 +151,19 @@ impl Scheduler {
Ok(false)
}
pub fn found_solution(&self, slot: Slot) -> bool {
slot.greater_than(self.config.total_days)
}
/// 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();
let other_resident = slot
pub fn valid_residents(
&self,
slot: Slot,
schedule: &MonthlySchedule,
tracker: &WorkloadTracker,
) -> Vec<ResidentId> {
let is_holiday_slot = self.config.is_holiday_or_weekend_slot(slot.day.0);
let other_resident_id = slot
.other_position()
.and_then(|partner_slot| schedule.get_resident_id(&partner_slot));
@@ -167,9 +171,26 @@ impl Scheduler {
.residents
.iter()
.filter(|r| {
Some(&r.id) != other_resident
&& !r.negative_shifts.contains(&slot.day)
&& r.allowed_types.contains(&required_type)
if let Some(other_id) = other_resident_id {
if &r.id == other_id {
return false;
}
if self
.config
.toxic_pairs
.iter()
.any(|tp| tp.matches(&ToxicPair::from((r.id, *other_id))))
{
return false;
}
}
!r.negative_shifts.contains(&slot.day)
&& r.allowed_types.contains(&slot.shift_type())
&& !tracker.reached_workload_limit(&self.bounds, &r.id)
&& (!is_holiday_slot || !tracker.reached_holiday_limit(&self.bounds, &r.id))
&& !tracker.reached_shift_type_limit(&self.bounds, &r.id, &slot)
})
.map(|r| r.id)
.collect()
@@ -185,7 +206,6 @@ mod tests {
resident::Resident,
schedule::MonthlySchedule,
scheduler::Scheduler,
slot::{Day, ShiftPosition, Slot},
workload::{WorkloadBounds, WorkloadTracker},
};
@@ -197,12 +217,12 @@ mod tests {
#[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"),
Resident::new(1, "R1"),
Resident::new(2, "R2"),
Resident::new(3, "R3"),
Resident::new(4, "R4"),
Resident::new(5, "R5"),
Resident::new(6, "R6"),
])
}
@@ -227,25 +247,8 @@ mod tests {
mut tracker: WorkloadTracker,
scheduler: Scheduler,
) {
assert!(scheduler.run(&mut schedule, &mut tracker).is_ok());
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 = tracker.current_workload(&r.id);
let limit = *scheduler.bounds.max_workloads.get(&r.id).unwrap();
assert!(workload <= limit);
}
let solved = scheduler.run(&mut schedule, &mut tracker);
assert!(solved.is_ok());
assert!(solved.unwrap());
}
}