github-actions[bot]
chore: sync uc-hospital Space
7596726
use chrono::Timelike;
use std::cmp::Reverse;
use crate::domain::{Employee, Shift};
use super::support::{
can_mark_preference_date, date_pressure_for_shift, mark_preference_date, preferred_shift_date,
shift_prefers_doctor_family, shift_same_shape, PreferenceKind,
};
use super::PreferenceAnalysis;
use crate::data::data_seed::employees::EmployeeBlueprint;
use crate::data::data_seed::skills::is_specialty_skill;
use crate::data::data_seed::vocabulary::DOCTOR;
use crate::data::data_seed::witness::{shift_priority_rank, WitnessRoster};
/// Adds a small number of witness-relative preference swaps.
///
/// This is the sharpest source of local-search signal in the dataset: one
/// employee is marked as disliking a date while another feasible employee is
/// marked as preferring it.
pub(super) fn assign_exchange_preferences(
employees: &mut [Employee],
blueprints: &[EmployeeBlueprint],
shifts: &[Shift],
witness: &WitnessRoster,
analysis: &PreferenceAnalysis,
max_exchange_marks_per_employee: usize,
) {
let mut exchange_marks_by_employee = vec![0usize; employees.len()];
let mut shift_order: Vec<usize> = (0..shifts.len()).collect();
shift_order.sort_by_key(|&shift_index| {
let shift = &shifts[shift_index];
(
Reverse(analysis.candidate_counts[shift_index]),
Reverse(date_pressure_for_shift(shift, &analysis.date_pressure)),
shift_priority_rank(shift),
shift.start,
shift_index,
)
});
for shift_index in shift_order {
let shift = &shifts[shift_index];
if analysis.candidate_counts[shift_index] < 6
|| is_specialty_skill(&shift.required_skill)
|| shift.start.time().hour() == 22
{
continue;
}
let holder = witness.assignments[shift_index];
let date = preferred_shift_date(shift, &analysis.date_pressure);
let Some(alternative) = analysis.candidate_lists[shift_index]
.iter()
.copied()
.filter(|&candidate| candidate != holder)
.filter(|&candidate| {
exchange_marks_by_employee[candidate] < max_exchange_marks_per_employee
})
.filter(|&candidate| {
can_mark_preference_date(&employees[candidate], date, PreferenceKind::Desired)
})
.min_by_key(|&candidate| {
exchange_alternative_key(candidate, shift, shifts, blueprints, witness, analysis)
})
else {
continue;
};
if mark_preference_date(&mut employees[alternative], date, PreferenceKind::Desired) {
exchange_marks_by_employee[alternative] += 1;
}
}
}
/// Lower keys mean "better alternative employee for this exchange mark".
fn exchange_alternative_key(
candidate: usize,
shift: &Shift,
shifts: &[Shift],
blueprints: &[EmployeeBlueprint],
witness: &WitnessRoster,
analysis: &PreferenceAnalysis,
) -> (usize, usize, usize, usize, usize, usize) {
let date = preferred_shift_date(shift, &analysis.date_pressure);
let same_date_in_witness =
usize::from(witness.employee_touched_dates[candidate].contains(&date));
let same_shape_load = analysis.witness_shifts_by_employee[candidate]
.iter()
.filter(|&&other_shift_index| shift_same_shape(shift, &shifts[other_shift_index]))
.count();
(
same_date_in_witness,
usize::from(
blueprints[candidate].skills.contains(DOCTOR) != shift_prefers_doctor_family(shift),
),
same_shape_load,
witness.employee_touched_dates[candidate].len(),
usize::MAX - *analysis.date_pressure.get(&date).unwrap_or(&0),
candidate,
)
}