Coverage Report

Created: 2026-05-16 06:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/hifitime-4.3.0/src/polynomial.rs
Line
Count
Source
1
//! Polynomial Duration wrapper used in interpolation processes
2
use crate::Duration;
3
4
use core::fmt;
5
6
#[cfg(not(feature = "std"))]
7
use num_traits::Float;
8
9
#[cfg(doc)]
10
use crate::TimeScale;
11
12
#[cfg(feature = "serde")]
13
use serde_derive::{Deserialize, Serialize};
14
15
#[cfg(feature = "python")]
16
use pyo3::prelude::*;
17
#[cfg(feature = "python")]
18
use pyo3::types::PyType;
19
20
/// Interpolation [Polynomial] used for example in [TimeScale]
21
/// maintenance, precise monitoring or conversions.
22
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
23
#[cfg_attr(feature = "python", pyclass)]
24
#[cfg_attr(feature = "python", pyo3(module = "hifitime"))]
25
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26
pub struct Polynomial {
27
    /// Constant offset [Duration], regardless of the interpolation interval
28
    pub constant: Duration,
29
    /// Rate or drift in seconds per second (s.s⁻¹) expressed as [Duration] for convenience.
30
    /// It is a linear scaling factor of the interpolation interval.
31
    pub rate: Duration,
32
    /// Acceleration or drift change in s.s⁻², expressed as [Duration] for convenience.
33
    /// It is a quadratic scaling of the interpolation interval.
34
    pub accel: Duration,
35
}
36
37
impl From<(f64, f64, f64)> for Polynomial {
38
    /// Converts (f64, f64, f64) triplet, oftentimes
39
    /// noted (a0, a1, a2) as (offset (s), drift (s.s⁻¹), drift change (s.s⁻²))
40
    /// to [Polynomial] structure, that allows precise [TimeScale] translation.
41
0
    fn from(triplet: (f64, f64, f64)) -> Self {
42
0
        Self {
43
0
            constant: Duration::from_seconds(triplet.0),
44
0
            rate: Duration::from_seconds(triplet.1),
45
0
            accel: Duration::from_seconds(triplet.2),
46
0
        }
47
0
    }
48
}
49
50
impl From<Polynomial> for (f64, f64, f64) {
51
    /// Converts [Polynomial] to (f64, f64, f64) triplet, oftentimes
52
    /// noted (a0, a1, a2) as (offset (s), drift (s.s⁻¹), drift change (s.s⁻²)).
53
0
    fn from(polynomial: Polynomial) -> Self {
54
0
        (
55
0
            polynomial.constant.to_seconds(),
56
0
            polynomial.rate.to_seconds(),
57
0
            polynomial.accel.to_seconds(),
58
0
        )
59
0
    }
60
}
61
62
#[cfg_attr(feature = "python", pymethods)]
63
impl Polynomial {
64
    /// Calculate the correction (as [Duration] once again) from [Self] and given
65
    /// the interpolation time interval
66
    /// :type time_interval: Duration
67
    /// :rtype: Duration
68
0
    pub fn correction_duration(&self, time_interval: Duration) -> Duration {
69
0
        let dt_s = time_interval.to_seconds();
70
0
        let (a0, a1, a2) = (
71
0
            self.constant.to_seconds(),
72
0
            self.rate.to_seconds(),
73
0
            self.accel.to_seconds(),
74
0
        );
75
0
        Duration::from_seconds(a0 + a1 * dt_s + a2 * dt_s.powi(2))
76
0
    }
77
}
78
79
impl Polynomial {
80
    /// Create a [Polynomial] structure that is only made of a static offset
81
0
    pub fn from_constant_offset(constant: Duration) -> Self {
82
0
        Self {
83
0
            constant,
84
0
            rate: Default::default(),
85
0
            accel: Default::default(),
86
0
        }
87
0
    }
88
89
    /// Create a [Polynomial] structure from a static offset expressed in nanoseconds
90
0
    pub fn from_constant_offset_nanoseconds(nanos: f64) -> Self {
91
0
        Self {
92
0
            constant: Duration::from_nanoseconds(nanos),
93
0
            rate: Default::default(),
94
0
            accel: Default::default(),
95
0
        }
96
0
    }
97
98
    /// Create a [Polynomial] structure from both static offset and rate of change:
99
0
    pub fn from_offset_and_rate(constant: Duration, rate: Duration) -> Self {
100
0
        Self {
101
0
            constant,
102
0
            rate,
103
0
            accel: Default::default(),
104
0
        }
105
0
    }
106
107
    /// Create a [Polynomial] structure from a static offset and drift,
108
    /// in nanoseconds and nanoseconds.s⁻¹
109
0
    pub fn from_offset_rate_nanoseconds(offset_ns: f64, drift_ns_s: f64) -> Self {
110
0
        Self {
111
0
            constant: Duration::from_nanoseconds(offset_ns),
112
0
            rate: Duration::from_nanoseconds(drift_ns_s),
113
0
            accel: Default::default(),
114
0
        }
115
0
    }
116
}
117
118
impl fmt::Display for Polynomial {
119
0
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120
0
        write!(
121
0
            f,
122
0
            "offset={} rate={} accel={}",
123
            self.constant, self.rate, self.accel
124
        )
125
0
    }
