///|
pub(all) enum AuditEventKind {
ProblemValidated
CandidateConsidered
CandidateRejected
AssignmentAdded
AssignmentMoved
ImprovementAccepted
CoverageChecked
SolutionCompleted
} derive(Eq, Show, ToJson)
///|
pub struct AuditEvent {
sequence : Int
kind : AuditEventKind
message : String
worker_id : String?
shift_id : String?
score_before : Int?
score_after : Int?
} derive(Eq, Show, ToJson)
///|
pub fn AuditEvent::new(
sequence : Int,
kind : AuditEventKind,
message : String,
) -> AuditEvent {
{
sequence,
kind,
message,
worker_id: None,
shift_id: None,
score_before: None,
score_after: None,
}
}
///|
pub fn AuditEvent::for_assignment(
self : AuditEvent,
worker_id : String,
shift_id : String,
) -> AuditEvent {
{ ..self, worker_id: Some(worker_id), shift_id: Some(shift_id) }
}
///|
pub fn AuditEvent::with_scores(
self : AuditEvent,
before : Int,
after : Int,
) -> AuditEvent {
{ ..self, score_before: Some(before), score_after: Some(after) }
}
///|
pub struct AuditTrail {
events : Array[AuditEvent]
} derive(Eq, Show, ToJson)
///|
pub fn AuditTrail::empty() -> AuditTrail {
{ events: [] }
}
///|
pub fn AuditTrail::record(
self : AuditTrail,
kind : AuditEventKind,
message : String,
) -> AuditTrail {
let events = self.events.copy()
events.push(AuditEvent::new(events.length(), kind, message))
{ events, }
}
///|
pub fn AuditTrail::record_assignment(
self : AuditTrail,
kind : AuditEventKind,
message : String,
worker_id : String,
shift_id : String,
) -> AuditTrail {
let events = self.events.copy()
events.push(
AuditEvent::new(events.length(), kind, message).for_assignment(
worker_id, shift_id,
),
)
{ events, }
}
///|
pub struct AuditedSolution {
solution : Solution
audit : AuditTrail
} derive(Eq, Show, ToJson)
///|
pub fn solve_audited(
problem : PlanningProblem,
config : SolverConfig,
) -> AuditedSolution {
let mut audit = AuditTrail::empty()
let validation = problem.validate()
audit = audit.record(
ProblemValidated,
"validation produced " + validation.length().to_string() + " diagnostic(s)",
)
let solution = solve_with_config(problem, config)
for shift in problem.shifts {
let eligible = eligible_workers(problem, Schedule::empty(), shift.id)
audit = audit.record(
CandidateConsidered,
shift.label +
" initially has " +
eligible.length().to_string() +
" eligible worker(s)",
)
for worker_id in solution.schedule.workers_for(shift.id) {
audit = audit.record_assignment(
AssignmentAdded,
"assignment appears in final solution",
worker_id,
shift.id,
)
}
let assigned = assignment_count_for_shift(solution.schedule, shift.id)
audit = audit.record(
CoverageChecked,
shift.label +
" has " +
assigned.to_string() +
"/" +
shift.required_workers.to_string() +
" required worker(s)",
)
}
audit = audit.record(
SolutionCompleted,
"solver completed with objective " + solution.score.total.to_string(),
)
{ solution, audit }
}
///|
pub fn render_audit(audit : AuditTrail) -> String {
let output = StringBuilder::new()
output.write_string("FairShift audit trail\n")
output.write_string("=====================\n")
for event in audit.events {
output.write_string("[")
output.write_string(event.sequence.to_string())
output.write_string("] ")
output.write_string(event.kind.to_string())
output.write_string(": ")
output.write_string(event.message)
match (event.worker_id, event.shift_id) {
(Some(worker), Some(shift)) =>
output.write_string(" (worker=" + worker + ", shift=" + shift + ")")
_ => ()
}
output.write_char('\n')
}
output.to_string()
}
///|
pub struct SolutionComparison {
left_score : Int
right_score : Int
score_delta : Int
left_coverage : Int
right_coverage : Int
coverage_delta : Int
left_workload_spread : Int
right_workload_spread : Int
workload_spread_delta : Int
changed_assignments : Int
recommendation : String
} derive(Eq, Show, ToJson)
///|
fn assignment_difference(left : Schedule, right : Schedule) -> Int {
let mut changed = 0
for assignment in left.assignments {
if !right.contains(assignment.worker_id, assignment.shift_id) {
changed = changed + 1
}
}
for assignment in right.assignments {
if !left.contains(assignment.worker_id, assignment.shift_id) {
changed = changed + 1
}
}
changed
}
///|
pub fn compare_solutions(
problem : PlanningProblem,
left : Solution,
right : Solution,
) -> SolutionComparison {
let left_coverage = coverage_ratio_per_mille(problem, left.schedule)
let right_coverage = coverage_ratio_per_mille(problem, right.schedule)
let left_spread = workload_range(problem, left.schedule)
let right_spread = workload_range(problem, right.schedule)
let recommendation = if right.feasible && !left.feasible {
"right"
} else if left.feasible && !right.feasible {
"left"
} else if right.score.total < left.score.total {
"right"
} else if left.score.total < right.score.total {
"left"
} else {
"equivalent"
}
{
left_score: left.score.total,
right_score: right.score.total,
score_delta: right.score.total - left.score.total,
left_coverage,
right_coverage,
coverage_delta: right_coverage - left_coverage,
left_workload_spread: left_spread,
right_workload_spread: right_spread,
workload_spread_delta: right_spread - left_spread,
changed_assignments: assignment_difference(left.schedule, right.schedule),
recommendation,
}
}