File size: 5,141 Bytes
7596726
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use chrono::{Duration, NaiveDate};
use rand::rngs::StdRng;
use std::collections::BTreeSet;

use crate::domain::{CareHub, Employee};

use super::time_utils::generate_name_permutations;
use super::vocabulary::*;

/// Draft workforce record used before we instantiate full `Employee` facts.
#[derive(Clone)]
pub(super) struct EmployeeBlueprint {
    pub(super) name: String,
    pub(super) skills: BTreeSet<String>,
    pub(super) home_hub: CareHub,
    pub(super) primary_off_weekday: usize,
}

/// Builds the fixed workforce composition for the public demo dataset.
pub(super) fn build_employee_blueprints(rng: &mut StdRng) -> Vec<EmployeeBlueprint> {
    let names = generate_name_permutations(rng);
    let mut skill_sets: Vec<Vec<&'static str>> = Vec::with_capacity(EMPLOYEE_COUNT);

    // The generator used to hand almost every day shift to a generic Doctor or
    // Nurse pool. That made most legal assignments interchangeable and flattened
    // local search almost immediately. The redesign keeps the same workforce
    // size, but assigns each employee to one or two service lines so every
    // shift has a smaller, more meaningful candidate set.
    //
    // We still retain the base DOCTOR/NURSE tags so the witness builder can
    // reason about role families, but public shifts now require service-line
    // skills such as `Critical care doctor` or `Outpatient nurse`.
    push_skill_sets(&mut skill_sets, 4, &[DOCTOR, CRITICAL_DOCTOR]);
    push_skill_sets(
        &mut skill_sets,
        2,
        &[DOCTOR, CRITICAL_DOCTOR, OUTPATIENT_DOCTOR],
    );
    push_skill_sets(&mut skill_sets, 4, &[DOCTOR, NEUROLOGY_DOCTOR, CARDIOLOGY]);
    push_skill_sets(
        &mut skill_sets,
        3,
        &[DOCTOR, AMBULATORY_DOCTOR, PEDIATRIC_DOCTOR],
    );
    push_skill_sets(&mut skill_sets, 4, &[DOCTOR, SURGERY_DOCTOR, ANAESTHETICS]);
    push_skill_sets(
        &mut skill_sets,
        1,
        &[DOCTOR, OUTPATIENT_DOCTOR, AMBULATORY_DOCTOR],
    );
    push_skill_sets(
        &mut skill_sets,
        4,
        &[DOCTOR, RADIOLOGY_CALL, OUTPATIENT_DOCTOR],
    );

    push_skill_sets(&mut skill_sets, 5, &[NURSE, CRITICAL_NURSE]);
    push_skill_sets(
        &mut skill_sets,
        3,
        &[NURSE, CRITICAL_NURSE, OUTPATIENT_NURSE],
    );
    push_skill_sets(
        &mut skill_sets,
        4,
        &[NURSE, AMBULATORY_NURSE, PEDIATRIC_NURSE],
    );
    push_skill_sets(
        &mut skill_sets,
        4,
        &[NURSE, NEUROLOGY_NURSE, PEDIATRIC_NURSE],
    );
    push_skill_sets(
        &mut skill_sets,
        4,
        &[NURSE, SURGERY_NURSE, OUTPATIENT_NURSE],
    );
    push_skill_sets(&mut skill_sets, 4, &[NURSE, RADIOLOGY_DAY, RADIOLOGY_NURSE]);
    push_skill_sets(
        &mut skill_sets,
        2,
        &[NURSE, RADIOLOGY_DAY, RADIOLOGY_NURSE, ANAESTHETICS],
    );
    push_skill_sets(
        &mut skill_sets,
        2,
        &[NURSE, AMBULATORY_NURSE, OUTPATIENT_NURSE],
    );

    assert_eq!(
        skill_sets.len(),
        EMPLOYEE_COUNT,
        "employee blueprint count should match workforce target"
    );

    skill_sets
        .into_iter()
        .enumerate()
        .map(|(index, skills)| {
            let home_hub = CareHub::infer_from_skills(skills.iter().copied());
            EmployeeBlueprint {
                name: names[index].clone(),
                skills: skills.into_iter().map(str::to_string).collect(),
                home_hub,
                primary_off_weekday: 0,
            }
        })
        .collect()
}

/// Appends `count` identical skill bundles to the blueprint list.
fn push_skill_sets(target: &mut Vec<Vec<&'static str>>, count: usize, skills: &[&'static str]) {
    for _ in 0..count {
        target.push(skills.to_vec());
    }
}

impl EmployeeBlueprint {
    /// Tiny convenience helper used by balancing heuristics.
    pub(super) fn has_skill(&self, skill: &'static str) -> bool {
        self.skills.contains(skill)
    }

    /// Counts the specialties that are intentionally scarce in this dataset.
    pub(super) fn specialty_count(&self) -> usize {
        usize::from(self.skills.contains(CARDIOLOGY))
            + usize::from(self.skills.contains(ANAESTHETICS))
            + usize::from(self.skills.contains(RADIOLOGY_CALL))
            + usize::from(self.skills.contains(RADIOLOGY_DAY))
    }
}

/// Turns the blueprints into the actual `Employee` facts published by the app.
pub(super) fn instantiate_employees(
    blueprints: &[EmployeeBlueprint],
    start_date: NaiveDate,
) -> Vec<Employee> {
    let mut employees = Vec::with_capacity(blueprints.len());
    for (index, blueprint) in blueprints.iter().enumerate() {
        let mut employee = Employee::new(index, blueprint.name.clone())
            .with_home_hub(blueprint.home_hub)
            .with_skills(blueprint.skills.iter().map(|skill| skill.as_str()));
        for week in 0..(DAYS_IN_SCHEDULE / 7) {
            let date = start_date + Duration::days(week * 7 + blueprint.primary_off_weekday as i64);
            employee.unavailable_dates.insert(date);
        }
        employees.push(employee);
    }
    employees
}