126
}
127
128
#[cfg(feature = "python")]
129
#[cfg_attr(feature = "python", pymethods)]
130
impl Polynomial {
131
    /// Create a [Polynomial] structure that is only made of a static offset
132
    /// :type constant: Duration
133
    /// :rtype: Polynomial
134
    #[pyo3(name = "from_constant_offset")]
135
    #[classmethod]
136
    pub fn py_from_constant_offset(_cls: &Bound<'_, PyType>, constant: Duration) -> Self {
137
        Self {
138
            constant,
139
            rate: Default::default(),
140
            accel: Default::default(),
141
        }
142
    }
143
144
    /// Create a [Polynomial] structure from a static offset expressed in nanoseconds
145
    /// :type nanos: float
146
    /// :rtype: Polynomial
147
    #[pyo3(name = "from_constant_offset_nanoseconds")]
148
    #[classmethod]
149
    pub fn py_from_constant_offset_nanoseconds(_cls: &Bound<'_, PyType>, nanos: f64) -> Self {
150
        Self {
151
            constant: Duration::from_nanoseconds(nanos),
152
            rate: Default::default(),
153
            accel: Default::default(),
154
        }
155
    }
156
157
    /// Create a [Polynomial] structure from both static offset and rate of change:
158
    /// :type constant: Duration
159
    /// :type rate: Duration
160
    /// :rtype: Polynomial
161
    #[pyo3(name = "from_offset_and_rate")]
162
    #[classmethod]
163
    pub fn py_from_offset_and_rate(
164
        _cls: &Bound<'_, PyType>,
165
        constant: Duration,
166
        rate: Duration,
167
    ) -> Self {
168
        Self {
169
            constant,
170
            rate,
171
            accel: Default::default(),
172
        }
173
    }
174
175
    /// Create a [Polynomial] structure from a static offset and drift, in nanoseconds and nanoseconds.s⁻¹
176
    /// :type offset_ns: float
177
    /// :type drift_ns_s: float
178
    /// :rtype: Polynomial
179
    #[pyo3(name = "from_offset_rate_nanoseconds")]
180
    #[classmethod]
181
    pub fn py_from_offset_rate_nanoseconds(
182
        _cls: &Bound<'_, PyType>,
183
        offset_ns: f64,
184
        drift_ns_s: f64,
185
    ) -> Self {
186
        Self {
187
            constant: Duration::from_nanoseconds(offset_ns),
188
            rate: Duration::from_nanoseconds(drift_ns_s),
189
            accel: Default::default(),
190
        }
191
    }
192
193
    #[cfg(feature = "python")]
194
    fn __str__(&self) -> String {
195
        format!("{self}")
196
    }
197
198
    #[cfg(feature = "python")]
199
    fn __eq__(&self, other: Self) -> bool {
200
        *self == other
201
    }
202
}
203
204
#[cfg(kani)]
205
mod kani_verif {
206
    use super::*;
207
208
    /// Verifies Polynomial::correction_duration does not panic and returns
209
    /// a normalized Duration.
210
    /// Note: proof_for_contract fails with "return_value is not assignable"
211
    /// error, likely a Kani limitation with pymethods impl blocks.
212
    /// Using standalone proof with explicit assertion instead.
213
    #[kani::proof]
214
    #[kani::stub_verified(Duration::from_seconds)]
215
    fn verify_polynomial_correction_duration() {
216
        let constant: Duration = kani::any();
217
        let rate: Duration = kani::any();
218
        let accel: Duration = kani::any();
219
        let interval: Duration = kani::any();
220
        // Verification budget constraints (not function preconditions):
221
        // Restrict to durations within ±1 century (~31.5 years) to ensure
222
        // the polynomial evaluation a0 + a1*dt + a2*dt^2 stays finite.
223
        // The function is correct for all Duration inputs, but without
224
        // stub_verified(to_seconds), CBMC explores paths where to_seconds
225
        // produces values that overflow the polynomial.
226
        kani::assume(constant.to_parts().0 > -1 && constant.to_parts().0 < 1);
227
        kani::assume(rate.to_parts().0 > -1 && rate.to_parts().0 < 1);
228
        kani::assume(accel.to_parts().0 > -1 && accel.to_parts().0 < 1);
229
        kani::assume(interval.to_parts().0 > -1 && interval.to_parts().0 < 1);
230
        let poly = Polynomial {
231
            constant,
232
            rate,
233
            accel,
234
        };
235
        let result = poly.correction_duration(interval);
236
        let (c, n) = result.to_parts();
237
        assert!(
238
            n < crate::NANOSECONDS_PER_CENTURY
239
                || (c == i16::MAX && n == crate::NANOSECONDS_PER_CENTURY)
240
                || (c == i16::MIN && n == 0)
241
        );
242
    }
243
}