Skip to main content

prism_q/qec/
matching.rs

1//! Minimum-weight perfect matching decoder over graphlike detector error models.
2//!
3//! Exact primal-dual blossom matching on each shot's defects, with defect-to-defect
4//! distances discovered on demand by one Dijkstra ball per defect.
5
6use std::cmp::Reverse;
7use std::collections::BinaryHeap;
8
9use super::DetectorErrorModel;
10use super::decoder::{
11    BOUNDARY, GraphEdges, ShotDecoder, Stuck, decode_batch, graphlike_edges, logical_error_rate,
12};
13use crate::error::{PrismError, Result};
14use crate::sim::compiled::PackedShots;
15
16const NONE: u32 = u32::MAX;
17const DEAD: u32 = u32::MAX - 1;
18const INF: i64 = i64::MAX / 4;
19/// Quantization levels for the largest edge weight; every quantized weight is
20/// then doubled so all dual events fall on integer times.
21const WEIGHT_LEVELS: f64 = (1u64 << 23) as f64;
22
23const EVENT_FRONTIER: u8 = 0;
24const EVENT_PAIR: u8 = 1;
25const EVENT_BOUNDARY: u8 = 2;
26const EVENT_EXPAND: u8 = 3;
27
28/// Exact minimum-weight perfect matching decoder compiled from a graphlike
29/// detector error model.
30///
31/// The decoding graph is the union-find decoder's: detectors are vertices, a
32/// two-detector mechanism an internal edge and a one-detector mechanism a
33/// boundary edge, each weighted `ln((1-p)/p)` clamped at zero, with mechanisms
34/// sharing one detector set collapsed to the most probable. Weights are
35/// quantized to `2^-23` of the largest edge weight, and the matching is exact
36/// for the quantized weights: each shot's correction has the minimum total
37/// weight of any edge set whose boundary is the shot's defects, the open
38/// boundary absorbing any number of chain ends. Decoding uses no randomness,
39/// so equal inputs give equal outputs on any thread count. The algorithm is
40/// described in the decoding section of `docs/architecture/qec-programs.md`.
41#[derive(Debug, Clone)]
42pub struct MatchingDecoder {
43    num_detectors: usize,
44    num_observables: usize,
45    obs_words: usize,
46    adj_offsets: Vec<u32>,
47    adj_target: Vec<u32>,
48    adj_weight: Vec<i64>,
49    adj_edge: Vec<u32>,
50    edge_obs: Vec<u64>,
51    boundary_dist: Vec<i64>,
52    boundary_obs: Vec<u64>,
53}
54
55impl MatchingDecoder {
56    /// Compile a decoder from a graphlike detector error model.
57    ///
58    /// # Errors
59    ///
60    /// As [`super::UnionFindDecoder::from_model`]: a mechanism flipping more
61    /// than two detectors is rejected with a pointer to
62    /// [`DetectorErrorModel::decompose_graphlike`], and probabilities must
63    /// lie in `[0, 1)`.
64    pub fn from_model(model: &DetectorErrorModel) -> Result<Self> {
65        Ok(Self::from_graph(graphlike_edges(model, "matching")?))
66    }
67
68    fn from_graph(graph: GraphEdges) -> Self {
69        let GraphEdges {
70            num_detectors,
71            num_observables,
72            edge_u,
73            edge_v,
74            edge_p,
75            edge_obs,
76        } = graph;
77        let obs_words = num_observables.div_ceil(64);
78        let weights = quantized_weights(&edge_p);
79
80        let mut adj_offsets = vec![0u32; num_detectors + 1];
81        let mut boundary_edge = vec![NONE; num_detectors];
82        for edge in 0..edge_u.len() {
83            if edge_v[edge] == BOUNDARY {
84                boundary_edge[edge_u[edge] as usize] = edge as u32;
85            } else {
86                adj_offsets[edge_u[edge] as usize + 1] += 1;
87                adj_offsets[edge_v[edge] as usize + 1] += 1;
88            }
89        }
90        for v in 0..num_detectors {
91            adj_offsets[v + 1] += adj_offsets[v];
92        }
93        let slots = *adj_offsets.last().unwrap() as usize;
94        let mut cursor = adj_offsets.clone();
95        let mut adj_target = vec![0u32; slots];
96        let mut adj_weight = vec![0i64; slots];
97        let mut adj_edge = vec![0u32; slots];
98        for edge in 0..edge_u.len() {
99            if edge_v[edge] == BOUNDARY {
100                continue;
101            }
102            for (from, to) in [(edge_u[edge], edge_v[edge]), (edge_v[edge], edge_u[edge])] {
103                let at = cursor[from as usize] as usize;
104                adj_target[at] = to;
105                adj_weight[at] = weights[edge];
106                adj_edge[at] = edge as u32;
107                cursor[from as usize] += 1;
108            }
109        }
110
111        // Multi-source Dijkstra from every boundary edge: the exact distance
112        // from each detector to the boundary, and the observable mask of one
113        // shortest such path.
114        let mut boundary_dist = vec![INF; num_detectors];
115        let mut boundary_obs = vec![0u64; num_detectors * obs_words];
116        let mut settled = vec![false; num_detectors];
117        let mut heap = Frontier::new();
118        for (v, &edge) in boundary_edge.iter().enumerate() {
119            if edge != NONE {
120                heap.push(Reverse((weights[edge as usize], v as u32, NONE, edge)));
121            }
122        }
123        while let Some(Reverse((dist, v, from, edge))) = heap.pop() {
124            let at = v as usize;
125            if settled[at] {
126                continue;
127            }
128            settled[at] = true;
129            boundary_dist[at] = dist;
130            let edge_row = &edge_obs[edge as usize * obs_words..(edge as usize + 1) * obs_words];
131            for word in 0..obs_words {
132                let base = if from == NONE {
133                    0
134                } else {
135                    boundary_obs[from as usize * obs_words + word]
136                };
137                boundary_obs[at * obs_words + word] = base ^ edge_row[word];
138            }
139            for slot in adj_offsets[at] as usize..adj_offsets[at + 1] as usize {
140                let next = adj_target[slot];
141                if !settled[next as usize] {
142                    heap.push(Reverse((dist + adj_weight[slot], next, v, adj_edge[slot])));
143                }
144            }
145        }
146
147        Self {
148            num_detectors,
149            num_observables,
150            obs_words,
151            adj_offsets,
152            adj_target,
153            adj_weight,
154            adj_edge,
155            edge_obs,
156            boundary_dist,
157            boundary_obs,
158        }
159    }
160
161    pub fn num_detectors(&self) -> usize {
162        self.num_detectors
163    }
164
165    pub fn num_observables(&self) -> usize {
166        self.num_observables
167    }
168
169    /// Decode packed detector samples into predicted observable flips.
170    ///
171    /// Layouts and bit order as [`super::UnionFindDecoder::decode_packed`].
172    ///
173    /// # Errors
174    ///
175    /// The input measurement count must equal the model's detector count, and
176    /// a shot with an odd number of defects in a detector component without a
177    /// boundary edge rejects the batch, naming the first such shot.
178    pub fn decode_packed(&self, detectors: &PackedShots) -> Result<PackedShots> {
179        decode_batch(self, detectors)
180    }
181
182    /// Decode `detectors` and return the fraction of shots whose predicted
183    /// flips differ from `observables` in any observable; `0.0` for no shots.
184    ///
185    /// # Errors
186    ///
187    /// As [`Self::decode_packed`], and `observables` must hold one bit per
188    /// observable for the same shot count.
189    pub fn logical_error_rate(
190        &self,
191        detectors: &PackedShots,
192        observables: &PackedShots,
193    ) -> Result<f64> {
194        logical_error_rate(&self.decode_packed(detectors)?, observables)
195    }
196
197    /// Match one shot, XOR its predicted flips into `out_row`, and return the
198    /// correction's quantized weight.
199    fn match_shot(
200        &self,
201        row: &[u64],
202        out_row: &mut [u64],
203        s: &mut MatchScratch,
204    ) -> std::result::Result<i64, Stuck> {
205        s.begin_shot();
206        for (word_index, &bits) in row.iter().enumerate() {
207            let mut bits = bits;
208            while bits != 0 {
209                s.defects
210                    .push((word_index * 64) as u32 + bits.trailing_zeros());
211                bits &= bits - 1;
212            }
213        }
214        let k = s.defects.len();
215        if k == 0 {
216            return Ok(0);
217        }
218        s.prepare(k);
219        for defect in 0..k as u32 {
220            let vertex = s.defects[defect as usize];
221            self.settle(s, defect, vertex, 0, NONE, NONE);
222        }
223        for defect in 0..k as u32 {
224            self.schedule_growth(s, defect);
225        }
226
227        while s.roots > 0 {
228            let Some(Reverse(event)) = s.events.pop() else {
229                return Err(s.stuck());
230            };
231            debug_assert!(event.time >= s.now);
232            s.now = event.time;
233            match event.kind {
234                EVENT_FRONTIER => self.on_frontier(s, event.id),
235                EVENT_PAIR => self.on_pair(s, event.id),
236                EVENT_BOUNDARY => self.on_boundary(s, event.id),
237                _ => self.on_expand(s, event.id),
238            }
239        }
240        Ok(self.extract(s, out_row))
241    }
242
243    /// Settle `vertex` into `defect`'s ball, offer every pair the new entry
244    /// meets (at the vertex or across one edge), and push unsettled neighbors.
245    fn settle(
246        &self,
247        s: &mut MatchScratch,
248        defect: u32,
249        vertex: u32,
250        dist: i64,
251        prev: u32,
252        via: u32,
253    ) {
254        let at = vertex as usize;
255        if s.vertex_stamp[at] != s.stamp {
256            s.vertex_stamp[at] = s.stamp;
257            s.vertex_head[at] = NONE;
258        }
259        let entry = s.entries.len() as u32;
260        let mut other = s.vertex_head[at];
261        while other != NONE {
262            let met = s.entries[other as usize];
263            s.offer(defect, entry, met.defect, other, dist + met.dist, NONE);
264            other = met.next;
265        }
266        s.entries.push(BallEntry {
267            defect,
268            dist,
269            prev,
270            via,
271            next: s.vertex_head[at],
272        });
273        s.vertex_head[at] = entry;
274
275        for slot in self.adj_offsets[at] as usize..self.adj_offsets[at + 1] as usize {
276            let next = self.adj_target[slot];
277            let reach = dist + self.adj_weight[slot];
278            let mut reached = false;
279            if s.vertex_stamp[next as usize] == s.stamp {
280                let mut other = s.vertex_head[next as usize];
281                while other != NONE {
282                    let met = s.entries[other as usize];
283                    if met.defect == defect {
284                        reached = true;
285                    } else {
286                        s.offer(
287                            defect,
288                            entry,
289                            met.defect,
290                            other,
291                            reach + met.dist,
292                            self.adj_edge[slot],
293                        );
294                    }
295                    other = met.next;
296                }
297            }
298            if !reached {
299                s.heaps[defect as usize].push(Reverse((reach, next, entry, self.adj_edge[slot])));
300            }
301        }
302    }
303
304    fn schedule_growth(&self, s: &mut MatchScratch, defect: u32) {
305        let radius = s.radius(defect);
306        if let Some(&Reverse((dist, ..))) = s.heaps[defect as usize].peek() {
307            s.push_event((dist - radius).max(0), EVENT_FRONTIER, defect);
308        }
309        let boundary = self.boundary_dist[s.defects[defect as usize] as usize];
310        if boundary < INF {
311            debug_assert!(boundary >= radius);
312            s.push_event(boundary - radius, EVENT_BOUNDARY, defect);
313        }
314    }
315
316    fn reschedule(&self, s: &mut MatchScratch, node: u32) {
317        let label = s.nodes[node as usize].label;
318        let tail = s.nodes[node as usize].tail;
319        let mut defect = s.nodes[node as usize].head;
320        loop {
321            for at in 0..s.incident[defect as usize].len() {
322                let pair = s.incident[defect as usize][at];
323                s.schedule_pair(pair);
324            }
325            if label == Label::S {
326                self.schedule_growth(s, defect);
327            }
328            if defect == tail {
329                break;
330            }
331            defect = s.defect_next[defect as usize];
332        }
333        if label == Label::T && node as usize >= s.defects.len() {
334            let delay = s.dual(node);
335            s.push_event(delay, EVENT_EXPAND, node);
336        }
337    }
338
339    fn on_frontier(&self, s: &mut MatchScratch, defect: u32) {
340        if s.nodes[s.top[defect as usize] as usize].label != Label::S {
341            return;
342        }
343        let radius = s.radius(defect);
344        while let Some(&Reverse((dist, vertex, prev, via))) = s.heaps[defect as usize].peek() {
345            if dist > radius {
346                break;
347            }
348            s.heaps[defect as usize].pop();
349            if s.vertex_stamp[vertex as usize] == s.stamp {
350                let mut other = s.vertex_head[vertex as usize];
351                let mut reached = false;
352                while other != NONE {
353                    let met = s.entries[other as usize];
354                    if met.defect == defect {
355                        reached = true;
356                        break;
357                    }
358                    other = met.next;
359                }
360                if reached {
361                    continue;
362                }
363            }
364            self.settle(s, defect, vertex, dist, prev, via);
365        }
366        if let Some(&Reverse((dist, ..))) = s.heaps[defect as usize].peek() {
367            s.push_event(dist - radius, EVENT_FRONTIER, defect);
368        }
369    }
370
371    fn on_pair(&self, s: &mut MatchScratch, pair: u32) {
372        let Pair { a, b, cand, .. } = s.pairs[pair as usize];
373        let (x, y) = (s.top[a as usize], s.top[b as usize]);
374        if x == y || cand - s.radius(a) - s.radius(b) != 0 {
375            return;
376        }
377        match (s.nodes[x as usize].label, s.nodes[y as usize].label) {
378            (Label::S, Label::S) => {
379                if s.nodes[x as usize].tree == s.nodes[y as usize].tree {
380                    self.form_blossom(s, x, y, pair);
381                } else {
382                    self.augment(s, x, y, pair);
383                }
384            }
385            (Label::S, Label::Frozen) => self.hit_frozen(s, x, y, pair),
386            (Label::Frozen, Label::S) => self.hit_frozen(s, y, x, pair),
387            _ => {}
388        }
389    }
390
391    fn on_boundary(&self, s: &mut MatchScratch, defect: u32) {
392        let x = s.top[defect as usize];
393        if s.nodes[x as usize].label != Label::S
394            || s.radius(defect) != self.boundary_dist[s.defects[defect as usize] as usize]
395        {
396            return;
397        }
398        let tree = s.nodes[x as usize].tree;
399        s.augment_path(x);
400        s.nodes[x as usize].mate = Mate::Boundary(defect);
401        self.dissolve(s, tree);
402    }
403
404    fn on_expand(&self, s: &mut MatchScratch, node: u32) {
405        let n = &s.nodes[node as usize];
406        if n.blossom_parent != NONE || n.label != Label::T || s.dual(node) != 0 {
407            return;
408        }
409        self.expand(s, node);
410    }
411
412    fn augment(&self, s: &mut MatchScratch, x: u32, y: u32, pair: u32) {
413        let (tx, ty) = (s.nodes[x as usize].tree, s.nodes[y as usize].tree);
414        s.augment_path(x);
415        s.augment_path(y);
416        s.nodes[x as usize].mate = Mate::Pair(pair);
417        s.nodes[y as usize].mate = Mate::Pair(pair);
418        self.dissolve(s, tx);
419        self.dissolve(s, ty);
420    }
421
422    /// Tree node `x` reaches the out-of-tree node `y` through `pair`: grow
423    /// the tree through `y` and its mate, or augment when `y` is matched to
424    /// the boundary.
425    fn hit_frozen(&self, s: &mut MatchScratch, x: u32, y: u32, pair: u32) {
426        let tree = s.nodes[x as usize].tree;
427        match s.nodes[y as usize].mate {
428            Mate::Pair(mate_pair) => {
429                let z = s.other_top(mate_pair, y);
430                s.set_slope(y, -1);
431                s.set_slope(z, 1);
432                let ny = &mut s.nodes[y as usize];
433                ny.label = Label::T;
434                ny.tree = tree;
435                ny.parent = pair;
436                let nz = &mut s.nodes[z as usize];
437                nz.label = Label::S;
438                nz.tree = tree;
439                nz.parent = mate_pair;
440                s.trees[tree as usize].push(y);
441                s.trees[tree as usize].push(z);
442                self.reschedule(s, y);
443                self.reschedule(s, z);
444            }
445            Mate::Boundary(_) => {
446                s.augment_path(x);
447                s.nodes[x as usize].mate = Mate::Pair(pair);
448                s.nodes[y as usize].mate = Mate::Pair(pair);
449                self.dissolve(s, tree);
450            }
451            Mate::None => unreachable!("out-of-tree nodes are matched"),
452        }
453    }
454
455    fn dissolve(&self, s: &mut MatchScratch, tree: u32) {
456        s.roots -= 1;
457        let mut members = std::mem::take(&mut s.trees[tree as usize]);
458        members.retain(|&node| {
459            let n = &s.nodes[node as usize];
460            n.blossom_parent == NONE && n.tree == tree
461        });
462        for &node in &members {
463            s.set_slope(node, 0);
464            let n = &mut s.nodes[node as usize];
465            n.label = Label::Frozen;
466            n.tree = NONE;
467            n.parent = NONE;
468        }
469        for &node in &members {
470            self.reschedule(s, node);
471        }
472        members.clear();
473        s.trees[tree as usize] = members;
474    }
475
476    fn form_blossom(&self, s: &mut MatchScratch, x: u32, y: u32, pair: u32) {
477        if s.mark.len() < s.nodes.len() {
478            s.mark.resize(s.nodes.len(), 0);
479        }
480        s.mark_stamp += 1;
481        let stamp = s.mark_stamp;
482        let mut n = x;
483        loop {
484            s.mark[n as usize] = stamp;
485            if s.nodes[n as usize].parent == NONE {
486                break;
487            }
488            n = s.parent_node(n);
489        }
490        s.path_y.clear();
491        let mut n = y;
492        while s.mark[n as usize] != stamp {
493            s.path_y.push(n);
494            n = s.parent_node(n);
495        }
496        let lca = n;
497        s.path_x.clear();
498        let mut n = x;
499        while n != lca {
500            s.path_x.push(n);
501            n = s.parent_node(n);
502        }
503
504        let blossom = s.nodes.len() as u32;
505        let cycle_start = s.cycles.len() as u32;
506        let first_link = match s.path_x.last() {
507            Some(&top_x) => s.nodes[top_x as usize].parent,
508            None => pair,
509        };
510        s.cycles.push(CycleLink {
511            child: lca,
512            pair: first_link,
513        });
514        for at in (0..s.path_x.len()).rev() {
515            let child = s.path_x[at];
516            let link = if at > 0 {
517                s.nodes[s.path_x[at - 1] as usize].parent
518            } else {
519                pair
520            };
521            s.cycles.push(CycleLink { child, pair: link });
522        }
523        for at in 0..s.path_y.len() {
524            let child = s.path_y[at];
525            let link = s.nodes[child as usize].parent;
526            s.cycles.push(CycleLink { child, pair: link });
527        }
528        let cycle_len = s.cycles.len() as u32 - cycle_start;
529        debug_assert!(cycle_len % 2 == 1);
530
531        let base = &s.nodes[lca as usize];
532        let node = Node {
533            y: 0,
534            y_time: s.now,
535            slope: 1,
536            label: Label::S,
537            tree: base.tree,
538            parent: base.parent,
539            mate: base.mate,
540            blossom_parent: NONE,
541            cycle_start,
542            cycle_len,
543            head: base.head,
544            tail: s.nodes[s.cycles[(cycle_start + cycle_len - 1) as usize].child as usize].tail,
545        };
546        s.nodes.push(node);
547        for at in cycle_start..cycle_start + cycle_len {
548            let child = s.cycles[at as usize].child;
549            s.set_slope(child, 0);
550            let c = &mut s.nodes[child as usize];
551            c.blossom_parent = blossom;
552            c.label = Label::Frozen;
553            c.tree = NONE;
554            let (head, tail, dual) = (c.head, c.tail, c.y);
555            if at + 1 < cycle_start + cycle_len {
556                let next = s.cycles[at as usize + 1].child;
557                s.defect_next[tail as usize] = s.nodes[next as usize].head;
558            }
559            let mut defect = head;
560            loop {
561                s.inner[defect as usize] += dual;
562                s.top[defect as usize] = blossom;
563                if defect == tail {
564                    break;
565                }
566                defect = s.defect_next[defect as usize];
567            }
568        }
569        let tree = s.nodes[blossom as usize].tree;
570        s.trees[tree as usize].push(blossom);
571        self.reschedule(s, blossom);
572    }
573
574    /// Expand a T blossom whose dual reached zero: the even-length cycle arc
575    /// from the child holding the tree edge to the child holding the match
576    /// stays in the tree as alternating T and S nodes, and the rest of the
577    /// cycle leaves the tree as matched pairs.
578    fn expand(&self, s: &mut MatchScratch, blossom: u32) {
579        let b = s.nodes[blossom as usize];
580        let Mate::Pair(out_pair) = b.mate else {
581            unreachable!("T blossoms are matched to their tree child");
582        };
583        let in_defect = s.endpoint_in(b.parent, blossom);
584        let out_defect = s.endpoint_in(out_pair, blossom);
585        let start = b.cycle_start as usize;
586        let len = b.cycle_len as usize;
587        for at in start..start + len {
588            let child = s.cycles[at].child;
589            let c = &mut s.nodes[child as usize];
590            c.blossom_parent = NONE;
591            let (head, tail, dual) = (c.head, c.tail, c.y);
592            let mut defect = head;
593            loop {
594                s.inner[defect as usize] -= dual;
595                s.top[defect as usize] = child;
596                if defect == tail {
597                    break;
598                }
599                defect = s.defect_next[defect as usize];
600            }
601        }
602        s.nodes[blossom as usize].blossom_parent = DEAD;
603
604        let child_in = s.top[in_defect as usize];
605        let child_out = s.top[out_defect as usize];
606        let position = |s: &MatchScratch, child: u32| {
607            (0..len)
608                .find(|&at| s.cycles[start + at].child == child)
609                .unwrap()
610        };
611        let p_in = position(s, child_in);
612        let p_out = position(s, child_out);
613        let forward = (p_out + len - p_in) % len;
614        let (step, path_len) = if forward.is_multiple_of(2) {
615            (1, forward)
616        } else {
617            (len - 1, len - forward)
618        };
619        // Link between cycle positions `at` and `at + step`.
620        let link = |s: &MatchScratch, at: usize| {
621            if step == 1 {
622                s.cycles[start + at].pair
623            } else {
624                s.cycles[start + (at + len - 1) % len].pair
625            }
626        };
627
628        let tree = b.tree;
629        let mut at = p_in;
630        let mut prev_link = b.parent;
631        for k in 0..=path_len {
632            let child = s.cycles[start + at].child;
633            let next_link = if k == path_len { out_pair } else { link(s, at) };
634            let (label, slope, mate) = if k % 2 == 0 {
635                (Label::T, -1, next_link)
636            } else {
637                (Label::S, 1, prev_link)
638            };
639            s.set_slope(child, slope);
640            let c = &mut s.nodes[child as usize];
641            c.label = label;
642            c.tree = tree;
643            c.parent = prev_link;
644            c.mate = Mate::Pair(mate);
645            s.trees[tree as usize].push(child);
646            prev_link = next_link;
647            at = (at + step) % len;
648        }
649        for _ in 0..(len - path_len - 1) / 2 {
650            let pair_link = link(s, at);
651            let u = s.cycles[start + at].child;
652            at = (at + step) % len;
653            let v = s.cycles[start + at].child;
654            at = (at + step) % len;
655            for node in [u, v] {
656                s.set_slope(node, 0);
657                let c = &mut s.nodes[node as usize];
658                c.label = Label::Frozen;
659                c.tree = NONE;
660                c.parent = NONE;
661                c.mate = Mate::Pair(pair_link);
662            }
663        }
664        for at in start..start + len {
665            let child = s.cycles[at].child;
666            self.reschedule(s, child);
667        }
668    }
669
670    /// Resolve every blossom to its internal matching, XOR the observable
671    /// mask of each matched path into `out_row`, and return the total weight.
672    fn extract(&self, s: &mut MatchScratch, out_row: &mut [u64]) -> i64 {
673        let k = s.defects.len() as u32;
674        let mut weight = 0i64;
675        s.stack.clear();
676        for node in 0..s.nodes.len() as u32 {
677            let Node {
678                blossom_parent,
679                mate,
680                ..
681            } = s.nodes[node as usize];
682            if blossom_parent != NONE {
683                continue;
684            }
685            match mate {
686                Mate::Pair(pair) => {
687                    let inside = s.endpoint_in(pair, node);
688                    if inside == s.pairs[pair as usize].a {
689                        weight += self.pair_flips(s, pair, out_row);
690                    }
691                    s.stack.push((node, inside));
692                }
693                Mate::Boundary(defect) => {
694                    weight += self.boundary_flips(s, defect, out_row);
695                    s.stack.push((node, defect));
696                }
697                Mate::None => unreachable!("every defect is matched when no tree is left"),
698            }
699        }
700        while let Some((node, outside_defect)) = s.stack.pop() {
701            if node < k {
702                continue;
703            }
704            let start = s.nodes[node as usize].cycle_start as usize;
705            let len = s.nodes[node as usize].cycle_len as usize;
706            let entry_child = s.child_holding(node, outside_defect);
707            let p = (0..len)
708                .find(|&at| s.cycles[start + at].child == entry_child)
709                .unwrap();
710            s.stack.push((entry_child, outside_defect));
711            for j in (1..len).step_by(2) {
712                let pair = s.cycles[start + (p + j) % len].pair;
713                let u = s.cycles[start + (p + j) % len].child;
714                let v = s.cycles[start + (p + j + 1) % len].child;
715                weight += self.pair_flips(s, pair, out_row);
716                let Pair { a, b, .. } = s.pairs[pair as usize];
717                let (in_u, in_v) = if s.child_holding(node, a) == u {
718                    (a, b)
719                } else {
720                    (b, a)
721                };
722                s.stack.push((u, in_u));
723                s.stack.push((v, in_v));
724            }
725        }
726        weight
727    }
728
729    fn pair_flips(&self, s: &MatchScratch, pair: u32, out_row: &mut [u64]) -> i64 {
730        let p = s.pairs[pair as usize];
731        if p.via != NONE {
732            self.xor_edge(p.via, out_row);
733        }
734        for start in [p.meet_a, p.meet_b] {
735            let mut entry = start;
736            while entry != NONE {
737                let e = s.entries[entry as usize];
738                if e.via != NONE {
739                    self.xor_edge(e.via, out_row);
740                }
741                entry = e.prev;
742            }
743        }
744        p.cand
745    }
746
747    fn boundary_flips(&self, s: &MatchScratch, defect: u32, out_row: &mut [u64]) -> i64 {
748        let vertex = s.defects[defect as usize] as usize;
749        let row = &self.boundary_obs[vertex * self.obs_words..(vertex + 1) * self.obs_words];
750        for (word, mask) in out_row.iter_mut().zip(row) {
751            *word ^= mask;
752        }
753        self.boundary_dist[vertex]
754    }
755
756    #[inline]
757    fn xor_edge(&self, edge: u32, out_row: &mut [u64]) {
758        let base = edge as usize * self.obs_words;
759        for (word, mask) in out_row
760            .iter_mut()
761            .zip(&self.edge_obs[base..base + self.obs_words])
762        {
763            *word ^= mask;
764        }
765    }
766}
767
768impl ShotDecoder for MatchingDecoder {
769    type Scratch = MatchScratch;
770    type Failure = Stuck;
771
772    fn num_detectors(&self) -> usize {
773        self.num_detectors
774    }
775
776    fn num_observables(&self) -> usize {
777        self.num_observables
778    }
779
780    fn scratch(&self) -> MatchScratch {
781        MatchScratch::new(self.num_detectors)
782    }
783
784    fn decode_shot(
785        &self,
786        row: &[u64],
787        out_row: &mut [u64],
788        scratch: &mut MatchScratch,
789    ) -> std::result::Result<(), Stuck> {
790        self.match_shot(row, out_row, scratch).map(|_| ())
791    }
792
793    fn failure_error(failure: Stuck, shot: usize) -> PrismError {
794        failure.into_error(shot)
795    }
796}
797
798/// Map each mechanism probability to an even integer weight proportional to
799/// `ln((1-p)/p)` clamped at zero.
800fn quantized_weights(edge_p: &[f64]) -> Vec<i64> {
801    let raw: Vec<f64> = edge_p
802        .iter()
803        .map(|&p| ((1.0 - p) / p).ln().max(0.0))
804        .collect();
805    let largest = raw.iter().copied().fold(0.0f64, f64::max);
806    let scale = if largest > 0.0 {
807        WEIGHT_LEVELS / largest
808    } else {
809        0.0
810    };
811    raw.iter()
812        .map(|&w| 2 * (w * scale).round() as i64)
813        .collect()
814}
815
816/// Dijkstra frontier: `(distance, vertex, predecessor, edge)` per pending
817/// vertex, nearest first.
818type Frontier = BinaryHeap<Reverse<(i64, u32, u32, u32)>>;
819
820/// One defect's settled vertex. `prev` and `via` trace the shortest path back
821/// to the defect; `next` chains the entries settled at the same vertex.
822#[derive(Debug, Clone, Copy)]
823struct BallEntry {
824    defect: u32,
825    dist: i64,
826    prev: u32,
827    via: u32,
828    next: u32,
829}
830
831/// Best known path between two defects: `cand` is the length through the
832/// meeting entries (joined by graph edge `via`, or at one vertex when `via`
833/// is `NONE`), exact once both balls cover it.
834#[derive(Debug, Clone, Copy)]
835struct Pair {
836    a: u32,
837    b: u32,
838    cand: i64,
839    meet_a: u32,
840    meet_b: u32,
841    via: u32,
842}
843
844#[derive(Debug, Clone, Copy, PartialEq, Eq)]
845enum Label {
846    S,
847    T,
848    Frozen,
849}
850
851#[derive(Debug, Clone, Copy)]
852enum Mate {
853    None,
854    Pair(u32),
855    Boundary(u32),
856}
857
858/// Matching node: a defect (ids below the shot's defect count) or a blossom.
859/// The dual is `y + slope * (now - y_time)`; only top-level nodes move. Tree
860/// and match edges are pair ids, resolved to nodes through `top`, so blossom
861/// formation and expansion rewire nothing outside the blossom.
862#[derive(Debug, Clone, Copy)]
863struct Node {
864    y: i64,
865    y_time: i64,
866    slope: i64,
867    label: Label,
868    tree: u32,
869    parent: u32,
870    mate: Mate,
871    blossom_parent: u32,
872    cycle_start: u32,
873    cycle_len: u32,
874    head: u32,
875    tail: u32,
876}
877
878/// Blossom cycle child and the pair linking it to the next child.
879#[derive(Debug, Clone, Copy)]
880struct CycleLink {
881    child: u32,
882    pair: u32,
883}
884
885#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
886struct Event {
887    time: i64,
888    kind: u8,
889    id: u32,
890}
891
892/// Reusable per-shot matching state. Vertex slots are validated by stamp, so
893/// nothing sized by the graph is cleared between shots; the per-defect pools
894/// keep their capacity.
895pub(super) struct MatchScratch {
896    stamp: u64,
897    vertex_stamp: Vec<u64>,
898    vertex_head: Vec<u32>,
899    entries: Vec<BallEntry>,
900    heaps: Vec<Frontier>,
901    defects: Vec<u32>,
902    pairs: Vec<Pair>,
903    incident: Vec<Vec<u32>>,
904    nodes: Vec<Node>,
905    cycles: Vec<CycleLink>,
906    defect_next: Vec<u32>,
907    inner: Vec<i64>,
908    top: Vec<u32>,
909    trees: Vec<Vec<u32>>,
910    events: BinaryHeap<Reverse<Event>>,
911    mark: Vec<u64>,
912    mark_stamp: u64,
913    path_x: Vec<u32>,
914    path_y: Vec<u32>,
915    stack: Vec<(u32, u32)>,
916    now: i64,
917    roots: usize,
918}
919
920impl MatchScratch {
921    fn new(num_detectors: usize) -> Self {
922        Self {
923            stamp: 0,
924            vertex_stamp: vec![0; num_detectors],
925            vertex_head: vec![NONE; num_detectors],
926            entries: Vec::new(),
927            heaps: Vec::new(),
928            defects: Vec::new(),
929            pairs: Vec::new(),
930            incident: Vec::new(),
931            nodes: Vec::new(),
932            cycles: Vec::new(),
933            defect_next: Vec::new(),
934            inner: Vec::new(),
935            top: Vec::new(),
936            trees: Vec::new(),
937            events: BinaryHeap::new(),
938            mark: Vec::new(),
939            mark_stamp: 0,
940            path_x: Vec::new(),
941            path_y: Vec::new(),
942            stack: Vec::new(),
943            now: 0,
944            roots: 0,
945        }
946    }
947
948    fn begin_shot(&mut self) {
949        self.stamp += 1;
950        self.defects.clear();
951    }
952
953    fn prepare(&mut self, k: usize) {
954        self.entries.clear();
955        self.pairs.clear();
956        self.nodes.clear();
957        self.cycles.clear();
958        self.events.clear();
959        self.defect_next.clear();
960        self.inner.clear();
961        self.top.clear();
962        if self.heaps.len() < k {
963            self.heaps.resize_with(k, BinaryHeap::new);
964            self.incident.resize_with(k, Vec::new);
965            self.trees.resize_with(k, Vec::new);
966        }
967        for defect in 0..k as u32 {
968            self.heaps[defect as usize].clear();
969            self.incident[defect as usize].clear();
970            self.trees[defect as usize].clear();
971            self.trees[defect as usize].push(defect);
972            self.nodes.push(Node {
973                y: 0,
974                y_time: 0,
975                slope: 1,
976                label: Label::S,
977                tree: defect,
978                parent: NONE,
979                mate: Mate::None,
980                blossom_parent: NONE,
981                cycle_start: 0,
982                cycle_len: 0,
983                head: defect,
984                tail: defect,
985            });
986            self.defect_next.push(NONE);
987            self.inner.push(0);
988            self.top.push(defect);
989        }
990        self.now = 0;
991        self.roots = k;
992    }
993
994    #[inline]
995    fn dual(&self, node: u32) -> i64 {
996        let n = &self.nodes[node as usize];
997        n.y + n.slope * (self.now - n.y_time)
998    }
999
1000    /// Total dual of every node containing `defect`: its ball radius.
1001    #[inline]
1002    fn radius(&self, defect: u32) -> i64 {
1003        self.inner[defect as usize] + self.dual(self.top[defect as usize])
1004    }
1005
1006    fn set_slope(&mut self, node: u32, slope: i64) {
1007        let y = self.dual(node);
1008        let now = self.now;
1009        let n = &mut self.nodes[node as usize];
1010        n.y = y;
1011        n.y_time = now;
1012        n.slope = slope;
1013    }
1014
1015    fn push_event(&mut self, delay: i64, kind: u8, id: u32) {
1016        self.events.push(Reverse(Event {
1017            time: self.now + delay,
1018            kind,
1019            id,
1020        }));
1021    }
1022
1023    fn offer(
1024        &mut self,
1025        defect: u32,
1026        entry: u32,
1027        other: u32,
1028        other_entry: u32,
1029        cand: i64,
1030        via: u32,
1031    ) {
1032        let found = self.incident[defect as usize]
1033            .iter()
1034            .copied()
1035            .find(|&pair| {
1036                let p = &self.pairs[pair as usize];
1037                p.a == other || p.b == other
1038            });
1039        let pair = match found {
1040            Some(pair) => {
1041                let p = &mut self.pairs[pair as usize];
1042                if cand >= p.cand {
1043                    return;
1044                }
1045                p.cand = cand;
1046                p.via = via;
1047                if p.a == defect {
1048                    p.meet_a = entry;
1049                    p.meet_b = other_entry;
1050                } else {
1051                    p.meet_a = other_entry;
1052                    p.meet_b = entry;
1053                }
1054                pair
1055            }
1056            None => {
1057                let pair = self.pairs.len() as u32;
1058                self.pairs.push(Pair {
1059                    a: defect,
1060                    b: other,
1061                    cand,
1062                    meet_a: entry,
1063                    meet_b: other_entry,
1064                    via,
1065                });
1066                self.incident[defect as usize].push(pair);
1067                self.incident[other as usize].push(pair);
1068                pair
1069            }
1070        };
1071        self.schedule_pair(pair);
1072    }
1073
1074    fn schedule_pair(&mut self, pair: u32) {
1075        let Pair { a, b, cand, .. } = self.pairs[pair as usize];
1076        let (x, y) = (self.top[a as usize], self.top[b as usize]);
1077        if x == y {
1078            return;
1079        }
1080        let rate = self.nodes[x as usize].slope + self.nodes[y as usize].slope;
1081        if rate <= 0 {
1082            return;
1083        }
1084        let slack = cand - self.radius(a) - self.radius(b);
1085        debug_assert!(slack >= 0, "dual infeasible pair: slack {slack}");
1086        let delay = if rate == 2 {
1087            debug_assert!(slack % 2 == 0, "odd slack {slack} between growing nodes");
1088            slack / 2
1089        } else {
1090            slack
1091        };
1092        self.push_event(delay, EVENT_PAIR, pair);
1093    }
1094
1095    /// The endpoint of `pair` inside top-level `node`.
1096    fn endpoint_in(&self, pair: u32, node: u32) -> u32 {
1097        let p = &self.pairs[pair as usize];
1098        if self.top[p.a as usize] == node {
1099            p.a
1100        } else {
1101            p.b
1102        }
1103    }
1104
1105    /// The child of `blossom` that contains `defect`.
1106    fn child_holding(&self, blossom: u32, defect: u32) -> u32 {
1107        let mut n = defect;
1108        while self.nodes[n as usize].blossom_parent != blossom {
1109            n = self.nodes[n as usize].blossom_parent;
1110        }
1111        n
1112    }
1113
1114    fn other_top(&self, pair: u32, node: u32) -> u32 {
1115        let p = &self.pairs[pair as usize];
1116        let ta = self.top[p.a as usize];
1117        if ta == node {
1118            self.top[p.b as usize]
1119        } else {
1120            ta
1121        }
1122    }
1123
1124    fn parent_node(&self, node: u32) -> u32 {
1125        self.other_top(self.nodes[node as usize].parent, node)
1126    }
1127
1128    /// Flip matched and unmatched tree edges from S node `node` up to its root.
1129    fn augment_path(&mut self, node: u32) {
1130        let mut n = node;
1131        while self.nodes[n as usize].parent != NONE {
1132            let t = self.parent_node(n);
1133            let edge = self.nodes[t as usize].parent;
1134            let s = self.parent_node(t);
1135            self.nodes[t as usize].mate = Mate::Pair(edge);
1136            self.nodes[s as usize].mate = Mate::Pair(edge);
1137            n = s;
1138        }
1139    }
1140
1141    fn stuck(&self) -> Stuck {
1142        let detector = (0..self.defects.len())
1143            .filter(|&d| self.nodes[self.top[d] as usize].label == Label::S)
1144            .map(|d| self.defects[d])
1145            .min()
1146            .unwrap_or(0);
1147        Stuck { detector }
1148    }
1149}
1150
1151#[cfg(test)]
1152#[path = "matching_tests.rs"]
1153mod tests;