solverforge-hospital / src /data /data_seed /availability.rs
blackopsrepl's picture
feat(app): add hospital scheduling application
b7e7f16
use chrono::NaiveDate;
use std::cmp::Reverse;
use std::collections::BTreeSet;
use crate::domain::{Employee, Shift};
use super::coverage::{candidate_redundancy_is_valid, public_candidate_counts};
use super::time_utils::horizon_dates;
use super::vocabulary::EXTRA_UNAVAILABLE_COUNT;
/// Adds a small amount of extra unavailability without breaking public feasibility.
///
/// The goal is not to make the dataset impossible. The goal is to remove some
/// trivial interchangeable assignments so local search has a clearer signal.
pub(super) fn add_extra_unavailability(
employees: &mut [Employee],
shifts: &[Shift],
witness_dates: &[BTreeSet<NaiveDate>],
) {
let horizon_dates = horizon_dates(shifts);
for _ in 0..EXTRA_UNAVAILABLE_COUNT {
let best_candidate = (0..employees.len())
.flat_map(|employee_index| {
horizon_dates
.iter()
.copied()
.map(move |date| (employee_index, date))
})
.filter(|&(employee_index, date)| {
!employees[employee_index].unavailable_dates.contains(&date)
&& !witness_dates[employee_index].contains(&date)
})
.filter_map(|(employee_index, date)| {
let score = extra_unavailability_score(employees, shifts, employee_index, date)?;
Some((score, employee_index, date))
})
.max_by_key(|&(score, employee_index, date)| {
(score, Reverse(employee_index), Reverse(date))
});
let Some((_, employee_index, date)) = best_candidate else {
break;
};
employees[employee_index].unavailable_dates.insert(date);
}
}
/// Scores one candidate "employee unavailable on date" mutation.
fn extra_unavailability_score(
employees: &[Employee],
shifts: &[Shift],
employee_index: usize,
date: NaiveDate,
) -> Option<(usize, usize, usize)> {
let mut cloned: Vec<Employee> = employees.to_vec();
cloned[employee_index].unavailable_dates.insert(date);
if !candidate_redundancy_is_valid(&cloned, shifts) {
return None;
}
let counts = public_candidate_counts(&cloned, shifts);
let affected: Vec<usize> = shifts
.iter()
.enumerate()
.filter(|(_, shift)| shift.touched_dates.contains(&date))
.map(|(index, _)| counts[index])
.collect();
let min_affected = affected.into_iter().min().unwrap_or(usize::MAX);
let shifts_with_three_plus = counts.iter().filter(|&&count| count >= 3).count();
Some((min_affected, shifts_with_three_plus, counts.iter().sum()))
}