///|
/// Plan-level counts useful for effort estimation and audit dashboards.
pub(all) struct PlanMetrics {
step_count : Int
single_count : Int
repeated_count : Int
scan_count : Int
device_count : Int
device_switches : Int
estimated_samples : Int
estimated_seconds : Double
total_uncertainty : Double
average_uncertainty : Double
maximum_uncertainty : Double
minimum_uncertainty : Double
datum_count : Int
normal_count : Int
warning_count : Int
}
///|
fn metric_strategy_sample_count(strategy : Strategy) -> Int {
match strategy {
SinglePoint => 1
RepeatedPoint(count~) => if count > 0 { count } else { 0 }
Scan(count~) => if count > 0 { count } else { 0 }
}
}
///|
fn strategy_seconds(strategy : Strategy) -> Double {
match strategy {
SinglePoint => 1.0
RepeatedPoint(count~) => count.to_double() * 1.5
Scan(count~) => count.to_double() * 2.0
}
}
///|
fn unique_step_devices(steps : Array[MeasurementStep]) -> Array[String] {
let result = []
for step in steps {
let mut seen = false
for id in result {
if id == step.device_id {
seen = true
}
}
if !seen {
result.push(step.device_id)
}
}
result
}
///|
/// Calculate deterministic effort metrics for a plan.
pub fn plan_metrics(plan : MeasurementPlan) -> PlanMetrics {
let mut single_count = 0
let mut repeated_count = 0
let mut scan_count = 0
let mut estimated_samples = 0
let mut estimated_seconds = 0.0
let mut total_uncertainty = 0.0
let mut maximum_uncertainty = 0.0
let mut minimum_uncertainty = 0.0
let mut datum_count = 0
let mut normal_count = 0
let mut first = true
for step in plan.steps {
match step.strategy {
SinglePoint => single_count += 1
RepeatedPoint(_) => repeated_count += 1
Scan(_) => scan_count += 1
}
estimated_samples += metric_strategy_sample_count(step.strategy)
estimated_seconds += strategy_seconds(step.strategy)
total_uncertainty += step.uncertainty
if first {
minimum_uncertainty = step.uncertainty
maximum_uncertainty = step.uncertainty
first = false
} else {
if step.uncertainty < minimum_uncertainty {
minimum_uncertainty = step.uncertainty
}
if step.uncertainty > maximum_uncertainty {
maximum_uncertainty = step.uncertainty
}
}
if step.direction == "datum-aligned" {
datum_count += 1
} else {
normal_count += 1
}
}
let step_count = plan.steps.length()
let average_uncertainty = if step_count == 0 {
0.0
} else {
total_uncertainty / step_count.to_double()
}
{
step_count,
single_count,
repeated_count,
scan_count,
device_count: unique_step_devices(plan.steps).length(),
device_switches: device_switch_count(plan.steps),
estimated_samples,
estimated_seconds,
total_uncertainty,
average_uncertainty,
maximum_uncertainty,
minimum_uncertainty,
datum_count,
normal_count,
warning_count: plan.summary.warnings.length(),
}
}
///|
/// Return the total number of planned samples.
pub fn planned_sample_count(plan : MeasurementPlan) -> Int {
plan_metrics(plan).estimated_samples
}
///|
/// Return estimated execution time in abstract seconds.
pub fn estimated_execution_seconds(plan : MeasurementPlan) -> Double {
plan_metrics(plan).estimated_seconds
}
///|
/// Return the fraction of steps that establish datums.
pub fn datum_step_ratio(plan : MeasurementPlan) -> Double {
let metrics = plan_metrics(plan)
if metrics.step_count == 0 {
0.0
} else {
metrics.datum_count.to_double() / metrics.step_count.to_double()
}
}