///|
fn optional_nonnegative(x : Double?, maximum : Double) -> Bool {
match x {
None => true
Some(value) => finite(value) && value >= 0.0 && value <= maximum
}
}
///|
/// Validate externally assembled summaries before exporting numeric tables.
/// This checks domain and internal arithmetic, not provenance or statistical meaning.
pub fn RqaSummary::validate(self : RqaSummary) -> Result[Unit, RqaError] {
if self.recurrent < 0 ||
self.eligible < self.recurrent ||
self.eligible > 1048576 {
return Err(Invalid("summary counts"))
}
let expected = if self.eligible == 0 {
None
} else {
Some(self.recurrent.to_double() / self.eligible.to_double())
}
match (self.recurrence_rate, expected) {
(None, None) => ()
(Some(a), Some(b)) =>
if !finite(a) || (a - b).abs() > 1.0e-12 {
return Err(Invalid("summary recurrence rate"))
}
_ => return Err(Invalid("summary undefined recurrence rate"))
}
for fraction in [self.determinism, self.laminarity] {
if !finite(fraction) || fraction < 0.0 || fraction > 1.0 {
return Err(Invalid("summary line fraction"))
}
}
for entropy in [self.diagonal_entropy, self.vertical_entropy] {
if !finite(entropy) || entropy < 0.0 || entropy > @math.ln(4096.0) + 1.0e-12 {
return Err(Invalid("summary entropy"))
}
}
if self.longest_diagonal < 0 ||
self.longest_diagonal > 4096 ||
self.longest_vertical < 0 ||
self.longest_vertical > 4096 {
return Err(Invalid("summary longest line"))
}
if !optional_nonnegative(self.mean_diagonal, 4096.0) ||
!optional_nonnegative(self.trapping_time, 4096.0) ||
!optional_nonnegative(self.mean_return_gap, 4096.0) ||
!optional_nonnegative(self.divergence, 1.0) {
return Err(Invalid("summary mean or divergence"))
}
match (self.mean_diagonal, self.divergence) {
(None, None) =>
if self.longest_diagonal != 0 {
return Err(Invalid("summary missing diagonal mean"))
}
(Some(mean), Some(divergence)) =>
if self.longest_diagonal < 1 ||
mean < 1.0 ||
mean > self.longest_diagonal.to_double() ||
(divergence - 1.0 / self.longest_diagonal.to_double()).abs() > 1.0e-12 {
return Err(Invalid("summary diagonal consistency"))
}
_ => return Err(Invalid("summary diagonal presence"))
}
match self.trapping_time {
None =>
if self.longest_vertical != 0 {
return Err(Invalid("summary missing vertical mean"))
}
Some(mean) =>
if mean < 1.0 || mean > self.longest_vertical.to_double() {
return Err(Invalid("summary vertical consistency"))
}
}
if self.recurrent == 0 &&
(
self.determinism != 0.0 ||
self.laminarity != 0.0 ||
self.longest_diagonal != 0 ||
self.longest_vertical != 0
) {
return Err(Invalid("summary empty recurrence consistency"))
}
Ok(())
}