///|
/// Severity attached to a machine-readable validation issue.
pub(all) enum ContractSeverity {
Info
Warning
Error
} derive(Debug, Eq)
///|
pub struct ContractIssue {
code : String
severity : ContractSeverity
message : String
} derive(Debug)
///|
pub fn ContractIssue::new(
code : String,
severity : ContractSeverity,
message : String,
) -> ContractIssue {
{ code, severity, message }
}
///|
pub fn ContractIssue::code(self : ContractIssue) -> String {
self.code
}
///|
pub fn ContractIssue::severity(self : ContractIssue) -> ContractSeverity {
self.severity
}
///|
pub fn ContractIssue::message(self : ContractIssue) -> String {
self.message
}
///|
pub struct ValidationReport {
name : String
mut checks : Int
mut passed : Int
issues : Array[ContractIssue]
}
///|
pub fn ValidationReport::new(name : String) -> ValidationReport {
{ name, checks: 0, passed: 0, issues: [] }
}
///|
pub fn ValidationReport::check(
self : ValidationReport,
code : String,
condition : Bool,
severity : ContractSeverity,
message : String,
) -> Bool {
self.checks = self.checks + 1
if condition {
self.passed = self.passed + 1
true
} else {
self.issues.push(ContractIssue::new(code, severity, message))
false
}
}
///|
pub fn ValidationReport::name(self : ValidationReport) -> String {
self.name
}
///|
pub fn ValidationReport::checks(self : ValidationReport) -> Int {
self.checks
}
///|
pub fn ValidationReport::passed(self : ValidationReport) -> Int {
self.passed
}
///|
pub fn ValidationReport::failed(self : ValidationReport) -> Int {
self.checks - self.passed
}
///|
pub fn ValidationReport::issues(
self : ValidationReport,
) -> Array[ContractIssue] {
self.issues.copy()
}
///|
pub fn ValidationReport::is_valid(self : ValidationReport) -> Bool {
self.failed() == 0
}
///|
pub fn ValidationReport::score(self : ValidationReport) -> Double {
if self.checks == 0 {
1.0
} else {
self.passed.to_double() / self.checks.to_double()
}
}
///|
pub fn ValidationReport::reset(self : ValidationReport) -> Unit {
self.checks = 0
self.passed = 0
self.issues.clear()
}
///|
pub fn validate_matrix(
name : String,
matrix : Matrix,
expected_rows : Int,
expected_cols : Int,
) -> ValidationReport {
let report = ValidationReport::new(name)
report.check(
"shape",
matrix.rows() == expected_rows && matrix.cols() == expected_cols,
Error,
"matrix shape mismatch",
)
|> ignore
report.check(
"finite",
matrix.is_finite(),
Error,
"matrix contains non-finite values",
)
|> ignore
if expected_rows == expected_cols {
report.check("square", matrix.is_square(), Error, "matrix must be square")
|> ignore
report.check(
"psd",
covariance_is_psd(matrix, 0.001),
Warning,
"matrix is not positive semidefinite",
)
|> ignore
}
report
}
///|
pub fn validate_state(
name : String,
state : Array[Double],
covariance : Matrix,
) -> ValidationReport {
let report = ValidationReport::new(name)
report.check("state_nonempty", state.length() > 0, Error, "state is empty")
|> ignore
report.check(
"state_finite",
vector_is_finite(state),
Error,
"state contains non-finite values",
)
|> ignore
report.check(
"covariance_shape",
covariance.rows() == state.length() && covariance.cols() == state.length(),
Error,
"covariance shape mismatch",
)
|> ignore
report.check(
"covariance_finite",
covariance.is_finite(),
Error,
"covariance is non-finite",
)
|> ignore
report.check(
"covariance_psd",
covariance_is_psd(covariance, 0.001),
Warning,
"covariance is not PSD",
)
|> ignore
report
}
///|
pub fn validate_packet(
name : String,
packet : ObservationPacket,
dimension : Int,
) -> ValidationReport {
let report = ValidationReport::new(name)
report.check(
"valid",
packet.is_valid(),
Error,
"packet failed finite/shape validation",
)
|> ignore
report.check(
"dimension",
packet.values().length() == dimension,
Error,
"packet dimension mismatch",
)
|> ignore
report.check(
"timestamp",
packet.timestamp() >= 0,
Warning,
"timestamp is negative",
)
|> ignore
report
}
///|
pub fn validate_sensor_configuration(
configuration : SensorConfiguration,
) -> ValidationReport {
let report = ValidationReport::new(configuration.name())
report.check(
"dimension",
configuration.dimension() > 0,
Error,
"sensor dimension must be positive",
)
|> ignore
report.check(
"period",
configuration.period() > 0,
Error,
"sensor period must be positive",
)
|> ignore
report.check(
"timeout",
configuration.timeout() >= configuration.period(),
Warning,
"sensor timeout is too short",
)
|> ignore
report.check(
"covariance",
covariance_is_psd(configuration.covariance(), 0.001),
Warning,
"sensor covariance is invalid",
)
|> ignore
report
}
///|
pub fn validate_trajectory(
name : String,
points : Array[TrajectoryPoint],
) -> ValidationReport {
let report = ValidationReport::new(name)
report.check("nonempty", points.length() > 0, Warning, "trajectory is empty")
|> ignore
let mut monotonic = true
let mut finite = true
for i, point in points {
if !point.is_valid() {
finite = false
}
if i > 0 && point.timestamp() < points[i - 1].timestamp() {
monotonic = false
}
}
report.check("finite", finite, Error, "trajectory contains an invalid point")
|> ignore
report.check(
"timestamps",
monotonic,
Error,
"trajectory timestamps are not monotonic",
)
|> ignore
report
}
///|
pub fn combine_validation_reports(
name : String,
reports : Array[ValidationReport],
) -> ValidationReport {
let result = ValidationReport::new(name)
for report in reports {
for issue in report.issues() {
result.checks = result.checks + 1
result.issues.push(issue)
}
result.checks = result.checks + report.passed()
result.passed = result.passed + report.passed()
}
result
}
///|
pub fn validation_summary(report : ValidationReport) -> String {
report.name() +
": " +
report.passed().to_string() +
"/" +
report.checks().to_string() +
" checks passed"
}
///|
pub fn validation_has_error(report : ValidationReport) -> Bool {
for issue in report.issues() {
if issue.severity() is Error {
return true
}
}
false
}
///|
pub fn validation_has_warning(report : ValidationReport) -> Bool {
for issue in report.issues() {
if issue.severity() is Warning {
return true
}
}
false
}