1use super::*;
2
3use crate::union_find::UnionFind;
8
9fn mate_bodies(mate: &AssemblyMate) -> impl Iterator<Item = usize> {
12 let (a, b) = (mate.body_a, mate.body_b);
13 let third = mate.body_c.filter(|&c| c != a && c != b);
14 [a, b].into_iter().chain(third)
15}
16
17struct DecompOutcome {
18 steps: Vec<SolveStepReport>,
19 run: LmRun,
20}
21
22fn solve_decomposed(
26 bodies: &[AssemblyBody],
27 mates: &[AssemblyMate],
28 prep: &Prepared,
29 poses: &mut Vec<Pose>,
30 max_iterations: usize,
31) -> Result<DecompOutcome, String> {
32 let nb = bodies.len();
33 let mut solved: Vec<bool> = bodies.iter().map(|b| b.fixed).collect();
34
35 let mut mate_atoms: Vec<Vec<usize>> = vec![Vec::new(); mates.len()];
37 for (ai, &mi) in prep.atom_mate.iter().enumerate() {
38 mate_atoms[mi].push(ai);
39 }
40 let mut incident: Vec<Vec<usize>> = vec![Vec::new(); nb];
41 for (mi, mate) in mates.iter().enumerate() {
42 for body in mate_bodies(mate) {
43 incident[body].push(mi);
44 }
45 }
46
47 let mut nodes: Vec<Option<Vec<usize>>> = Vec::new();
50 let mut node_of: Vec<Option<usize>> = vec![None; nb];
51 for &b in &prep.movable {
52 node_of[b] = Some(nodes.len());
53 nodes.push(Some(vec![b]));
54 }
55
56 let ids = |members: &[usize]| -> Vec<String> {
57 members.iter().map(|&b| bodies[b].id.clone()).collect()
58 };
59 let atoms_of = |mate_set: &[usize]| -> Vec<Atom> {
60 mate_set
61 .iter()
62 .flat_map(|&mi| mate_atoms[mi].iter().map(|&ai| prep.atoms[ai].clone()))
63 .collect()
64 };
65
66 let mut steps: Vec<SolveStepReport> = Vec::new();
67 let mut run = LmRun::default();
68
69 loop {
70 let mut peeled = false;
80 loop {
81 let mut pick: Option<(usize, Vec<usize>)> = None;
82 for (ni, node) in nodes.iter().enumerate() {
83 let Some(members) = node else { continue };
84 let mut to_solved: Vec<usize> = Vec::new();
86 let mut deferred = false;
87 for &b in members {
91 for &mi in &incident[b] {
92 let mut outside = mate_bodies(&mates[mi]).filter(|&other| node_of[other] != Some(ni)).peekable();
93 if outside.peek().is_none() {
94 continue; }
96 if outside.all(|other| solved[other]) {
97 to_solved.push(mi);
98 } else {
99 deferred = true;
100 }
101 }
102 }
103 to_solved.sort_unstable();
104 to_solved.dedup();
105 let ready = if !deferred {
106 true
107 } else if to_solved.is_empty() {
108 false
109 } else {
110 let atoms = atoms_of(&to_solved);
112 let m: usize = atoms.iter().map(Atom::rows).sum();
113 let groups = vec![SolveGroup {
114 members: members.clone(),
115 }];
116 let jac = finite_diff_jacobian(
117 &atoms,
118 poses,
119 &groups,
120 &column_steps(1, prep.scale),
121 m,
122 );
123 run.rows += 2 * 6 * m;
124 numerical_rank(jac, m, 6) == 6
125 };
126 if ready {
127 pick = Some((ni, to_solved));
128 break;
129 }
130 }
131 let Some((ni, mate_set)) = pick else { break };
132 let members = nodes[ni].take().expect("picked node is live");
133 let atoms = atoms_of(&mate_set);
134 let (method, iterations) = if atoms.is_empty() {
135 ("unconstrained", 0)
136 } else {
137 let groups = vec![SolveGroup {
138 members: members.clone(),
139 }];
140 let sub = lm_core(
141 &atoms,
142 poses,
143 &groups,
144 prep.scale,
145 prep.tight,
146 max_iterations,
147 prep.linear,
148 )?;
149 let iters = sub.iterations;
150 run.absorb(sub);
151 let method = if members.len() == 1 {
152 "sequential"
153 } else {
154 "cluster_sequential"
155 };
156 (method, iters)
157 };
158 steps.push(SolveStepReport {
159 bodies: ids(&members),
160 method: method.into(),
161 iterations,
162 });
163 for &b in &members {
164 solved[b] = true;
165 node_of[b] = None;
166 }
167 peeled = true;
168 }
169 if nodes.iter().all(|n| n.is_none()) {
170 break; }
172
173 let mut contracted = false;
181 let lone = |b: usize, nodes: &[Option<Vec<usize>>], node_of: &[Option<usize>]| -> bool {
182 node_of[b]
183 .and_then(|ni| nodes[ni].as_ref())
184 .is_some_and(|members| members.len() == 1)
185 };
186 let mut pair_mates: std::collections::BTreeMap<(usize, usize), Vec<usize>> =
187 std::collections::BTreeMap::new();
188 for (mi, mate) in mates.iter().enumerate() {
189 let (a, b) = (mate.body_a, mate.body_b);
190 if mate_bodies(mate).count() > 2 {
192 continue;
193 }
194 if !solved[a] && !solved[b] && lone(a, &nodes, &node_of) && lone(b, &nodes, &node_of) {
195 pair_mates.entry((a.min(b), a.max(b))).or_default().push(mi);
196 }
197 }
198 let mut uf = UnionFind::new(nb);
199 let mut any_binding = false;
200 for (&(a, b), pm) in &pair_mates {
201 let atoms = atoms_of(pm);
202 let m: usize = atoms.iter().map(Atom::rows).sum();
203 let groups = singleton_groups(&[b]);
204 let jac = finite_diff_jacobian(&atoms, poses, &groups, &column_steps(1, prep.scale), m);
205 run.rows += 2 * 6 * m;
206 if numerical_rank(jac, m, 6) == 6 {
207 uf.union(a, b);
208 any_binding = true;
209 }
210 }
211 if any_binding {
212 let mut components: std::collections::BTreeMap<usize, Vec<usize>> =
213 std::collections::BTreeMap::new();
214 for &(a, b) in pair_mates.keys() {
215 for body in [a, b] {
216 let root = uf.find(body);
217 let entry = components.entry(root).or_default();
218 if !entry.contains(&body) {
219 entry.push(body);
220 }
221 }
222 }
223 for (_, mut comp) in components {
224 if comp.len() < 2 {
225 continue;
226 }
227 comp.sort_unstable();
228 let saved: Vec<Pose> = comp.iter().map(|&b| poses[b]).collect();
230 let mut internal: Vec<usize> = comp
231 .iter()
232 .flat_map(|&b| incident[b].iter().copied())
233 .filter(|&mi| mate_bodies(&mates[mi]).all(|body| comp.contains(&body)))
234 .collect();
235 internal.sort_unstable();
236 internal.dedup();
237 let atoms = atoms_of(&internal);
238 let groups = singleton_groups(&comp[1..]);
239 let sub = lm_core(
240 &atoms,
241 poses,
242 &groups,
243 prep.scale,
244 prep.tight,
245 max_iterations,
246 prep.linear,
247 )?;
248 let sub_iters = sub.iterations;
249 run.absorb(sub);
250 let m_int: usize = atoms.iter().map(Atom::rows).sum();
252 let all_groups = singleton_groups(&comp);
253 let mut scratch: Vec<Pose> = Vec::new();
254 let mut r_int: Vec<f64> = Vec::new();
255 eval_residuals(&atoms, poses, &[], &[], &mut scratch, &mut r_int);
256 let max_int = r_int.iter().fold(0.0f64, |acc, &x| acc.max(x.abs()));
257 let jac = finite_diff_jacobian(
258 &atoms,
259 poses,
260 &all_groups,
261 &column_steps(comp.len(), prep.scale),
262 m_int,
263 );
264 let rank = numerical_rank(jac, m_int, 6 * comp.len());
265 run.rows += m_int + 2 * 6 * comp.len() * m_int;
266 if max_int <= prep.tight && rank == 6 * (comp.len() - 1) {
267 steps.push(SolveStepReport {
268 bodies: ids(&comp),
269 method: "cluster_internal".into(),
270 iterations: sub_iters,
271 });
272 let cluster = nodes.len();
274 for &b in &comp {
275 if let Some(old) = node_of[b] {
276 nodes[old] = None;
277 }
278 node_of[b] = Some(cluster);
279 }
280 nodes.push(Some(comp));
281 contracted = true;
282 } else {
283 for (&b, pose) in comp.iter().zip(&saved) {
285 poses[b] = *pose;
286 }
287 }
288 }
289 }
290 if peeled || contracted {
291 continue;
292 }
293
294 let remaining: Vec<usize> = (0..nb).filter(|&b| node_of[b].is_some()).collect();
299 let mate_set: Vec<usize> = (0..mates.len())
300 .filter(|&mi| mate_bodies(&mates[mi]).any(|body| !solved[body]))
301 .collect();
302 let atoms = atoms_of(&mate_set);
303 let groups = singleton_groups(&remaining);
304 let sub = lm_core(
305 &atoms,
306 poses,
307 &groups,
308 prep.scale,
309 prep.tight,
310 max_iterations,
311 prep.linear,
312 )?;
313 let iters = sub.iterations;
314 run.absorb(sub);
315 steps.push(SolveStepReport {
316 bodies: ids(&remaining),
317 method: "monolithic_fallback".into(),
318 iterations: iters,
319 });
320 for node in nodes.iter_mut() {
321 *node = None;
322 }
323 for &b in &remaining {
324 solved[b] = true;
325 node_of[b] = None;
326 }
327 break;
328 }
329
330 Ok(DecompOutcome { steps, run })
331}
332
333pub fn solve_assembly(
348 bodies: &[AssemblyBody],
349 mates: &[AssemblyMate],
350 options: &AssemblySolveOptions,
351) -> Result<AssemblySolution, String> {
352 let result = solve_with(bodies, mates, options);
353 if cfg!(test) && options.linear_algebra == LinearAlgebra::Sparse && std::env::var_os("BREP_ASSEMBLY_AB").is_some() {
357 let dense = solve_with(bodies, mates, &AssemblySolveOptions { linear_algebra: LinearAlgebra::Dense, ..*options });
358 if let Err(difference) = compare_solutions(&result, &dense) {
359 panic!("sparse/dense A/B: {difference}");
360 }
361 }
362 result
363}
364
365fn compare_solutions(
371 sparse: &Result<AssemblySolution, String>,
372 dense: &Result<AssemblySolution, String>,
373) -> Result<(), String> {
374 match (sparse, dense) {
375 (Err(_), Err(_)) => Ok(()),
376 (Ok(_), Err(error)) => Err(format!("sparse solved, dense failed: {error}")),
377 (Err(error), Ok(_)) => Err(format!("dense solved, sparse failed: {error}")),
378 (Ok(a), Ok(b)) => {
379 for (pa, pb) in a.poses.iter().zip(&b.poses) {
380 let dt = (0..3).map(|i| (pa.translation[i] - pb.translation[i]).abs()).fold(0.0, f64::max);
381 let (qa, qb) = (pa.rotation, pb.rotation);
383 let rel = quat_mul(qa, [qb[0], -qb[1], -qb[2], -qb[3]]);
384 let angle = 2.0 * (rel[1] * rel[1] + rel[2] * rel[2] + rel[3] * rel[3]).sqrt();
385 let bar = if a.dof > 0 { 1e-6 } else { 1e-9 };
386 if dt > bar || angle > bar {
387 return Err(format!("body {}: translation {dt:.3e}, rotation {angle:.3e} (dof {})", pa.id, a.dof));
388 }
389 if dt > 1e-9 || angle > 1e-9 {
390 eprintln!("AB-DRIFT body {} dof {}: translation {dt:.3e}, rotation {angle:.3e}", pa.id, a.dof);
391 }
392 }
393 if a.iterations != b.iterations && a.status != b.status {
394 return Err(format!(
395 "iterations {} vs {} and status {} vs {}",
396 a.iterations, b.iterations, a.status, b.status
397 ));
398 }
399 Ok(())
400 }
401 }
402}
403
404fn solve_with(
405 bodies: &[AssemblyBody],
406 mates: &[AssemblyMate],
407 options: &AssemblySolveOptions,
408) -> Result<AssemblySolution, String> {
409 if !(options.tolerance.is_finite() && options.tolerance > 0.0) {
410 return Err("options.tolerance must be finite and positive".into());
411 }
412 let prep = prepare(bodies, mates, options)?;
413 let movable_ids: Vec<String> = prep.movable.iter().map(|&b| bodies[b].id.clone()).collect();
414
415 let run_monolithic = |poses: &mut Vec<Pose>| -> Result<LmRun, String> {
416 let groups = singleton_groups(&prep.movable);
417 lm_core(
418 &prep.atoms,
419 poses,
420 &groups,
421 prep.scale,
422 prep.tight,
423 options.max_iterations,
424 prep.linear,
425 )
426 };
427
428 if options.strategy == SolveStrategy::Monolithic {
432 let mut poses = prep.base.clone();
433 let run = run_monolithic(&mut poses)?;
434 let steps = vec![SolveStepReport {
435 bodies: movable_ids,
436 method: "monolithic".into(),
437 iterations: run.iterations,
438 }];
439 return finalize(bodies, mates, &prep, &poses, run, "monolithic", steps);
440 }
441
442 let mut poses = prep.base.clone();
444 let outcome = solve_decomposed(bodies, mates, &prep, &mut poses, options.max_iterations)?;
445 let mut run = outcome.run;
446
447 let mut scratch: Vec<Pose> = Vec::new();
448 let mut r: Vec<f64> = Vec::new();
449 eval_residuals(&prep.atoms, &poses, &[], &[], &mut scratch, &mut r);
450 run.rows += prep.m;
451 let max_res = r.iter().fold(0.0f64, |acc, &x| acc.max(x.abs()));
452 if max_res.is_finite() && max_res <= prep.tight {
453 return finalize(
454 bodies,
455 mates,
456 &prep,
457 &poses,
458 run,
459 "decomposed",
460 outcome.steps,
461 );
462 }
463
464 let mut poses = prep.base.clone();
470 let sub = run_monolithic(&mut poses)?;
471 let iters = sub.iterations;
472 run.absorb(sub);
473 let mut method = "monolithic_fallback";
474 if let Some((flipped, sub)) = flip_restart(mates, &prep, &poses, &run_monolithic) {
475 run.absorb(sub);
476 poses = flipped;
477 method = "flip_restart";
478 }
479 let steps = vec![SolveStepReport {
480 bodies: movable_ids,
481 method: method.into(),
482 iterations: iters,
483 }];
484 finalize(bodies, mates, &prep, &poses, run, method, steps)
485}
486
487const FLIP_RESTART_BODIES: usize = 4;
489const FLIP_RESTART_MAX_MOVABLE: usize = 64;
492
493fn flip_restart(
507 mates: &[AssemblyMate],
508 prep: &Prepared,
509 solved: &[Pose],
510 run_monolithic: &dyn Fn(&mut Vec<Pose>) -> Result<LmRun, String>,
511) -> Option<(Vec<Pose>, LmRun)> {
512 if prep.movable.len() > FLIP_RESTART_MAX_MOVABLE {
513 return None;
514 }
515 let residuals = |poses: &[Pose]| -> Vec<f64> {
516 let (mut scratch, mut r) = (Vec::new(), Vec::new());
517 eval_residuals(&prep.atoms, poses, &[], &[], &mut scratch, &mut r);
518 r
519 };
520 let converged = |poses: &[Pose]| residuals(poses).iter().all(|x| x.is_finite() && x.abs() <= prep.tight);
521 let r = residuals(solved);
522 let mut per_mate = vec![0.0f64; mates.len()];
523 for (row, &mi) in prep.row_mate.iter().enumerate() {
524 per_mate[mi] = per_mate[mi].max(r[row].abs());
525 }
526 let mut failing: Vec<usize> = (0..mates.len()).filter(|&mi| per_mate[mi] > prep.tight).collect();
527 if failing.is_empty() {
528 return None;
529 }
530 failing.sort_by(|&a, &b| per_mate[b].total_cmp(&per_mate[a]));
531 let mut candidates: Vec<usize> = Vec::new();
532 for &mi in &failing {
533 for body in mate_bodies(&mates[mi]) {
534 if prep.movable.contains(&body) && !candidates.contains(&body) {
535 candidates.push(body);
536 }
537 }
538 }
539 candidates.truncate(FLIP_RESTART_BODIES);
540 let moved = |poses: &[Pose]| -> f64 {
541 poses.iter().zip(&prep.base).map(|(p, b)| p.t.sub(b.t).length()).sum()
542 };
543 for body in candidates {
544 let mut best: Option<(f64, Vec<Pose>, LmRun)> = None;
545 let nudge = 1e-3 * prep.scale.max(1e-9);
546 let starts = [
547 ([0.0, 1.0, 0.0, 0.0], [0.0; 3]),
548 ([0.0, 0.0, 1.0, 0.0], [0.0; 3]),
549 ([0.0, 0.0, 0.0, 1.0], [0.0; 3]),
550 ([1.0, 0.0, 0.0, 0.0], [nudge, 0.0, 0.0]),
551 ([1.0, 0.0, 0.0, 0.0], [0.0, nudge, 0.0]),
552 ([1.0, 0.0, 0.0, 0.0], [0.0, 0.0, nudge]),
553 ];
554 for (turn, shift) in starts {
555 let mut poses = prep.base.clone();
556 poses[body].q = quat_mul(turn, poses[body].q);
557 poses[body].t = poses[body].t.add(crate::Vec3::new(shift[0], shift[1], shift[2]));
558 let Ok(run) = run_monolithic(&mut poses) else { continue };
559 if converged(&poses) {
560 let cost = moved(&poses);
561 if best.as_ref().is_none_or(|(c, _, _)| cost < *c - 1e-9) {
562 best = Some((cost, poses, run));
563 }
564 }
565 }
566 if let Some((_, poses, run)) = best {
567 return Some((poses, run));
568 }
569 }
570 None
571}