1use nalgebra::{Vector3, Vector6};
21use rayon::prelude::*;
22use rigidity_core::PointCloud;
23use rigidity_core::icp::{IcpConfig, Kernel, register, surface};
24use rigidity_core::lie::Se3;
25use rigidity_core::normals::estimate_normals;
26use rigidity_core::observability::{
27 Conditioning, Correspondence, Observability, ObservabilityCriteria, analyse,
28};
29use rigidity_scenes::rng::Rng;
30use rigidity_scenes::{Scene, SceneKind, SceneParams};
31use rigidity_spatial::KdTree;
32
33#[derive(Debug, Clone, Copy)]
35pub struct TrialConfig {
36 pub trials: usize,
38 pub points_per_face: usize,
40 pub scale: f64,
42 pub noise_sigma: f64,
44 pub tolerance: f64,
46 pub initial_translation: f64,
48 pub initial_rotation: f64,
50 pub estimated_normals: bool,
52 pub seed: u64,
54}
55
56impl Default for TrialConfig {
57 fn default() -> Self {
58 Self {
59 trials: 1_000,
60 points_per_face: 800,
61 scale: 1.0,
62 noise_sigma: 1e-3,
63 tolerance: 1e-4,
64 initial_translation: 0.02,
65 initial_rotation: 0.01,
66 estimated_normals: false,
67 seed: 0x7E57_5EED,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy)]
74pub struct DirectionOutcome {
75 pub index: usize,
77 pub predicted: f64,
79 pub empirical: f64,
81 pub bias: f64,
83 pub observability: Observability,
85}
86
87impl DirectionOutcome {
88 pub fn ratio(&self) -> f64 {
94 self.empirical / self.predicted
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct SceneOutcome {
101 pub kind: SceneKind,
103 pub estimated_normals: bool,
105 pub converged: usize,
107 pub trials: usize,
109 pub condition_number: f64,
111 pub directions: Vec<DirectionOutcome>,
113}
114
115fn random_direction(rng: &mut Rng) -> Vector3<f64> {
117 loop {
118 let candidate = Vector3::new(rng.normal(1.0), rng.normal(1.0), rng.normal(1.0));
119 if candidate.norm() > 1e-9 {
120 return candidate.normalize();
121 }
122 }
123}
124
125fn build_normals(
126 cloud: &PointCloud,
127 analytic: &[Vector3<f64>],
128 estimate: bool,
129) -> Vec<Vector3<f64>> {
130 if estimate {
131 let tree = KdTree::build(cloud).expect("the tree builds");
132 estimate_normals(cloud, &tree, 16)
133 } else {
134 analytic.to_vec()
135 }
136}
137
138pub fn run(kind: SceneKind, config: &TrialConfig) -> SceneOutcome {
140 let scene = Scene::generate(
141 kind,
142 SceneParams {
143 points_per_face: config.points_per_face,
144 scale: config.scale,
145 ..SceneParams::default()
146 },
147 );
148
149 let target_normals = build_normals(&scene.cloud, &scene.normals, config.estimated_normals);
150 let tree = KdTree::build(&scene.cloud).expect("the tree builds");
151
152 let prediction = analyse(scene.len(), Kernel::Squared, |index| {
155 Some(Correspondence {
156 point: scene.cloud.point(index),
157 normal: target_normals[index],
158 residual: 0.0,
159 })
160 })
161 .expect("the scene is non-empty");
162 let conditioning: &Conditioning = &prediction.conditioning;
163
164 let criteria = ObservabilityCriteria {
165 noise_sigma: config.noise_sigma,
166 tolerance: config.tolerance,
167 };
168 let states = conditioning.classify(&criteria);
169 let predicted = conditioning.uncertainty(config.noise_sigma);
170
171 let truth = Se3::exp(&Vector6::new(
172 0.031 * config.scale,
173 -0.017 * config.scale,
174 0.024 * config.scale,
175 0.021,
176 -0.013,
177 0.018,
178 ));
179 let icp = IcpConfig {
180 kernel: Kernel::Squared,
181 max_correspondence_distance: 0.5 * config.scale,
182 max_iterations: 60,
183 ..IcpConfig::default()
184 };
185
186 let outcomes: Vec<Option<([f64; 6], bool)>> = (0..config.trials)
187 .into_par_iter()
188 .map(|trial| {
189 let mut rng = Rng::new(config.seed ^ (trial as u64).wrapping_mul(0x9E37_79B9));
190
191 let inverse = truth.inverse();
194 let rotation = *inverse.rotation().matrix();
195 let mut source = PointCloud::with_capacity(scene.len());
196 for index in 0..scene.len() {
197 let jitter = Vector3::new(
198 rng.normal(config.noise_sigma),
199 rng.normal(config.noise_sigma),
200 rng.normal(config.noise_sigma),
201 );
202 source.push(inverse.transform_point(&(scene.points[index] + jitter)));
203 }
204 let source_normals = if config.estimated_normals {
205 let source_tree = KdTree::build(&source).ok()?;
206 estimate_normals(&source, &source_tree, 16)
207 } else {
208 scene.normals.iter().map(|n| rotation * n).collect()
209 };
210
211 let offset = random_direction(&mut rng) * config.initial_translation * config.scale;
212 let turn = random_direction(&mut rng) * config.initial_rotation;
213 let start = Se3::exp(&Vector6::new(
214 offset.x, offset.y, offset.z, turn.x, turn.y, turn.z,
215 )) * truth;
216
217 let result = register(
218 &surface(&source, &source_normals),
219 &surface(&scene.cloud, &target_normals),
220 &tree,
221 start,
222 &icp,
223 );
224
225 let error = (truth * result.pose.inverse()).log();
227 let mut components = [0.0f64; 6];
228 for (index, slot) in components.iter_mut().enumerate() {
229 *slot = conditioning.component(index, error);
230 }
231 if components.iter().any(|v| !v.is_finite()) {
232 return None;
233 }
234 Some((components, result.converged))
235 })
236 .collect();
237
238 let samples: Vec<[f64; 6]> = outcomes.iter().flatten().map(|(c, _)| *c).collect();
239 let converged = outcomes.iter().flatten().filter(|(_, ok)| *ok).count();
240 let count = samples.len().max(1) as f64;
241
242 let directions = (0..6)
243 .map(|index| {
244 let mean: f64 = samples.iter().map(|c| c[index]).sum::<f64>() / count;
245 let variance: f64 = samples
246 .iter()
247 .map(|c| (c[index] - mean) * (c[index] - mean))
248 .sum::<f64>()
249 / count;
250 DirectionOutcome {
251 index,
252 predicted: predicted[index],
253 empirical: variance.sqrt(),
254 bias: mean,
255 observability: states[index],
256 }
257 })
258 .collect();
259
260 SceneOutcome {
261 kind,
262 estimated_normals: config.estimated_normals,
263 converged,
264 trials: samples.len(),
265 condition_number: conditioning.condition_number(),
266 directions,
267 }
268}
269
270pub fn run_all(config: &TrialConfig) -> Vec<SceneOutcome> {
272 SceneKind::ALL
273 .iter()
274 .map(|kind| run(*kind, config))
275 .collect()
276}