///|
/// Per-worker load retained for fairness reports.
pub(all) struct WorkerLoad {
worker_id : String
visit_count : Int
service_minutes : Int
travel_minutes : Int
utilization_percent : Int
} derive(Debug, Eq)
///|
/// Compute a worker's load from schedule assignments.
pub fn worker_load(
worker : Worker,
assignments : Array[Assignment],
) -> WorkerLoad {
let own = assignments_for_worker(assignments, worker.id)
let service = assigned_minutes(assignments, worker.id)
let mut travel = 0
for assignment in own {
travel = travel + assignment.travel_before_minutes
}
let utilization = if worker.max_minutes > 0 {
clamp_int(service * 100 / worker.max_minutes, 0, 1000)
} else {
0
}
{
worker_id: worker.id,
visit_count: own.length(),
service_minutes: service,
travel_minutes: travel,
utilization_percent: utilization,
}
}
///|
/// Compute loads for all workers in input order.
pub fn all_worker_loads(
workers : Array[Worker],
assignments : Array[Assignment],
) -> Array[WorkerLoad] {
let loads : Array[WorkerLoad] = []
for worker in workers {
loads.push(worker_load(worker, assignments))
}
loads
}
///|
/// Sum absolute deviations from the average assigned service time.
pub fn fairness_deviation(loads : Array[WorkerLoad]) -> Int {
if loads.length() == 0 {
return 0
}
let mut total = 0
for load in loads {
total = total + load.service_minutes
}
let average = total / loads.length()
let mut deviation = 0
for load in loads {
deviation = deviation + abs_int(load.service_minutes - average)
}
deviation
}
///|
/// Number of assigned visits in a continuity group handled by one worker.
fn continuity_count_for_worker(
group : String,
worker_id : String,
assignments : Array[Assignment],
visits : Array[Visit],
) -> Int {
let mut count = 0
for assignment in assignments {
if assignment.worker_id == worker_id {
match find_visit(visits, assignment.visit_id) {
Some(visit) =>
if visit.continuity_group == Some(group) {
count = count + 1
}
None => ()
}
}
}
count
}
///|
/// Reward additional visits in a group assigned to the same worker.
pub fn continuity_units(
assignments : Array[Assignment],
visits : Array[Visit],
workers : Array[Worker],
) -> Int {
let groups : Array[String] = []
for visit in visits {
match visit.continuity_group {
Some(group) => push_unique_string(groups, group)
None => ()
}
}
let mut units = 0
for group in groups {
for worker in workers {
let count = continuity_count_for_worker(
group,
worker.id,
assignments,
visits,
)
if count > 1 {
units = units + count - 1
}
}
}
units
}
///|
/// Count assignments that satisfy explicit worker preferences.
pub fn preference_units(
assignments : Array[Assignment],
visits : Array[Visit],
) -> Int {
let mut count = 0
for assignment in assignments {
match find_visit(visits, assignment.visit_id) {
Some(visit) =>
if visit.prefers(assignment.worker_id) {
count = count + 1
}
None => ()
}
}
count
}
///|
/// Weighted assigned priority across the schedule.
pub fn assigned_priority_units(
assignments : Array[Assignment],
visits : Array[Visit],
) -> Int {
let mut total = 0
for assignment in assignments {
match find_visit(visits, assignment.visit_id) {
Some(visit) => total = total + priority_value(visit.priority)
None => ()
}
}
total
}
///|
/// Count assignments changed relative to an optional baseline schedule.
pub fn disruption_units(
assignments : Array[Assignment],
baseline : Schedule?,
) -> Int {
match baseline {
None => 0
Some(previous) => {
let mut disruptions = 0
for assignment in assignments {
match find_assignment(previous.assignments, assignment.visit_id) {
Some(old) =>
if old.worker_id != assignment.worker_id ||
old.start_minute != assignment.start_minute {
disruptions = disruptions + 1
}
None => disruptions = disruptions + 1
}
}
for old in previous.assignments {
if find_assignment(assignments, old.visit_id) is None {
disruptions = disruptions + 1
}
}
disruptions
}
}
}
///|
/// Build a transparent multi-dimensional schedule score.
pub fn score_schedule(
assignments : Array[Assignment],
workers : Array[Worker],
visits : Array[Visit],
policy : SchedulePolicy,
baseline : Schedule?,
) -> ScheduleScore {
let priority = assigned_priority_units(assignments, visits) *
policy.priority_weight
let travel = total_schedule_travel(assignments) * policy.travel_weight
let fairness = fairness_deviation(all_worker_loads(workers, assignments)) *
policy.fairness_weight
let continuity = continuity_units(assignments, visits, workers) *
policy.continuity_weight
let preference = preference_units(assignments, visits) *
policy.preference_weight
let disruption = disruption_units(assignments, baseline) *
policy.disruption_weight
{
total: priority + continuity + preference - travel - fairness - disruption,
assigned_priority: priority,
travel_penalty: travel,
fairness_penalty: fairness,
continuity_bonus: continuity,
preference_bonus: preference,
disruption_penalty: disruption,
}
}
///|
/// Short deterministic score summary.
pub fn score_summary(value : ScheduleScore) -> String {
"total=\{value.total}, priority=\{value.assigned_priority}, travel=-\{value.travel_penalty}, fairness=-\{value.fairness_penalty}, continuity=+\{value.continuity_bonus}, preference=+\{value.preference_bonus}, disruption=-\{value.disruption_penalty}"
}