///|
/// Maintenance record for a machine or cutting tool.
pub(all) struct MaintenanceRecord {
asset : String
event : String
timestamp : String
runtime_hours : Double
severity : Int
note : String
} derive(Debug, ToJson)
///|
/// Tool-life observation linked to a production run.
pub(all) struct ToolLifePoint {
tool : String
runtime_hours : Double
wear : Double
threshold : Double
exceeded : Bool
} derive(Debug, ToJson)
///|
/// Create a maintenance record.
pub fn MaintenanceRecord::new(
asset~ : String,
event~ : String,
timestamp~ : String,
runtime_hours~ : Double,
severity~ : Int,
note~ : String,
) -> MaintenanceRecord {
{
asset,
event,
timestamp,
runtime_hours,
severity: severity.max(0).min(5),
note,
}
}
///|
/// Create a tool-life observation.
pub fn ToolLifePoint::new(
tool~ : String,
runtime_hours~ : Double,
wear~ : Double,
threshold~ : Double,
) -> ToolLifePoint {
{ tool, runtime_hours, wear, threshold, exceeded: wear >= threshold }
}
///|
/// Return maintenance records for one asset.
pub fn asset_records(
records : Array[MaintenanceRecord],
asset : String,
) -> Array[MaintenanceRecord] {
let result : Array[MaintenanceRecord] = []
for record in records {
if record.asset == asset {
result.push(record)
}
}
result
}
///|
/// Return records above a severity threshold.
pub fn severe_records(
records : Array[MaintenanceRecord],
threshold : Int,
) -> Array[MaintenanceRecord] {
let result : Array[MaintenanceRecord] = []
for record in records {
if record.severity >= threshold {
result.push(record)
}
}
result
}
///|
/// Count records by event name.
pub fn maintenance_event_counts(
records : Array[MaintenanceRecord],
) -> Array[CategoryCount] {
let names : Array[String] = []
for record in records {
names.push(record.event)
}
category_counts(names)
}
///|
/// Sum runtime recorded for an asset.
pub fn asset_runtime(
records : Array[MaintenanceRecord],
asset : String,
) -> Double {
let mut total = 0.0
for record in records {
if record.asset == asset {
total += record.runtime_hours.max(0.0)
}
}
total
}
///|
/// Average repair severity for an asset.
pub fn average_severity(
records : Array[MaintenanceRecord],
asset : String,
) -> Double? {
let selected = asset_records(records, asset)
if selected.length() == 0 {
None
} else {
let mut total = 0
for record in selected {
total += record.severity
}
Some(total.to_double() / selected.length().to_double())
}
}
///|
/// Mean time between recorded maintenance events.
pub fn mean_time_between_events(
records : Array[MaintenanceRecord],
asset : String,
) -> Double? {
let selected = asset_records(records, asset)
if selected.length() < 2 {
None
} else {
let mut total = 0.0
for index in 1.. ReliabilitySummary {
let selected = asset_records(records, asset)
let severe = severe_records(selected, severe_threshold).length()
let severity = average_severity(selected, asset).unwrap_or(0.0)
let runtime = asset_runtime(selected, asset)
let risk = if runtime <= 0.0 {
severity
} else {
severity * (1.0 + severe.to_double() / runtime)
}
{
asset,
events: selected.length(),
runtime_hours: runtime,
mean_between_events: mean_time_between_events(selected, asset),
severe_events: severe,
risk_score: risk,
}
}
///|
/// Fit a linear wear trend against runtime.
pub fn wear_trend(points : Array[ToolLifePoint]) -> Trend? {
let runtime : Array[Double] = []
let wear : Array[Double] = []
for point in points {
runtime.push(point.runtime_hours)
wear.push(point.wear)
}
if runtime.length() < 2 {
None
} else {
let matrix : Array[Double] = []
for index in 0.. Double? {
if points.length() < 2 {
None
} else {
let values : Array[Double] = []
let runtime : Array[Double] = []
for point in points {
values.push(point.wear)
runtime.push(point.runtime_hours)
}
match linear_trend(values) {
Some(trend) =>
if trend.slope <= 0.0 {
None
} else {
Some((points[0].threshold - trend.intercept) / trend.slope)
}
None => None
}
}
}
///|
/// Count tools that have exceeded their wear threshold.
pub fn exceeded_tool_count(points : Array[ToolLifePoint]) -> Int {
let mut count = 0
for point in points {
if point.exceeded {
count += 1
}
}
count
}
///|
/// Return tools with the highest current wear.
pub fn highest_wear(
points : Array[ToolLifePoint],
limit : Int,
) -> Array[ToolLifePoint] {
let sorted = points.copy()
for index in 1.. 0 && sorted[cursor - 1].wear < value.wear {
sorted[cursor] = sorted[cursor - 1]
cursor -= 1
}
sorted[cursor] = value
}
let output : Array[ToolLifePoint] = []
for index in 0.. Double {
(point.threshold - point.wear).max(0.0)
}
///|
/// Estimate a normalized wear percentage.
pub fn wear_percentage(point : ToolLifePoint) -> Double {
if point.threshold <= 0.0 {
1.0
} else {
(point.wear / point.threshold).max(0.0)
}
}
///|
/// Return a warning when wear reaches a configurable fraction.
pub fn wear_warning(point : ToolLifePoint, fraction : Double) -> Bool {
wear_percentage(point) >= fraction
}
///|
/// Group wear observations by tool identifier.
pub fn group_tool_life(points : Array[ToolLifePoint]) -> Array[String] {
let result : Array[String] = []
for point in points {
let mut found = false
for tool in result {
if tool == point.tool {
found = true
}
}
if !found {
result.push(point.tool)
}
}
result
}
///|
/// Average wear for one tool.
pub fn average_tool_wear(
points : Array[ToolLifePoint],
tool : String,
) -> Double? {
let mut total = 0.0
let mut count = 0
for point in points {
if point.tool == tool {
total += point.wear
count += 1
}
}
if count == 0 {
None
} else {
Some(total / count.to_double())
}
}
///|
/// Maximum runtime observed for one tool.
pub fn maximum_tool_runtime(
points : Array[ToolLifePoint],
tool : String,
) -> Double? {
let mut result : Double? = None
for point in points {
if point.tool == tool {
result = match result {
None => Some(point.runtime_hours)
Some(value) => Some(value.max(point.runtime_hours))
}
}
}
result
}
///|
/// Normalize runtime/wear points to a common interval.
pub fn normalize_wear_points(points : Array[ToolLifePoint]) -> Array[Double] {
let values : Array[Double] = []
for point in points {
values.push(wear_percentage(point))
}
values
}