///|
/// Severity assigned to an automated audit finding.
pub enum AuditSeverity {
AuditInfo
AuditWarning
AuditError
AuditCritical
}
///|
/// One actionable waveform audit finding.
pub struct AuditIssue {
code : String
severity : AuditSeverity
message : String
station : String
channel : String
score_penalty : Double
}
///|
/// Audit result for one trace.
pub struct TraceAudit {
station : String
channel : String
issues : Array[AuditIssue]
score : Double
status : String
report : StationQualityReport
}
///|
/// Aggregate audit result for a network.
pub struct NetworkAudit {
traces : Int
passed : Int
failed : Int
critical_findings : Int
mean_score : Double
audits : Array[TraceAudit]
}
///|
/// Return all audit severity constructors.
pub fn supported_audit_severities() -> Array[AuditSeverity] {
[AuditInfo, AuditWarning, AuditError, AuditCritical]
}
///|
/// Return a stable severity label.
pub fn audit_severity_name(severity : AuditSeverity) -> String {
match severity {
AuditInfo => "info"
AuditWarning => "warning"
AuditError => "error"
AuditCritical => "critical"
}
}
///|
/// Create an audit issue.
pub fn make_audit_issue(
code : String,
severity : AuditSeverity,
message : String,
trace : Trace,
score_penalty : Double,
) -> AuditIssue {
{
code,
severity,
message,
station: trace.station,
channel: trace.channel,
score_penalty: score_penalty.max(0.0),
}
}
///|
/// Audit metadata, sample integrity, and strong-motion plausibility.
pub fn audit_trace(
trace : Trace,
expected_samples : Int,
full_scale : Double,
baseline : Array[Double],
) -> TraceAudit {
let quality = station_quality_report(
trace, expected_samples, full_scale, 1.0e-12, baseline,
)
let issues : Array[AuditIssue] = Array::new()
if trace.sampling_rate <= 0.0 {
issues.push(
make_audit_issue(
"META_RATE",
AuditCritical,
"sample rate is not positive",
trace,
0.5,
),
)
}
if trace.npts() == 0 {
issues.push(
make_audit_issue(
"DATA_EMPTY",
AuditCritical,
"trace has no samples",
trace,
0.6,
),
)
}
if trace.station == "" || trace.channel == "" {
issues.push(
make_audit_issue(
"META_ID",
AuditError,
"station or channel identifier is empty",
trace,
0.2,
),
)
}
if quality.completeness < 0.95 {
issues.push(
make_audit_issue(
"DATA_GAP",
AuditWarning,
"observed sample count is below expectation",
trace,
0.15,
),
)
}
if quality.clipping_fraction > 0.001 {
issues.push(
make_audit_issue(
"DATA_CLIP",
AuditError,
"full-scale clipping detected",
trace,
0.25,
),
)
}
if quality.flatline_fraction > 0.9 {
issues.push(
make_audit_issue(
"DATA_FLAT",
AuditError,
"channel is substantially flatlined",
trace,
0.3,
),
)
}
if quality.signal_to_noise_db < 3.0 {
issues.push(
make_audit_issue(
"NOISE_SNR",
AuditWarning,
"signal-to-noise ratio is low",
trace,
0.1,
),
)
}
let mut penalty = 0.0
for issue in issues {
penalty += issue.score_penalty
}
let score = (quality.score - penalty).max(0.0).min(1.0)
{
station: trace.station,
channel: trace.channel,
issues,
score,
status: quality_status(score),
report: quality,
}
}
///|
/// Audit a network of traces using a common expectation.
pub fn audit_network(
traces : Array[Trace],
expected_samples : Int,
full_scale : Double,
) -> NetworkAudit {
let audits : Array[TraceAudit] = Array::new()
for trace in traces {
audits.push(audit_trace(trace, expected_samples, full_scale, trace.data))
}
let mut passed = 0
let mut critical = 0
let mut total = 0.0
for audit in audits {
total += audit.score
if audit.score >= 0.65 {
passed += 1
}
for issue in audit.issues {
if issue.severity is AuditCritical {
critical += 1
}
}
}
{
traces: audits.length(),
passed,
failed: audits.length() - passed,
critical_findings: critical,
mean_score: if audits.is_empty() {
0.0
} else {
total / audits.length().to_double()
},
audits,
}
}
///|
/// Return a risk score that emphasizes critical findings.
pub fn network_audit_risk(audit : NetworkAudit) -> Double {
let failure_fraction = if audit.traces == 0 {
1.0
} else {
audit.failed.to_double() / audit.traces.to_double()
}
(failure_fraction * 0.6 +
audit.critical_findings.to_double() / audit.traces.max(1).to_double() * 0.4).min(
1.0,
)
}
///|
/// Export all audit issues for a review queue.
pub fn audit_issues_to_csv(audit : NetworkAudit) -> String {
let mut output = "station,channel,code,severity,message,penalty\n"
for trace_audit in audit.audits {
for issue in trace_audit.issues {
output = output +
issue.station +
"," +
issue.channel +
"," +
issue.code +
"," +
audit_severity_name(issue.severity) +
"," +
issue.message +
"," +
issue.score_penalty.to_string() +
"\n"
}
}
output
}
///|
/// Format an audit headline for the CLI.
pub fn network_audit_text(audit : NetworkAudit) -> String {
"traces=" +
audit.traces.to_string() +
" passed=" +
audit.passed.to_string() +
" failed=" +
audit.failed.to_string() +
" critical=" +
audit.critical_findings.to_string() +
" mean_score=" +
audit.mean_score.to_string() +
" risk=" +
network_audit_risk(audit).to_string()
}