///|
/// One quality observation aggregated at batch level.
pub(all) struct TrendPoint {
batch_id : String
sample_count : Int
defect_count : Int
}
///|
/// Aggregate quality trend over an ordered batch sequence.
pub(all) struct QualityTrendSummary {
batch_count : Int
total_samples : Int
total_defects : Int
defect_rate : Double
minimum_rate : Double
maximum_rate : Double
average_rate : Double
}
///|
/// Construct a bounded batch-level trend point.
pub fn trend_point(
batch_id : String,
sample_count : Int,
defect_count : Int,
) -> TrendPoint {
{
batch_id,
sample_count: if sample_count > 0 {
sample_count
} else {
0
},
defect_count: if defect_count > 0 {
defect_count
} else {
0
},
}
}
///|
fn trend_rate(point : TrendPoint) -> Double {
if point.sample_count <= 0 {
0.0
} else {
let defects = if point.defect_count > point.sample_count {
point.sample_count
} else {
point.defect_count
}
defects.to_double() / point.sample_count.to_double()
}
}
///|
/// Summarize samples, defects, and per-batch rates.
pub fn summarize_quality_trend(
points : Array[TrendPoint],
) -> QualityTrendSummary {
let mut total_samples = 0
let mut total_defects = 0
let mut total_rate = 0.0
let mut minimum_rate = 0.0
let mut maximum_rate = 0.0
let mut first = true
for point in points {
total_samples += point.sample_count
total_defects += if point.defect_count > point.sample_count {
point.sample_count
} else {
point.defect_count
}
let rate = trend_rate(point)
total_rate += rate
if first {
minimum_rate = rate
maximum_rate = rate
first = false
} else {
if rate < minimum_rate {
minimum_rate = rate
}
if rate > maximum_rate {
maximum_rate = rate
}
}
}
let count = points.length()
{
batch_count: count,
total_samples,
total_defects,
defect_rate: if total_samples == 0 {
0.0
} else {
total_defects.to_double() / total_samples.to_double()
},
minimum_rate,
maximum_rate,
average_rate: if count == 0 {
0.0
} else {
total_rate / count.to_double()
},
}
}
///|
/// Classify the direction of the ordered defect-rate sequence.
pub fn trend_direction(points : Array[TrendPoint]) -> String {
if points.length() < 2 {
"stable"
} else {
let first = trend_rate(points[0])
let last = trend_rate(points[points.length() - 1])
if last - first > 0.001 {
"rising"
} else if first - last > 0.001 {
"falling"
} else {
"stable"
}
}
}
///|
/// Calculate a trailing defect rate for every point.
pub fn rolling_defect_rates(
points : Array[TrendPoint],
window : Int,
) -> Array[Double] {
let result = []
if window <= 0 {
result
} else {
for index in 0.. window { index + 1 - window } else { 0 }
let mut samples = 0
let mut defects = 0
for cursor in start..<=index {
samples += points[cursor].sample_count
defects += if points[cursor].defect_count > points[cursor].sample_count {
points[cursor].sample_count
} else {
points[cursor].defect_count
}
}
result.push(
if samples == 0 {
0.0
} else {
defects.to_double() / samples.to_double()
},
)
}
result
}
}
///|
/// Return the most recent point, when one exists.
pub fn latest_trend_point(points : Array[TrendPoint]) -> TrendPoint? {
points.get(points.length() - 1)
}
///|
/// Return whether the aggregate rate is below a configured limit.
pub fn trend_is_within_limit(
points : Array[TrendPoint],
limit : Double,
) -> Bool {
limit >= 0.0 && summarize_quality_trend(points).defect_rate <= limit
}
///|
/// Return the number of points with a non-zero defect rate.
pub fn defect_batch_count(points : Array[TrendPoint]) -> Int {
let mut result = 0
for point in points {
if trend_rate(point) > 0.0 {
result += 1
}
}
result
}
///|
/// Return a compact trend label for reports.
pub fn render_trend_summary(summary : QualityTrendSummary) -> String {
"batches=" +
summary.batch_count.to_string() +
" samples=" +
summary.total_samples.to_string() +
" defects=" +
summary.total_defects.to_string() +
" rate=" +
summary.defect_rate.to_string()
}