///|
/// Human-readable metrics are kept as typed records so dashboards and CI can
/// consume the same result without parsing log text.
pub struct MotionReport2 {
name : String
metrics : Array[Double]
budget : MotionBudget2
quality : RenderQualityMetrics2
passed : Bool
} derive(Debug)
///|
pub fn motion_report2(
name : String,
metrics : Array[Double],
budget : MotionBudget2,
quality : RenderQualityMetrics2,
) -> MotionReport2 {
{
name,
metrics: metrics.copy(),
budget,
quality,
passed: budget.accepts({
path_length: metrics.get(0).unwrap_or_default(),
peak_speed: metrics.get(1).unwrap_or_default(),
peak_acceleration: metrics.get(2).unwrap_or_default(),
average_speed: metrics.get(3).unwrap_or_default(),
settled_time: metrics.get(4).unwrap_or_default(),
samples: 0,
}) &&
quality.coverage() >= 1.0,
}
}
///|
pub fn MotionReport2::name(self : MotionReport2) -> String {
self.name
}
///|
pub fn MotionReport2::metrics(self : MotionReport2) -> Array[Double] {
self.metrics.copy()
}
///|
pub fn MotionReport2::budget(self : MotionReport2) -> MotionBudget2 {
self.budget
}
///|
pub fn MotionReport2::quality(self : MotionReport2) -> RenderQualityMetrics2 {
self.quality
}
///|
pub fn MotionReport2::passed(self : MotionReport2) -> Bool {
self.passed
}
///|
pub fn MotionReport2::score(self : MotionReport2) -> Double {
let motion_score = if self.metrics.length() == 0 {
0.0
} else {
1.0 /
(1.0 + self.metrics.fold(init=0.0, fn(sum, value) { sum + value.abs() }))
}
(motion_score + self.quality.score()) * 0.5
}
///|
pub fn MotionReport2::merge(
self : MotionReport2,
other : MotionReport2,
) -> MotionReport2 {
let metrics = self.metrics.copy()
for value in other.metrics {
metrics.push(value)
}
{
name: self.name,
metrics,
budget: self.budget,
quality: other.quality,
passed: self.passed && other.passed,
}
}
///|
pub fn MotionReport2::metric(self : MotionReport2, index : Int) -> Double {
self.metrics.get(index).unwrap_or_default()
}
///|
pub fn MotionReport2::metric_count(self : MotionReport2) -> Int {
self.metrics.length()
}
///|
pub fn MotionReport2::budget_violations(self : MotionReport2) -> Array[Double] {
let values : MotionMetrics2 = {
path_length: self.metric(0),
peak_speed: self.metric(1),
peak_acceleration: self.metric(2),
average_speed: self.metric(3),
settled_time: self.metric(4),
samples: 0,
}
self.budget.violations(values)
}
///|
pub fn MotionReport2::quality_coverage(self : MotionReport2) -> Double {
self.quality.coverage()
}
///|
pub fn MotionReport2::is_actionable(self : MotionReport2) -> Bool {
self.metric_count() >= 3 && self.quality_coverage() > 0.0
}
///|
pub fn MotionReport2::has_motion_violation(self : MotionReport2) -> Bool {
self.budget_violations().length() > 0
}
///|
pub fn MotionReport2::quality_passed(self : MotionReport2) -> Bool {
self.quality.coverage() >= 1.0 && self.quality.dropped_frames() == 0
}
///|
pub fn MotionReport2::summary_score(self : MotionReport2) -> Double {
if self.is_actionable() {
self.score()
} else {
0.0
}
}
///|
pub fn MotionReport2::meets_all_budgets(self : MotionReport2) -> Bool {
!self.has_motion_violation() && self.quality_passed()
}
///|
pub fn MotionReport2::metric_sum(self : MotionReport2) -> Double {
self.metrics.fold(init=0.0, fn(sum, value) { sum + value })
}
///|
pub fn MotionReport2::metric_average(self : MotionReport2) -> Double {
if self.metrics.length() == 0 {
0.0
} else {
self.metric_sum() / self.metrics.length().to_double()
}
}
///|
pub fn MotionReport2::quality_error(self : MotionReport2) -> Double {
self.quality.temporal_error() +
self.quality.spatial_error() +
self.quality.color_error()
}
///|
pub fn MotionReport2::needs_review(self : MotionReport2) -> Bool {
!self.passed || self.quality_error() > 0.01
}
///|
pub fn MotionReport2::status_code(self : MotionReport2) -> Int {
if self.meets_all_budgets() {
0
} else if self.needs_review() {
2
} else {
1
}
}
///|
pub fn MotionReport2::coverage_gap(self : MotionReport2) -> Double {
(1.0 - self.quality_coverage()).max(0.0)
}
///|
pub fn MotionReport2::budget_score(self : MotionReport2) -> Double {
let violations = self.budget_violations()
if violations.length() == 0 {
1.0
} else {
1.0 / (1.0 + violations.fold(init=0.0, fn(sum, value) { sum + value }))
}
}
///|
pub fn MotionReport2::weighted_score(
self : MotionReport2,
motion_weight : Double,
) -> Double {
let weight = clamp01(motion_weight)
self.budget_score() * weight + self.quality.score() * (1.0 - weight)
}
///|
pub fn MotionReport2::complete(self : MotionReport2) -> Bool {
self.status_code() == 0 && self.metric_count() > 0
}
///|
pub fn MotionReport2::error_budget(self : MotionReport2) -> Double {
self.quality_error() +
self.budget_violations().fold(init=0.0, fn(sum, value) { sum + value })
}
///|
pub fn MotionReport2::stable(self : MotionReport2) -> Bool {
self.error_budget() <= 0.001
}
///|
pub fn MotionReport2::coverage_ok(self : MotionReport2) -> Bool {
self.quality_coverage() >= 0.999
}
///|
pub fn MotionReport2::has_frames(self : MotionReport2) -> Bool {
self.quality.dropped_frames() == 0
}
///|
pub fn MotionReport2::ready_for_export(self : MotionReport2) -> Bool {
self.complete() && self.coverage_ok() && self.has_frames()
}
///|
pub fn MotionReport2::quality_score(self : MotionReport2) -> Double {
self.quality.score()
}
///|
pub fn MotionReport2::export_score(self : MotionReport2) -> Double {
self.weighted_score(0.5)
}
///|
pub fn MotionReport2::valid(self : MotionReport2) -> Bool {
self.ready_for_export()
}