///|
/// A timestamped action that can be applied to a linear filter. Keeping the
/// action separate from the filter makes offline reproduction and incident
/// investigation deterministic: the same event stream produces the same
/// state stream.
pub(all) enum ReplayEvent {
Predict(Int)
Measure(Int, Array[Double])
Missing(Int)
Restore(FilterCheckpoint)
} derive(Debug)
///|
/// One durable observation of a replay step.
pub struct ReplayRecord {
timestamp : Int
result : UpdateResult
state : Array[Double]
covariance : Matrix
nis : Double
} derive(Debug)
///|
pub fn ReplayRecord::new(
timestamp : Int,
result : UpdateResult,
state : Array[Double],
covariance : Matrix,
nis : Double,
) -> ReplayRecord {
{ timestamp, result, state: state.copy(), covariance: covariance.copy(), nis }
}
///|
pub fn ReplayRecord::timestamp(self : ReplayRecord) -> Int {
self.timestamp
}
///|
pub fn ReplayRecord::result(self : ReplayRecord) -> UpdateResult {
self.result
}
///|
pub fn ReplayRecord::state(self : ReplayRecord) -> Array[Double] {
self.state.copy()
}
///|
pub fn ReplayRecord::covariance(self : ReplayRecord) -> Matrix {
self.covariance.copy()
}
///|
pub fn ReplayRecord::nis(self : ReplayRecord) -> Double {
self.nis
}
///|
/// Counts and records accumulated by a replay session.
pub struct ReplayReport {
steps : Int
accepted : Int
rejected : Int
missing : Int
invalid : Int
final_timestamp : Int
final_state : Array[Double]
final_covariance : Matrix
} derive(Debug)
///|
pub fn ReplayReport::steps(self : ReplayReport) -> Int {
self.steps
}
///|
pub fn ReplayReport::accepted(self : ReplayReport) -> Int {
self.accepted
}
///|
pub fn ReplayReport::rejected(self : ReplayReport) -> Int {
self.rejected
}
///|
pub fn ReplayReport::missing(self : ReplayReport) -> Int {
self.missing
}
///|
pub fn ReplayReport::invalid(self : ReplayReport) -> Int {
self.invalid
}
///|
pub fn ReplayReport::final_timestamp(self : ReplayReport) -> Int {
self.final_timestamp
}
///|
pub fn ReplayReport::final_state(self : ReplayReport) -> Array[Double] {
self.final_state.copy()
}
///|
pub fn ReplayReport::final_covariance(self : ReplayReport) -> Matrix {
self.final_covariance.copy()
}
///|
/// An append-only, bounded replay ledger. The bounded mode is useful for
/// embedded applications where a diagnostic trail must not grow forever.
pub struct ReplayTrace {
mut records : Array[ReplayRecord]
capacity : Int
}
///|
pub fn ReplayTrace::new(capacity : Int) -> ReplayTrace {
{ records: [], capacity: if capacity < 0 { 0 } else { capacity } }
}
///|
pub fn ReplayTrace::length(self : ReplayTrace) -> Int {
self.records.length()
}
///|
pub fn ReplayTrace::capacity(self : ReplayTrace) -> Int {
self.capacity
}
///|
pub fn ReplayTrace::records(self : ReplayTrace) -> Array[ReplayRecord] {
self.records.copy()
}
///|
fn ReplayTrace::append(self : ReplayTrace, record : ReplayRecord) -> Unit {
if self.capacity == 0 {
return
}
if self.records.length() < self.capacity {
self.records.push(record)
return
}
for i in 1.. ReplayRecord? {
if self.records.length() == 0 {
None
} else {
Some(self.records[0])
}
}
///|
pub fn ReplayTrace::last(self : ReplayTrace) -> ReplayRecord? {
if self.records.length() == 0 {
None
} else {
Some(self.records[self.records.length() - 1])
}
}
///|
pub fn ReplayTrace::clear(self : ReplayTrace) -> Unit {
self.records = []
}
///|
/// Stateful replay runner for `KalmanND`. A runner owns its filter so callers
/// can replay a log without mutating the production filter used elsewhere.
pub struct ReplaySession {
filter : KalmanND
trace : ReplayTrace
mut timestamp : Int
mut steps : Int
mut accepted : Int
mut rejected : Int
mut missing : Int
mut invalid : Int
}
///|
pub fn ReplaySession::new(
filter : KalmanND,
trace_capacity : Int,
) -> ReplaySession {
{
filter,
trace: ReplayTrace::new(trace_capacity),
timestamp: 0,
steps: 0,
accepted: 0,
rejected: 0,
missing: 0,
invalid: 0,
}
}
///|
pub fn ReplaySession::step(
self : ReplaySession,
event : ReplayEvent,
) -> UpdateResult {
let mut timestamp = self.timestamp
let result = match event {
Predict(time) => {
timestamp = time
self.filter.predict()
Accepted
}
Measure(time, values) => {
timestamp = time
self.filter.update(values)
}
Missing(time) => {
timestamp = time
self.filter.update_missing()
}
Restore(checkpoint) => {
timestamp = checkpoint.timestamp()
if self.filter.restore_checkpoint(checkpoint) {
Accepted
} else {
InvalidMeasurement
}
}
}
self.timestamp = timestamp
self.steps = self.steps + 1
match result {
Accepted => self.accepted = self.accepted + 1
RejectedByGate => self.rejected = self.rejected + 1
MissingMeasurement => self.missing = self.missing + 1
InvalidMeasurement | SingularInnovation => self.invalid = self.invalid + 1
}
let record = ReplayRecord::new(
self.timestamp,
result,
self.filter.state(),
self.filter.covariance(),
self.filter.normalized_innovation_squared(),
)
self.trace.append(record)
result
}
///|
pub fn ReplaySession::run(
self : ReplaySession,
events : Array[ReplayEvent],
) -> Array[UpdateResult] {
let results : Array[UpdateResult] = []
for event in events {
results.push(self.step(event))
}
results
}
///|
pub fn ReplaySession::filter(self : ReplaySession) -> KalmanND {
self.filter
}
///|
pub fn ReplaySession::trace(self : ReplaySession) -> ReplayTrace {
self.trace
}
///|
pub fn ReplaySession::timestamp(self : ReplaySession) -> Int {
self.timestamp
}
///|
pub fn ReplaySession::steps(self : ReplaySession) -> Int {
self.steps
}
///|
pub fn ReplaySession::accepted(self : ReplaySession) -> Int {
self.accepted
}
///|
pub fn ReplaySession::rejected(self : ReplaySession) -> Int {
self.rejected
}
///|
pub fn ReplaySession::missing(self : ReplaySession) -> Int {
self.missing
}
///|
pub fn ReplaySession::invalid(self : ReplaySession) -> Int {
self.invalid
}
///|
pub fn ReplaySession::reset(self : ReplaySession) -> Unit {
self.filter.reset()
self.trace.clear()
self.timestamp = 0
self.steps = 0
self.accepted = 0
self.rejected = 0
self.missing = 0
self.invalid = 0
}
///|
pub fn ReplaySession::report(self : ReplaySession) -> ReplayReport {
{
steps: self.steps,
accepted: self.accepted,
rejected: self.rejected,
missing: self.missing,
invalid: self.invalid,
final_timestamp: self.timestamp,
final_state: self.filter.state(),
final_covariance: self.filter.covariance(),
}
}
///|
/// Make a reproducible sequence of scalar observations with periodic drops.
/// This is intentionally small and deterministic so it is useful in examples,
/// acceptance tests, and regression tests without a random dependency.
pub fn make_replay_events(
start_timestamp : Int,
count : Int,
first_value : Double,
velocity : Double,
missing_period : Int,
) -> Array[ReplayEvent] {
let events : Array[ReplayEvent] = []
let safe_count = if count < 0 { 0 } else { count }
let safe_period = if missing_period < 0 { 0 } else { missing_period }
for index in 0.. Bool {
result is Accepted
}
///|
pub fn replay_result_is_data_loss(result : UpdateResult) -> Bool {
match result {
MissingMeasurement | InvalidMeasurement => true
Accepted | RejectedByGate | SingularInnovation => false
}
}