///|
/// Priority used by a maintenance control room when dispatching work.
pub(all) enum ResourceMaintenancePriority {
ResourceEmergency
ResourceUrgent
ResourceRoutine
ResourceDeferred
} derive(Debug, Eq)
///|
/// Lifecycle state of a maintenance work order.
pub(all) enum ResourceWorkOrderState {
ResourcePlanned
ResourceQueued
ResourceAssigned
ResourceInProgress
ResourceCompleted
ResourceBlocked
ResourceCancelled
} derive(Debug, Eq)
///|
/// A schedulable maintenance work order with a measurable service target.
pub struct ResourceWorkOrder {
work_id : Int
asset_id : Int
opened_at : Double
due_at : Double
estimated_hours : Double
priority : ResourceMaintenancePriority
state : ResourceWorkOrderState
required_skill : String
required_parts : Array[Int]
consequence : Double
customer_impact : Double
}
///|
pub fn resource_work_order(
work_id : Int,
asset_id : Int,
opened_at : Double,
due_at : Double,
estimated_hours : Double,
priority : ResourceMaintenancePriority,
state : ResourceWorkOrderState,
required_skill : String,
required_parts : Array[Int],
consequence : Double,
customer_impact : Double,
) -> ResourceWorkOrder {
if work_id < 0 ||
asset_id < 0 ||
due_at < opened_at ||
estimated_hours <= 0.0 ||
consequence < 0.0 ||
customer_impact < 0.0 {
abort("invalid resource work order")
}
for part in required_parts {
if part < 0 {
abort("part identifiers must be non-negative")
}
}
{
work_id,
asset_id,
opened_at,
due_at,
estimated_hours,
priority,
state,
required_skill,
required_parts,
consequence,
customer_impact,
}
}
///|
pub fn resource_work_order_id(order : ResourceWorkOrder) -> Int {
order.work_id
}
///|
pub fn resource_work_order_asset_id(order : ResourceWorkOrder) -> Int {
order.asset_id
}
///|
pub fn resource_work_order_age(
order : ResourceWorkOrder,
now : Double,
) -> Double {
(now - order.opened_at).max(0.0)
}
///|
pub fn resource_work_order_slack(
order : ResourceWorkOrder,
now : Double,
) -> Double {
order.due_at - now
}
///|
pub fn resource_work_order_is_late(
order : ResourceWorkOrder,
now : Double,
) -> Bool {
order.state != ResourceWorkOrderState::ResourceCompleted &&
order.state != ResourceWorkOrderState::ResourceCancelled &&
now > order.due_at
}
///|
pub fn resource_work_order_is_open(order : ResourceWorkOrder) -> Bool {
order.state != ResourceWorkOrderState::ResourceCompleted &&
order.state != ResourceWorkOrderState::ResourceCancelled
}
///|
pub fn resource_priority_weight(
priority : ResourceMaintenancePriority,
) -> Double {
match priority {
ResourceMaintenancePriority::ResourceEmergency => 1.0
ResourceMaintenancePriority::ResourceUrgent => 0.7
ResourceMaintenancePriority::ResourceRoutine => 0.4
ResourceMaintenancePriority::ResourceDeferred => 0.1
}
}
///|
pub fn resource_work_order_score(
order : ResourceWorkOrder,
now : Double,
) -> Double {
let lateness = if resource_work_order_is_late(order, now) {
1.0 + (now - order.due_at).max(0.0)
} else {
1.0 / (1.0 + order.due_at - now)
}
resource_priority_weight(order.priority) *
(1.0 + order.consequence) *
(1.0 + order.customer_impact) *
lateness
}
///|
pub fn resource_work_order_with_state(
order : ResourceWorkOrder,
state : ResourceWorkOrderState,
) -> ResourceWorkOrder {
{ ..order, state, }
}
///|
/// A maintenance crew with skill, shift, and utilization constraints.
pub struct ResourceCrew {
crew_id : Int
name : String
skill : String
shift_start : Double
shift_end : Double
capacity_hours : Double
assigned_hours : Double
hourly_cost : Double
overtime_limit : Double
}
///|
pub fn resource_crew(
crew_id : Int,
name : String,
skill : String,
shift_start : Double,
shift_end : Double,
capacity_hours : Double,
assigned_hours : Double,
hourly_cost : Double,
overtime_limit : Double,
) -> ResourceCrew {
if crew_id < 0 ||
shift_end < shift_start ||
capacity_hours < 0.0 ||
assigned_hours < 0.0 ||
hourly_cost < 0.0 ||
overtime_limit < 0.0 {
abort("invalid resource crew")
}
{
crew_id,
name,
skill,
shift_start,
shift_end,
capacity_hours,
assigned_hours,
hourly_cost,
overtime_limit,
}
}
///|
pub fn resource_crew_id(crew : ResourceCrew) -> Int {
crew.crew_id
}
///|
pub fn resource_crew_shift_hours(crew : ResourceCrew) -> Double {
(crew.shift_end - crew.shift_start).max(0.0)
}
///|
pub fn resource_crew_available_hours(crew : ResourceCrew) -> Double {
(crew.capacity_hours + crew.overtime_limit - crew.assigned_hours).max(0.0)
}
///|
pub fn resource_crew_utilization(crew : ResourceCrew) -> Double {
if crew.capacity_hours <= 0.0 {
0.0
} else {
crew.assigned_hours / crew.capacity_hours
}
}
///|
pub fn resource_crew_can_accept(
crew : ResourceCrew,
order : ResourceWorkOrder,
now : Double,
) -> Bool {
crew.skill == order.required_skill &&
resource_work_order_is_open(order) &&
resource_crew_available_hours(crew) >= order.estimated_hours &&
crew.shift_end >= now
}
///|
pub fn resource_crew_assign(
crew : ResourceCrew,
order : ResourceWorkOrder,
) -> ResourceCrew {
if crew.skill != order.required_skill {
abort("crew skill does not match work order")
}
if resource_crew_available_hours(crew) < order.estimated_hours {
abort("crew has insufficient capacity")
}
{ ..crew, assigned_hours: crew.assigned_hours + order.estimated_hours }
}
///|
pub fn resource_crew_release(
crew : ResourceCrew,
hours : Double,
) -> ResourceCrew {
if hours < 0.0 {
abort("released hours must be non-negative")
}
{ ..crew, assigned_hours: (crew.assigned_hours - hours).max(0.0) }
}
///|
/// An inventory position for a service part.
pub struct ResourceSpare {
part_id : Int
on_hand : Int
on_order : Int
reserved : Int
reorder_point : Int
reorder_quantity : Int
unit_cost : Double
lead_time : Double
criticality : Double
}
///|
pub fn resource_spare(
part_id : Int,
on_hand : Int,
on_order : Int,
reserved : Int,
reorder_point : Int,
reorder_quantity : Int,
unit_cost : Double,
lead_time : Double,
criticality : Double,
) -> ResourceSpare {
if part_id < 0 ||
on_hand < 0 ||
on_order < 0 ||
reserved < 0 ||
reorder_point < 0 ||
reorder_quantity < 1 ||
unit_cost < 0.0 ||
lead_time < 0.0 ||
criticality < 0.0 {
abort("invalid resource spare")
}
{
part_id,
on_hand,
on_order,
reserved,
reorder_point,
reorder_quantity,
unit_cost,
lead_time,
criticality,
}
}
///|
pub fn resource_spare_id(spare : ResourceSpare) -> Int {
spare.part_id
}
///|
pub fn resource_spare_available(spare : ResourceSpare) -> Int {
(spare.on_hand - spare.reserved).max(0)
}
///|
pub fn resource_spare_inventory_position(spare : ResourceSpare) -> Int {
spare.on_hand + spare.on_order - spare.reserved
}
///|
pub fn resource_spare_shortage(spare : ResourceSpare, demand : Int) -> Int {
(demand - resource_spare_available(spare)).max(0)
}
///|
pub fn resource_spare_needs_reorder(spare : ResourceSpare) -> Bool {
resource_spare_inventory_position(spare) <= spare.reorder_point
}
///|
pub fn resource_spare_reserve(
spare : ResourceSpare,
quantity : Int,
) -> ResourceSpare {
if quantity < 0 || resource_spare_available(spare) < quantity {
abort("insufficient spare availability")
}
{ ..spare, reserved: spare.reserved + quantity }
}
///|
pub fn resource_spare_consume(
spare : ResourceSpare,
quantity : Int,
) -> ResourceSpare {
if quantity < 0 || spare.on_hand < quantity {
abort("insufficient spare stock")
}
{
..spare,
on_hand: spare.on_hand - quantity,
reserved: (spare.reserved - quantity).max(0),
}
}
///|
pub fn resource_spare_receive(
spare : ResourceSpare,
quantity : Int,
) -> ResourceSpare {
if quantity < 0 {
abort("received quantity must be non-negative")
}
{
..spare,
on_hand: spare.on_hand + quantity,
on_order: (spare.on_order - quantity).max(0),
}
}
///|
pub fn resource_spare_order_quantity(spare : ResourceSpare) -> Int {
if resource_spare_needs_reorder(spare) {
spare.reorder_quantity
} else {
0
}
}
///|
pub fn resource_spare_expected_stock_cost(spare : ResourceSpare) -> Double {
resource_spare_inventory_position(spare).to_double() * spare.unit_cost
}
///|
/// A scheduled time window for a crew and a set of work orders.
pub struct ResourceWindow {
window_id : Int
start : Double
end : Double
crew_id : Int
order_ids : Array[Int]
planned_hours : Double
}
///|
pub fn resource_window(
window_id : Int,
start : Double,
end : Double,
crew_id : Int,
order_ids : Array[Int],
planned_hours : Double,
) -> ResourceWindow {
if window_id < 0 || end <= start || crew_id < 0 || planned_hours < 0.0 {
abort("invalid maintenance window")
}
{ window_id, start, end, crew_id, order_ids, planned_hours }
}
///|
pub fn resource_window_duration(window : ResourceWindow) -> Double {
window.end - window.start
}
///|
pub fn resource_window_slack(window : ResourceWindow) -> Double {
resource_window_duration(window) - window.planned_hours
}
///|
pub fn resource_window_is_overloaded(window : ResourceWindow) -> Bool {
resource_window_slack(window) < 0.0
}
///|
pub fn resource_window_utilization(window : ResourceWindow) -> Double {
if resource_window_duration(window) <= 0.0 {
0.0
} else {
window.planned_hours / resource_window_duration(window)
}
}
///|
pub fn resource_window_add_order(
window : ResourceWindow,
order : ResourceWorkOrder,
) -> ResourceWindow {
if window.crew_id < 0 {
abort("invalid crew in maintenance window")
}
let order_ids = window.order_ids.copy()
order_ids.push(order.work_id)
{
..window,
order_ids,
planned_hours: window.planned_hours + order.estimated_hours,
}
}
///|
/// A dispatch result that records why work was accepted or held.
pub struct ResourceDispatch {
order_id : Int
crew_id : Int
part_ids : Array[Int]
accepted : Bool
reason : String
completion_time : Double
direct_cost : Double
shortage_cost : Double
}
///|
pub fn resource_dispatch(
order_id : Int,
crew_id : Int,
part_ids : Array[Int],
accepted : Bool,
reason : String,
completion_time : Double,
direct_cost : Double,
shortage_cost : Double,
) -> ResourceDispatch {
if order_id < 0 ||
crew_id < 0 ||
completion_time < 0.0 ||
direct_cost < 0.0 ||
shortage_cost < 0.0 {
abort("invalid dispatch result")
}
{
order_id,
crew_id,
part_ids,
accepted,
reason,
completion_time,
direct_cost,
shortage_cost,
}
}
///|
pub fn resource_dispatch_total_cost(dispatch : ResourceDispatch) -> Double {
dispatch.direct_cost + dispatch.shortage_cost
}
///|
pub fn resource_dispatch_success_rate(
dispatches : Array[ResourceDispatch],
) -> Double {
if dispatches.is_empty() {
0.0
} else {
let accepted = dispatches.fold(init=0, (count, item) => {
if item.accepted {
count + 1
} else {
count
}
})
accepted.to_double() / dispatches.length().to_double()
}
}
///|
pub fn resource_dispatch_average_cost(
dispatches : Array[ResourceDispatch],
) -> Double {
if dispatches.is_empty() {
0.0
} else {
dispatches.fold(init=0.0, (sum, item) => {
sum + resource_dispatch_total_cost(item)
}) /
dispatches.length().to_double()
}
}
///|
/// Aggregate resource status for a planning horizon.
pub struct ResourceSnapshot {
now : Double
open_orders : Int
late_orders : Int
crew_hours : Double
available_hours : Double
spare_value : Double
shortage_units : Int
backlog_hours : Double
service_level : Double
}
///|
pub fn resource_snapshot(
now : Double,
orders : Array[ResourceWorkOrder],
crews : Array[ResourceCrew],
spares : Array[ResourceSpare],
) -> ResourceSnapshot {
let open_orders = orders.fold(init=0, (count, order) => {
if resource_work_order_is_open(order) {
count + 1
} else {
count
}
})
let late_orders = orders.fold(init=0, (count, order) => {
if resource_work_order_is_late(order, now) {
count + 1
} else {
count
}
})
let backlog_hours = orders.fold(init=0.0, (sum, order) => {
if resource_work_order_is_open(order) {
sum + order.estimated_hours
} else {
sum
}
})
let crew_hours = crews.fold(init=0.0, (sum, crew) => sum + crew.capacity_hours)
let available_hours = crews.fold(init=0.0, (sum, crew) => {
sum + resource_crew_available_hours(crew)
})
let spare_value = spares.fold(init=0.0, (sum, spare) => {
sum + resource_spare_expected_stock_cost(spare)
})
let shortage_units = spares.fold(init=0, (sum, spare) => {
sum + resource_spare_shortage(spare, spare.reorder_point)
})
let service_level = if open_orders == 0 {
1.0
} else {
(open_orders - late_orders).to_double() / open_orders.to_double()
}
{
now,
open_orders,
late_orders,
crew_hours,
available_hours,
spare_value,
shortage_units,
backlog_hours,
service_level,
}
}
///|
pub fn resource_snapshot_capacity_ratio(snapshot : ResourceSnapshot) -> Double {
if snapshot.backlog_hours <= 0.0 {
1.0
} else {
snapshot.available_hours / snapshot.backlog_hours
}
}
///|
pub fn resource_snapshot_health(snapshot : ResourceSnapshot) -> Double {
let capacity = resource_snapshot_capacity_ratio(snapshot).min(1.0)
let stock = if snapshot.spare_value <= 0.0 {
1.0
} else {
1.0 / (1.0 + snapshot.shortage_units.to_double())
}
(snapshot.service_level.max(0.0) * capacity * stock).max(0.0).min(1.0)
}
///|
pub fn resource_orders_by_priority(
orders : Array[ResourceWorkOrder],
now : Double,
) -> Array[ResourceWorkOrder] {
let result = orders.copy()
result.sort_by((left, right) => {
let left_score = resource_work_order_score(left, now)
let right_score = resource_work_order_score(right, now)
if left_score > right_score {
-1
} else if left_score < right_score {
1
} else {
0
}
})
result
}
///|
pub fn resource_open_order_ids(orders : Array[ResourceWorkOrder]) -> Array[Int] {
orders.filter_map(order => {
if resource_work_order_is_open(order) {
Some(order.work_id)
} else {
None
}
})
}
///|
pub fn resource_late_order_ids(
orders : Array[ResourceWorkOrder],
now : Double,
) -> Array[Int] {
orders.filter_map(order => {
if resource_work_order_is_late(order, now) {
Some(order.work_id)
} else {
None
}
})
}
///|
pub fn resource_skill_demand(
orders : Array[ResourceWorkOrder],
skill : String,
) -> Double {
orders.fold(init=0.0, (sum, order) => {
if resource_work_order_is_open(order) && order.required_skill == skill {
sum + order.estimated_hours
} else {
sum
}
})
}
///|
pub fn resource_part_demand(
orders : Array[ResourceWorkOrder],
part_id : Int,
) -> Int {
orders.fold(init=0, (sum, order) => {
if resource_work_order_is_open(order) {
sum +
order.required_parts.fold(init=0, (count, part) => {
if part == part_id {
count + 1
} else {
count
}
})
} else {
sum
}
})
}
///|
pub fn resource_crew_match(
orders : Array[ResourceWorkOrder],
crew : ResourceCrew,
now : Double,
) -> Array[Int] {
let mut hours = resource_crew_available_hours(crew)
let result = Array::new()
for order in resource_orders_by_priority(orders, now) {
if resource_work_order_is_open(order) &&
order.required_skill == crew.skill &&
hours >= order.estimated_hours {
result.push(order.work_id)
hours -= order.estimated_hours
}
}
result
}
///|
pub fn resource_backlog_cost(
orders : Array[ResourceWorkOrder],
crews : Array[ResourceCrew],
now : Double,
penalty_per_hour : Double,
) -> Double {
if penalty_per_hour < 0.0 {
abort("backlog penalty must be non-negative")
}
let labor = crews.fold(init=0.0, (sum, crew) => {
sum + crew.assigned_hours * crew.hourly_cost
})
let penalty = orders.fold(init=0.0, (sum, order) => {
if resource_work_order_is_late(order, now) {
sum + (now - order.due_at) * penalty_per_hour
} else {
sum
}
})
labor + penalty
}
///|
pub fn resource_sla_breach_count(
orders : Array[ResourceWorkOrder],
now : Double,
) -> Int {
orders.fold(init=0, (count, order) => {
if resource_work_order_is_late(order, now) {
count + 1
} else {
count
}
})
}
///|
pub fn resource_sla_breach_fraction(
orders : Array[ResourceWorkOrder],
now : Double,
) -> Double {
if orders.is_empty() {
0.0
} else {
resource_sla_breach_count(orders, now).to_double() /
orders.length().to_double()
}
}
///|
pub fn resource_planning_checksum(snapshot : ResourceSnapshot) -> Double {
snapshot.now +
snapshot.open_orders.to_double() * 3.0 +
snapshot.late_orders.to_double() * 5.0 +
snapshot.available_hours +
snapshot.spare_value * 0.01 +
snapshot.backlog_hours +
snapshot.service_level
}