Skip to main content

wai_quantum/
quantum_atom.rs

1//! Deterministic neutral-atom REARRANGEMENT — `wai.quantum.rearrange`
2//! (extensions/quantum-ops § Register preparation).
3//!
4//! A neutral-atom quantum processor loads atoms **stochastically** into a large
5//! array of optical traps — each site fills with probability ~½ — then, before the
6//! computation, mobile optical tweezers **rearrange** the loaded atoms into a
7//! defect-free target register. Which atom moves to which site is a combinatorial
8//! optimization: the **linear sum assignment problem** (assign every target site a
9//! distinct atom, minimizing total move distance), solved exactly by the
10//! **Hungarian algorithm** in `O(n³)`. Getting it right — every target filled, from
11//! the atoms actually present, at minimum cost — is the difference between a
12//! defect-free register and a failed shot, and today no planner signs its plan.
13//!
14//! This module is the WAI take: a deterministic Hungarian solver over integer
15//! Manhattan costs (byte-identical on every machine), turning a low-yield load into
16//! a defect-free array, sealing the plan into a signed
17//! [`crate::quantum_ops::RearrangeReceipt`]. The `success` verdict — every target
18//! site covered by a valid bijection of present atoms — is re-checkable from the
19//! bound configs alone; the same check rejects a tampered plan.
20//!
21//! Honest boundary: a geometric/combinatorial model of the rearrangement (the
22//! solver and its optimality are exact and reproducible); the physics of tweezer
23//! transport, loss, and AOD parallelization is out of scope. The move distance is
24//! the plan's cost, not a wall-clock or fidelity claim.
25
26use crate::quantum_ops::{content_hash, GrantRef, RearrangeReceipt};
27use ed25519_dalek::SigningKey;
28
29fn splitmix64(state: &mut u64) -> u64 {
30    *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
31    let mut z = *state;
32    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
33    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
34    z ^ (z >> 31)
35}
36
37// ===========================================================================
38// Trap array
39// ===========================================================================
40
41/// A `w × h` array of optical traps; site `(x, y)` has index `y*w + x`.
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct Array {
44    pub w: u32,
45    pub h: u32,
46    /// Occupancy per site (row-major).
47    pub occ: Vec<bool>,
48}
49
50impl Array {
51    pub fn empty(w: u32, h: u32) -> Array {
52        Array { w, h, occ: vec![false; (w * h) as usize] }
53    }
54    /// Stochastically load each site with fill probability `fill_permille`/1000.
55    pub fn load(w: u32, h: u32, fill_permille: u32, seed: u64) -> Array {
56        let mut st = seed.wrapping_mul(0xA24B_AED4).wrapping_add(1);
57        let occ = (0..w * h)
58            .map(|_| (splitmix64(&mut st) % 1000) < fill_permille as u64)
59            .collect();
60        Array { w, h, occ }
61    }
62    pub fn atoms(&self) -> Vec<usize> {
63        (0..self.occ.len()).filter(|&i| self.occ[i]).collect()
64    }
65    pub fn count(&self) -> u32 {
66        self.occ.iter().filter(|&&o| o).count() as u32
67    }
68    fn pos(&self, idx: usize) -> (i64, i64) {
69        ((idx as u32 % self.w) as i64, (idx as u32 / self.w) as i64)
70    }
71    /// Canonical bytes of the occupancy (for content-hashing).
72    pub fn bytes(&self) -> Vec<u8> {
73        let mut b = Vec::with_capacity(self.occ.len() / 8 + 12);
74        b.extend_from_slice(b"wai:qc-array\x01");
75        b.extend_from_slice(&self.w.to_le_bytes());
76        b.extend_from_slice(&self.h.to_le_bytes());
77        for chunk in self.occ.chunks(8) {
78            let mut byte = 0u8;
79            for (i, &o) in chunk.iter().enumerate() {
80                if o {
81                    byte |= 1 << i;
82                }
83            }
84            b.push(byte);
85        }
86        b
87    }
88}
89
90/// A centered `tw × th` block of target sites (the defect-free register).
91pub fn target_block(w: u32, h: u32, tw: u32, th: u32) -> Vec<usize> {
92    let x0 = (w - tw) / 2;
93    let y0 = (h - th) / 2;
94    let mut t = Vec::with_capacity((tw * th) as usize);
95    for y in y0..y0 + th {
96        for x in x0..x0 + tw {
97            t.push((y * w + x) as usize);
98        }
99    }
100    t
101}
102
103/// Canonical bytes of a target site set.
104pub fn target_bytes(target: &[usize], w: u32, h: u32) -> Vec<u8> {
105    let mut b = Vec::with_capacity(target.len() * 4 + 16);
106    b.extend_from_slice(b"wai:qc-target\x01");
107    b.extend_from_slice(&w.to_le_bytes());
108    b.extend_from_slice(&h.to_le_bytes());
109    let mut sorted = target.to_vec();
110    sorted.sort_unstable();
111    for &s in &sorted {
112        b.extend_from_slice(&(s as u32).to_le_bytes());
113    }
114    b
115}
116
117// ===========================================================================
118// Hungarian algorithm (min-cost linear sum assignment)
119// ===========================================================================
120
121/// Optimal assignment of `n` rows to distinct columns of an `n × m` cost matrix
122/// (`n ≤ m`), minimizing total cost. `O(n²m)`. Returns `assignment[row] = column`.
123/// The classic Kuhn–Munkres with potentials (deterministic).
124fn hungarian(cost: &[Vec<i64>], n: usize, m: usize) -> Vec<usize> {
125    const INF: i64 = i64::MAX / 4;
126    let mut u = vec![0i64; n + 1];
127    let mut v = vec![0i64; m + 1];
128    let mut p = vec![0usize; m + 1]; // p[col] = row matched to col (1-indexed; 0 = none)
129    let mut way = vec![0usize; m + 1];
130    for i in 1..=n {
131        p[0] = i;
132        let mut j0 = 0usize;
133        let mut minv = vec![INF; m + 1];
134        let mut used = vec![false; m + 1];
135        loop {
136            used[j0] = true;
137            let i0 = p[j0];
138            let mut delta = INF;
139            let mut j1 = 0usize;
140            for j in 1..=m {
141                if !used[j] {
142                    let cur = cost[i0 - 1][j - 1] - u[i0] - v[j];
143                    if cur < minv[j] {
144                        minv[j] = cur;
145                        way[j] = j0;
146                    }
147                    if minv[j] < delta {
148                        delta = minv[j];
149                        j1 = j;
150                    }
151                }
152            }
153            for j in 0..=m {
154                if used[j] {
155                    u[p[j]] += delta;
156                    v[j] -= delta;
157                } else {
158                    minv[j] -= delta;
159                }
160            }
161            j0 = j1;
162            if p[j0] == 0 {
163                break;
164            }
165        }
166        loop {
167            let j1 = way[j0];
168            p[j0] = p[j1];
169            j0 = j1;
170            if j0 == 0 {
171                break;
172            }
173        }
174    }
175    let mut assignment = vec![usize::MAX; n];
176    for j in 1..=m {
177        if p[j] != 0 {
178            assignment[p[j] - 1] = j - 1;
179        }
180    }
181    assignment
182}
183
184// ===========================================================================
185// Planning + verification
186// ===========================================================================
187
188/// A rearrangement plan: `moves[k] = (from_site, to_site)`.
189#[derive(Clone, Debug, PartialEq, Eq)]
190pub struct Plan {
191    pub moves: Vec<(usize, usize)>,
192    pub total_distance: u64,
193    pub atoms_moved: u32,
194    /// Every target site is covered by a distinct present atom.
195    pub feasible: bool,
196}
197
198impl Plan {
199    pub fn bytes(&self) -> Vec<u8> {
200        let mut b = Vec::with_capacity(self.moves.len() * 8 + 8);
201        b.extend_from_slice(b"wai:qc-plan\x01");
202        for &(f, t) in &self.moves {
203            b.extend_from_slice(&(f as u32).to_le_bytes());
204            b.extend_from_slice(&(t as u32).to_le_bytes());
205        }
206        b
207    }
208    pub fn plan_hash(&self) -> [u8; 32] {
209        content_hash(&self.bytes())
210    }
211}
212
213/// Plan the rearrangement of `array` into `target` by minimum-distance assignment.
214/// If fewer atoms are loaded than target sites, the plan is infeasible.
215pub fn solve(array: &Array, target: &[usize]) -> Plan {
216    let atoms = array.atoms();
217    let nt = target.len();
218    let na = atoms.len();
219    if na < nt {
220        return Plan { moves: Vec::new(), total_distance: 0, atoms_moved: 0, feasible: false };
221    }
222    // cost[target_i][atom_j] = Manhattan distance
223    let cost: Vec<Vec<i64>> = target
224        .iter()
225        .map(|&t| {
226            let (tx, ty) = array.pos(t);
227            atoms
228                .iter()
229                .map(|&a| {
230                    let (ax, ay) = array.pos(a);
231                    (tx - ax).abs() + (ty - ay).abs()
232                })
233                .collect()
234        })
235        .collect();
236    let assign = hungarian(&cost, nt, na);
237    let mut moves = Vec::new();
238    let mut total = 0u64;
239    let mut moved = 0u32;
240    for (i, &t) in target.iter().enumerate() {
241        let a = atoms[assign[i]];
242        let d = cost[i][assign[i]] as u64;
243        total += d;
244        if a != t {
245            moves.push((a, t));
246            moved += 1;
247        }
248    }
249    Plan { moves, total_distance: total, atoms_moved: moved, feasible: true }
250}
251
252/// Does the plan turn `array` into a configuration covering every `target` site?
253/// Order-independent set check: sources must be present atoms, targets a bijection,
254/// and the resulting occupancy must include all target sites.
255pub fn verify(array: &Array, target: &[usize], plan: &Plan) -> bool {
256    if !plan.feasible {
257        return false;
258    }
259    let mut occ = array.occ.clone();
260    let mut froms = std::collections::HashSet::new();
261    let mut tos = std::collections::HashSet::new();
262    for &(f, t) in &plan.moves {
263        if f >= occ.len() || t >= occ.len() || !array.occ[f] {
264            return false; // must move an atom that is actually present
265        }
266        if !froms.insert(f) || !tos.insert(t) {
267            return false; // no atom or site used twice
268        }
269    }
270    // apply as a set: vacate sources, fill targets
271    for &(f, _) in &plan.moves {
272        occ[f] = false;
273    }
274    for &(_, t) in &plan.moves {
275        occ[t] = true;
276    }
277    target.iter().all(|&t| occ[t])
278}
279
280/// Solve + verify + seal a `RearrangeReceipt` binding the initial array, target,
281/// and plan to the outcome.
282#[allow(clippy::too_many_arguments)]
283pub fn rearrange_and_seal(
284    signer: &SigningKey,
285    signer_id: &str,
286    array: &Array,
287    target: &[usize],
288    joules_micro: u64,
289    grant: GrantRef,
290) -> (Plan, bool, RearrangeReceipt) {
291    let plan = solve(array, target);
292    let success = verify(array, target, &plan);
293    let receipt = RearrangeReceipt::seal(
294        signer,
295        signer_id,
296        "hungarian-lsap",
297        content_hash(&array.bytes()),
298        content_hash(&target_bytes(target, array.w, array.h)),
299        plan.plan_hash(),
300        success,
301        plan.atoms_moved,
302        plan.total_distance,
303        joules_micro,
304        grant,
305        None,
306    );
307    (plan, success, receipt)
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    fn key(s: u8) -> SigningKey {
315        SigningKey::from_bytes(&[s; 32])
316    }
317
318    // brute-force min assignment cost for small n, to check Hungarian optimality
319    fn brute(cost: &[Vec<i64>], n: usize, m: usize) -> i64 {
320        let mut cols: Vec<usize> = (0..m).collect();
321        let mut best = i64::MAX;
322        // permutations of choosing n distinct columns in order
323        fn rec(cost: &[Vec<i64>], n: usize, i: usize, used: &mut Vec<bool>, acc: i64, best: &mut i64) {
324            if i == n {
325                *best = (*best).min(acc);
326                return;
327            }
328            for j in 0..cost[i].len() {
329                if !used[j] {
330                    used[j] = true;
331                    rec(cost, n, i + 1, used, acc + cost[i][j], best);
332                    used[j] = false;
333                }
334            }
335        }
336        let mut used = vec![false; m];
337        rec(cost, n, 0, &mut used, 0, &mut best);
338        let _ = &mut cols;
339        best
340    }
341
342    #[test]
343    fn hungarian_matches_brute_force() {
344        let mut st = 12345u64;
345        for _ in 0..30 {
346            let n = 3 + (splitmix64(&mut st) % 3) as usize;
347            let m = n + (splitmix64(&mut st) % 3) as usize;
348            let cost: Vec<Vec<i64>> = (0..n)
349                .map(|_| (0..m).map(|_| (splitmix64(&mut st) % 20) as i64).collect())
350                .collect();
351            let assign = hungarian(&cost, n, m);
352            let got: i64 = (0..n).map(|i| cost[i][assign[i]]).sum();
353            assert_eq!(got, brute(&cost, n, m), "Hungarian must be optimal");
354            // valid: distinct columns
355            let mut s = std::collections::HashSet::new();
356            assert!(assign.iter().all(|&c| s.insert(c)));
357        }
358    }
359
360    #[test]
361    fn rearrangement_makes_defect_free() {
362        // load a half-full array, rearrange into a centered block, verify it fills.
363        let arr = Array::load(12, 12, 550, 7);
364        let target = target_block(12, 12, 6, 6);
365        assert!(arr.count() as usize >= target.len(), "enough atoms for the test");
366        let plan = solve(&arr, &target);
367        assert!(plan.feasible);
368        assert!(verify(&arr, &target, &plan), "target must be defect-free after the plan");
369    }
370
371    #[test]
372    fn already_placed_atoms_do_not_move() {
373        // an array already defect-free over the target needs zero moves.
374        let mut arr = Array::empty(8, 8);
375        let target = target_block(8, 8, 4, 4);
376        for &t in &target {
377            arr.occ[t] = true;
378        }
379        let plan = solve(&arr, &target);
380        assert!(plan.feasible);
381        assert_eq!(plan.atoms_moved, 0);
382        assert_eq!(plan.total_distance, 0);
383        assert!(verify(&arr, &target, &plan));
384    }
385
386    #[test]
387    fn too_few_atoms_is_infeasible() {
388        let mut arr = Array::empty(8, 8);
389        arr.occ[0] = true; // one atom
390        let target = target_block(8, 8, 3, 3); // 9 sites
391        let plan = solve(&arr, &target);
392        assert!(!plan.feasible);
393        assert!(!verify(&arr, &target, &plan));
394    }
395
396    #[test]
397    fn tamper_breaks_verification() {
398        let arr = Array::load(10, 10, 600, 3);
399        let target = target_block(10, 10, 5, 5);
400        let mut plan = solve(&arr, &target);
401        assert!(verify(&arr, &target, &plan));
402        // corrupt a move's destination to a non-target site → target no longer covered
403        if let Some(mv) = plan.moves.first_mut() {
404            mv.1 = 0; // divert to corner (not in the centered target)
405        }
406        // (only asserts when the diverted site actually left a target uncovered)
407        if !target.contains(&0) {
408            assert!(!verify(&arr, &target, &plan), "a diverted move must fail verification");
409        }
410    }
411
412    #[test]
413    fn solve_is_deterministic() {
414        let arr = Array::load(14, 14, 520, 99);
415        let target = target_block(14, 14, 7, 7);
416        assert_eq!(solve(&arr, &target), solve(&arr, &target));
417    }
418
419    #[test]
420    fn seals_verifying_receipt() {
421        let arr = Array::load(12, 12, 560, 42);
422        let target = target_block(12, 12, 6, 6);
423        let (plan, success, rec) = rearrange_and_seal(
424            &key(1), "did:key:lab", &arr, &target, 400_000, GrantRef::unbounded("quantum.rearrange"),
425        );
426        assert!(success);
427        assert!(rec.verify());
428        assert!(rec.success);
429        assert!(rec.plan_matches(&plan.bytes()));
430    }
431}