///|
/// Human- and machine-readable audit summaries for process studies.
pub(all) enum Severity {
Pass
Notice
Fail
} derive(Debug, Eq, ToJson)
///|
pub(all) struct Finding {
code : String
severity : Severity
title : String
detail : String
measured : Double
limit : Double
} derive(Debug, Eq, ToJson)
///|
pub fn Finding::pass(
code~ : String,
title~ : String,
measured~ : Double,
limit~ : Double,
detail~ : String,
) -> Finding {
{ code, severity: Pass, title, detail, measured, limit }
}
///|
pub fn Finding::notice(
code~ : String,
title~ : String,
measured~ : Double,
limit~ : Double,
detail~ : String,
) -> Finding {
{ code, severity: Notice, title, detail, measured, limit }
}
///|
pub fn Finding::fail(
code~ : String,
title~ : String,
measured~ : Double,
limit~ : Double,
detail~ : String,
) -> Finding {
{ code, severity: Fail, title, detail, measured, limit }
}
///|
pub(all) struct AuditReport {
name : String
findings : Array[Finding]
passed : Bool
score : Double
} derive(Debug, Eq, ToJson)
///|
pub fn AuditReport::new(name~ : String) -> AuditReport {
{ name, findings: [], passed: true, score: 1.0 }
}
///|
pub fn AuditReport::add(self : AuditReport, finding : Finding) -> AuditReport {
let findings = self.findings.copy()
findings.push(finding)
let mut failures = 0
let mut notices = 0
for item in findings {
match item.severity {
Pass => ()
Notice => notices += 1
Fail => failures += 1
}
}
let score = (findings.length() - failures * 2 - notices).to_double() /
findings.length().max(1).to_double()
{
name: self.name,
findings,
passed: failures == 0,
score: score.max(0.0).min(1.0),
}
}
///|
pub fn AuditReport::finding_count(
self : AuditReport,
severity : Severity,
) -> Int {
let mut count = 0
for item in self.findings {
if item.severity == severity {
count += 1
}
}
count
}
///|
pub fn AuditReport::to_csv(self : AuditReport) -> String {
let lines = ["code,severity,title,measured,limit,detail"]
for item in self.findings {
let severity = match item.severity {
Pass => "pass"
Notice => "notice"
Fail => "fail"
}
lines.push(
"\{item.code},\{severity},\{escape_csv(item.title)},\{item.measured},\{item.limit},\{escape_csv(item.detail)}",
)
}
lines.join("\n")
}
///|
pub fn AuditReport::to_markdown(self : AuditReport) -> String {
let lines = [
"# \{self.name}",
"",
"| Code | Severity | Finding | Measured | Limit |",
"|---|---|---|---:|---:|",
]
for item in self.findings {
let severity = match item.severity {
Pass => "PASS"
Notice => "NOTICE"
Fail => "FAIL"
}
lines.push(
"| \{item.code} | \{severity} | \{item.title} | \{item.measured} | \{item.limit} |",
)
}
lines.push("")
let result = if self.passed { "PASS" } else { "FAIL" }
lines.push("Result: \{result} (score=\{self.score})")
lines.join("\n")
}
///|
fn escape_csv(value : String) -> String {
if value.contains(",") || value.contains("\"") || value.contains("\n") {
"\"" + value.replace(old="\"", new="\"\"") + "\""
} else {
value
}
}
///|
pub fn balance_report(
name~ : String,
residuals : ArrayView[Double],
tolerance~ : Double,
) -> AuditReport raise @domain.ExtractionError {
@domain.require_positive("balance tolerance", tolerance)
let report = AuditReport::new(name~)
let mut result = report
for index, residual in residuals {
@domain.require_finite("balance residual", residual)
let absolute = residual.abs()
result = result.add(
if absolute <= tolerance {
Finding::pass(
code="BAL-\{index}",
title="material balance",
measured=absolute,
limit=tolerance,
detail="residual within tolerance",
)
} else {
Finding::fail(
code="BAL-\{index}",
title="material balance",
measured=absolute,
limit=tolerance,
detail="residual exceeds tolerance",
)
},
)
}
result
}
///|
/// Build a report from the analysis package's aggregate balance audit.
pub fn analysis_balance_report(
summary : @analysis.BalanceAudit,
tolerance~ : Double,
) -> AuditReport raise @domain.ExtractionError {
let report = balance_report(
name="scenario balance audit",
[summary.maximum_absolute_residual],
tolerance~,
)
if summary.passed {
report
} else {
report.add(
Finding::fail(
code="BAL-SUMMARY",
title="scenario aggregate",
measured=summary.maximum_absolute_residual,
limit=tolerance,
detail="one or more scenarios failed",
),
)
}
}