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
120fn gain(sample_time: f64, cutoff_frequency: f64) -> f64 {
122 sample_time / (sample_time + (1.0 / (2.0 * std::f64::consts::PI * cutoff_frequency)))
123}
124
125fn quaternion_from_matrix(m: &Matrix3<f64>) -> Quaternion<f64> {
126 *UnitQuaternion::from_rotation_matrix(&Rotation3::from_matrix_unchecked(*m)).quaternion()
127}
128
129fn slerp(from: &Quaternion<f64>, t: f64, to: &Quaternion<f64>) -> Quaternion<f64> {
131 const ONE: f64 = 1.0 - f64::EPSILON;
132 let d = from.dot(to);
133 let abs_d = d.abs();
134 let (scale0, mut scale1) = if abs_d >= ONE {
135 (1.0 - t, t)
136 } else {
137 let theta = abs_d.acos();
138 let sin_theta = theta.sin();
139 (
140 ((1.0 - t) * theta).sin() / sin_theta,
141 (t * theta).sin() / sin_theta,
142 )
143 };
144 if d < 0.0 {
145 scale1 = -scale1;
146 }
147 from * scale0 + to * scale1
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::math_utils::differentiate_one_sample_pose as differentiate_one_sample;
154
155 fn pose(linear_row_major: [f64; 9], translation: [f64; 3]) -> [f64; 16] {
157 let mut out = [0.0; 16];
158 for row in 0..3 {
159 for col in 0..3 {
160 out[col * 4 + row] = linear_row_major[row * 3 + col];
161 }
162 }
163 out[12] = translation[0];
164 out[13] = translation[1];
165 out[14] = translation[2];
166 out[15] = 1.0;
167 out
168 }
169
170 #[test]
172 fn keeps_value_if_no_change() {
173 for cutoff in [100.0, 500.0, 1000.0] {
174 let filtered = low_pass_filter(0.001, 1.0, 1.0, cutoff).unwrap();
175 assert!((filtered - 1.0).abs() < 1e-6, "cutoff {cutoff}: {filtered}");
176 }
177 }
178
179 #[test]
181 fn does_filter() {
182 for (cutoff, expected) in [(100.0, 0.3859), (500.0, 0.7585), (900.0, 0.8497)] {
183 let filtered = low_pass_filter(0.001, 1.0, 0.0, cutoff).unwrap();
184 assert!(
185 (filtered - expected).abs() < 1e-4,
186 "cutoff {cutoff}: {filtered} vs {expected}"
187 );
188 }
189 }
190
191 #[test]
193 fn can_fix_non_orthonormal_rotation() {
194 let pose1 = pose(
197 [
198 0.00462567,
199 0.999974,
200 0.00335239, 0.0145489,
202 -0.00341934,
203 0.999888, 0.999874,
205 -0.00457638,
206 -0.0145646,
207 ],
208 [1.0, 1.0, 1.0],
209 );
210 let pose2 = pose(
211 [
212 0.00463526,
213 0.999984,
214 0.00335239, 0.014549,
216 -0.00341951,
217 0.999888, 0.999883,
219 -0.00458597,
220 -0.0145646,
221 ],
222 [1.0, 1.0, 1.0],
223 );
224 let pose3 = pose(
225 [
226 0.00465436,
227 0.999984,
228 0.00335239, 0.0145489,
230 -0.00341979,
231 0.999888, 0.999883,
233 -0.00460507,
234 -0.0145646,
235 ],
236 [1.0, 1.0, 1.0],
237 );
238
239 let output1 = cartesian_low_pass_filter(0.001, &pose2, &pose1, 100.0).unwrap();
240 let output2 = cartesian_low_pass_filter(0.001, &pose3, &pose2, 100.0).unwrap();
241 let velocity1 = differentiate_one_sample(&output1, &pose1, 0.001);
242 let velocity2 = differentiate_one_sample(&output2, &pose2, 0.001);
243
244 let mut jerk = [0.0; 6];
245 for i in 0..6 {
246 let acceleration1 = velocity1[i] / 0.001;
247 let acceleration2 = (velocity2[i] - velocity1[i]) / 0.001;
248 jerk[i] = (acceleration2 - acceleration1) / 0.001;
249 }
250 let total_jerk = (jerk[3].powi(2) + jerk[4].powi(2) + jerk[5].powi(2)).sqrt();
251 assert!(total_jerk < 1000.0, "total jerk {total_jerk}");
252 }
253
254 #[test]
256 fn rejects_invalid_arguments() {
257 let message = |e: FrankaError| match e {
258 FrankaError::InvalidArgument(msg) => msg,
259 other => panic!("expected InvalidArgument, got {other:?}"),
260 };
261 let identity = pose([1., 0., 0., 0., 1., 0., 0., 0., 1.], [0., 0., 0.]);
262
263 for bad_sample_time in [-1e-3, f64::NAN, f64::INFINITY] {
264 assert_eq!(
265 message(low_pass_filter(bad_sample_time, 1.0, 0.0, 100.0).unwrap_err()),
266 "lowpass-filter: sample_time is negative, infinite or NaN."
267 );
268 assert_eq!(
269 message(
270 cartesian_low_pass_filter(bad_sample_time, &identity, &identity, 100.0)
271 .unwrap_err()
272 ),
273 "Cartesian lowpass-filter: sample_time is negative, infinite or NaN."
274 );
275 }
276 for bad_cutoff in [0.0, -1.0, f64::NAN, f64::INFINITY] {
277 assert_eq!(
278 message(low_pass_filter(0.001, 1.0, 0.0, bad_cutoff).unwrap_err()),
279 "lowpass-filter: cutoff_frequency is zero, negative, infinite or NaN."
280 );
281 assert_eq!(
282 message(
283 cartesian_low_pass_filter(0.001, &identity, &identity, bad_cutoff).unwrap_err()
284 ),
285 "Cartesian lowpass-filter: cutoff_frequency is zero, negative, infinite or NaN."
286 );
287 }
288 for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
289 assert_eq!(
290 message(low_pass_filter(0.001, bad, 0.0, 100.0).unwrap_err()),
291 "lowpass-filter: current or past input value of the signal to be filtered is \
292 infinite or NaN."
293 );
294 assert_eq!(
295 message(low_pass_filter(0.001, 1.0, bad, 100.0).unwrap_err()),
296 "lowpass-filter: current or past input value of the signal to be filtered is \
297 infinite or NaN."
298 );
299 let mut bad_pose = identity;
300 bad_pose[5] = bad;
301 assert_eq!(
302 message(cartesian_low_pass_filter(0.001, &bad_pose, &identity, 100.0).unwrap_err()),
303 "Cartesian lowpass-filter: current or past input value of the signal to be \
304 filtered is infinite or NaN."
305 );
306 assert_eq!(
307 message(cartesian_low_pass_filter(0.001, &identity, &bad_pose, 100.0).unwrap_err()),
308 "Cartesian lowpass-filter: current or past input value of the signal to be \
309 filtered is infinite or NaN."
310 );
311 }
312 }
313
314 #[test]
315 fn cartesian_filter_keeps_pose_if_no_change() {
316 let p = pose(
317 [
318 0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
321 ],
322 [0.3, -0.2, 0.5],
323 );
324 let filtered = cartesian_low_pass_filter(0.001, &p, &p, 100.0).unwrap();
325 for i in 0..16 {
326 assert!(
327 (filtered[i] - p[i]).abs() < 1e-12,
328 "element {i}: {} vs {}",
329 filtered[i],
330 p[i]
331 );
332 }
333 }
334
335 #[test]
336 fn constants_match_libfranka() {
337 assert_eq!(MAX_CUTOFF_FREQUENCY, 1000.0);
338 assert_eq!(DEFAULT_CUTOFF_FREQUENCY, 100.0);
339 }
340}