///|
pub fn diagnostic_event(
code : String,
stage : String,
detail : String,
value : Double,
) -> DiagnosticEvent {
{ code, stage, detail, value }
}
///|
pub fn diagnostic_ok(stage : String, detail : String) -> PipelineDiagnostic {
{ stage, message: detail, severity: Pass }
}
///|
pub fn diagnostic_warning(
stage : String,
detail : String,
) -> PipelineDiagnostic {
{ stage, message: detail, severity: Warn }
}
///|
pub fn diagnostic_error(stage : String, detail : String) -> PipelineDiagnostic {
{ stage, message: detail, severity: Fail }
}
///|
pub fn diagnostics_for_image(image : GrayImage) -> Array[DiagnosticEvent] {
[
diagnostic_event(
"DIMENSIONS",
"image",
"width",
Double::from_int(image.width),
),
diagnostic_event(
"HEIGHT",
"image",
"height",
Double::from_int(image.height),
),
diagnostic_event("MEAN", "image", "mean intensity", image_mean(image)),
diagnostic_event(
"VARIANCE",
"image",
"intensity variance",
image_variance(image),
),
diagnostic_event("FOCUS", "image", "focus score", focus_score(image)),
diagnostic_event("NOISE", "image", "texture noise", texture_noise(image)),
]
}
///|
pub fn diagnostics_for_calibration(
fit : CalibrationFit,
) -> Array[DiagnosticEvent] {
[
diagnostic_event(
"RMS",
"calibration",
"root mean square residual",
fit.rms_error,
),
diagnostic_event("MAX", "calibration", "maximum residual", fit.max_error),
diagnostic_event(
"QUALITY",
"calibration",
"quality score",
calibration_quality_score(fit, fit.residuals.length()),
),
]
}
///|
pub fn diagnostics_for_batch(batch : BatchRun) -> Array[DiagnosticEvent] {
let summary = batch.summary()
[
diagnostic_event(
"TOTAL",
"batch",
"measurement count",
Double::from_int(summary.total),
),
diagnostic_event(
"PASS",
"batch",
"pass count",
Double::from_int(summary.pass),
),
diagnostic_event(
"WARN",
"batch",
"warning count",
Double::from_int(summary.warn),
),
diagnostic_event(
"FAIL",
"batch",
"failure count",
Double::from_int(summary.fail),
),
diagnostic_event(
"RATE",
"batch",
"acceptance rate",
summary.acceptance_rate,
),
]
}
///|
pub fn diagnostics_merge(
groups : Array[Array[DiagnosticEvent]],
) -> Array[DiagnosticEvent] {
let result : Array[DiagnosticEvent] = []
for group in groups {
result.append(group)
}
result
}
///|
pub fn diagnostics_filter(
events : Array[DiagnosticEvent],
code : String,
) -> Array[DiagnosticEvent] {
events.filter(fn(event) { event.code == code })
}
///|
pub fn diagnostics_max(events : Array[DiagnosticEvent]) -> DiagnosticEvent? {
if events.is_empty() {
None
} else {
let mut result = events[0]
for event in events[1:] {
if event.value > result.value {
result = event
}
}
Some(result)
}
}
///|
pub fn diagnostics_summary(events : Array[DiagnosticEvent]) -> StatsSummary {
describe_samples(events.map(fn(event) { event.value }))
}
///|
pub fn diagnostics_text(events : Array[DiagnosticEvent]) -> String {
let out = StringBuilder()
for event in events {
out.write_string("[")
out.write_string(event.code)
out.write_string("] ")
out.write_string(event.stage)
out.write_string(" - ")
out.write_string(event.detail)
out.write_string(": ")
out.write_string(event.value.to_string())
out.write_string("\n")
}
out.to_string()
}
///|
pub fn validate_image(image : GrayImage) -> Array[PipelineDiagnostic] {
let result : Array[PipelineDiagnostic] = []
if image.width <= 0 || image.height <= 0 {
result.push(diagnostic_error("image", "image has no pixels"))
}
if image.pixels.length() != image.width * image.height {
result.push(
diagnostic_error("image", "pixel buffer length does not match dimensions"),
)
}
for value in image.pixels {
if !is_finite(value) {
result.push(
diagnostic_error("image", "image contains a non-finite value"),
)
break
}
}
if result.is_empty() {
result.push(diagnostic_ok("image", "image validation passed"))
}
result
}
///|
pub fn validate_caliper_config(
config : CaliperConfig,
) -> Array[PipelineDiagnostic] {
let result : Array[PipelineDiagnostic] = []
if distance(config.start, config.finish) <= 0.0 {
result.push(
diagnostic_error("config", "measurement segment has zero length"),
)
}
if config.threshold < 0.0 {
result.push(diagnostic_error("config", "threshold must be non-negative"))
}
if config.expected_edges <= 0 {
result.push(
diagnostic_error("config", "expected edge count must be positive"),
)
}
if result.is_empty() {
result.push(diagnostic_ok("config", "caliper configuration passed"))
}
result
}
///|
pub fn validate_tolerance(tolerance : Tolerance) -> Array[PipelineDiagnostic] {
let result : Array[PipelineDiagnostic] = []
if tolerance.lower > tolerance.upper {
result.push(
diagnostic_error("tolerance", "lower bound exceeds upper bound"),
)
}
if tolerance.nominal < tolerance.lower || tolerance.nominal > tolerance.upper {
result.push(
diagnostic_warning("tolerance", "nominal is outside the declared bounds"),
)
}
if result.is_empty() {
result.push(diagnostic_ok("tolerance", "tolerance validation passed"))
}
result
}
///|
pub fn diagnostic_severity(
events : Array[PipelineDiagnostic],
) -> MeasurementDecision {
let mut result = Pass
for event in events {
match event.severity {
Fail => result = Fail
Warn => if result == Pass { result = Warn }
Pass => ()
}
}
result
}
///|
pub fn diagnostics_ok(events : Array[PipelineDiagnostic]) -> Bool {
diagnostic_severity(events) == Pass
}
///|
pub fn diagnostics_to_markdown(events : Array[PipelineDiagnostic]) -> String {
let out = StringBuilder()
for event in events {
out.write_string("- ")
out.write_string(decision_name(event.severity))
out.write_string(" ")
out.write_string(event.stage)
out.write_string(": ")
out.write_string(event.message)
out.write_string("\n")
}
out.to_string()
}