/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 | | } |