1use nalgebra::{Matrix3, Matrix4, Quaternion, Rotation3, UnitQuaternion};
6
7use crate::error::{FrankaError, FrankaResult};
8use crate::math_utils::{linear_of, orthonormalized_rotation, translation_of};
9
10pub const MAX_CUTOFF_FREQUENCY: f64 = 1000.0;
12pub const DEFAULT_CUTOFF_FREQUENCY: f64 = 100.0;
14
15pub fn low_pass_filter(
29 sample_time: f64,
30 y: f64,
31 y_last: f64,
32 cutoff_frequency: f64,
33) -> FrankaResult<f64> {
34 if sample_time < 0.0 || !sample_time.is_finite() {
35 return Err(FrankaError::InvalidArgument(
36 "lowpass-filter: sample_time is negative, infinite or NaN.".to_string(),
37 ));
38 }
39 if cutoff_frequency <= 0.0 || !cutoff_frequency.is_finite() {
40 return Err(FrankaError::InvalidArgument(
41 "lowpass-filter: cutoff_frequency is zero, negative, infinite or NaN.".to_string(),
42 ));
43 }
44 if !y.is_finite() || !y_last.is_finite() {
45 return Err(FrankaError::InvalidArgument(
46 "lowpass-filter: current or past input value of the signal to be filtered is infinite \
47 or NaN."
48 .to_string(),
49 ));
50 }
51 let gain = gain(sample_time, cutoff_frequency);
52 Ok(gain * y + (1.0 - gain) * y_last)
53}
54
55pub fn cartesian_low_pass_filter(
66 sample_time: f64,
67 y: &[f64; 16],
68 y_last: &[f64; 16],
69 cutoff_frequency: f64,
70) -> FrankaResult<[f64; 16]> {
71 if sample_time < 0.0 || !sample_time.is_finite() {
72 return Err(FrankaError::InvalidArgument(
73 "Cartesian lowpass-filter: sample_time is negative, infinite or NaN.".to_string(),
74 ));
75 }
76 if cutoff_frequency <= 0.0 || !cutoff_frequency.is_finite() {
77 return Err(FrankaError::InvalidArgument(
78 "Cartesian lowpass-filter: cutoff_frequency is zero, negative, infinite or NaN."
79 .to_string(),
80 ));
81 }
82 for i in 0..16 {
83 if !y[i].is_finite() || !y_last[i].is_finite() {
84 return Err(FrankaError::InvalidArgument(
85 "Cartesian lowpass-filter: current or past input value of the signal to be \
86 filtered is infinite or NaN."
87 .to_string(),
88 ));
89 }
90 }
91
92 let transform = Matrix4::from_column_slice(y);
93 let transform_last = Matrix4::from_column_slice(y_last);
94 let orientation = quaternion_from_matrix(&orthonormalized_rotation(&linear_of(&transform)));
97 let orientation_last =
98 quaternion_from_matrix(&orthonormalized_rotation(&linear_of(&transform_last)));
99
100 let gain = gain(sample_time, cutoff_frequency);
101 let translation =
102 gain * translation_of(&transform) + (1.0 - gain) * translation_of(&transform_last);
103 let orientation = slerp(&orientation_last, gain, &orientation);
104
105 let rotation = UnitQuaternion::new_normalize(orientation).to_rotation_matrix();
106
107 let mut filtered_values = *y;
108 let rotation = rotation.matrix();
109 for col in 0..3 {
110 for row in 0..3 {
111 filtered_values[col * 4 + row] = rotation[(row, col)];
112 }
113 }
114 filtered_values[12] = translation[0];
115 filtered_values[13] = translation[1];
116 filtered_values[14] = translation[2];
117 Ok(filtered_values)
118}
119
120pub(crate) fn gain(sample_time: f64, cutoff_frequency: f64) -> f64 {
125 sample_time / (sample_time + (1.0 / (2.0 * std::f64::consts::PI * cutoff_frequency)))
126}
127
128fn quaternion_from_matrix(m: &Matrix3<f64>) -> Quaternion<f64> {
129 *UnitQuaternion::from_rotation_matrix(&Rotation3::from_matrix_unchecked(*m)).quaternion()
130}
131
132fn slerp(from: &Quaternion<f64>, t: f64, to: &Quaternion<f64>) -> Quaternion<f64> {
134 const ONE: f64 = 1.0 - f64::EPSILON;
135 let d = from.dot(to);
136 let abs_d = d.abs();
137 let (scale0, mut scale1) = if abs_d >= ONE {
138 (1.0 - t, t)
139 } else {
140 let theta = abs_d.acos();
141 let sin_theta = theta.sin();
142 (
143 ((1.0 - t) * theta).sin() / sin_theta,
144 (t * theta).sin() / sin_theta,
145 )
146 };
147 if d < 0.0 {
148 scale1 = -scale1;
149 }
150 from * scale0 + to * scale1
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::math_utils::differentiate_one_sample_pose as differentiate_one_sample;
157
158 fn pose(linear_row_major: [f64; 9], translation: [f64; 3]) -> [f64; 16] {
160 let mut out = [0.0; 16];
161 for row in 0..3 {
162 for col in 0..3 {
163 out[col * 4 + row] = linear_row_major[row * 3 + col];
164 }
165 }
166 out[12] = translation[0];
167 out[13] = translation[1];
168 out[14] = translation[2];
169 out[15] = 1.0;
170 out
171 }
172
173 #[test]
175 fn keeps_value_if_no_change() {
176 for cutoff in [100.0, 500.0, 1000.0] {
177 let filtered = low_pass_filter(0.001, 1.0, 1.0, cutoff).unwrap();
178 assert!((filtered - 1.0).abs() < 1e-6, "cutoff {cutoff}: {filtered}");
179 }
180 }
181
182 #[test]
184 fn does_filter() {
185 for (cutoff, expected) in [(100.0, 0.3859), (500.0, 0.7585), (900.0, 0.8497)] {
186 let filtered = low_pass_filter(0.001, 1.0, 0.0, cutoff).unwrap();
187 assert!(
188 (filtered - expected).abs() < 1e-4,
189 "cutoff {cutoff}: {filtered} vs {expected}"
190 );
191 }
192 }
193
194 #[test]
196 fn can_fix_non_orthonormal_rotation() {
197 let pose1 = pose(
200 [
201 0.00462567,
202 0.999974,
203 0.00335239, 0.0145489,
205 -0.00341934,
206 0.999888, 0.999874,
208 -0.00457638,
209 -0.0145646,
210 ],
211 [1.0, 1.0, 1.0],
212 );
213 let pose2 = pose(
214 [
215 0.00463526,
216 0.999984,
217 0.00335239, 0.014549,
219 -0.00341951,
220 0.999888, 0.999883,
222 -0.00458597,
223 -0.0145646,
224 ],
225 [1.0, 1.0, 1.0],
226 );
227 let pose3 = pose(
228 [
229 0.00465436,
230 0.999984,
231 0.00335239, 0.0145489,
233 -0.00341979,
234 0.999888, 0.999883,
236 -0.00460507,
237 -0.0145646,
238 ],
239 [1.0, 1.0, 1.0],
240 );
241
242 let output1 = cartesian_low_pass_filter(0.001, &pose2, &pose1, 100.0).unwrap();
243 let output2 = cartesian_low_pass_filter(0.001, &pose3, &pose2, 100.0).unwrap();
244 let velocity1 = differentiate_one_sample(&output1, &pose1, 0.001);
245 let velocity2 = differentiate_one_sample(&output2, &pose2, 0.001);
246
247 let mut jerk = [0.0; 6];
248 for i in 0..6 {
249 let acceleration1 = velocity1[i] / 0.001;
250 let acceleration2 = (velocity2[i] - velocity1[i]) / 0.001;
251 jerk[i] = (acceleration2 - acceleration1) / 0.001;
252 }
253 let total_jerk = (jerk[3].powi(2) + jerk[4].powi(2) + jerk[5].powi(2)).sqrt();
254 assert!(total_jerk < 1000.0, "total jerk {total_jerk}");
255 }
256
257 #[test]
259 fn rejects_invalid_arguments() {
260 let message = |e: FrankaError| match e {
261 FrankaError::InvalidArgument(msg) => msg,
262 other => panic!("expected InvalidArgument, got {other:?}"),
263 };
264 let identity = pose([1., 0., 0., 0., 1., 0., 0., 0., 1.], [0., 0., 0.]);
265
266 for bad_sample_time in [-1e-3, f64::NAN, f64::INFINITY] {
267 assert_eq!(
268 message(low_pass_filter(bad_sample_time, 1.0, 0.0, 100.0).unwrap_err()),
269 "lowpass-filter: sample_time is negative, infinite or NaN."
270 );
271 assert_eq!(
272 message(
273 cartesian_low_pass_filter(bad_sample_time, &identity, &identity, 100.0)
274 .unwrap_err()
275 ),
276 "Cartesian lowpass-filter: sample_time is negative, infinite or NaN."
277 );
278 }
279 for bad_cutoff in [0.0, -1.0, f64::NAN, f64::INFINITY] {
280 assert_eq!(
281 message(low_pass_filter(0.001, 1.0, 0.0, bad_cutoff).unwrap_err()),
282 "lowpass-filter: cutoff_frequency is zero, negative, infinite or NaN."
283 );
284 assert_eq!(
285 message(
286 cartesian_low_pass_filter(0.001, &identity, &identity, bad_cutoff).unwrap_err()
287 ),
288 "Cartesian lowpass-filter: cutoff_frequency is zero, negative, infinite or NaN."
289 );
290 }
291 for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
292 assert_eq!(
293 message(low_pass_filter(0.001, bad, 0.0, 100.0).unwrap_err()),
294 "lowpass-filter: current or past input value of the signal to be filtered is \
295 infinite or NaN."
296 );
297 assert_eq!(
298 message(low_pass_filter(0.001, 1.0, bad, 100.0).unwrap_err()),
299 "lowpass-filter: current or past input value of the signal to be filtered is \
300 infinite or NaN."
301 );
302 let mut bad_pose = identity;
303 bad_pose[5] = bad;
304 assert_eq!(
305 message(cartesian_low_pass_filter(0.001, &bad_pose, &identity, 100.0).unwrap_err()),
306 "Cartesian lowpass-filter: current or past input value of the signal to be \
307 filtered is infinite or NaN."
308 );
309 assert_eq!(
310 message(cartesian_low_pass_filter(0.001, &identity, &bad_pose, 100.0).unwrap_err()),
311 "Cartesian lowpass-filter: current or past input value of the signal to be \
312 filtered is infinite or NaN."
313 );
314 }
315 }
316
317 #[test]
318 fn cartesian_filter_keeps_pose_if_no_change() {
319 let p = pose(
320 [
321 0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
324 ],
325 [0.3, -0.2, 0.5],
326 );
327 let filtered = cartesian_low_pass_filter(0.001, &p, &p, 100.0).unwrap();
328 for i in 0..16 {
329 assert!(
330 (filtered[i] - p[i]).abs() < 1e-12,
331 "element {i}: {} vs {}",
332 filtered[i],
333 p[i]
334 );
335 }
336 }
337
338 #[test]
339 fn constants_match_libfranka() {
340 assert_eq!(MAX_CUTOFF_FREQUENCY, 1000.0);
341 assert_eq!(DEFAULT_CUTOFF_FREQUENCY, 100.0);
342 }
343}