1use crate::quantum_backend::QuantumError;
25
26#[derive(Debug, Clone, Copy)]
28pub struct RegisterConstraints {
29 pub min_distance_um: f64,
31 pub max_extent_um: f64,
33 pub max_atoms: usize,
35}
36
37impl RegisterConstraints {
38 pub fn neutral_atom_default() -> Self {
40 Self {
41 min_distance_um: 4.0,
42 max_extent_um: 75.0,
43 max_atoms: 100,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53pub enum LayoutMethod {
54 #[default]
56 Spectral,
57 ForceDirected,
59}
60
61pub fn build_register(
71 adjacency: &[Vec<(usize, f64)>],
72 constraints: RegisterConstraints,
73) -> Result<Vec<(String, [f64; 2])>, QuantumError> {
74 build_register_with(adjacency, constraints, LayoutMethod::Spectral)
75}
76
77pub fn build_register_with(
79 adjacency: &[Vec<(usize, f64)>],
80 constraints: RegisterConstraints,
81 method: LayoutMethod,
82) -> Result<Vec<(String, [f64; 2])>, QuantumError> {
83 let n = adjacency.len();
84 if n == 0 {
85 return Ok(Vec::new());
86 }
87 if n > constraints.max_atoms {
88 return Err(QuantumError::GraphTooLarge {
89 nodes: n,
90 max: constraints.max_atoms,
91 });
92 }
93
94 let positions = match method {
95 LayoutMethod::Spectral => {
96 match spectral_embedding_layout(adjacency) {
97 Some(pos) if layout_min_pairwise(&pos) > 1e-9 => pos,
98 _ => force_directed_layout(adjacency, 200),
100 }
101 }
102 LayoutMethod::ForceDirected => force_directed_layout(adjacency, 200),
103 };
104
105 let scaled = scale_to_constraints(&positions, constraints)?;
106
107 Ok(scaled
108 .into_iter()
109 .enumerate()
110 .map(|(i, p)| (format!("q{}", i), p))
111 .collect())
112}
113
114fn spectral_embedding_layout(adjacency: &[Vec<(usize, f64)>]) -> Option<Vec<[f64; 2]>> {
124 let n = adjacency.len();
125 if n == 0 {
126 return Some(Vec::new());
127 }
128 if n == 1 {
129 return Some(vec![[0.0, 0.0]]);
130 }
131
132 let mut a = vec![vec![0.0f64; n]; n];
135 for (i, edges) in adjacency.iter().enumerate() {
136 for &(j, w) in edges {
137 if j >= n || i == j {
138 continue;
139 }
140 let ww = w.max(0.0);
141 if ww > a[i][j] {
142 a[i][j] = ww;
143 a[j][i] = ww;
144 }
145 }
146 }
147
148 let mut l = vec![vec![0.0f64; n]; n];
150 for i in 0..n {
151 let mut deg = 0.0f64;
152 for j in 0..n {
153 if i != j {
154 deg += a[i][j];
155 l[i][j] = -a[i][j];
156 }
157 }
158 l[i][i] = deg;
159 }
160
161 let (evals, evecs) = dense_jacobi_eigen(&l, n, 200 * n.max(1));
162
163 let mut order: Vec<usize> = (0..n).collect();
165 order.sort_by(|&i, &j| {
166 evals[i]
167 .partial_cmp(&evals[j])
168 .unwrap_or(std::cmp::Ordering::Equal)
169 });
170
171 const NULL_FLOOR: f64 = 1e-8;
176 let mut axes: Vec<usize> = Vec::with_capacity(2);
177 for &idx in &order {
178 if evals[idx] > NULL_FLOOR {
179 axes.push(idx);
180 if axes.len() == 2 {
181 break;
182 }
183 }
184 }
185
186 if axes.is_empty() {
189 return None;
190 }
191
192 let mut pos = vec![[0.0f64; 2]; n];
193 let vx = &evecs[axes[0]];
194 for i in 0..n {
195 pos[i][0] = vx[i];
196 }
197
198 if axes.len() >= 2 {
199 let vy = &evecs[axes[1]];
200 for i in 0..n {
201 pos[i][1] = vy[i];
202 }
203 } else {
204 let mut y: Vec<f64> = (0..n)
206 .map(|i| ((i as f64) + 0.5) - (n as f64) / 2.0)
207 .collect();
208 let dot: f64 = y.iter().zip(vx.iter()).map(|(a, b)| a * b).sum();
209 let norm_x2: f64 = vx.iter().map(|x| x * x).sum::<f64>().max(1e-18);
210 for i in 0..n {
211 y[i] -= dot / norm_x2 * vx[i];
212 }
213 let norm_y: f64 = y.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-18);
214 for i in 0..n {
215 pos[i][1] = y[i] / norm_y;
216 }
217 }
218
219 for axis in 0..2 {
222 let mut flip = false;
223 for p in pos.iter().take(n) {
224 if p[axis].abs() > 1e-12 {
225 flip = p[axis] < 0.0;
226 break;
227 }
228 }
229 if flip {
230 for p in pos.iter_mut().take(n) {
231 p[axis] = -p[axis];
232 }
233 }
234 }
235
236 if pos.iter().any(|p| !p[0].is_finite() || !p[1].is_finite()) {
238 return None;
239 }
240
241 Some(pos)
242}
243
244#[allow(clippy::needless_range_loop)]
251fn dense_jacobi_eigen(mat: &[Vec<f64>], m: usize, max_iter: usize) -> (Vec<f64>, Vec<Vec<f64>>) {
252 if m == 0 {
253 return (Vec::new(), Vec::new());
254 }
255 if m == 1 {
256 return (vec![mat[0][0]], vec![vec![1.0]]);
257 }
258
259 let mut a = mat.to_vec();
260 let mut v = vec![vec![0.0f64; m]; m];
261 for (i, row) in v.iter_mut().enumerate() {
262 row[i] = 1.0;
263 }
264
265 for _ in 0..max_iter {
266 let mut max_off = 0.0f64;
267 let mut p = 0usize;
268 let mut q = 1usize;
269 for i in 0..m {
270 for j in (i + 1)..m {
271 if a[i][j].abs() > max_off {
272 max_off = a[i][j].abs();
273 p = i;
274 q = j;
275 }
276 }
277 }
278
279 if max_off < 1e-15 {
280 break;
281 }
282
283 let theta = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
284 let t = if theta == 0.0 || !theta.is_finite() {
285 1.0
286 } else {
287 theta.signum() / (theta.abs() + (1.0 + theta * theta).sqrt())
288 };
289 let c = 1.0 / (1.0 + t * t).sqrt();
290 let s = t * c;
291
292 let app = a[p][p];
293 let aqq = a[q][q];
294 let apq = a[p][q];
295 a[p][p] = c * c * app - 2.0 * s * c * apq + s * s * aqq;
296 a[q][q] = s * s * app + 2.0 * s * c * apq + c * c * aqq;
297 a[p][q] = 0.0;
298 a[q][p] = 0.0;
299
300 for r in 0..m {
301 if r != p && r != q {
302 let arp = a[r][p];
303 let arq = a[r][q];
304 a[r][p] = c * arp - s * arq;
305 a[p][r] = a[r][p];
306 a[r][q] = s * arp + c * arq;
307 a[q][r] = a[r][q];
308 }
309 }
310
311 for row in v.iter_mut() {
312 let vp = row[p];
313 let vq = row[q];
314 row[p] = c * vp - s * vq;
315 row[q] = s * vp + c * vq;
316 }
317 }
318
319 let eigenvalues: Vec<f64> = (0..m).map(|i| a[i][i]).collect();
320 let eigenvectors: Vec<Vec<f64>> = (0..m).map(|j| (0..m).map(|i| v[i][j]).collect()).collect();
321 (eigenvalues, eigenvectors)
322}
323
324fn layout_min_pairwise(positions: &[[f64; 2]]) -> f64 {
325 if positions.len() < 2 {
326 return f64::INFINITY;
327 }
328 let mut min_d = f64::INFINITY;
329 for i in 0..positions.len() {
330 for j in (i + 1)..positions.len() {
331 let dx = positions[i][0] - positions[j][0];
332 let dy = positions[i][1] - positions[j][1];
333 let d = (dx * dx + dy * dy).sqrt();
334 if d < min_d {
335 min_d = d;
336 }
337 }
338 }
339 min_d
340}
341
342fn force_directed_layout(adjacency: &[Vec<(usize, f64)>], iterations: u32) -> Vec<[f64; 2]> {
347 let n = adjacency.len();
348 let mut pos: Vec<[f64; 2]> = (0..n)
350 .map(|i| {
351 let theta = 2.0 * std::f64::consts::PI * (i as f64) / (n as f64);
352 [theta.cos(), theta.sin()]
353 })
354 .collect();
355
356 let k = (1.0 / (n as f64).max(1.0)).sqrt();
357 let k2 = k * k;
358
359 for iter in 0..iterations {
360 let temperature = 0.1 * (1.0 - iter as f64 / iterations as f64).max(0.01);
361 let mut disp = vec![[0.0_f64; 2]; n];
362
363 for i in 0..n {
365 for j in 0..n {
366 if i == j {
367 continue;
368 }
369 let dx = pos[i][0] - pos[j][0];
370 let dy = pos[i][1] - pos[j][1];
371 let dist2 = (dx * dx + dy * dy).max(1e-9);
372 let force = k2 / dist2;
373 disp[i][0] += dx * force;
374 disp[i][1] += dy * force;
375 }
376 }
377
378 for (i, edges) in adjacency.iter().enumerate() {
380 for &(j, w) in edges {
381 if j >= n {
382 continue;
383 }
384 let dx = pos[i][0] - pos[j][0];
385 let dy = pos[i][1] - pos[j][1];
386 let dist = (dx * dx + dy * dy).sqrt().max(1e-9);
387 let force = w * dist / k;
388 disp[i][0] -= dx / dist * force;
389 disp[i][1] -= dy / dist * force;
390 }
391 }
392
393 for i in 0..n {
394 let mag = (disp[i][0].powi(2) + disp[i][1].powi(2)).sqrt().max(1e-9);
395 let step = mag.min(temperature);
396 pos[i][0] += disp[i][0] / mag * step;
397 pos[i][1] += disp[i][1] / mag * step;
398 }
399 }
400 pos
401}
402
403fn scale_to_constraints(
404 positions: &[[f64; 2]],
405 c: RegisterConstraints,
406) -> Result<Vec<[f64; 2]>, QuantumError> {
407 if positions.is_empty() {
408 return Ok(Vec::new());
409 }
410
411 let mut min_d = f64::INFINITY;
413 for i in 0..positions.len() {
414 for j in (i + 1)..positions.len() {
415 let dx = positions[i][0] - positions[j][0];
416 let dy = positions[i][1] - positions[j][1];
417 let d = (dx * dx + dy * dy).sqrt();
418 if d < min_d {
419 min_d = d;
420 }
421 }
422 }
423
424 if positions.len() == 1 {
425 return Ok(vec![[0.0, 0.0]]);
426 }
427
428 if !min_d.is_finite() || min_d <= 0.0 {
429 return Err(QuantumError::InvalidRegister(
430 "degenerate layout: atoms collapsed to same point".into(),
431 ));
432 }
433
434 let scale = c.min_distance_um / min_d;
436 let mut scaled: Vec<[f64; 2]> = positions
437 .iter()
438 .map(|p| [p[0] * scale, p[1] * scale])
439 .collect();
440
441 let (mut xmin, mut xmax, mut ymin, mut ymax) = (
443 f64::INFINITY,
444 f64::NEG_INFINITY,
445 f64::INFINITY,
446 f64::NEG_INFINITY,
447 );
448 for p in &scaled {
449 xmin = xmin.min(p[0]);
450 xmax = xmax.max(p[0]);
451 ymin = ymin.min(p[1]);
452 ymax = ymax.max(p[1]);
453 }
454 let extent = (xmax - xmin).max(ymax - ymin);
455 if extent > c.max_extent_um {
456 return Err(QuantumError::InvalidRegister(format!(
457 "register extent {:.1}um exceeds device max {:.1}um",
458 extent, c.max_extent_um
459 )));
460 }
461
462 let cx = (xmin + xmax) / 2.0;
464 let cy = (ymin + ymax) / 2.0;
465 for p in &mut scaled {
466 p[0] -= cx;
467 p[1] -= cy;
468 }
469 Ok(scaled)
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 #[test]
477 fn empty_graph_returns_empty_register() {
478 let r = build_register(&[], RegisterConstraints::neutral_atom_default()).unwrap();
479 assert!(r.is_empty());
480 }
481
482 #[test]
483 fn single_node_places_at_origin() {
484 let adj = vec![vec![]];
485 let r = build_register(&adj, RegisterConstraints::neutral_atom_default()).unwrap();
486 assert_eq!(r.len(), 1);
487 assert_eq!(r[0].0, "q0");
488 assert_eq!(r[0].1, [0.0, 0.0]);
489 }
490
491 #[test]
492 fn too_large_graph_errors() {
493 let adj: Vec<Vec<(usize, f64)>> = (0..5).map(|_| vec![]).collect();
494 let constraints = RegisterConstraints {
495 min_distance_um: 4.0,
496 max_extent_um: 75.0,
497 max_atoms: 3,
498 };
499 let err = build_register(&adj, constraints).unwrap_err();
500 matches!(err, QuantumError::GraphTooLarge { nodes: 5, max: 3 });
501 }
502
503 #[test]
504 fn small_graph_respects_min_distance() {
505 let adj = vec![
506 vec![(1, 1.0), (2, 1.0)],
507 vec![(0, 1.0), (2, 1.0)],
508 vec![(0, 1.0), (1, 1.0)],
509 ];
510 let c = RegisterConstraints::neutral_atom_default();
511 let r = build_register(&adj, c).unwrap();
512 assert_eq!(r.len(), 3);
513 for i in 0..3 {
514 for j in (i + 1)..3 {
515 let dx = r[i].1[0] - r[j].1[0];
516 let dy = r[i].1[1] - r[j].1[1];
517 let d = (dx * dx + dy * dy).sqrt();
518 assert!(
519 d >= c.min_distance_um - 1e-6,
520 "atoms {} and {} too close: {}",
521 i,
522 j,
523 d
524 );
525 }
526 }
527 }
528
529 #[test]
530 fn layout_is_deterministic() {
531 let adj = vec![vec![(1, 1.0)], vec![(0, 1.0), (2, 1.0)], vec![(1, 1.0)]];
532 let c = RegisterConstraints::neutral_atom_default();
533 let r1 = build_register(&adj, c).unwrap();
534 let r2 = build_register(&adj, c).unwrap();
535 assert_eq!(r1, r2);
536 }
537
538 #[test]
539 fn spectral_path_graph_places_endpoints_farther() {
540 let adj = vec![
542 vec![(1, 1.0)],
543 vec![(0, 1.0), (2, 1.0)],
544 vec![(1, 1.0), (3, 1.0)],
545 vec![(2, 1.0)],
546 ];
547 let c = RegisterConstraints::neutral_atom_default();
548 let r = build_register_with(&adj, c, LayoutMethod::Spectral).unwrap();
549 assert_eq!(r.len(), 4);
550
551 let dist = |i: usize, j: usize| {
553 let dx = r[i].1[0] - r[j].1[0];
554 let dy = r[i].1[1] - r[j].1[1];
555 (dx * dx + dy * dy).sqrt()
556 };
557 let end = dist(0, 3);
558 let mid = dist(1, 2);
559 assert!(
560 end + 1e-9 >= mid,
561 "path endpoints should not be closer than middle edge: end={end} mid={mid}"
562 );
563
564 for i in 0..4 {
566 for j in (i + 1)..4 {
567 assert!(dist(i, j) >= c.min_distance_um - 1e-6);
568 }
569 }
570 }
571
572 #[test]
573 fn spectral_layout_raw_is_some_for_triangle() {
574 let adj = vec![
575 vec![(1, 1.0), (2, 1.0)],
576 vec![(0, 1.0), (2, 1.0)],
577 vec![(0, 1.0), (1, 1.0)],
578 ];
579 let pos = spectral_embedding_layout(&adj).expect("spectral layout");
580 assert_eq!(pos.len(), 3);
581 assert!(layout_min_pairwise(&pos) > 1e-9);
582 }
583
584 #[test]
585 fn force_directed_still_works_explicitly() {
586 let adj = vec![vec![(1, 1.0)], vec![(0, 1.0)]];
587 let c = RegisterConstraints::neutral_atom_default();
588 let r = build_register_with(&adj, c, LayoutMethod::ForceDirected).unwrap();
589 assert_eq!(r.len(), 2);
590 let dx = r[0].1[0] - r[1].1[0];
591 let dy = r[0].1[1] - r[1].1[1];
592 let d = (dx * dx + dy * dy).sqrt();
593 assert!(d >= c.min_distance_um - 1e-6);
594 }
595
596 #[test]
597 fn spectral_and_force_both_deterministic() {
598 let adj = vec![
599 vec![(1, 1.0), (3, 1.0)],
600 vec![(0, 1.0), (2, 1.0)],
601 vec![(1, 1.0), (3, 1.0)],
602 vec![(0, 1.0), (2, 1.0)],
603 ];
604 let c = RegisterConstraints::neutral_atom_default();
605 for method in [LayoutMethod::Spectral, LayoutMethod::ForceDirected] {
606 let a = build_register_with(&adj, c, method).unwrap();
607 let b = build_register_with(&adj, c, method).unwrap();
608 assert_eq!(a, b, "method {method:?} not deterministic");
609 }
610 }
611
612 #[test]
613 fn default_method_is_spectral() {
614 assert_eq!(LayoutMethod::default(), LayoutMethod::Spectral);
615 }
616}