blob: bcc40e78d2e9cca9aa2c594ac9a6eb49f0034cf8 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::{estimator::OSCILLATOR_ERROR_STD_DEV_PPM, time_source::Sample},
anyhow::{anyhow, Error},
fuchsia_zircon as zx,
lazy_static::lazy_static,
time_util::Transform,
};
/// One million for PPM calculations
const MILLION: u64 = 1_000_000;
lazy_static! {
/// The variance (i.e. standard deviation squared) of the system oscillator frequency error,
/// used to control the growth in uncertainty during the prediction phase.
static ref OSCILLATOR_ERROR_VARIANCE: f64 =
(OSCILLATOR_ERROR_STD_DEV_PPM as f64 / MILLION as f64).powi(2);
}
/// The minimum covariance allowed for the UTC estimate in nanoseconds squared. This helps the
/// kalman filter not drink its own bathwater after receiving very low uncertainly updates from a
/// time source (i.e. become so confident in its internal estimate that it effectively stops
/// accepting new information).
const MIN_COVARIANCE: f64 = 1e12;
/// The factor to apply to standard deviations when producing an error bound. The current setting of
/// two sigma approximately corresponds to a 95% confidence interval.
const ERROR_BOUND_FACTOR: u32 = 2;
/// Converts a zx::Duration to a floating point number of nanoseconds.
fn duration_to_f64(duration: zx::Duration) -> f64 {
duration.into_nanos() as f64
}
/// Converts a floating point number of nanoseconds to a zx::Duration.
fn f64_to_duration(float: f64) -> zx::Duration {
zx::Duration::from_nanos(float as i64)
}
/// Maintains an estimate of the offset between true UTC time and monotonic time on this
/// device, based on time samples received from one or more time sources.
///
/// The UTC estimate is implemented as a two dimensional Kalman filter where
/// state vector = [estimated_utc, estimated_frequency]
///
/// estimated_utc is maintained as f64 nanoseconds since a reference UTC (initialized as the first
/// UTC received by the filter). This keeps the absolute values and therefore the floating point
/// exponents lower than if we worked with time since UNIX epoch, so minimizes floating point
/// conversion errors. The filter can run for ~100 days from the reference point before a conversion
/// error of 1ns can occur.
///
/// estimated_frequency is considered a fixed value by the filter, i.e. has a covariance of zero
/// and an observation model term of zero.
#[derive(Debug)]
pub struct KalmanFilter {
/// A reference utc from which the estimate is maintained.
reference_utc: zx::Time,
/// The monotonic time at which the estimate applies.
monotonic: zx::Time,
/// Element 0 of the state vector, i.e. estimated utc after reference_utc, in nanoseconds.
estimate_0: f64,
/// Element 1 of the state vector, i.e. estimated oscillator frequency as a factor.
estimate_1: f64,
/// Element 0,0 of the covariance matrix, i.e. utc estimate covariance in nanoseconds squared.
/// Note 0,0 is the only non-zero element in the matrix.
covariance_00: f64,
}
impl KalmanFilter {
/// Construct a new KalmanFilter initialized to the supplied sample.
pub fn new(sample: Sample) -> Self {
let Sample { utc, monotonic, std_dev } = sample;
let covariance_00 = duration_to_f64(std_dev).powf(2.0).max(MIN_COVARIANCE);
KalmanFilter {
reference_utc: utc,
monotonic,
estimate_0: 0f64,
estimate_1: 1f64,
covariance_00,
}
}
/// Propagate the estimate forward to the requested monotonic time.
fn predict(&mut self, monotonic: zx::Time) {
let monotonic_step = duration_to_f64(monotonic - self.monotonic);
self.monotonic = monotonic;
// Estimated UTC increases by (change in monotonic time) * frequency.
self.estimate_0 += self.estimate_1 * monotonic_step;
// Estimated covariance increases as a function of the time step and oscillator error.
self.covariance_00 += monotonic_step.powf(2.0) * *OSCILLATOR_ERROR_VARIANCE;
}
/// Correct the estimate by incorporating measurement data.
fn correct(&mut self, utc: zx::Time, std_dev: zx::Duration) {
let measurement_variance = duration_to_f64(std_dev).powf(2.0);
let measurement_utc_offset = duration_to_f64(utc - self.reference_utc);
// Gain is based on the relative variance of the apriori estimate and the new measurement...
let k_0 = self.covariance_00 / (self.covariance_00 + measurement_variance);
// ...and determines how much the measurement impacts the apriori estimate...
self.estimate_0 += k_0 * (measurement_utc_offset - self.estimate_0);
// ...and how much the covariance shrinks.
self.covariance_00 = ((1f64 - k_0) * self.covariance_00).max(MIN_COVARIANCE);
}
/// Update the estimate to include the supplied sample.
pub fn update(&mut self, Sample { utc, monotonic, std_dev }: Sample) -> Result<(), Error> {
// Ignore any updates that are earlier than the current filter state. Samples from a single
// time source should arrive in order due to the validation in time_source_manager, but its
// not impossible that a backwards step occurs during a time source switch.
if monotonic < self.monotonic {
return Err(anyhow!(
"sample monotonic={} prior to previous monotonic={}",
monotonic.into_nanos(),
self.monotonic.into_nanos()
));
}
// Calculate apriori by moving the estimate forward to the measurement's monotonic time.
self.predict(monotonic);
// Then correct to aposteriori by merging in the measurement.
self.correct(utc, std_dev);
Ok(())
}
/// Returns a `Transform` describing the estimated synthetic time and error as a function
/// of the monotonic time.
pub fn transform(&self) -> Transform {
Transform {
monotonic_offset: self.monotonic.into_nanos(),
synthetic_offset: (self.reference_utc + f64_to_duration(self.estimate_0)).into_nanos(),
// TODO(jsankey): Accommodate an oscillator frequency error when implementing the
// frequency correction algorithm.
rate_adjust_ppm: 0,
error_bound_at_offset: ERROR_BOUND_FACTOR as u64 * self.covariance_00.sqrt() as u64,
error_bound_growth_ppm: ERROR_BOUND_FACTOR * OSCILLATOR_ERROR_STD_DEV_PPM as u32,
}
}
/// Returns the last updated monotonic to UTC offset.
pub fn offset(&self) -> zx::Duration {
self.reference_utc + f64_to_duration(self.estimate_0) - self.monotonic
}
/// Returns the square root of the last updated filter covariance.
pub fn sqrt_covariance(&self) -> zx::Duration {
f64_to_duration(self.covariance_00.sqrt())
}
}
#[cfg(test)]
mod test {
use {super::*, test_util::assert_near, zx::DurationNum};
const TIME_1: zx::Time = zx::Time::from_nanos(10_000_000_000);
const TIME_2: zx::Time = zx::Time::from_nanos(20_000_000_000);
const OFFSET_1: zx::Duration = zx::Duration::from_seconds(777);
const OFFSET_2: zx::Duration = zx::Duration::from_seconds(999);
const STD_DEV_1: zx::Duration = zx::Duration::from_millis(22);
const ZERO_DURATION: zx::Duration = zx::Duration::from_nanos(0);
const SQRT_COV_1: u64 = STD_DEV_1.into_nanos() as u64;
#[fuchsia::test]
fn initialize() {
let filter = KalmanFilter::new(Sample::new(TIME_1 + OFFSET_1, TIME_1, STD_DEV_1));
let transform = filter.transform();
assert_eq!(
transform,
Transform {
monotonic_offset: TIME_1.into_nanos(),
synthetic_offset: (TIME_1 + OFFSET_1).into_nanos(),
rate_adjust_ppm: 0,
error_bound_at_offset: 2 * SQRT_COV_1,
error_bound_growth_ppm: 2 * OSCILLATOR_ERROR_STD_DEV_PPM as u32,
}
);
assert_eq!(transform.synthetic(TIME_1), TIME_1 + OFFSET_1);
assert_eq!(transform.synthetic(TIME_2), TIME_2 + OFFSET_1);
assert_eq!(transform.error_bound(TIME_1), 2 * SQRT_COV_1);
// Earlier time should return same error bound.
assert_eq!(transform.error_bound(TIME_1 - 1.second()), 2 * SQRT_COV_1);
// Later time should have a higher bound.
assert_eq!(
transform.error_bound(TIME_1 + 1.second()),
2 * SQRT_COV_1 + 2000 * OSCILLATOR_ERROR_STD_DEV_PPM
);
assert_eq!(filter.offset(), OFFSET_1);
assert_eq!(filter.sqrt_covariance(), STD_DEV_1);
}
#[fuchsia::test]
fn kalman_filter_performance() {
// Note: The expected outputs for these test inputs have been validated using the time
// synchronization simulator we created during algorithm development.
let mut filter = KalmanFilter::new(Sample::new(
zx::Time::from_nanos(10001_000000000),
zx::Time::from_nanos(1_000000000),
zx::Duration::from_millis(50),
));
assert_eq!(filter.reference_utc, zx::Time::from_nanos(10001_000000000));
assert_near!(filter.estimate_0, 0f64, 1.0);
assert_near!(filter.covariance_00, 2.5e15, 1.0);
assert!(filter
.update(Sample::new(
zx::Time::from_nanos(10101_100000000),
zx::Time::from_nanos(101_000000000),
zx::Duration::from_millis(200),
))
.is_ok());
assert_near!(filter.estimate_0, 100_005887335.0, 1.0);
assert_near!(filter.covariance_00, 2.3549341505449715e15, 1.0);
assert!(filter
.update(Sample::new(
zx::Time::from_nanos(10300_900000000),
zx::Time::from_nanos(301_000000000),
zx::Duration::from_millis(100),
))
.is_ok());
assert_near!(filter.estimate_0, 299_985642106.0, 1.0);
assert_near!(filter.covariance_00, 1.9119595120463945e15, 1.0);
}
#[fuchsia::test]
fn covariance_minimum() {
let mut filter = KalmanFilter::new(Sample::new(TIME_1 + OFFSET_1, TIME_1, ZERO_DURATION));
assert_eq!(filter.covariance_00, MIN_COVARIANCE);
assert!(filter.update(Sample::new(TIME_2 + OFFSET_2, TIME_2, ZERO_DURATION)).is_ok());
assert_eq!(filter.covariance_00, MIN_COVARIANCE);
}
#[fuchsia::test]
fn earlier_monotonic_ignored() {
let mut filter = KalmanFilter::new(Sample::new(TIME_2 + OFFSET_1, TIME_2, STD_DEV_1));
assert_near!(filter.estimate_0, 0.0, 1.0);
assert!(filter.update(Sample::new(TIME_1 + OFFSET_1, TIME_1, STD_DEV_1)).is_err());
assert_near!(filter.estimate_0, 0.0, 1.0);
}
}