///|
/// Operational policy for selecting an action from a reliability signal.
pub(all) enum PolicyAction {
ContinueMonitoring
InspectSoon
ScheduleMaintenance
RemoveFromService
} derive(Debug, Eq)
///|
pub struct PolicyDecision {
action : PolicyAction
score : Double
reason : String
urgency_hours : Double
}
///|
pub fn policy_decision(
action~ : PolicyAction,
score~ : Double,
reason~ : String,
urgency_hours~ : Double,
) -> PolicyDecision {
{ action, score, reason, urgency_hours }
}
///|
pub fn decide_policy(
survival : Double,
hazard : Double,
target_survival : Double,
hazard_limit : Double,
hours_since_service : Double,
) -> PolicyDecision {
if survival < target_survival * 0.5 {
policy_decision(
action=RemoveFromService,
score=0.0,
reason="mission survival is materially below the operating target",
urgency_hours=0.0,
)
} else if hazard > hazard_limit {
policy_decision(
action=ScheduleMaintenance,
score=hazard / hazard_limit,
reason="estimated hazard exceeds the maintenance threshold",
urgency_hours=24.0,
)
} else if hours_since_service > 1000.0 {
policy_decision(
action=InspectSoon,
score=hours_since_service / 1000.0,
reason="service interval has elapsed",
urgency_hours=168.0,
)
} else {
policy_decision(
action=ContinueMonitoring,
score=survival,
reason="current indicators remain within operating policy",
urgency_hours=720.0,
)
}
}
///|
pub fn policy_action_label(action : PolicyAction) -> String {
match action {
ContinueMonitoring => "monitor"
InspectSoon => "inspect"
ScheduleMaintenance => "maintain"
RemoveFromService => "remove"
}
}
///|
pub fn policy_risk_score(
model : ReliabilityModel,
time : Double,
target : Double,
hazard_limit : Double,
) -> Double {
let survival_gap = (target - model.survival(time)).max(0.0)
let hazard_gap = (model_hazard(model, time) - hazard_limit).max(0.0)
survival_gap + hazard_gap
}
///|
pub fn policy_score_curve(
model : ReliabilityModel,
times : Array[Double],
target : Double,
hazard_limit : Double,
) -> Array[Double] {
times.map(time => policy_risk_score(model, time, target, hazard_limit))
}
///|
pub fn policy_breach_count(scores : Array[Double], threshold : Double) -> Int {
scores.fold(init=0, (count, score) => {
if score > threshold {
count + 1
} else {
count
}
})
}
///|
pub fn monitoring_interval(score : Double, base_hours : Double) -> Double {
if score <= 0.0 {
base_hours
} else {
base_hours / (1.0 + score).min(10.0)
}
}
///|
pub fn alert_budget(score : Double, daily_budget : Int) -> Int {
if daily_budget < 0 {
abort("daily budget cannot be negative")
}
(daily_budget.to_double() * (1.0 - score.min(1.0))).floor().to_int()
}
///|
pub fn action_from_metric(
metric : MetricEstimate,
target : Double,
) -> PolicyAction {
if metric.lower < target * 0.5 {
RemoveFromService
} else if metric.lower < target {
ScheduleMaintenance
} else {
ContinueMonitoring
}
}
///|
pub fn policy_report(decisions : Array[PolicyDecision]) -> String {
let builder = StringBuilder::new()
builder.write_string("action,score,urgency_hours,reason\n")
for decision in decisions {
builder.write_string(
"\{policy_action_label(decision.action)},\{decision.score},\{decision.urgency_hours},\{decision.reason}\n",
)
}
builder.to_string()
}
///|
pub fn policy_decisions_for_curve(
model : ReliabilityModel,
times : Array[Double],
target : Double,
hazard_limit : Double,
) -> Array[PolicyDecision] {
times.map(time => {
decide_policy(
model.survival(time),
model_hazard(model, time),
target,
hazard_limit,
0.0,
)
})
}
///|
pub fn policy_compliance(decisions : Array[PolicyDecision]) -> Double {
if decisions.is_empty() {
1.0
} else {
decisions
.filter(decision => decision.action is ContinueMonitoring)
.length()
.to_double() /
decisions.length().to_double()
}
}