///|
/// Metric families included in a signal profile.
pub enum ProfileMetricKind {
TransitionCount
RisingCount
FallingCount
UnknownCount
DutyCycle
Frequency
Entropy
MeanChangeInterval
} derive(Debug, Eq)
///|
/// One measured profile metric.
pub struct ProfileMetric {
signal_id : String
kind : ProfileMetricKind
value : Double?
count : Int?
unit : String
quality : Int
} derive(Debug)
///|
/// A complete profile for one trace.
pub struct TraceProfile {
trace_end : Int64
signal_count : Int
metrics : Array[ProfileMetric]
quality : Int
} derive(Debug)
///|
/// List profile metric families.
pub fn profile_metric_catalog() -> Array[ProfileMetricKind] {
[
TransitionCount,
RisingCount,
FallingCount,
UnknownCount,
DutyCycle,
Frequency,
Entropy,
MeanChangeInterval,
]
}
///|
/// Build signal profiles from the existing analysis primitives.
pub fn build_trace_profile(trace : Trace) -> TraceProfile {
let metrics : Array[ProfileMetric] = []
for signal in trace.signals {
let summary = stats(trace, signal.id)
metrics.push({
signal_id: signal.id,
kind: TransitionCount,
value: None,
count: Some(summary.changes),
unit: "changes",
quality: profile_quality(summary.changes),
})
metrics.push({
signal_id: signal.id,
kind: RisingCount,
value: None,
count: Some(summary.rising_edges),
unit: "edges",
quality: 100,
})
metrics.push({
signal_id: signal.id,
kind: FallingCount,
value: None,
count: Some(summary.falling_edges),
unit: "edges",
quality: 100,
})
metrics.push({
signal_id: signal.id,
kind: UnknownCount,
value: None,
count: Some(summary.unknown_values),
unit: "samples",
quality: if summary.changes == 0 {
0
} else {
100
},
})
metrics.push({
signal_id: signal.id,
kind: DutyCycle,
value: duty_cycle(trace, signal.id),
count: None,
unit: "ratio",
quality: if trace.end_time > 0L {
100
} else {
0
},
})
let frequency = frequency_estimate(trace, signal.id)
metrics.push({
signal_id: signal.id,
kind: Frequency,
value: frequency.frequency,
count: None,
unit: "1/time",
quality: if frequency.period_count > 0 {
100
} else {
0
},
})
metrics.push({
signal_id: signal.id,
kind: Entropy,
value: Some(value_entropy(trace, signal.id)),
count: None,
unit: "bits",
quality: 100,
})
metrics.push({
signal_id: signal.id,
kind: MeanChangeInterval,
value: mean_change_interval(trace, signal.id),
count: None,
unit: "time",
quality: if summary.changes > 1 {
100
} else {
0
},
})
}
let quality = if metrics.length() == 0 {
0
} else {
metrics.fold(init=0, fn(sum, item) { sum + item.quality }) /
metrics.length()
}
{
trace_end: trace.end_time,
signal_count: trace.signals.length(),
metrics,
quality,
}
}
///|
/// Return metrics for one signal.
pub fn TraceProfile::for_signal(
self : TraceProfile,
signal_id : String,
) -> Array[ProfileMetric] {
self.metrics.filter(fn(item) { item.signal_id == signal_id })
}
///|
/// Return a metric by signal and kind.
pub fn TraceProfile::metric(
self : TraceProfile,
signal_id : String,
kind : ProfileMetricKind,
) -> ProfileMetric? {
for item in self.metrics {
if item.signal_id == signal_id && item.kind == kind {
return Some(item)
}
}
None
}
///|
/// Export profile metrics as CSV.
pub fn profile_metrics_csv(metrics : Array[ProfileMetric]) -> String {
let out = StringBuilder()
out.write_string("signal,metric,value,count,unit,quality\n")
for item in metrics {
out.write_string(
escape_text(item.signal_id) +
"," +
profile_metric_name(item.kind) +
"," +
profile_optional_double(item.value) +
"," +
profile_optional_int(item.count) +
"," +
item.unit +
"," +
item.quality.to_string() +
"\n",
)
}
out.to_string()
}
///|
/// Render profile summary.
pub fn TraceProfile::to_text(self : TraceProfile) -> String {
"signals=" +
self.signal_count.to_string() +
" metrics=" +
self.metrics.length().to_string() +
" quality=" +
self.quality.to_string()
}
///|
/// Render profile metrics as JSON.
pub fn TraceProfile::to_json(self : TraceProfile) -> String {
"{\"trace_end\":" +
self.trace_end.to_string() +
",\"signals\":" +
self.signal_count.to_string() +
",\"quality\":" +
self.quality.to_string() +
",\"metrics\":[" +
self.metrics.map(profile_metric_json).join(",") +
"]}"
}
///|
/// Assign a data-quality score to a change count.
fn profile_quality(changes : Int) -> Int {
if changes == 0 {
0
} else {
100
}
}
///|
/// Name a profile metric.
pub fn profile_metric_name(kind : ProfileMetricKind) -> String {
match kind {
TransitionCount => "transition-count"
RisingCount => "rising-count"
FallingCount => "falling-count"
UnknownCount => "unknown-count"
DutyCycle => "duty-cycle"
Frequency => "frequency"
Entropy => "entropy"
MeanChangeInterval => "mean-change-interval"
}
}
///|
/// Encode an optional profile number.
fn profile_optional_double(value : Double?) -> String {
match value {
Some(item) => item.to_string()
None => ""
}
}
///|
/// Encode an optional profile count.
fn profile_optional_int(value : Int?) -> String {
match value {
Some(item) => item.to_string()
None => ""
}
}
///|
/// Encode a profile metric.
fn profile_metric_json(item : ProfileMetric) -> String {
"{\"signal\":\"" +
escape_text(item.signal_id) +
"\",\"metric\":\"" +
profile_metric_name(item.kind) +
"\",\"value\":" +
profile_optional_json_double(item.value) +
",\"count\":" +
profile_optional_json_int(item.count) +
",\"quality\":" +
item.quality.to_string() +
"}"
}
///|
/// Encode an optional profile number as JSON.
fn profile_optional_json_double(value : Double?) -> String {
match value {
Some(item) => item.to_string()
None => "null"
}
}
///|
/// Encode an optional profile count as JSON.
fn profile_optional_json_int(value : Int?) -> String {
match value {
Some(item) => item.to_string()
None => "null"
}
}