1use num_traits::float::FloatCore;
2use vqm::{Matrix3x3f32, Vector3, Vector3f32};
3
4pub type Anchor3df32 = Anchor3d<f32>;
6pub type Anchor3df64 = Anchor3d<f64>;
8
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct Anchor3d<T> {
13 pub pos: Vector3<T>,
14 pub distance: T,
15 pub weight: T,
17}
18
19impl<T> Default for Anchor3d<T>
20where
21 T: FloatCore,
22{
23 fn default() -> Self {
24 Self::new(T::zero(), T::zero(), T::zero())
25 }
26}
27
28impl<T> Anchor3d<T>
29where
30 T: FloatCore,
31{
32 #[must_use]
34 pub fn new(x: T, y: T, z: T) -> Self {
35 Anchor3d { pos: Vector3 { x, y, z }, distance: T::zero(), weight: T::one() }
36 }
37}
38
39#[must_use]
41pub fn trilaterate_3d_weighted(anchors: &[Anchor3df32]) -> Option<Vector3f32> {
42 if anchors.len() < 4 {
43 return None;
44 }
45
46 let mut base_idx = 0;
49 let mut min_distance = anchors[0].distance;
50 let mut max_weight = anchors[0].weight;
51
52 for (ii, anchor) in anchors.iter().enumerate().skip(1) {
53 #[allow(clippy::float_cmp)]
54 if anchor.weight > max_weight || (anchor.weight == max_weight && anchor.distance < min_distance) {
55 max_weight = anchor.weight;
56 min_distance = anchor.distance;
57 base_idx = ii;
58 }
59 }
60 if max_weight < f32::EPSILON {
62 return None;
63 }
64
65 let base = anchors[base_idx];
66 let base_norm_squared = base.pos.norm_squared();
67 let base_distance_squared = base.distance * base.distance;
68
69 let mut atwa_00 = 0.0;
78 let mut atwa_01 = 0.0;
79 let mut atwa_02 = 0.0;
80 let mut atwa_11 = 0.0;
81 let mut atwa_12 = 0.0;
82 let mut atwa_22 = 0.0;
83
84 let mut atwb = Vector3f32::default();
86
87 for (ii, anchor) in anchors.iter().enumerate() {
89 if ii == base_idx {
90 continue;
91 }
92
93 let weight = anchor.weight * base.weight;
95 if weight < f32::EPSILON {
96 continue;
97 }
98
99 let a_row = (base.pos - anchor.pos) * 2.0;
101 atwa_00 += a_row.x * a_row.x * weight;
103 atwa_01 += a_row.x * a_row.y * weight;
104 atwa_02 += a_row.x * a_row.z * weight;
105 atwa_11 += a_row.y * a_row.y * weight;
106 atwa_12 += a_row.y * a_row.z * weight;
107 atwa_22 += a_row.z * a_row.z * weight;
108
109 let b_row = ((anchor.distance * anchor.distance - base_distance_squared)
111 - (anchor.pos.norm_squared() - base_norm_squared))
112 * weight;
113 atwb += a_row * b_row;
115 }
116
117 let m = Matrix3x3f32::new([
118 atwa_00, atwa_01, atwa_02, atwa_01, atwa_11, atwa_12, atwa_02, atwa_12, atwa_22, ]);
122
123 let (adjugate, determinant) = m.adjugate_symmetric();
124 if determinant.abs() < f32::EPSILON { None } else { Some((adjugate * atwb) / determinant) }
125}
126
127#[cfg(test)]
128mod tests {
129 #![allow(clippy::expect_used)]
130 use super::*;
131
132 fn calculate_distances(target: Vector3f32, anchors: &mut [Anchor3df32]) {
134 for anchor in anchors.iter_mut() {
135 anchor.distance = anchor.pos.distance(target);
136 }
137 }
138
139 fn _is_normal<T: Sized + Send + Sync + Unpin>() {}
140 fn is_full<T: Sized + Send + Sync + Unpin + Copy + Clone + Default + PartialEq>() {}
141
142 #[test]
143 fn normal_types() {
144 is_full::<Anchor3df32>();
145 }
146
147 #[test]
148 fn test_perfect_geometric_data() {
149 let position = Vector3f32::new(4.5, 2.1, 7.8);
151
152 let mut anchors = [
154 Anchor3df32::new(0.0, 0.0, 0.0),
155 Anchor3df32::new(10.0, 0.0, 2.0),
156 Anchor3df32::new(0.0, 12.0, 1.0),
157 Anchor3df32::new(2.0, 1.0, 15.0),
158 Anchor3df32::new(8.0, 9.0, 5.0),
159 ];
160
161 calculate_distances(position, &mut anchors);
162
163 let result = trilaterate_3d_weighted(&anchors).expect("Should successfully calculate coordinate");
164
165 let epsilon = 0.007;
167 assert!((result.x - position.x).abs() < epsilon, "X mismatch: expected {}, got {}", position.x, result.x);
168 assert!((result.y - position.y).abs() < epsilon, "Y mismatch: expected {}, got {}", position.y, result.y);
169 assert!((result.z - position.z).abs() < epsilon, "Z mismatch: expected {}, got {}", position.z, result.z);
170 }
171
172 #[test]
173 fn test_insufficient_anchors_fails() {
174 let anchors = [
176 Anchor3df32 { pos: Vector3f32 { x: 0.0, y: 0.0, z: 0.0 }, distance: 5.0, weight: 1.0 },
177 Anchor3df32 { pos: Vector3f32 { x: 10.0, y: 0.0, z: 0.0 }, distance: 5.0, weight: 1.0 },
178 Anchor3df32 { pos: Vector3f32 { x: 0.0, y: 10.0, z: 0.0 }, distance: 5.0, weight: 1.0 },
179 ];
180
181 let result = trilaterate_3d_weighted(&anchors);
182 assert!(result.is_none(), "Should fail with fewer than 4 anchors");
183 }
184
185 #[test]
186 fn test_outlier_suppression_via_weights() {
187 let position = Vector3f32::new(5.0, 5.0, 5.0);
188
189 let mut anchors = [
190 Anchor3df32 { pos: Vector3f32 { x: 0.0, y: 0.0, z: 0.0 }, distance: 0.0, weight: 1.0 },
191 Anchor3df32 { pos: Vector3f32 { x: 10.0, y: 0.0, z: 2.0 }, distance: 0.0, weight: 1.0 },
192 Anchor3df32 { pos: Vector3f32 { x: 0.0, y: 10.0, z: 1.0 }, distance: 0.0, weight: 1.0 },
193 Anchor3df32 { pos: Vector3f32 { x: 2.0, y: 1.0, z: 10.0 }, distance: 0.0, weight: 1.0 },
194 Anchor3df32 { pos: Vector3f32 { x: 8.0, y: 8.0, z: 8.0 }, distance: 0.0, weight: 0.001 },
196 ];
197
198 calculate_distances(position, &mut anchors);
199
200 anchors[4].distance += 15.0;
202
203 let result = trilaterate_3d_weighted(&anchors).expect("Should compute coordinate despite noise");
204
205 assert!((result.x - position.x).abs() < 0.05, "X deviated too far due to outlier: {}", result.x);
207 assert!((result.y - position.y).abs() < 0.05, "Y deviated too far due to outlier: {}", result.y);
208 assert!((result.z - position.z).abs() < 0.05, "Z deviated too far due to outlier: {}", result.z);
209 }
210
211 #[test]
212 fn test_coplanar_anchors_fail() {
213 let anchors = [
216 Anchor3df32 { pos: Vector3f32 { x: 0.0, y: 0.0, z: 3.0 }, distance: 5.0, weight: 1.0 },
217 Anchor3df32 { pos: Vector3f32 { x: 10.0, y: 0.0, z: 3.0 }, distance: 5.0, weight: 1.0 },
218 Anchor3df32 { pos: Vector3f32 { x: 0.0, y: 10.0, z: 3.0 }, distance: 5.0, weight: 1.0 },
219 Anchor3df32 { pos: Vector3f32 { x: 10.0, y: 10.0, z: 3.0 }, distance: 5.0, weight: 1.0 },
220 ];
221
222 let result = trilaterate_3d_weighted(&anchors);
223 assert!(result.is_none(), "Matrix inversion should fail when anchors are flatly coplanar");
224 }
225
226 #[test]
227 fn test_zero_weight_anchors_ignored() {
228 let position = Vector3f32::new(3.0, 3.0, 3.0);
229
230 let mut anchors = [
231 Anchor3df32 { pos: Vector3f32 { x: 0.0, y: 0.0, z: 0.0 }, distance: 0.0, weight: 1.0 },
232 Anchor3df32 { pos: Vector3f32 { x: 6.0, y: 0.0, z: 1.0 }, distance: 0.0, weight: 1.0 },
233 Anchor3df32 { pos: Vector3f32 { x: 0.0, y: 6.0, z: 2.0 }, distance: 0.0, weight: 1.0 },
234 Anchor3df32 { pos: Vector3f32 { x: 2.0, y: 1.0, z: 6.0 }, distance: 0.0, weight: 1.0 },
235 Anchor3df32 { pos: Vector3f32 { x: 99.0, y: 99.0, z: 99.0 }, distance: 999.0, weight: 0.0 },
237 ];
238
239 calculate_distances(position, &mut anchors);
240 anchors[4].distance = 999.0;
242
243 let result = trilaterate_3d_weighted(&anchors).expect("Should compute coordinate");
244 let epsilon = 0.002;
245 assert!((result.x - position.x).abs() < epsilon);
246 assert!((result.y - position.y).abs() < epsilon);
247 assert!((result.z - position.z).abs() < epsilon);
248 }
249}