1use super::*;
2
3struct UnionFind {
9 parent: Vec<usize>,
10}
11
12impl UnionFind {
13 fn new(n: usize) -> Self {
14 Self {
15 parent: (0..n).collect(),
16 }
17 }
18
19 fn find(&mut self, x: usize) -> usize {
20 let mut root = x;
21 while self.parent[root] != root {
22 root = self.parent[root];
23 }
24 let mut cur = x;
25 while self.parent[cur] != root {
26 let next = self.parent[cur];
27 self.parent[cur] = root;
28 cur = next;
29 }
30 root
31 }
32
33 fn union(&mut self, a: usize, b: usize) {
34 let (ra, rb) = (self.find(a), self.find(b));
35 if ra != rb {
36 let (lo, hi) = if ra < rb { (ra, rb) } else { (rb, ra) };
37 self.parent[hi] = lo;
38 }
39 }
40}
41
42fn mate_bodies(mate: &AssemblyMate) -> impl Iterator<Item = usize> {
45 let (a, b) = (mate.body_a, mate.body_b);
46 let third = mate.body_c.filter(|&c| c != a && c != b);
47 [a, b].into_iter().chain(third)
48}
49
50struct DecompOutcome {
51 steps: Vec<SolveStepReport>,
52 run: LmRun,
53}
54
55fn solve_decomposed(
59 bodies: &[AssemblyBody],
60 mates: &[AssemblyMate],
61 prep: &Prepared,
62 poses: &mut Vec<Pose>,
63 max_iterations: usize,
64) -> Result<DecompOutcome, String> {
65 let nb = bodies.len();
66 let mut solved: Vec<bool> = bodies.iter().map(|b| b.fixed).collect();
67
68 let mut mate_atoms: Vec<Vec<usize>> = vec![Vec::new(); mates.len()];
70 for (ai, &mi) in prep.atom_mate.iter().enumerate() {
71 mate_atoms[mi].push(ai);
72 }
73 let mut incident: Vec<Vec<usize>> = vec![Vec::new(); nb];
74 for (mi, mate) in mates.iter().enumerate() {
75 for body in mate_bodies(mate) {
76 incident[body].push(mi);
77 }
78 }
79
80 let mut nodes: Vec<Option<Vec<usize>>> = Vec::new();
83 let mut node_of: Vec<Option<usize>> = vec![None; nb];
84 for &b in &prep.movable {
85 node_of[b] = Some(nodes.len());
86 nodes.push(Some(vec![b]));
87 }
88
89 let ids = |members: &[usize]| -> Vec<String> {
90 members.iter().map(|&b| bodies[b].id.clone()).collect()
91 };
92 let atoms_of = |mate_set: &[usize]| -> Vec<Atom> {
93 mate_set
94 .iter()
95 .flat_map(|&mi| mate_atoms[mi].iter().map(|&ai| prep.atoms[ai].clone()))
96 .collect()
97 };
98
99 let mut steps: Vec<SolveStepReport> = Vec::new();
100 let mut run = LmRun::default();
101
102 loop {
103 let mut peeled = false;
113 loop {
114 let mut pick: Option<(usize, Vec<usize>)> = None;
115 for (ni, node) in nodes.iter().enumerate() {
116 let Some(members) = node else { continue };
117 let mut to_solved: Vec<usize> = Vec::new();
119 let mut deferred = false;
120 for &b in members {
124 for &mi in &incident[b] {
125 let mut outside = mate_bodies(&mates[mi]).filter(|&other| node_of[other] != Some(ni)).peekable();
126 if outside.peek().is_none() {
127 continue; }
129 if outside.all(|other| solved[other]) {
130 to_solved.push(mi);
131 } else {
132 deferred = true;
133 }
134 }
135 }
136 to_solved.sort_unstable();
137 to_solved.dedup();
138 let ready = if !deferred {
139 true
140 } else if to_solved.is_empty() {
141 false
142 } else {
143 let atoms = atoms_of(&to_solved);
145 let m: usize = atoms.iter().map(Atom::rows).sum();
146 let groups = vec![SolveGroup {
147 members: members.clone(),
148 }];
149 let jac = finite_diff_jacobian(
150 &atoms,
151 poses,
152 &groups,
153 &column_steps(1, prep.scale),
154 m,
155 );
156 run.rows += 2 * 6 * m;
157 numerical_rank(jac, m, 6) == 6
158 };
159 if ready {
160 pick = Some((ni, to_solved));
161 break;
162 }
163 }
164 let Some((ni, mate_set)) = pick else { break };
165 let members = nodes[ni].take().expect("picked node is live");
166 let atoms = atoms_of(&mate_set);
167 let (method, iterations) = if atoms.is_empty() {
168 ("unconstrained", 0)
169 } else {
170 let groups = vec![SolveGroup {
171 members: members.clone(),
172 }];
173 let sub = lm_core(
174 &atoms,
175 poses,
176 &groups,
177 prep.scale,
178 prep.tight,
179 max_iterations,
180 prep.linear,
181 )?;
182 let iters = sub.iterations;
183 run.absorb(sub);
184 let method = if members.len() == 1 {
185 "sequential"
186 } else {
187 "cluster_sequential"
188 };
189 (method, iters)
190 };
191 steps.push(SolveStepReport {
192 bodies: ids(&members),
193 method: method.into(),
194 iterations,
195 });
196 for &b in &members {
197 solved[b] = true;
198 node_of[b] = None;
199 }
200 peeled = true;
201 }
202 if nodes.iter().all(|n| n.is_none()) {
203 break; }
205
206 let mut contracted = false;
214 let lone = |b: usize, nodes: &[Option<Vec<usize>>], node_of: &[Option<usize>]| -> bool {
215 node_of[b]
216 .and_then(|ni| nodes[ni].as_ref())
217 .is_some_and(|members| members.len() == 1)
218 };
219 let mut pair_mates: std::collections::BTreeMap<(usize, usize), Vec<usize>> =
220 std::collections::BTreeMap::new();
221 for (mi, mate) in mates.iter().enumerate() {
222 let (a, b) = (mate.body_a, mate.body_b);
223 if mate_bodies(mate).count() > 2 {
225 continue;
226 }
227 if !solved[a] && !solved[b] && lone(a, &nodes, &node_of) && lone(b, &nodes, &node_of) {
228 pair_mates.entry((a.min(b), a.max(b))).or_default().push(mi);
229 }
230 }
231 let mut uf = UnionFind::new(nb);
232 let mut any_binding = false;
233 for (&(a, b), pm) in &pair_mates {
234 let atoms = atoms_of(pm);
235 let m: usize = atoms.iter().map(Atom::rows).sum();
236 let groups = singleton_groups(&[b]);
237 let jac = finite_diff_jacobian(&atoms, poses, &groups, &column_steps(1, prep.scale), m);
238 run.rows += 2 * 6 * m;
239 if numerical_rank(jac, m, 6) == 6 {
240 uf.union(a, b);
241 any_binding = true;
242 }
243 }
244 if any_binding {
245 let mut components: std::collections::BTreeMap<usize, Vec<usize>> =
246 std::collections::BTreeMap::new();
247 for &(a, b) in pair_mates.keys() {
248 for body in [a, b] {
249 let root = uf.find(body);
250 let entry = components.entry(root).or_default();
251 if !entry.contains(&body) {
252 entry.push(body);
253 }
254 }
255 }
256 for (_, mut comp) in components {
257 if comp.len() < 2 {
258 continue;
259 }
260 comp.sort_unstable();
261 let saved: Vec<Pose> = comp.iter().map(|&b| poses[b]).collect();
263 let mut internal: Vec<usize> = comp
264 .iter()
265 .flat_map(|&b| incident[b].iter().copied())
266 .filter(|&mi| mate_bodies(&mates[mi]).all(|body| comp.contains(&body)))
267 .collect();
268 internal.sort_unstable();
269 internal.dedup();
270 let atoms = atoms_of(&internal);
271 let groups = singleton_groups(&comp[1..]);
272 let sub = lm_core(
273 &atoms,
274 poses,
275 &groups,
276 prep.scale,
277 prep.tight,
278 max_iterations,
279 prep.linear,
280 )?;
281 let sub_iters = sub.iterations;
282 run.absorb(sub);
283 let m_int: usize = atoms.iter().map(Atom::rows).sum();
285 let all_groups = singleton_groups(&comp);
286 let mut scratch: Vec<Pose> = Vec::new();
287 let mut r_int: Vec<f64> = Vec::new();
288 eval_residuals(&atoms, poses, &[], &[], &mut scratch, &mut r_int);
289 let max_int = r_int.iter().fold(0.0f64, |acc, &x| acc.max(x.abs()));
290 let jac = finite_diff_jacobian(
291 &atoms,
292 poses,
293 &all_groups,
294 &column_steps(comp.len(), prep.scale),
295 m_int,
296 );
297 let rank = numerical_rank(jac, m_int, 6 * comp.len());
298 run.rows += m_int + 2 * 6 * comp.len() * m_int;
299 if max_int <= prep.tight && rank == 6 * (comp.len() - 1) {
300 steps.push(SolveStepReport {
301 bodies: ids(&comp),
302 method: "cluster_internal".into(),
303 iterations: sub_iters,
304 });
305 let cluster = nodes.len();
307 for &b in &comp {
308 if let Some(old) = node_of[b] {
309 nodes[old] = None;
310 }
311 node_of[b] = Some(cluster);
312 }
313 nodes.push(Some(comp));
314 contracted = true;
315 } else {
316 for (&b, pose) in comp.iter().zip(&saved) {
318 poses[b] = *pose;
319 }
320 }
321 }
322 }
323 if peeled || contracted {
324 continue;
325 }
326
327 let remaining: Vec<usize> = (0..nb).filter(|&b| node_of[b].is_some()).collect();
332 let mate_set: Vec<usize> = (0..mates.len())
333 .filter(|&mi| mate_bodies(&mates[mi]).any(|body| !solved[body]))
334 .collect();
335 let atoms = atoms_of(&mate_set);
336 let groups = singleton_groups(&remaining);
337 let sub = lm_core(
338 &atoms,
339 poses,
340 &groups,
341 prep.scale,
342 prep.tight,
343 max_iterations,
344 prep.linear,
345 )?;
346 let iters = sub.iterations;
347 run.absorb(sub);
348 steps.push(SolveStepReport {
349 bodies: ids(&remaining),
350 method: "monolithic_fallback".into(),
351 iterations: iters,
352 });
353 for node in nodes.iter_mut() {
354 *node = None;
355 }
356 for &b in &remaining {
357 solved[b] = true;
358 node_of[b] = None;
359 }
360 break;
361 }
362
363 Ok(DecompOutcome { steps, run })
364}
365
366pub fn solve_assembly(
381 bodies: &[AssemblyBody],
382 mates: &[AssemblyMate],
383 options: &AssemblySolveOptions,
384) -> Result<AssemblySolution, String> {
385 let result = solve_with(bodies, mates, options);
386 if cfg!(test) && options.linear_algebra == LinearAlgebra::Sparse && std::env::var_os("BREP_ASSEMBLY_AB").is_some() {
390 let dense = solve_with(bodies, mates, &AssemblySolveOptions { linear_algebra: LinearAlgebra::Dense, ..*options });
391 if let Err(difference) = compare_solutions(&result, &dense) {
392 panic!("sparse/dense A/B: {difference}");
393 }
394 }
395 result
396}
397
398fn compare_solutions(
404 sparse: &Result<AssemblySolution, String>,
405 dense: &Result<AssemblySolution, String>,
406) -> Result<(), String> {
407 match (sparse, dense) {
408 (Err(_), Err(_)) => Ok(()),
409 (Ok(_), Err(error)) => Err(format!("sparse solved, dense failed: {error}")),
410 (Err(error), Ok(_)) => Err(format!("dense solved, sparse failed: {error}")),
411 (Ok(a), Ok(b)) => {
412 for (pa, pb) in a.poses.iter().zip(&b.poses) {
413 let dt = (0..3).map(|i| (pa.translation[i] - pb.translation[i]).abs()).fold(0.0, f64::max);
414 let (qa, qb) = (pa.rotation, pb.rotation);
416 let rel = quat_mul(qa, [qb[0], -qb[1], -qb[2], -qb[3]]);
417 let angle = 2.0 * (rel[1] * rel[1] + rel[2] * rel[2] + rel[3] * rel[3]).sqrt();
418 let bar = if a.dof > 0 { 1e-6 } else { 1e-9 };
419 if dt > bar || angle > bar {
420 return Err(format!("body {}: translation {dt:.3e}, rotation {angle:.3e} (dof {})", pa.id, a.dof));
421 }
422 if dt > 1e-9 || angle > 1e-9 {
423 eprintln!("AB-DRIFT body {} dof {}: translation {dt:.3e}, rotation {angle:.3e}", pa.id, a.dof);
424 }
425 }
426 if a.iterations != b.iterations && a.status != b.status {
427 return Err(format!(
428 "iterations {} vs {} and status {} vs {}",
429 a.iterations, b.iterations, a.status, b.status
430 ));
431 }
432 Ok(())
433 }
434 }
435}
436
437fn solve_with(
438 bodies: &[AssemblyBody],
439 mates: &[AssemblyMate],
440 options: &AssemblySolveOptions,
441) -> Result<AssemblySolution, String> {
442 if !(options.tolerance.is_finite() && options.tolerance > 0.0) {
443 return Err("options.tolerance must be finite and positive".into());
444 }
445 let prep = prepare(bodies, mates, options)?;
446 let movable_ids: Vec<String> = prep.movable.iter().map(|&b| bodies[b].id.clone()).collect();
447
448 let run_monolithic = |poses: &mut Vec<Pose>| -> Result<LmRun, String> {
449 let groups = singleton_groups(&prep.movable);
450 lm_core(
451 &prep.atoms,
452 poses,
453 &groups,
454 prep.scale,
455 prep.tight,
456 options.max_iterations,
457 prep.linear,
458 )
459 };
460
461 if options.strategy == SolveStrategy::Monolithic {
465 let mut poses = prep.base.clone();
466 let run = run_monolithic(&mut poses)?;
467 let steps = vec![SolveStepReport {
468 bodies: movable_ids,
469 method: "monolithic".into(),
470 iterations: run.iterations,
471 }];
472 return finalize(bodies, mates, &prep, &poses, run, "monolithic", steps);
473 }
474
475 let mut poses = prep.base.clone();
477 let outcome = solve_decomposed(bodies, mates, &prep, &mut poses, options.max_iterations)?;
478 let mut run = outcome.run;
479
480 let mut scratch: Vec<Pose> = Vec::new();
481 let mut r: Vec<f64> = Vec::new();
482 eval_residuals(&prep.atoms, &poses, &[], &[], &mut scratch, &mut r);
483 run.rows += prep.m;
484 let max_res = r.iter().fold(0.0f64, |acc, &x| acc.max(x.abs()));
485 if max_res.is_finite() && max_res <= prep.tight {
486 return finalize(
487 bodies,
488 mates,
489 &prep,
490 &poses,
491 run,
492 "decomposed",
493 outcome.steps,
494 );
495 }
496
497 let mut poses = prep.base.clone();
503 let sub = run_monolithic(&mut poses)?;
504 let iters = sub.iterations;
505 run.absorb(sub);
506 let mut method = "monolithic_fallback";
507 if let Some((flipped, sub)) = flip_restart(mates, &prep, &poses, &run_monolithic) {
508 run.absorb(sub);
509 poses = flipped;
510 method = "flip_restart";
511 }
512 let steps = vec![SolveStepReport {
513 bodies: movable_ids,
514 method: method.into(),
515 iterations: iters,
516 }];
517 finalize(bodies, mates, &prep, &poses, run, method, steps)
518}
519
520const FLIP_RESTART_BODIES: usize = 4;
522const FLIP_RESTART_MAX_MOVABLE: usize = 64;
525
526fn flip_restart(
540 mates: &[AssemblyMate],
541 prep: &Prepared,
542 solved: &[Pose],
543 run_monolithic: &dyn Fn(&mut Vec<Pose>) -> Result<LmRun, String>,
544) -> Option<(Vec<Pose>, LmRun)> {
545 if prep.movable.len() > FLIP_RESTART_MAX_MOVABLE {
546 return None;
547 }
548 let residuals = |poses: &[Pose]| -> Vec<f64> {
549 let (mut scratch, mut r) = (Vec::new(), Vec::new());
550 eval_residuals(&prep.atoms, poses, &[], &[], &mut scratch, &mut r);
551 r
552 };
553 let converged = |poses: &[Pose]| residuals(poses).iter().all(|x| x.is_finite() && x.abs() <= prep.tight);
554 let r = residuals(solved);
555 let mut per_mate = vec![0.0f64; mates.len()];
556 for (row, &mi) in prep.row_mate.iter().enumerate() {
557 per_mate[mi] = per_mate[mi].max(r[row].abs());
558 }
559 let mut failing: Vec<usize> = (0..mates.len()).filter(|&mi| per_mate[mi] > prep.tight).collect();
560 if failing.is_empty() {
561 return None;
562 }
563 failing.sort_by(|&a, &b| per_mate[b].total_cmp(&per_mate[a]));
564 let mut candidates: Vec<usize> = Vec::new();
565 for &mi in &failing {
566 for body in mate_bodies(&mates[mi]) {
567 if prep.movable.contains(&body) && !candidates.contains(&body) {
568 candidates.push(body);
569 }
570 }
571 }
572 candidates.truncate(FLIP_RESTART_BODIES);
573 let moved = |poses: &[Pose]| -> f64 {
574 poses.iter().zip(&prep.base).map(|(p, b)| p.t.sub(b.t).length()).sum()
575 };
576 for body in candidates {
577 let mut best: Option<(f64, Vec<Pose>, LmRun)> = None;
578 let nudge = 1e-3 * prep.scale.max(1e-9);
579 let starts = [
580 ([0.0, 1.0, 0.0, 0.0], [0.0; 3]),
581 ([0.0, 0.0, 1.0, 0.0], [0.0; 3]),
582 ([0.0, 0.0, 0.0, 1.0], [0.0; 3]),
583 ([1.0, 0.0, 0.0, 0.0], [nudge, 0.0, 0.0]),
584 ([1.0, 0.0, 0.0, 0.0], [0.0, nudge, 0.0]),
585 ([1.0, 0.0, 0.0, 0.0], [0.0, 0.0, nudge]),
586 ];
587 for (turn, shift) in starts {
588 let mut poses = prep.base.clone();
589 poses[body].q = quat_mul(turn, poses[body].q);
590 poses[body].t = poses[body].t.add(crate::Vec3::new(shift[0], shift[1], shift[2]));
591 let Ok(run) = run_monolithic(&mut poses) else { continue };
592 if converged(&poses) {
593 let cost = moved(&poses);
594 if best.as_ref().is_none_or(|(c, _, _)| cost < *c - 1e-9) {
595 best = Some((cost, poses, run));
596 }
597 }
598 }
599 if let Some((_, poses, run)) = best {
600 return Some((poses, run));
601 }
602 }
603 None
604}