///|
/// Operational state of an installed or tested asset.
pub(all) enum FleetAssetState {
FleetActive
FleetFailed
FleetRetired
FleetMaintenance
} derive(Debug, Eq)
///|
/// A unit-level record used by fleet reliability and availability analysis.
pub struct FleetAsset {
asset_id : Int
model_code : String
cohort : Int
commissioned_at : Double
as_of : Double
state : FleetAssetState
operating_hours : Double
failure_count : Int
repair_hours : Double
criticality : Double
utilization : Double
}
///|
pub fn fleet_asset(
asset_id : Int,
model_code : String,
cohort : Int,
commissioned_at : Double,
as_of : Double,
state : FleetAssetState,
operating_hours : Double,
failure_count : Int,
repair_hours : Double,
criticality : Double,
utilization : Double,
) -> FleetAsset {
if asset_id < 0 ||
cohort < 0 ||
commissioned_at < 0.0 ||
as_of < commissioned_at ||
operating_hours < 0.0 ||
failure_count < 0 ||
repair_hours < 0.0 ||
criticality < 0.0 ||
utilization < 0.0 ||
utilization > 1.0 {
abort("invalid fleet asset")
}
{
asset_id,
model_code,
cohort,
commissioned_at,
as_of,
state,
operating_hours,
failure_count,
repair_hours,
criticality,
utilization,
}
}
///|
pub fn fleet_asset_age(asset : FleetAsset) -> Double {
(asset.as_of - asset.commissioned_at).max(0.0)
}
///|
pub fn fleet_asset_exposure(asset : FleetAsset) -> Double {
asset.operating_hours * asset.utilization
}
///|
pub fn fleet_asset_is_operational(asset : FleetAsset) -> Bool {
asset.state is FleetActive
}
///|
pub fn fleet_asset_is_unavailable(asset : FleetAsset) -> Bool {
asset.state is FleetFailed || asset.state is FleetMaintenance
}
///|
pub fn fleet_asset_failure_rate(asset : FleetAsset) -> Double {
if fleet_asset_exposure(asset) <= 0.0 {
0.0
} else {
asset.failure_count.to_double() / fleet_asset_exposure(asset)
}
}
///|
pub fn fleet_asset_repair_rate(asset : FleetAsset) -> Double {
if asset.failure_count == 0 || asset.repair_hours <= 0.0 {
0.0
} else {
asset.failure_count.to_double() / asset.repair_hours
}
}
///|
pub fn fleet_asset_mtbf(asset : FleetAsset) -> Double {
if asset.failure_count == 0 {
fleet_asset_exposure(asset)
} else {
fleet_asset_exposure(asset) / asset.failure_count.to_double()
}
}
///|
pub fn fleet_asset_mttr(asset : FleetAsset) -> Double {
if asset.failure_count == 0 {
0.0
} else {
asset.repair_hours / asset.failure_count.to_double()
}
}
///|
pub fn fleet_asset_inherent_availability(asset : FleetAsset) -> Double {
let mtbf = fleet_asset_mtbf(asset)
let mttr = fleet_asset_mttr(asset)
if mtbf + mttr == 0.0 {
1.0
} else {
mtbf / (mtbf + mttr)
}
}
///|
pub fn fleet_asset_weight(asset : FleetAsset) -> Double {
asset.criticality.max(0.0) * (1.0 + asset.utilization)
}
///|
pub struct FleetObservation {
assets : Array[FleetAsset]
start : Double
end : Double
name : String
}
///|
pub fn fleet_observation(
assets : Array[FleetAsset],
start : Double,
end : Double,
name : String,
) -> FleetObservation {
if end <= start {
abort("fleet observation end must exceed start")
}
if assets.is_empty() {
abort("fleet observation requires at least one asset")
}
{ assets, start, end, name }
}
///|
pub fn fleet_observation_duration(observation : FleetObservation) -> Double {
observation.end - observation.start
}
///|
pub fn fleet_asset_count(observation : FleetObservation) -> Int {
observation.assets.length()
}
///|
pub fn fleet_active_count(observation : FleetObservation) -> Int {
observation.assets.fold(init=0, (count, asset) => {
if asset.state is FleetActive {
count + 1
} else {
count
}
})
}
///|
pub fn fleet_failed_count(observation : FleetObservation) -> Int {
observation.assets.fold(init=0, (count, asset) => {
if asset.state is FleetFailed {
count + 1
} else {
count
}
})
}
///|
pub fn fleet_maintenance_count(observation : FleetObservation) -> Int {
observation.assets.fold(init=0, (count, asset) => {
if asset.state is FleetMaintenance {
count + 1
} else {
count
}
})
}
///|
pub fn fleet_retired_count(observation : FleetObservation) -> Int {
observation.assets.fold(init=0, (count, asset) => {
if asset.state is FleetRetired {
count + 1
} else {
count
}
})
}
///|
pub fn fleet_unavailable_fraction(observation : FleetObservation) -> Double {
let total = fleet_asset_count(observation)
if total == 0 {
0.0
} else {
(fleet_failed_count(observation) + fleet_maintenance_count(observation)).to_double() /
total.to_double()
}
}
///|
pub fn fleet_state_fraction(
observation : FleetObservation,
state : FleetAssetState,
) -> Double {
let count = observation.assets.fold(init=0, (total, asset) => {
if asset.state == state {
total + 1
} else {
total
}
})
count.to_double() / fleet_asset_count(observation).to_double()
}
///|
pub fn fleet_total_operating_hours(observation : FleetObservation) -> Double {
observation.assets.fold(init=0.0, (total, asset) => {
total + asset.operating_hours
})
}
///|
pub fn fleet_total_failure_count(observation : FleetObservation) -> Int {
observation.assets.fold(init=0, (total, asset) => total + asset.failure_count)
}
///|
pub fn fleet_total_repair_hours(observation : FleetObservation) -> Double {
observation.assets.fold(init=0.0, (total, asset) => total + asset.repair_hours)
}
///|
pub fn fleet_total_criticality(observation : FleetObservation) -> Double {
observation.assets.fold(init=0.0, (total, asset) => total + asset.criticality)
}
///|
pub fn fleet_weighted_availability(observation : FleetObservation) -> Double {
let denominator = observation.assets.fold(init=0.0, (total, asset) => {
total + fleet_asset_weight(asset)
})
if denominator == 0.0 {
1.0
} else {
observation.assets.fold(init=0.0, (total, asset) => {
total +
fleet_asset_weight(asset) * fleet_asset_inherent_availability(asset)
}) /
denominator
}
}
///|
pub fn fleet_average_age(observation : FleetObservation) -> Double {
mean(observation.assets.map(asset => fleet_asset_age(asset)))
}
///|
pub fn fleet_age_quantile(
observation : FleetObservation,
probability : Double,
) -> Double {
quantile(observation.assets.map(asset => fleet_asset_age(asset)), probability)
}
///|
pub fn fleet_failure_rate(observation : FleetObservation) -> Double {
let exposure = fleet_total_operating_hours(observation)
if exposure == 0.0 {
0.0
} else {
fleet_total_failure_count(observation).to_double() / exposure
}
}
///|
pub fn fleet_repair_rate(observation : FleetObservation) -> Double {
let hours = fleet_total_repair_hours(observation)
if hours == 0.0 {
0.0
} else {
fleet_total_failure_count(observation).to_double() / hours
}
}
///|
pub fn fleet_mtbf(observation : FleetObservation) -> Double {
let failures = fleet_total_failure_count(observation)
if failures == 0 {
fleet_total_operating_hours(observation)
} else {
fleet_total_operating_hours(observation) / failures.to_double()
}
}
///|
pub fn fleet_mttr(observation : FleetObservation) -> Double {
let failures = fleet_total_failure_count(observation)
if failures == 0 {
0.0
} else {
fleet_total_repair_hours(observation) / failures.to_double()
}
}
///|
pub fn fleet_inherent_availability(observation : FleetObservation) -> Double {
let mtbf = fleet_mtbf(observation)
let mttr = fleet_mttr(observation)
if mtbf + mttr == 0.0 {
1.0
} else {
mtbf / (mtbf + mttr)
}
}
///|
pub fn fleet_expected_failures(
observation : FleetObservation,
horizon : Double,
) -> Double {
if horizon < 0.0 {
abort("horizon must be non-negative")
}
fleet_failure_rate(observation) *
horizon *
fleet_asset_count(observation).to_double()
}
///|
pub fn fleet_expected_downtime(
observation : FleetObservation,
horizon : Double,
) -> Double {
fleet_expected_failures(observation, horizon) * fleet_mttr(observation)
}
///|
pub fn fleet_availability_projection(
observation : FleetObservation,
horizons : Array[Double],
) -> Array[Double] {
horizons.map(horizon => {
let downtime = fleet_expected_downtime(observation, horizon)
(1.0 - downtime / horizon.max(1.0e-12)).max(0.0).min(1.0)
})
}
///|
pub fn fleet_reliability_projection(
observation : FleetObservation,
horizons : Array[Double],
) -> Array[Double] {
let rate = fleet_failure_rate(observation)
horizons.map(horizon => {
@math.exp(-rate * horizon * fleet_asset_count(observation).to_double())
})
}
///|
pub fn fleet_critical_assets(
observation : FleetObservation,
minimum_criticality : Double,
) -> Array[FleetAsset] {
observation.assets.filter(asset => asset.criticality >= minimum_criticality)
}
///|
pub fn fleet_high_risk_assets(
observation : FleetObservation,
failure_rate_limit : Double,
availability_limit : Double,
) -> Array[FleetAsset] {
observation.assets.filter(asset => {
fleet_asset_failure_rate(asset) >= failure_rate_limit ||
fleet_asset_inherent_availability(asset) <= availability_limit
})
}
///|
pub fn fleet_rank_by_risk(observation : FleetObservation) -> Array[FleetAsset] {
let result = observation.assets.copy()
result.sort_by((left, right) => {
let left_score = fleet_asset_failure_rate(left) * fleet_asset_weight(left)
let right_score = fleet_asset_failure_rate(right) *
fleet_asset_weight(right)
if left_score > right_score {
-1
} else if left_score < right_score {
1
} else {
0
}
})
result
}
///|
pub fn fleet_risk_score(asset : FleetAsset) -> Double {
fleet_asset_failure_rate(asset) *
fleet_asset_weight(asset) *
(1.0 + fleet_asset_age(asset))
}
///|
pub fn fleet_risk_scores(observation : FleetObservation) -> Array[Double] {
observation.assets.map(asset => fleet_risk_score(asset))
}
///|
pub fn fleet_risk_concentration(
observation : FleetObservation,
top_count : Int,
) -> Double {
if top_count <= 0 {
abort("top_count must be positive")
}
let ranked = fleet_rank_by_risk(observation)
let limit = top_count.min(ranked.length())
let top = Array::makei(limit, i => fleet_risk_score(ranked[i]))
let total = fleet_risk_scores(observation).fold(init=0.0, (sum, value) => {
sum + value
})
if total == 0.0 {
0.0
} else {
top.fold(init=0.0, (sum, value) => sum + value) / total
}
}
///|
pub fn fleet_group_ids(observation : FleetObservation) -> Array[Int] {
let ids = []
for asset in observation.assets {
if !ids.contains(asset.cohort) {
ids.push(asset.cohort)
}
}
ids.sort()
ids
}
///|
pub fn fleet_model_codes(observation : FleetObservation) -> Array[String] {
let codes : Array[String] = []
for asset in observation.assets {
if !codes.contains(asset.model_code) {
codes.push(asset.model_code)
}
}
codes.sort()
codes
}
///|
pub fn fleet_filter_cohort(
observation : FleetObservation,
cohort : Int,
) -> FleetObservation {
fleet_observation(
observation.assets.filter(asset => asset.cohort == cohort),
observation.start,
observation.end,
"cohort-{cohort}",
)
}
///|
pub fn fleet_filter_model(
observation : FleetObservation,
model_code : String,
) -> FleetObservation {
fleet_observation(
observation.assets.filter(asset => asset.model_code == model_code),
observation.start,
observation.end,
model_code,
)
}
///|
pub fn fleet_group_summary(
observation : FleetObservation,
cohort : Int,
) -> FleetSummary {
fleet_summary(fleet_filter_cohort(observation, cohort))
}
///|
pub struct FleetSummary {
asset_count : Int
active_count : Int
failed_count : Int
maintenance_count : Int
retired_count : Int
average_age : Double
total_exposure : Double
failure_rate : Double
mtbf : Double
mttr : Double
availability : Double
weighted_availability : Double
expected_failures : Double
criticality : Double
}
///|
pub fn fleet_summary(observation : FleetObservation) -> FleetSummary {
{
asset_count: fleet_asset_count(observation),
active_count: fleet_active_count(observation),
failed_count: fleet_failed_count(observation),
maintenance_count: fleet_maintenance_count(observation),
retired_count: fleet_retired_count(observation),
average_age: fleet_average_age(observation),
total_exposure: fleet_total_operating_hours(observation),
failure_rate: fleet_failure_rate(observation),
mtbf: fleet_mtbf(observation),
mttr: fleet_mttr(observation),
availability: fleet_inherent_availability(observation),
weighted_availability: fleet_weighted_availability(observation),
expected_failures: fleet_expected_failures(
observation,
fleet_observation_duration(observation),
),
criticality: fleet_total_criticality(observation),
}
}
///|
pub fn fleet_summaries_by_cohort(
observation : FleetObservation,
) -> Array[FleetSummary] {
fleet_group_ids(observation).map(cohort => {
fleet_group_summary(observation, cohort)
})
}
///|
pub fn fleet_summaries_by_model(
observation : FleetObservation,
) -> Array[FleetSummary] {
fleet_model_codes(observation).map(model => {
fleet_summary(fleet_filter_model(observation, model))
})
}
///|
pub struct FleetReliabilityPoint {
horizon : Double
reliability : Double
availability : Double
expected_failures : Double
expected_downtime : Double
}
///|
pub fn fleet_reliability_point(
horizon : Double,
reliability : Double,
availability : Double,
expected_failures : Double,
expected_downtime : Double,
) -> FleetReliabilityPoint {
{ horizon, reliability, availability, expected_failures, expected_downtime }
}
///|
pub fn fleet_reliability_curve(
observation : FleetObservation,
horizons : Array[Double],
) -> Array[FleetReliabilityPoint] {
let rate = fleet_failure_rate(observation)
horizons.map(horizon => {
let reliability = @math.exp(
-rate * horizon * fleet_asset_count(observation).to_double(),
)
let failures = fleet_expected_failures(observation, horizon)
let downtime = fleet_expected_downtime(observation, horizon)
fleet_reliability_point(
horizon,
reliability,
(1.0 - downtime / horizon.max(1.0e-12)).max(0.0).min(1.0),
failures,
downtime,
)
})
}
///|
pub fn fleet_curve_checksum(curve : Array[FleetReliabilityPoint]) -> Double {
curve.fold(init=0.0, (total, point) => {
total +
point.horizon +
point.reliability +
point.availability +
point.expected_failures +
point.expected_downtime
})
}
///|
pub fn fleet_reliability_at(
observation : FleetObservation,
horizon : Double,
) -> Double {
if horizon < 0.0 {
abort("horizon must be non-negative")
}
fleet_reliability_projection(observation, [horizon])[0]
}
///|
pub fn fleet_availability_at(
observation : FleetObservation,
horizon : Double,
) -> Double {
if horizon < 0.0 {
abort("horizon must be non-negative")
}
fleet_availability_projection(observation, [horizon])[0]
}
///|
pub fn fleet_target_horizon(
observation : FleetObservation,
target_reliability : Double,
) -> Double {
if target_reliability <= 0.0 || target_reliability >= 1.0 {
abort("target reliability must be between zero and one")
}
let rate = fleet_failure_rate(observation) *
fleet_asset_count(observation).to_double()
if rate == 0.0 {
1.0e300
} else {
-@math.ln(target_reliability) / rate
}
}
///|
pub fn fleet_target_failures(
observation : FleetObservation,
horizon : Double,
maximum_expected_failures : Double,
) -> Bool {
fleet_expected_failures(observation, horizon) <= maximum_expected_failures
}
///|
pub fn fleet_replacement_candidates(
observation : FleetObservation,
age_limit : Double,
risk_limit : Double,
) -> Array[FleetAsset] {
observation.assets.filter(asset => {
fleet_asset_age(asset) >= age_limit || fleet_risk_score(asset) >= risk_limit
})
}
///|
pub fn fleet_maintenance_candidates(
observation : FleetObservation,
availability_limit : Double,
) -> Array[FleetAsset] {
observation.assets.filter(asset => {
asset.state is FleetActive &&
fleet_asset_inherent_availability(asset) < availability_limit
})
}
///|
pub fn fleet_retirement_candidates(
observation : FleetObservation,
age_limit : Double,
minimum_utilization : Double,
) -> Array[FleetAsset] {
observation.assets.filter(asset => {
fleet_asset_age(asset) >= age_limit &&
asset.utilization < minimum_utilization
})
}
///|
pub fn fleet_capacity(observation : FleetObservation) -> Double {
observation.assets.fold(init=0.0, (total, asset) => total + asset.utilization)
}
///|
pub fn fleet_used_capacity(observation : FleetObservation) -> Double {
observation.assets.fold(init=0.0, (total, asset) => {
if fleet_asset_is_operational(asset) {
total + asset.utilization
} else {
total
}
})
}
///|
pub fn fleet_capacity_utilization(observation : FleetObservation) -> Double {
let capacity = fleet_capacity(observation)
if capacity == 0.0 {
0.0
} else {
fleet_used_capacity(observation) / capacity
}
}
///|
pub fn fleet_load_headroom(
observation : FleetObservation,
target_utilization : Double,
) -> Double {
if target_utilization < 0.0 || target_utilization > 1.0 {
abort("target utilization must be in [0, 1]")
}
(fleet_capacity(observation) * target_utilization -
fleet_used_capacity(observation)).max(0.0)
}
///|
pub fn fleet_asset_by_id(
observation : FleetObservation,
asset_id : Int,
) -> FleetAsset? {
for asset in observation.assets {
if asset.asset_id == asset_id {
return Some(asset)
}
}
None
}
///|
pub fn fleet_ids(observation : FleetObservation) -> Array[Int] {
observation.assets.map(asset => asset.asset_id)
}
///|
pub fn fleet_exposure_by_cohort(
observation : FleetObservation,
) -> Array[Double] {
fleet_group_ids(observation).map(cohort => {
observation.assets.fold(init=0.0, (total, asset) => {
if asset.cohort == cohort {
total + fleet_asset_exposure(asset)
} else {
total
}
})
})
}
///|
pub fn fleet_failures_by_cohort(observation : FleetObservation) -> Array[Int] {
fleet_group_ids(observation).map(cohort => {
observation.assets.fold(init=0, (total, asset) => {
if asset.cohort == cohort {
total + asset.failure_count
} else {
total
}
})
})
}
///|
pub fn fleet_failure_rate_by_cohort(
observation : FleetObservation,
) -> Array[Double] {
let exposures = fleet_exposure_by_cohort(observation)
let failures = fleet_failures_by_cohort(observation)
Array::makei(exposures.length(), i => {
if exposures[i] == 0.0 {
0.0
} else {
failures[i].to_double() / exposures[i]
}
})
}
///|
pub fn fleet_cohort_comparison(observation : FleetObservation) -> Array[Double] {
let rates = fleet_failure_rate_by_cohort(observation)
if rates.is_empty() {
[]
} else {
rates.map(rate => rate - mean(rates))
}
}
///|
pub fn fleet_model_failure_rates(
observation : FleetObservation,
) -> Array[Double] {
fleet_model_codes(observation).map(model => {
fleet_failure_rate(fleet_filter_model(observation, model))
})
}
///|
pub fn fleet_model_availability(
observation : FleetObservation,
) -> Array[Double] {
fleet_model_codes(observation).map(model => {
fleet_inherent_availability(fleet_filter_model(observation, model))
})
}
///|
pub fn fleet_model_risk_rank(observation : FleetObservation) -> Array[String] {
let models = fleet_model_codes(observation)
let result = models.copy()
result.sort_by((left, right) => {
let left_rate = fleet_failure_rate(fleet_filter_model(observation, left))
let right_rate = fleet_failure_rate(fleet_filter_model(observation, right))
if left_rate > right_rate {
-1
} else if left_rate < right_rate {
1
} else {
0
}
})
result
}
///|
pub fn fleet_change_rate(
previous : FleetObservation,
current : FleetObservation,
) -> Double {
let previous_rate = fleet_failure_rate(previous)
let current_rate = fleet_failure_rate(current)
if previous_rate == 0.0 {
current_rate
} else {
(current_rate - previous_rate) / previous_rate
}
}
///|
pub fn fleet_availability_change(
previous : FleetObservation,
current : FleetObservation,
) -> Double {
fleet_inherent_availability(current) - fleet_inherent_availability(previous)
}
///|
pub fn fleet_health_index(observation : FleetObservation) -> Double {
let availability = fleet_weighted_availability(observation)
let risk = fleet_failure_rate(observation) * fleet_average_age(observation)
(availability * @math.exp(-risk)).max(0.0).min(1.0)
}
///|
pub fn fleet_health_label(index : Double) -> String {
if index >= 0.9 {
"healthy"
} else if index >= 0.7 {
"watch"
} else if index >= 0.5 {
"degraded"
} else {
"critical"
}
}
///|
pub fn fleet_health_gap(
observation : FleetObservation,
target : Double,
) -> Double {
(target - fleet_health_index(observation)).max(0.0)
}
///|
pub fn fleet_action_priority(
asset : FleetAsset,
target_availability : Double,
) -> Double {
let availability_gap = (target_availability -
fleet_asset_inherent_availability(asset)).max(0.0)
availability_gap * (1.0 + asset.criticality) * (1.0 + fleet_asset_age(asset))
}
///|
pub fn fleet_action_priorities(
observation : FleetObservation,
target_availability : Double,
) -> Array[Double] {
observation.assets.map(asset => {
fleet_action_priority(asset, target_availability)
})
}
///|
pub fn fleet_action_rank(
observation : FleetObservation,
target_availability : Double,
) -> Array[FleetAsset] {
let result = observation.assets.copy()
result.sort_by((left, right) => {
let left_priority = fleet_action_priority(left, target_availability)
let right_priority = fleet_action_priority(right, target_availability)
if left_priority > right_priority {
-1
} else if left_priority < right_priority {
1
} else {
0
}
})
result
}
///|
pub fn fleet_action_budget(
observation : FleetObservation,
target_availability : Double,
budget : Int,
) -> Array[FleetAsset] {
if budget < 0 {
abort("budget must be non-negative")
}
let ranked = fleet_action_rank(observation, target_availability)
let count = budget.min(ranked.length())
Array::makei(count, i => ranked[i])
}
///|
pub fn fleet_snapshot_checksum(observation : FleetObservation) -> Double {
observation.assets.fold(init=0.0, (total, asset) => {
total +
asset.asset_id.to_double() +
fleet_asset_age(asset) +
asset.operating_hours +
asset.failure_count.to_double() +
asset.repair_hours +
asset.criticality +
asset.utilization
})
}
///|
pub fn fleet_merge_observations(
left : FleetObservation,
right : FleetObservation,
) -> FleetObservation {
if left.end > right.start && right.end > left.start {
abort("fleet observation windows overlap")
}
fleet_observation(
left.assets + right.assets,
left.start.min(right.start),
left.end.max(right.end),
"{left.name}+{right.name}",
)
}
///|
pub fn fleet_asset_age_bands(
observation : FleetObservation,
boundaries : Array[Double],
) -> Array[Int] {
if boundaries.is_empty() {
abort("age boundaries must not be empty")
}
let sorted = boundaries.copy()
sorted.sort()
Array::makei(sorted.length() + 1, i => {
let lower = if i == 0 { -1.0e300 } else { sorted[i - 1] }
let upper = if i == sorted.length() { 1.0e300 } else { sorted[i] }
observation.assets.fold(init=0, (count, asset) => {
let age = fleet_asset_age(asset)
if age >= lower && age < upper {
count + 1
} else {
count
}
})
})
}
///|
pub fn fleet_utilization_bands(
observation : FleetObservation,
boundaries : Array[Double],
) -> Array[Int] {
if boundaries.is_empty() {
abort("utilization boundaries must not be empty")
}
let sorted = boundaries.copy()
sorted.sort()
Array::makei(sorted.length() + 1, i => {
let lower = if i == 0 { -1.0e300 } else { sorted[i - 1] }
let upper = if i == sorted.length() { 1.0e300 } else { sorted[i] }
observation.assets.fold(init=0, (count, asset) => {
if asset.utilization >= lower && asset.utilization < upper {
count + 1
} else {
count
}
})
})
}
///|
pub fn fleet_criticality_bands(
observation : FleetObservation,
boundaries : Array[Double],
) -> Array[Int] {
if boundaries.is_empty() {
abort("criticality boundaries must not be empty")
}
let sorted = boundaries.copy()
sorted.sort()
Array::makei(sorted.length() + 1, i => {
let lower = if i == 0 { -1.0e300 } else { sorted[i - 1] }
let upper = if i == sorted.length() { 1.0e300 } else { sorted[i] }
observation.assets.fold(init=0, (count, asset) => {
if asset.criticality >= lower && asset.criticality < upper {
count + 1
} else {
count
}
})
})
}
///|
pub fn fleet_failure_count_by_state(
observation : FleetObservation,
) -> Array[Int] {
[
observation.assets.fold(init=0, (total, asset) => {
if asset.state is FleetActive {
total + asset.failure_count
} else {
total
}
}),
observation.assets.fold(init=0, (total, asset) => {
if asset.state is FleetFailed {
total + asset.failure_count
} else {
total
}
}),
observation.assets.fold(init=0, (total, asset) => {
if asset.state is FleetMaintenance {
total + asset.failure_count
} else {
total
}
}),
observation.assets.fold(init=0, (total, asset) => {
if asset.state is FleetRetired {
total + asset.failure_count
} else {
total
}
}),
]
}
///|
pub fn fleet_failure_share_by_state(
observation : FleetObservation,
) -> Array[Double] {
let counts = fleet_failure_count_by_state(observation)
let total = counts.fold(init=0, (sum, value) => sum + value)
if total == 0 {
counts.map(_ => 0.0)
} else {
counts.map(value => value.to_double() / total.to_double())
}
}
///|
pub fn fleet_service_level(
observation : FleetObservation,
target_availability : Double,
) -> Double {
if target_availability <= 0.0 || target_availability > 1.0 {
abort("target availability must be in (0, 1]")
}
(fleet_weighted_availability(observation) / target_availability).min(1.0)
}
///|
pub fn fleet_service_level_label(value : Double) -> String {
if value >= 1.0 {
"met"
} else if value >= 0.95 {
"near-target"
} else {
"missed"
}
}
///|
pub fn fleet_target_gap(
observation : FleetObservation,
target : Double,
) -> Double {
(target - fleet_weighted_availability(observation)).max(0.0)
}
///|
pub fn fleet_expected_loss(
observation : FleetObservation,
horizon : Double,
loss_per_failure : Double,
loss_per_downtime_hour : Double,
) -> Double {
if horizon < 0.0 || loss_per_failure < 0.0 || loss_per_downtime_hour < 0.0 {
abort("invalid fleet loss inputs")
}
fleet_expected_failures(observation, horizon) * loss_per_failure +
fleet_expected_downtime(observation, horizon) * loss_per_downtime_hour
}
///|
pub fn fleet_preventive_value(
observation : FleetObservation,
horizon : Double,
preventive_cost : Double,
avoided_failure_fraction : Double,
loss_per_failure : Double,
) -> Double {
if avoided_failure_fraction < 0.0 || avoided_failure_fraction > 1.0 {
abort("avoided failure fraction must be in [0, 1]")
}
avoided_failure_fraction *
fleet_expected_failures(observation, horizon) *
loss_per_failure -
preventive_cost
}
///|
pub fn fleet_replacement_value(
asset : FleetAsset,
remaining_horizon : Double,
replacement_cost : Double,
avoided_failure_fraction : Double,
loss_per_failure : Double,
) -> Double {
if remaining_horizon < 0.0 ||
replacement_cost < 0.0 ||
avoided_failure_fraction < 0.0 ||
avoided_failure_fraction > 1.0 {
abort("invalid replacement value inputs")
}
avoided_failure_fraction *
fleet_asset_failure_rate(asset) *
remaining_horizon *
loss_per_failure -
replacement_cost
}
///|
pub fn fleet_batch_expected_failures(
observations : Array[FleetObservation],
horizons : Array[Double],
) -> Array[Double] {
if observations.length() != horizons.length() {
abort("batch inputs must have the same length")
}
Array::makei(observations.length(), i => {
fleet_expected_failures(observations[i], horizons[i])
})
}
///|
pub fn fleet_batch_availability(
observations : Array[FleetObservation],
) -> Array[Double] {
observations.map(observation => fleet_weighted_availability(observation))
}
///|
pub fn fleet_batch_health(
observations : Array[FleetObservation],
) -> Array[Double] {
observations.map(observation => fleet_health_index(observation))
}
///|
pub fn fleet_batch_checksum(observations : Array[FleetObservation]) -> Double {
observations.fold(init=0.0, (total, observation) => {
total + fleet_snapshot_checksum(observation)
})
}