///|
/// Lifecycle state for an asset service case.
pub(all) enum ReliabilityServiceCaseState {
ReliabilityCaseNew
ReliabilityCaseTriaged
ReliabilityCaseDispatched
ReliabilityCaseOnSite
ReliabilityCaseWaitingPart
ReliabilityCaseResolved
ReliabilityCaseClosed
} derive(Debug, Eq)
///|
/// Urgency used by a field service dispatch center.
pub(all) enum ReliabilityServiceUrgency {
ReliabilityServiceCritical
ReliabilityServiceHigh
ReliabilityServiceNormal
ReliabilityServiceLow
} derive(Debug, Eq)
///|
/// A customer-visible service case linked to a reliability event.
pub struct ReliabilityServiceCase {
case_id : String
asset_id : Int
opened_at : Double
promised_at : Double
state : ReliabilityServiceCaseState
urgency : ReliabilityServiceUrgency
symptom_code : String
estimated_hours : Double
customer_impact : Double
repeat_failure : Bool
warranty_covered : Bool
}
///|
pub fn reliability_service_case(
case_id : String,
asset_id : Int,
opened_at : Double,
promised_at : Double,
state : ReliabilityServiceCaseState,
urgency : ReliabilityServiceUrgency,
symptom_code : String,
estimated_hours : Double,
customer_impact : Double,
repeat_failure : Bool,
warranty_covered : Bool,
) -> ReliabilityServiceCase {
if asset_id < 0 ||
opened_at < 0.0 ||
promised_at < opened_at ||
estimated_hours < 0.0 ||
customer_impact < 0.0 {
abort("invalid service case")
}
{
case_id,
asset_id,
opened_at,
promised_at,
state,
urgency,
symptom_code,
estimated_hours,
customer_impact,
repeat_failure,
warranty_covered,
}
}
///|
pub fn reliability_service_case_is_open(case : ReliabilityServiceCase) -> Bool {
case.state != ReliabilityServiceCaseState::ReliabilityCaseClosed &&
case.state != ReliabilityServiceCaseState::ReliabilityCaseResolved
}
///|
pub fn reliability_service_case_is_late(
case : ReliabilityServiceCase,
as_of : Double,
) -> Bool {
reliability_service_case_is_open(case) && as_of > case.promised_at
}
///|
pub fn reliability_service_case_age(
case : ReliabilityServiceCase,
as_of : Double,
) -> Double {
(as_of - case.opened_at).max(0.0)
}
///|
pub fn reliability_service_case_slack(
case : ReliabilityServiceCase,
as_of : Double,
) -> Double {
case.promised_at - as_of
}
///|
pub fn reliability_service_urgency_weight(
urgency : ReliabilityServiceUrgency,
) -> Double {
match urgency {
ReliabilityServiceCritical => 1.0
ReliabilityServiceHigh => 0.7
ReliabilityServiceNormal => 0.4
ReliabilityServiceLow => 0.1
}
}
///|
pub fn reliability_service_case_priority(
case : ReliabilityServiceCase,
as_of : Double,
) -> Double {
let lateness = if reliability_service_case_is_late(case, as_of) {
1.0 + (as_of - case.promised_at)
} else {
1.0 / (1.0 + case.promised_at - as_of)
}
reliability_service_urgency_weight(case.urgency) *
(1.0 + case.customer_impact) *
(if case.repeat_failure { 1.5 } else { 1.0 }) *
lateness
}
///|
pub fn reliability_service_case_with_state(
case : ReliabilityServiceCase,
state : ReliabilityServiceCaseState,
) -> ReliabilityServiceCase {
{ ..case, state, }
}
///|
/// A field technician's shift, skill, and travel capacity.
pub struct ReliabilityTechnician {
technician_id : String
name : String
skill : String
home_latitude : Double
home_longitude : Double
shift_start : Double
shift_end : Double
available_hours : Double
hourly_cost : Double
enabled : Bool
}
///|
pub fn reliability_technician(
technician_id : String,
name : String,
skill : String,
home_latitude : Double,
home_longitude : Double,
shift_start : Double,
shift_end : Double,
available_hours : Double,
hourly_cost : Double,
enabled : Bool,
) -> ReliabilityTechnician {
if shift_start < 0.0 ||
shift_end < shift_start ||
available_hours < 0.0 ||
hourly_cost < 0.0 {
abort("invalid field technician")
}
{
technician_id,
name,
skill,
home_latitude,
home_longitude,
shift_start,
shift_end,
available_hours,
hourly_cost,
enabled,
}
}
///|
pub fn reliability_technician_shift_hours(
technician : ReliabilityTechnician,
) -> Double {
technician.shift_end - technician.shift_start
}
///|
pub fn reliability_technician_utilization(
technician : ReliabilityTechnician,
assigned_hours : Double,
) -> Double {
if technician.available_hours == 0.0 {
0.0
} else {
assigned_hours / technician.available_hours
}
}
///|
pub fn reliability_technician_can_take(
technician : ReliabilityTechnician,
required_skill : String,
hours : Double,
) -> Bool {
technician.enabled &&
technician.skill == required_skill &&
hours >= 0.0 &&
technician.available_hours >= hours
}
///|
pub fn reliability_technician_after_assignment(
technician : ReliabilityTechnician,
hours : Double,
) -> ReliabilityTechnician {
if hours < 0.0 || hours > technician.available_hours {
abort("technician capacity exceeded")
}
{ ..technician, available_hours: technician.available_hours - hours }
}
///|
/// A planned or completed service visit.
pub struct ReliabilityServiceVisit {
visit_id : String
case_id : String
technician_id : String
scheduled_at : Double
arrival_at : Double
departure_at : Double
travel_hours : Double
work_hours : Double
distance : Double
successful : Bool
notes_quality : Double
}
///|
pub fn reliability_service_visit(
visit_id : String,
case_id : String,
technician_id : String,
scheduled_at : Double,
arrival_at : Double,
departure_at : Double,
travel_hours : Double,
work_hours : Double,
distance : Double,
successful : Bool,
notes_quality : Double,
) -> ReliabilityServiceVisit {
if scheduled_at < 0.0 ||
arrival_at < scheduled_at ||
departure_at < arrival_at ||
travel_hours < 0.0 ||
work_hours < 0.0 ||
distance < 0.0 ||
notes_quality < 0.0 ||
notes_quality > 1.0 {
abort("invalid service visit")
}
{
visit_id,
case_id,
technician_id,
scheduled_at,
arrival_at,
departure_at,
travel_hours,
work_hours,
distance,
successful,
notes_quality,
}
}
///|
pub fn reliability_service_visit_response_time(
visit : ReliabilityServiceVisit,
) -> Double {
visit.arrival_at - visit.scheduled_at
}
///|
pub fn reliability_service_visit_duration(
visit : ReliabilityServiceVisit,
) -> Double {
visit.departure_at - visit.arrival_at
}
///|
pub fn reliability_service_visit_cycle_time(
visit : ReliabilityServiceVisit,
) -> Double {
visit.departure_at - visit.scheduled_at
}
///|
pub fn reliability_service_visit_productivity(
visit : ReliabilityServiceVisit,
) -> Double {
let total = reliability_service_visit_cycle_time(visit)
if total == 0.0 {
0.0
} else {
visit.work_hours / total
}
}
///|
pub fn reliability_service_visit_is_late(
visit : ReliabilityServiceVisit,
promised_at : Double,
) -> Bool {
visit.departure_at > promised_at
}
///|
pub fn reliability_service_visit_cost(
visit : ReliabilityServiceVisit,
technician : ReliabilityTechnician,
travel_rate : Double,
) -> Double {
if travel_rate < 0.0 {
abort("travel rate must be non-negative")
}
(visit.work_hours + visit.travel_hours) * technician.hourly_cost +
visit.distance * travel_rate
}
///|
/// Parts consumed during service and the resulting cost.
pub struct ReliabilityServicePartUse {
case_id : String
part_id : Int
quantity : Int
unit_cost : Double
returned : Int
stockout : Bool
}
///|
pub fn reliability_service_part_use(
case_id : String,
part_id : Int,
quantity : Int,
unit_cost : Double,
returned : Int,
stockout : Bool,
) -> ReliabilityServicePartUse {
if part_id < 0 ||
quantity < 0 ||
unit_cost < 0.0 ||
returned < 0 ||
returned > quantity {
abort("invalid service part use")
}
{ case_id, part_id, quantity, unit_cost, returned, stockout }
}
///|
pub fn reliability_service_part_net_quantity(
use_record : ReliabilityServicePartUse,
) -> Int {
use_record.quantity - use_record.returned
}
///|
pub fn reliability_service_part_cost(
use_record : ReliabilityServicePartUse,
) -> Double {
reliability_service_part_net_quantity(use_record).to_double() *
use_record.unit_cost
}
///|
pub fn reliability_service_part_is_shortage(
use_record : ReliabilityServicePartUse,
) -> Bool {
use_record.stockout
}
///|
pub fn reliability_service_case_parts_cost(
uses : Array[ReliabilityServicePartUse],
case_id : String,
) -> Double {
uses.fold(init=0.0, (sum, use_record) => {
if use_record.case_id == case_id {
sum + reliability_service_part_cost(use_record)
} else {
sum
}
})
}
///|
/// Service-level agreement thresholds for response and restoration.
pub struct ReliabilityServiceSla {
sla_id : String
response_target : Double
restoration_target : Double
target_fraction : Double
penalty_per_hour : Double
critical_multiplier : Double
}
///|
pub fn reliability_service_sla(
sla_id : String,
response_target : Double,
restoration_target : Double,
target_fraction : Double,
penalty_per_hour : Double,
critical_multiplier : Double,
) -> ReliabilityServiceSla {
if response_target < 0.0 ||
restoration_target < 0.0 ||
target_fraction < 0.0 ||
target_fraction > 1.0 ||
penalty_per_hour < 0.0 ||
critical_multiplier < 0.0 {
abort("invalid service SLA")
}
{
sla_id,
response_target,
restoration_target,
target_fraction,
penalty_per_hour,
critical_multiplier,
}
}
///|
pub fn reliability_service_sla_response_passes(
sla : ReliabilityServiceSla,
visit : ReliabilityServiceVisit,
) -> Bool {
reliability_service_visit_response_time(visit) <= sla.response_target
}
///|
pub fn reliability_service_sla_restoration_passes(
sla : ReliabilityServiceSla,
visit : ReliabilityServiceVisit,
) -> Bool {
reliability_service_visit_cycle_time(visit) <= sla.restoration_target
}
///|
pub fn reliability_service_sla_penalty(
sla : ReliabilityServiceSla,
visit : ReliabilityServiceVisit,
critical : Bool,
) -> Double {
let response_over = (reliability_service_visit_response_time(visit) -
sla.response_target).max(0.0)
let restoration_over = (reliability_service_visit_cycle_time(visit) -
sla.restoration_target).max(0.0)
let multiplier = if critical { sla.critical_multiplier } else { 1.0 }
(response_over + restoration_over) * sla.penalty_per_hour * multiplier
}
///|
pub fn reliability_service_sla_compliant_fraction(
sla : ReliabilityServiceSla,
visits : Array[ReliabilityServiceVisit],
) -> Double {
if visits.is_empty() {
1.0
} else {
visits
.fold(init=0, (count, visit) => {
if reliability_service_sla_response_passes(sla, visit) &&
reliability_service_sla_restoration_passes(sla, visit) {
count + 1
} else {
count
}
})
.to_double() /
visits.length().to_double()
}
}
///|
pub fn reliability_service_sla_is_met(
sla : ReliabilityServiceSla,
visits : Array[ReliabilityServiceVisit],
) -> Bool {
reliability_service_sla_compliant_fraction(sla, visits) >= sla.target_fraction
}
///|
/// Resolution outcome used for continuous improvement of service procedures.
pub struct ReliabilityServiceOutcome {
case_id : String
root_cause : String
corrective_action : String
verified : Bool
customer_satisfied : Bool
repeat_within_window : Bool
downtime : Double
total_cost : Double
learning_value : Double
}
///|
pub fn reliability_service_outcome(
case_id : String,
root_cause : String,
corrective_action : String,
verified : Bool,
customer_satisfied : Bool,
repeat_within_window : Bool,
downtime : Double,
total_cost : Double,
learning_value : Double,
) -> ReliabilityServiceOutcome {
if downtime < 0.0 || total_cost < 0.0 || learning_value < 0.0 {
abort("invalid service outcome")
}
{
case_id,
root_cause,
corrective_action,
verified,
customer_satisfied,
repeat_within_window,
downtime,
total_cost,
learning_value,
}
}
///|
pub fn reliability_service_outcome_effective(
outcome : ReliabilityServiceOutcome,
) -> Bool {
outcome.verified &&
outcome.customer_satisfied &&
!outcome.repeat_within_window
}
///|
pub fn reliability_service_outcome_quality(
outcome : ReliabilityServiceOutcome,
) -> Double {
let verification = if outcome.verified { 0.4 } else { 0.0 }
let satisfaction = if outcome.customer_satisfied { 0.3 } else { 0.0 }
let repeat = if outcome.repeat_within_window { 0.0 } else { 0.3 }
verification + satisfaction + repeat
}
///|
pub fn reliability_service_outcome_cost_per_downtime_hour(
outcome : ReliabilityServiceOutcome,
) -> Double {
if outcome.downtime == 0.0 {
outcome.total_cost
} else {
outcome.total_cost / outcome.downtime
}
}
///|
/// Operational service summary for a period.
pub struct ReliabilityServiceSummary {
case_count : Int
open_count : Int
late_count : Int
visit_count : Int
successful_visits : Int
repeat_count : Int
average_response : Double
average_cycle : Double
total_cost : Double
total_downtime : Double
sla_fraction : Double
first_time_fix_rate : Double
}
///|
pub fn reliability_service_summary(
as_of : Double,
cases : Array[ReliabilityServiceCase],
visits : Array[ReliabilityServiceVisit],
outcomes : Array[ReliabilityServiceOutcome],
sla : ReliabilityServiceSla,
) -> ReliabilityServiceSummary {
let open = cases
.filter(case => reliability_service_case_is_open(case))
.length()
let late = cases
.filter(case => reliability_service_case_is_late(case, as_of))
.length()
let successful = visits.filter(visit => visit.successful).length()
let response = if visits.is_empty() {
0.0
} else {
visits.fold(init=0.0, (sum, visit) => {
sum + reliability_service_visit_response_time(visit)
}) /
visits.length().to_double()
}
let cycle = if visits.is_empty() {
0.0
} else {
visits.fold(init=0.0, (sum, visit) => {
sum + reliability_service_visit_cycle_time(visit)
}) /
visits.length().to_double()
}
let cost = outcomes.fold(init=0.0, (sum, outcome) => sum + outcome.total_cost)
let downtime = outcomes.fold(init=0.0, (sum, outcome) => {
sum + outcome.downtime
})
let repeats = outcomes
.filter(outcome => outcome.repeat_within_window)
.length()
let first_fix = if visits.is_empty() {
0.0
} else {
successful.to_double() / visits.length().to_double()
}
{
case_count: cases.length(),
open_count: open,
late_count: late,
visit_count: visits.length(),
successful_visits: successful,
repeat_count: repeats,
average_response: response,
average_cycle: cycle,
total_cost: cost,
total_downtime: downtime,
sla_fraction: reliability_service_sla_compliant_fraction(sla, visits),
first_time_fix_rate: first_fix,
}
}
///|
pub fn reliability_service_summary_readiness(
summary : ReliabilityServiceSummary,
) -> Double {
let response = if summary.average_response == 0.0 {
1.0
} else {
1.0 / (1.0 + summary.average_response)
}
let repeat = if summary.visit_count == 0 {
1.0
} else {
1.0 - summary.repeat_count.to_double() / summary.visit_count.to_double()
}
(0.35 * summary.sla_fraction +
0.35 * summary.first_time_fix_rate +
0.2 * repeat +
0.1 * response)
.min(1.0)
.max(0.0)
}
///|
pub fn reliability_service_case_ranked(
cases : Array[ReliabilityServiceCase],
as_of : Double,
) -> Array[ReliabilityServiceCase] {
let result = cases.copy()
result.sort_by((left, right) => {
let a = reliability_service_case_priority(left, as_of)
let b = reliability_service_case_priority(right, as_of)
if a > b {
-1
} else if a < b {
1
} else {
0
}
})
result
}
///|
pub fn reliability_service_symptom_count(
cases : Array[ReliabilityServiceCase],
symptom_code : String,
) -> Int {
cases.fold(init=0, (count, case) => {
if case.symptom_code == symptom_code {
count + 1
} else {
count
}
})
}
///|
pub fn reliability_service_asset_case_count(
cases : Array[ReliabilityServiceCase],
asset_id : Int,
) -> Int {
cases.fold(init=0, (count, case) => {
if case.asset_id == asset_id {
count + 1
} else {
count
}
})
}
///|
pub fn reliability_service_warranty_fraction(
cases : Array[ReliabilityServiceCase],
) -> Double {
if cases.is_empty() {
0.0
} else {
cases.filter(case => case.warranty_covered).length().to_double() /
cases.length().to_double()
}
}
///|
pub fn reliability_service_repeat_fraction(
cases : Array[ReliabilityServiceCase],
) -> Double {
if cases.is_empty() {
0.0
} else {
cases.filter(case => case.repeat_failure).length().to_double() /
cases.length().to_double()
}
}
///|
pub fn reliability_service_dispatch_match(
case : ReliabilityServiceCase,
technicians : Array[ReliabilityTechnician],
required_skill : String,
) -> Int {
let mut best = -1
let mut best_capacity = -1.0
for i in 0.. best_capacity {
best = i
best_capacity = technician.available_hours
}
}
best
}
///|
pub fn reliability_service_total_visit_cost(
visits : Array[ReliabilityServiceVisit],
technicians : Array[ReliabilityTechnician],
travel_rate : Double,
) -> Double {
visits.fold(init=0.0, (sum, visit) => {
let technician = technicians.filter_map(item => {
if item.technician_id == visit.technician_id {
Some(item)
} else {
None
}
})
if technician.is_empty() {
sum
} else {
sum + reliability_service_visit_cost(visit, technician[0], travel_rate)
}
})
}
///|
pub fn reliability_service_checksum(
summary : ReliabilityServiceSummary,
) -> Double {
summary.case_count.to_double() +
summary.visit_count.to_double() * 2.0 +
summary.total_cost +
summary.total_downtime +
summary.sla_fraction * 100.0 +
summary.first_time_fix_rate * 10.0
}
///|
pub fn reliability_service_case_state_label(
state : ReliabilityServiceCaseState,
) -> String {
match state {
ReliabilityCaseNew => "new"
ReliabilityCaseTriaged => "triaged"
ReliabilityCaseDispatched => "dispatched"
ReliabilityCaseOnSite => "on_site"
ReliabilityCaseWaitingPart => "waiting_part"
ReliabilityCaseResolved => "resolved"
ReliabilityCaseClosed => "closed"
}
}
///|
pub fn reliability_service_critical_count(
cases : Array[ReliabilityServiceCase],
) -> Int {
cases.fold(init=0, (count, case) => {
if case.urgency is ReliabilityServiceCritical {
count + 1
} else {
count
}
})
}
///|
pub fn reliability_service_visit_success_fraction(
visits : Array[ReliabilityServiceVisit],
) -> Double {
if visits.is_empty() {
0.0
} else {
visits.filter(visit => visit.successful).length().to_double() /
visits.length().to_double()
}
}
///|
pub fn reliability_service_verified_fraction(
outcomes : Array[ReliabilityServiceOutcome],
) -> Double {
if outcomes.is_empty() {
0.0
} else {
outcomes.filter(outcome => outcome.verified).length().to_double() /
outcomes.length().to_double()
}
}
///|
pub fn reliability_service_customer_satisfaction(
outcomes : Array[ReliabilityServiceOutcome],
) -> Double {
if outcomes.is_empty() {
0.0
} else {
outcomes.filter(outcome => outcome.customer_satisfied).length().to_double() /
outcomes.length().to_double()
}
}
///|
pub fn reliability_service_summary_cost_per_case(
summary : ReliabilityServiceSummary,
) -> Double {
if summary.case_count == 0 {
0.0
} else {
summary.total_cost / summary.case_count.to_double()
}
}
///|
pub fn reliability_service_summary_late_fraction(
summary : ReliabilityServiceSummary,
) -> Double {
if summary.case_count == 0 {
0.0
} else {
summary.late_count.to_double() / summary.case_count.to_double()
}
}
///|
pub fn reliability_service_summary_repeat_fraction(
summary : ReliabilityServiceSummary,
) -> Double {
if summary.visit_count == 0 {
0.0
} else {
summary.repeat_count.to_double() / summary.visit_count.to_double()
}
}
///|
pub fn reliability_service_visit_distance_per_hour(
visit : ReliabilityServiceVisit,
) -> Double {
if visit.travel_hours == 0.0 {
visit.distance
} else {
visit.distance / visit.travel_hours
}
}
///|
pub fn reliability_service_dispatch_capacity(
technicians : Array[ReliabilityTechnician],
) -> Double {
technicians.fold(init=0.0, (sum, technician) => {
if technician.enabled {
sum + technician.available_hours
} else {
sum
}
})
}