///|
/// A named diagnostic value suitable for logs, dashboards and CSV export.
pub struct DiagnosticField {
name : String
value : Double
unit : String
category : String
healthy : Bool
} derive(Debug)
///|
pub fn DiagnosticField::new(
name : String,
value : Double,
unit : String,
category : String,
healthy : Bool,
) -> DiagnosticField {
{ name, value, unit, category, healthy }
}
///|
pub fn DiagnosticField::name(self : DiagnosticField) -> String {
self.name
}
///|
pub fn DiagnosticField::value(self : DiagnosticField) -> Double {
self.value
}
///|
pub fn DiagnosticField::unit(self : DiagnosticField) -> String {
self.unit
}
///|
pub fn DiagnosticField::category(self : DiagnosticField) -> String {
self.category
}
///|
pub fn DiagnosticField::healthy(self : DiagnosticField) -> Bool {
self.healthy
}
///|
pub fn DiagnosticField::csv(self : DiagnosticField) -> String {
self.name +
"," +
self.value.to_string() +
"," +
self.unit +
"," +
self.category +
"," +
self.healthy.to_string()
}
///|
pub struct DiagnosticSnapshot {
timestamp : Int
source : String
fields : Array[DiagnosticField]
state_dimension : Int
covariance_dimension : Int
healthy : Bool
score : Double
} derive(Debug)
///|
pub fn DiagnosticSnapshot::new(
timestamp : Int,
source : String,
fields : Array[DiagnosticField],
state_dimension : Int,
covariance_dimension : Int,
) -> DiagnosticSnapshot {
let mut healthy = true
let mut score_sum = 0.0
for field in fields {
if !field.healthy() {
healthy = false
}
if field.healthy() {
score_sum = score_sum + 1.0
}
}
let score = if fields.length() == 0 {
0.0
} else {
score_sum / fields.length().to_double()
}
{
timestamp,
source,
fields: fields.copy(),
state_dimension: state_dimension.max(0),
covariance_dimension: covariance_dimension.max(0),
healthy,
score,
}
}
///|
pub fn DiagnosticSnapshot::timestamp(self : DiagnosticSnapshot) -> Int {
self.timestamp
}
///|
pub fn DiagnosticSnapshot::source(self : DiagnosticSnapshot) -> String {
self.source
}
///|
pub fn DiagnosticSnapshot::fields(
self : DiagnosticSnapshot,
) -> Array[DiagnosticField] {
self.fields.copy()
}
///|
pub fn DiagnosticSnapshot::state_dimension(self : DiagnosticSnapshot) -> Int {
self.state_dimension
}
///|
pub fn DiagnosticSnapshot::covariance_dimension(
self : DiagnosticSnapshot,
) -> Int {
self.covariance_dimension
}
///|
pub fn DiagnosticSnapshot::healthy(self : DiagnosticSnapshot) -> Bool {
self.healthy
}
///|
pub fn DiagnosticSnapshot::score(self : DiagnosticSnapshot) -> Double {
self.score
}
///|
pub fn DiagnosticSnapshot::field(
self : DiagnosticSnapshot,
name : String,
) -> DiagnosticField? {
for field in self.fields {
if field.name() == name {
return Some(field)
}
}
None
}
///|
pub fn DiagnosticSnapshot::csv_header(self : DiagnosticSnapshot) -> String {
ignore(self)
"timestamp,source,state_dimension,covariance_dimension,healthy,score"
}
///|
pub fn DiagnosticSnapshot::csv_row(self : DiagnosticSnapshot) -> String {
self.timestamp.to_string() +
"," +
self.source +
"," +
self.state_dimension.to_string() +
"," +
self.covariance_dimension.to_string() +
"," +
self.healthy.to_string() +
"," +
self.score.to_string()
}
///|
pub(all) enum DiagnosticSeverity {
Info
Warning
Error
Critical
} derive(Debug, Eq)
///|
pub struct DiagnosticEvent {
timestamp : Int
code : String
severity : DiagnosticSeverity
message : String
value : Double
mut acknowledged : Bool
} derive(Debug)
///|
pub fn DiagnosticEvent::new(
timestamp : Int,
code : String,
severity : DiagnosticSeverity,
message : String,
value : Double,
) -> DiagnosticEvent {
{ timestamp, code, severity, message, value, acknowledged: false }
}
///|
pub fn DiagnosticEvent::timestamp(self : DiagnosticEvent) -> Int {
self.timestamp
}
///|
pub fn DiagnosticEvent::code(self : DiagnosticEvent) -> String {
self.code
}
///|
pub fn DiagnosticEvent::severity(self : DiagnosticEvent) -> DiagnosticSeverity {
self.severity
}
///|
pub fn DiagnosticEvent::message(self : DiagnosticEvent) -> String {
self.message
}
///|
pub fn DiagnosticEvent::value(self : DiagnosticEvent) -> Double {
self.value
}
///|
pub fn DiagnosticEvent::acknowledged(self : DiagnosticEvent) -> Bool {
self.acknowledged
}
///|
pub fn DiagnosticEvent::acknowledge(self : DiagnosticEvent) -> Unit {
self.acknowledged = true
}
///|
pub fn DiagnosticEvent::severity_text(self : DiagnosticEvent) -> String {
match self.severity {
Info => "info"
Warning => "warning"
Error => "error"
Critical => "critical"
}
}
///|
pub fn DiagnosticEvent::csv(self : DiagnosticEvent) -> String {
self.timestamp.to_string() +
"," +
self.code +
"," +
self.severity_text() +
"," +
self.message +
"," +
self.value.to_string() +
"," +
self.acknowledged.to_string()
}
///|
pub struct DiagnosticReport {
run_id : String
started_at : Int
mut finished_at : Int?
snapshots : Array[DiagnosticSnapshot]
events : Array[DiagnosticEvent]
mut closed : Bool
} derive(Debug)
///|
pub fn DiagnosticReport::new(
run_id : String,
started_at : Int,
) -> DiagnosticReport {
{
run_id,
started_at,
finished_at: None,
snapshots: [],
events: [],
closed: false,
}
}
///|
pub fn DiagnosticReport::run_id(self : DiagnosticReport) -> String {
self.run_id
}
///|
pub fn DiagnosticReport::started_at(self : DiagnosticReport) -> Int {
self.started_at
}
///|
pub fn DiagnosticReport::finished_at(self : DiagnosticReport) -> Int? {
self.finished_at
}
///|
pub fn DiagnosticReport::snapshots(
self : DiagnosticReport,
) -> Array[DiagnosticSnapshot] {
self.snapshots.copy()
}
///|
pub fn DiagnosticReport::events(
self : DiagnosticReport,
) -> Array[DiagnosticEvent] {
self.events.copy()
}
///|
pub fn DiagnosticReport::closed(self : DiagnosticReport) -> Bool {
self.closed
}
///|
pub fn DiagnosticReport::add_snapshot(
self : DiagnosticReport,
snapshot : DiagnosticSnapshot,
) -> Bool {
if self.closed {
false
} else {
self.snapshots.push(snapshot)
true
}
}
///|
pub fn DiagnosticReport::add_event(
self : DiagnosticReport,
event : DiagnosticEvent,
) -> Bool {
if self.closed {
false
} else {
self.events.push(event)
true
}
}
///|
pub fn DiagnosticReport::close(
self : DiagnosticReport,
timestamp : Int,
) -> Unit {
if !self.closed {
self.finished_at = Some(timestamp)
self.closed = true
}
}
///|
pub fn DiagnosticReport::duration(self : DiagnosticReport) -> Int {
match self.finished_at {
None => 0
Some(value) => value - self.started_at
}
}
///|
pub fn DiagnosticReport::healthy(self : DiagnosticReport) -> Bool {
for snapshot in self.snapshots {
if !snapshot.healthy() {
return false
}
}
for event in self.events {
match event.severity {
Error | Critical => if !event.acknowledged() { return false }
Info | Warning => ()
}
}
true
}
///|
pub fn DiagnosticReport::score(self : DiagnosticReport) -> Double {
if self.snapshots.length() == 0 {
return 0.0
}
let mut total = 0.0
for snapshot in self.snapshots {
total = total + snapshot.score()
}
let mut penalty = 0.0
for event in self.events {
match event.severity {
Info => ()
Warning => penalty = penalty + 0.05
Error => penalty = penalty + 0.2
Critical => penalty = penalty + 0.5
}
}
(total / self.snapshots.length().to_double() - penalty).clamp(
min=0.0,
max=1.0,
)
}
///|
pub fn DiagnosticReport::summary(self : DiagnosticReport) -> String {
"run=" +
self.run_id +
",duration=" +
self.duration().to_string() +
",snapshots=" +
self.snapshots.length().to_string() +
",events=" +
self.events.length().to_string() +
",healthy=" +
self.healthy().to_string() +
",score=" +
self.score().to_string()
}
///|
pub fn DiagnosticReport::snapshots_csv(self : DiagnosticReport) -> String {
let mut output = "timestamp,source,state_dimension,covariance_dimension,healthy,score\n"
for snapshot in self.snapshots {
output = output + snapshot.csv_row() + "\n"
}
output
}
///|
pub fn DiagnosticReport::events_csv(self : DiagnosticReport) -> String {
let mut output = "timestamp,code,severity,message,value,acknowledged\n"
for event in self.events {
output = output + event.csv() + "\n"
}
output
}
///|
pub struct DiagnosticAccumulator {
reports : Array[DiagnosticReport]
capacity : Int
mut discarded : Int
} derive(Debug)
///|
pub fn DiagnosticAccumulator::new(capacity : Int) -> DiagnosticAccumulator {
{ reports: [], capacity: capacity.max(1), discarded: 0 }
}
///|
pub fn DiagnosticAccumulator::reports(
self : DiagnosticAccumulator,
) -> Array[DiagnosticReport] {
self.reports.copy()
}
///|
pub fn DiagnosticAccumulator::discarded(self : DiagnosticAccumulator) -> Int {
self.discarded
}
///|
pub fn DiagnosticAccumulator::push(
self : DiagnosticAccumulator,
report : DiagnosticReport,
) -> Unit {
if self.reports.length() >= self.capacity {
self.reports.remove(0) |> ignore
self.discarded = self.discarded + 1
}
self.reports.push(report)
}
///|
pub fn DiagnosticAccumulator::length(self : DiagnosticAccumulator) -> Int {
self.reports.length()
}
///|
pub fn DiagnosticAccumulator::average_score(
self : DiagnosticAccumulator,
) -> Double {
if self.reports.length() == 0 {
0.0
} else {
let mut total = 0.0
for report in self.reports {
total = total + report.score()
}
total / self.reports.length().to_double()
}
}
///|
pub fn DiagnosticAccumulator::healthy_fraction(
self : DiagnosticAccumulator,
) -> Double {
if self.reports.length() == 0 {
0.0
} else {
let mut healthy = 0
for report in self.reports {
if report.healthy() {
healthy = healthy + 1
}
}
healthy.to_double() / self.reports.length().to_double()
}
}
///|
pub fn diagnostic_field_from_covariance(
name : String,
covariance : Matrix,
unit : String,
) -> DiagnosticField {
let trace = covariance_trace3d(covariance).max(0.0)
DiagnosticField::new(
name,
trace.sqrt(),
unit,
"uncertainty",
covariance_is_psd(covariance, 0.001),
)
}
///|
pub fn diagnostic_field_from_state(
name : String,
state : Array[Double],
unit : String,
index : Int,
minimum : Double,
maximum : Double,
) -> DiagnosticField {
let value = if index < 0 || index >= state.length() {
0.0
} else {
state[index]
}
DiagnosticField::new(
name,
value,
unit,
"state",
value >= minimum && value <= maximum,
)
}
///|
pub fn diagnostic_snapshot_from_filter(
timestamp : Int,
source : String,
state : Array[Double],
covariance : Matrix,
) -> DiagnosticSnapshot {
let fields : Array[DiagnosticField] = []
for i, value in state {
fields.push(
DiagnosticField::new(
"state_" + i.to_string(),
value,
"unitless",
"state",
!value.is_nan() && !value.is_inf(),
),
)
}
fields.push(
diagnostic_field_from_covariance("covariance_trace", covariance, "unitless"),
)
DiagnosticSnapshot::new(
timestamp,
source,
fields,
state.length(),
covariance.rows(),
)
}
///|
pub fn diagnostic_report_from_quality(
run_id : String,
timestamp : Int,
report : TrajectoryQualityReport,
) -> DiagnosticReport {
let fields = [
DiagnosticField::new(
"quality_score",
report.score(),
"ratio",
"trajectory",
report.score() >= 0.5,
),
DiagnosticField::new(
"total_length",
report.total_length(),
"unit",
"trajectory",
true,
),
DiagnosticField::new(
"max_speed",
report.max_speed(),
"unit_per_s",
"kinematics",
true,
),
DiagnosticField::new(
"mean_uncertainty",
report.mean_uncertainty(),
"unit",
"uncertainty",
report.mean_uncertainty() >= 0.0,
),
]
let snapshot = DiagnosticSnapshot::new(timestamp, "trajectory", fields, 0, 0)
let result = DiagnosticReport::new(run_id, timestamp)
result.add_snapshot(snapshot) |> ignore
for event in report.events() {
let severity = if event.severity() >= 0.8 {
Critical
} else if event.severity() >= 0.6 {
Error
} else if event.severity() >= 0.4 {
Warning
} else {
Info
}
result.add_event(
DiagnosticEvent::new(
event.timestamp(),
"trajectory-quality",
severity,
event.message(),
event.severity(),
),
)
|> ignore
}
result.close(timestamp)
result
}
///|
pub fn diagnostic_merge_reports(
left : DiagnosticReport,
right : DiagnosticReport,
run_id : String,
) -> DiagnosticReport {
let start = left.started_at().min(right.started_at())
let merged = DiagnosticReport::new(run_id, start)
for snapshot in left.snapshots() {
merged.add_snapshot(snapshot) |> ignore
}
for snapshot in right.snapshots() {
merged.add_snapshot(snapshot) |> ignore
}
for event in left.events() {
merged.add_event(event) |> ignore
}
for event in right.events() {
merged.add_event(event) |> ignore
}
let end_time : Int? = match left.finished_at() {
Some(value) => Some(value)
None => right.finished_at()
}
match end_time {
None => ()
Some(value) => merged.close(value)
}
merged
}
///|
pub fn diagnostic_event_counts(
events : Array[DiagnosticEvent],
) -> Array[(DiagnosticSeverity, Int)] {
let result : Array[(DiagnosticSeverity, Int)] = []
for event in events {
let mut found = false
for i in 0.. Double {
if fields.length() == 0 {
0.0
} else {
let mut score = 0.0
for field in fields {
if field.healthy() {
score = score + 1.0
}
}
score / fields.length().to_double()
}
}
///|
pub fn diagnostic_value_within(
value : Double,
minimum : Double,
maximum : Double,
) -> Bool {
!value.is_nan() && !value.is_inf() && value >= minimum && value <= maximum
}
///|
pub fn diagnostic_covariance_health(
covariance : Matrix,
tolerance : Double,
) -> Double {
if covariance.rows() == 0 || covariance.rows() != covariance.cols() {
0.0
} else {
let report = KalmanDiagnostics::new().report(covariance)
if report.is_healthy() {
1.0
} else if report.finite() {
tolerance.max(0.0).min(1.0)
} else {
0.0
}
}
}
///|
pub fn diagnostic_snapshot_summary(snapshot : DiagnosticSnapshot) -> String {
"source=" +
snapshot.source() +
",timestamp=" +
snapshot.timestamp().to_string() +
",fields=" +
snapshot.fields().length().to_string() +
",score=" +
snapshot.score().to_string()
}