///|
/// Service-level metrics for operational plans.
pub struct ServiceObservation {
id : Int
promised : Int
actual : Int
quantity : Int
penalty : Int
}
///|
/// Create a service observation.
pub fn service_observation(
id : Int,
promised : Int,
actual : Int,
quantity : Int,
penalty : Int,
) -> ServiceObservation {
{
id,
promised,
actual,
quantity: if quantity < 0 {
0
} else {
quantity
},
penalty: if penalty < 0 {
0
} else {
penalty
},
}
}
///|
/// Return lateness.
pub fn ServiceObservation::lateness(self : ServiceObservation) -> Int {
if self.actual > self.promised {
self.actual - self.promised
} else {
0
}
}
///|
/// Return earliness.
pub fn ServiceObservation::earliness(self : ServiceObservation) -> Int {
if self.actual < self.promised {
self.promised - self.actual
} else {
0
}
}
///|
/// Return on-time status.
pub fn ServiceObservation::on_time(self : ServiceObservation) -> Bool {
self.actual <= self.promised
}
///|
/// Return weighted lateness penalty.
pub fn ServiceObservation::cost(self : ServiceObservation) -> Int {
self.lateness() * self.penalty
}
///|
/// Return a stable observation line.
pub fn ServiceObservation::describe(self : ServiceObservation) -> String {
"\{self.id}: promised=\{self.promised}, actual=\{self.actual}, late=\{self.lateness()}, quantity=\{self.quantity}"
}
///|
/// A service-level aggregate.
pub struct ServiceMetrics {
orders : Int
on_time : Int
late : Int
total_lateness : Int
maximum_lateness : Int
quantity : Int
cost : Int
}
///|
/// Aggregate observations.
pub fn service_metrics(
observations : Array[ServiceObservation],
) -> ServiceMetrics {
let mut on_time = 0
let mut late = 0
let mut total_lateness = 0
let mut maximum_lateness = 0
let mut quantity = 0
let mut cost = 0
for observation in observations {
if observation.on_time() {
on_time += 1
} else {
late += 1
}
total_lateness += observation.lateness()
if observation.lateness() > maximum_lateness {
maximum_lateness = observation.lateness()
}
quantity += observation.quantity
cost += observation.cost()
}
{
orders: observations.length(),
on_time,
late,
total_lateness,
maximum_lateness,
quantity,
cost,
}
}
///|
/// Return on-time percentage.
pub fn ServiceMetrics::on_time_percent(self : ServiceMetrics) -> Int {
if self.orders == 0 {
0
} else {
self.on_time * 100 / self.orders
}
}
///|
/// Return late percentage.
pub fn ServiceMetrics::late_percent(self : ServiceMetrics) -> Int {
if self.orders == 0 {
0
} else {
self.late * 100 / self.orders
}
}
///|
/// Return average lateness.
pub fn ServiceMetrics::average_lateness(self : ServiceMetrics) -> Int {
if self.orders == 0 {
0
} else {
self.total_lateness / self.orders
}
}
///|
/// Return whether a target service level is met.
pub fn ServiceMetrics::meets_target(
self : ServiceMetrics,
target_percent : Int,
) -> Bool {
self.on_time_percent() >= target_percent
}
///|
/// Return a stable metrics line.
pub fn ServiceMetrics::describe(self : ServiceMetrics) -> String {
"orders=\{self.orders}, on_time=\{self.on_time_percent()}%, average_late=\{self.average_lateness()}, max_late=\{self.maximum_lateness}, quantity=\{self.quantity}, cost=\{self.cost}"
}
///|
/// Return late observations.
pub fn late_observations(
observations : Array[ServiceObservation],
) -> Array[Int] {
let result : Array[Int] = []
for observation in observations {
if !observation.on_time() {
result.push(observation.id)
}
}
result
}
///|
/// Return on-time observations.
pub fn on_time_observations(
observations : Array[ServiceObservation],
) -> Array[Int] {
let result : Array[Int] = []
for observation in observations {
if observation.on_time() {
result.push(observation.id)
}
}
result
}
///|
/// Return total promised time.
pub fn total_promised(observations : Array[ServiceObservation]) -> Int {
let mut result = 0
for observation in observations {
result += observation.promised
}
result
}
///|
/// Return total actual time.
pub fn total_actual(observations : Array[ServiceObservation]) -> Int {
let mut result = 0
for observation in observations {
result += observation.actual
}
result
}
///|
/// Return weighted service score.
pub fn service_score(
observations : Array[ServiceObservation],
lateness_weight : Int,
quantity_weight : Int,
) -> Int {
let metrics = service_metrics(observations)
metrics.on_time * quantity_weight -
metrics.total_lateness * lateness_weight +
metrics.quantity
}
///|
/// Return a service-level gap.
pub fn service_gap(metrics : ServiceMetrics, target_percent : Int) -> Int {
target_percent - metrics.on_time_percent()
}
///|
/// Return whether all observations are ordered by id.
pub fn service_ids_ordered(observations : Array[ServiceObservation]) -> Bool {
for index in 1.. Bool {
for index, observation in observations {
if observation.id != index {
return false
}
}
true
}
///|
/// Return the largest quantity observation.
pub fn largest_service_order(observations : Array[ServiceObservation]) -> Int? {
if observations.length() == 0 {
return None
}
let mut result = observations[0]
for observation in observations {
if observation.quantity > result.quantity {
result = observation
}
}
Some(result.id)
}
///|
/// Return the most late observation.
pub fn latest_service_order(observations : Array[ServiceObservation]) -> Int? {
if observations.length() == 0 {
return None
}
let mut result = observations[0]
for observation in observations {
if observation.lateness() > result.lateness() {
result = observation
}
}
Some(result.id)
}
///|
/// Return a sorted lateness vector.
pub fn service_lateness_values(
observations : Array[ServiceObservation],
) -> Array[Int] {
let result : Array[Int] = []
for observation in observations {
result.push(observation.lateness())
}
sort_integers(result)
result
}
///|
/// Return the lateness percentile.
pub fn service_lateness_percentile(
observations : Array[ServiceObservation],
percent : Int,
) -> Int? {
integer_percentile(service_lateness_values(observations), percent)
}
///|
/// Return a stable aggregate signature.
pub fn service_metrics_signature(
observations : Array[ServiceObservation],
) -> Int {
let mut result = 31
for observation in observations {
result = result * 37 +
observation.id * 3 +
observation.promised * 5 +
observation.actual * 7 +
observation.quantity
}
result
}
///|
/// Create observations from promised and actual sequences.
pub fn observations_from_times(
promised : Array[Int],
actual : Array[Int],
penalty : Int,
) -> Array[ServiceObservation] {
let result : Array[ServiceObservation] = []
let limit = if promised.length() < actual.length() {
promised.length()
} else {
actual.length()
}
for index in 0.. String {
let left = service_metrics(baseline)
let right = service_metrics(candidate)
"on_time_delta=\{right.on_time_percent() - left.on_time_percent()}, lateness_delta=\{right.total_lateness - left.total_lateness}, cost_delta=\{right.cost - left.cost}"
}