Skip to main content

prism_q/qec/
decoder.rs

1//! Union-find decoder over graphlike detector error models, and the batch driver
2//! every in-crate decoder runs through.
3//!
4//! Weighted cluster growth in the `ln((1-p)/p)` metric followed by peeling on
5//! the grown erasure, per Delfosse and Nickerson (arXiv:1709.06218).
6
7use std::collections::HashMap;
8use std::collections::hash_map::Entry;
9
10use super::DetectorErrorModel;
11use super::dem::symptom_label;
12use crate::error::{PrismError, Result};
13use crate::sim::compiled::{PackedShots, ShotLayout};
14
15pub(super) const BOUNDARY: u32 = u32::MAX;
16const EDGE_NONE: u32 = u32::MAX;
17const VERTEX_NONE: u32 = u32::MAX;
18const GROWTH_EPS: f64 = 1e-9;
19
20/// Union-find decoder compiled from a graphlike detector error model.
21///
22/// Detectors are vertices. A two-detector mechanism becomes an internal edge
23/// and a one-detector mechanism a boundary edge, each weighted `ln((1-p)/p)`
24/// clamped at zero. Mechanisms flipping no detector cannot enter the graph;
25/// their probability mass bounds the logical error rate any decoder over the
26/// model can reach. Mechanisms sharing one detector set collapse to the most
27/// probable of them. Decoding uses no randomness: growth, fusion, and peeling
28/// break every tie by ascending edge index in mechanism order, so equal
29/// inputs give equal outputs on any thread count.
30#[derive(Debug, Clone)]
31pub struct UnionFindDecoder {
32    num_detectors: usize,
33    num_observables: usize,
34    obs_words: usize,
35    edge_u: Vec<u32>,
36    edge_v: Vec<u32>,
37    edge_weight: Vec<f64>,
38    edge_obs: Vec<u64>,
39    adj_offsets: Vec<u32>,
40    adj_edge: Vec<u32>,
41}
42
43impl UnionFindDecoder {
44    /// Compile a decoder from a graphlike detector error model.
45    ///
46    /// # Errors
47    ///
48    /// A mechanism flipping more than two detectors is rejected with a
49    /// pointer to [`DetectorErrorModel::decompose_graphlike`]. Mechanism
50    /// probabilities must lie in `[0, 1)`; a zero-probability mechanism is
51    /// skipped rather than rejected.
52    pub fn from_model(model: &DetectorErrorModel) -> Result<Self> {
53        let GraphEdges {
54            num_detectors,
55            num_observables,
56            edge_u,
57            edge_v,
58            edge_p,
59            edge_obs,
60        } = graphlike_edges(model, "union-find")?;
61        let obs_words = num_observables.div_ceil(64);
62        let edge_weight: Vec<f64> = edge_p
63            .iter()
64            .map(|&p| ((1.0 - p) / p).ln().max(0.0))
65            .collect();
66
67        let mut adj_offsets = vec![0u32; num_detectors + 1];
68        for edge in 0..edge_u.len() {
69            adj_offsets[edge_u[edge] as usize + 1] += 1;
70            if edge_v[edge] != BOUNDARY {
71                adj_offsets[edge_v[edge] as usize + 1] += 1;
72            }
73        }
74        for v in 0..num_detectors {
75            adj_offsets[v + 1] += adj_offsets[v];
76        }
77        let mut cursor = adj_offsets.clone();
78        let mut adj_edge = vec![0u32; *adj_offsets.last().unwrap() as usize];
79        for edge in 0..edge_u.len() {
80            let u = edge_u[edge] as usize;
81            adj_edge[cursor[u] as usize] = edge as u32;
82            cursor[u] += 1;
83            if edge_v[edge] != BOUNDARY {
84                let v = edge_v[edge] as usize;
85                adj_edge[cursor[v] as usize] = edge as u32;
86                cursor[v] += 1;
87            }
88        }
89
90        Ok(Self {
91            num_detectors,
92            num_observables,
93            obs_words,
94            edge_u,
95            edge_v,
96            edge_weight,
97            edge_obs,
98            adj_offsets,
99            adj_edge,
100        })
101    }
102
103    pub fn num_detectors(&self) -> usize {
104        self.num_detectors
105    }
106
107    pub fn num_observables(&self) -> usize {
108        self.num_observables
109    }
110
111    /// Decode packed detector samples into predicted observable flips.
112    ///
113    /// Accepts either layout with one bit per detector per shot, detector `d`
114    /// at bit index `d`. Returns shot-major records with one bit per
115    /// observable per shot, observable `o` at bit index `o`.
116    ///
117    /// # Errors
118    ///
119    /// The input measurement count must equal the model's detector count, and
120    /// every shot must be explainable: a detector component with odd defect
121    /// parity and no boundary edge rejects the batch, naming the first such
122    /// shot.
123    pub fn decode_packed(&self, detectors: &PackedShots) -> Result<PackedShots> {
124        decode_batch(self, detectors)
125    }
126
127    /// Decode `detectors` and return the fraction of shots whose predicted
128    /// flips differ from `observables` in any observable; `0.0` for no shots.
129    ///
130    /// # Errors
131    ///
132    /// As [`Self::decode_packed`], and `observables` must hold one bit per
133    /// observable for the same shot count.
134    pub fn logical_error_rate(
135        &self,
136        detectors: &PackedShots,
137        observables: &PackedShots,
138    ) -> Result<f64> {
139        logical_error_rate(&self.decode_packed(detectors)?, observables)
140    }
141
142    fn decode_shot(
143        &self,
144        row: &[u64],
145        out_row: &mut [u64],
146        s: &mut DecodeScratch,
147    ) -> std::result::Result<(), Stuck> {
148        s.stamp += 1;
149        s.shot_stamp = s.stamp;
150
151        s.defects.clear();
152        for (word_index, &bits) in row.iter().enumerate() {
153            let mut bits = bits;
154            while bits != 0 {
155                s.defects
156                    .push((word_index * 64) as u32 + bits.trailing_zeros());
157                bits &= bits - 1;
158            }
159        }
160        if s.defects.is_empty() {
161            return Ok(());
162        }
163
164        s.active.clear();
165        let mut i = 0;
166        while i < s.defects.len() {
167            let defect = s.defects[i];
168            i += 1;
169            s.activate(defect);
170            s.parity[defect as usize] = true;
171            s.defect_stamp[defect as usize] = s.shot_stamp;
172            s.active.push(defect);
173        }
174
175        self.grow_clusters(s)?;
176
177        let mut i = 0;
178        while i < s.defects.len() {
179            let root = s.find(s.defects[i]);
180            i += 1;
181            if s.peeled_stamp[root as usize] == s.shot_stamp {
182                continue;
183            }
184            s.peeled_stamp[root as usize] = s.shot_stamp;
185            self.peel_cluster(root, out_row, s);
186        }
187        Ok(())
188    }
189
190    // Each round grows every active cluster's non-saturated incident edges by
191    // one shared increment: the minimum slack over those edges, divided by how
192    // many active clusters touch the edge, so at least one edge saturates per
193    // round and the loop is bounded by the edge count.
194    fn grow_clusters(&self, s: &mut DecodeScratch) -> std::result::Result<(), Stuck> {
195        loop {
196            s.stamp += 1;
197            let round = s.stamp;
198
199            let mut live = 0usize;
200            let mut i = 0;
201            while i < s.active.len() {
202                let root = s.find(s.active[i]);
203                i += 1;
204                if s.seen_stamp[root as usize] == round {
205                    continue;
206                }
207                s.seen_stamp[root as usize] = round;
208                if s.parity[root as usize] && !s.boundary[root as usize] {
209                    s.active[live] = root;
210                    live += 1;
211                }
212            }
213            s.active.truncate(live);
214            if s.active.is_empty() {
215                return Ok(());
216            }
217
218            s.touched.clear();
219            for &root in &s.active {
220                let mut grew = false;
221                let mut lowest = root;
222                let mut v = root;
223                while v != VERTEX_NONE {
224                    lowest = lowest.min(v);
225                    let begin = self.adj_offsets[v as usize] as usize;
226                    let end = self.adj_offsets[v as usize + 1] as usize;
227                    for &edge in &self.adj_edge[begin..end] {
228                        let e = edge as usize;
229                        if s.edge_stamp[e] == s.shot_stamp && s.edge_saturated[e] {
230                            continue;
231                        }
232                        grew = true;
233                        if s.touch_stamp[e] == round {
234                            s.touch_count[e] += 1;
235                        } else {
236                            s.touch_stamp[e] = round;
237                            s.touch_count[e] = 1;
238                            s.touched.push(edge);
239                        }
240                    }
241                    v = s.list_next[v as usize];
242                }
243                if !grew {
244                    return Err(Stuck { detector: lowest });
245                }
246            }
247
248            let mut delta = f64::INFINITY;
249            for &edge in &s.touched {
250                let e = edge as usize;
251                let growth = if s.edge_stamp[e] == s.shot_stamp {
252                    s.edge_growth[e]
253                } else {
254                    0.0
255                };
256                let step = (self.edge_weight[e] - growth) / f64::from(s.touch_count[e]);
257                if step < delta {
258                    delta = step;
259                }
260            }
261
262            s.fused.clear();
263            for &edge in &s.touched {
264                let e = edge as usize;
265                if s.edge_stamp[e] != s.shot_stamp {
266                    s.edge_stamp[e] = s.shot_stamp;
267                    s.edge_growth[e] = 0.0;
268                    s.edge_saturated[e] = false;
269                }
270                s.edge_growth[e] += f64::from(s.touch_count[e]) * delta;
271                if s.edge_growth[e] + GROWTH_EPS >= self.edge_weight[e] {
272                    s.fused.push(e as u32);
273                }
274            }
275            s.fused.sort_unstable();
276            let mut i = 0;
277            while i < s.fused.len() {
278                let edge = s.fused[i];
279                i += 1;
280                s.edge_saturated[edge as usize] = true;
281                let u = self.edge_u[edge as usize];
282                let v = self.edge_v[edge as usize];
283                s.activate(u);
284                if v == BOUNDARY {
285                    let root = s.find(u);
286                    s.boundary[root as usize] = true;
287                    s.boundary_edge[root as usize] = s.boundary_edge[root as usize].min(edge);
288                } else {
289                    s.activate(v);
290                    let ru = s.find(u);
291                    let rv = s.find(v);
292                    if ru != rv {
293                        s.union(ru, rv);
294                    }
295                }
296            }
297        }
298    }
299
300    // Spanning-forest peel: leaves flush their defect through the tree edge
301    // toward the root, which is the interior endpoint of the designated
302    // boundary edge when the cluster touches the boundary.
303    fn peel_cluster(&self, root: u32, out_row: &mut [u64], s: &mut DecodeScratch) {
304        let start = if s.boundary[root as usize] {
305            self.edge_u[s.boundary_edge[root as usize] as usize]
306        } else {
307            let mut lowest = root;
308            let mut v = root;
309            while v != VERTEX_NONE {
310                lowest = lowest.min(v);
311                v = s.list_next[v as usize];
312            }
313            lowest
314        };
315
316        s.order.clear();
317        s.stack.clear();
318        s.dfs_stamp[start as usize] = s.shot_stamp;
319        s.stack.push(start);
320        while let Some(v) = s.stack.pop() {
321            let begin = self.adj_offsets[v as usize] as usize;
322            let end = self.adj_offsets[v as usize + 1] as usize;
323            for &edge in &self.adj_edge[begin..end] {
324                let e = edge as usize;
325                if s.edge_stamp[e] != s.shot_stamp || !s.edge_saturated[e] {
326                    continue;
327                }
328                if self.edge_v[e] == BOUNDARY {
329                    continue;
330                }
331                let other = if self.edge_u[e] == v {
332                    self.edge_v[e]
333                } else {
334                    self.edge_u[e]
335                };
336                if s.dfs_stamp[other as usize] == s.shot_stamp {
337                    continue;
338                }
339                s.dfs_stamp[other as usize] = s.shot_stamp;
340                s.order.push((other, edge, v));
341                s.stack.push(other);
342            }
343        }
344
345        for &(vertex, edge, parent) in s.order.iter().rev() {
346            if s.defect_stamp[vertex as usize] != s.shot_stamp {
347                continue;
348            }
349            s.defect_stamp[vertex as usize] = 0;
350            if s.defect_stamp[parent as usize] == s.shot_stamp {
351                s.defect_stamp[parent as usize] = 0;
352            } else {
353                s.defect_stamp[parent as usize] = s.shot_stamp;
354            }
355            self.xor_edge_observables(edge, out_row);
356        }
357
358        if s.defect_stamp[start as usize] == s.shot_stamp {
359            s.defect_stamp[start as usize] = 0;
360            debug_assert!(s.boundary[root as usize]);
361            self.xor_edge_observables(s.boundary_edge[root as usize], out_row);
362        }
363    }
364
365    #[inline]
366    fn xor_edge_observables(&self, edge: u32, out_row: &mut [u64]) {
367        let base = edge as usize * self.obs_words;
368        for (word, mask) in out_row
369            .iter_mut()
370            .zip(&self.edge_obs[base..base + self.obs_words])
371        {
372            *word ^= mask;
373        }
374    }
375}
376
377/// Deduplicated edges of a graphlike model. `edge_v` is [`BOUNDARY`] for a
378/// one-detector mechanism; `edge_obs` packs `ceil(num_observables / 64)` words
379/// per edge.
380pub(super) struct GraphEdges {
381    pub(super) num_detectors: usize,
382    pub(super) num_observables: usize,
383    pub(super) edge_u: Vec<u32>,
384    pub(super) edge_v: Vec<u32>,
385    pub(super) edge_p: Vec<f64>,
386    pub(super) edge_obs: Vec<u64>,
387}
388
389/// Collect a graphlike model's edges, collapsing mechanisms that share one
390/// detector set to the most probable of them. `decoding` names the decoder in
391/// the hypergraph rejection message.
392pub(super) fn graphlike_edges(model: &DetectorErrorModel, decoding: &str) -> Result<GraphEdges> {
393    if model.num_detectors() >= BOUNDARY as usize {
394        return Err(PrismError::InvalidParameter {
395            message: format!(
396                "{} detectors exceed the decoder's index range",
397                model.num_detectors()
398            ),
399        });
400    }
401    let obs_words = model.num_observables().div_ceil(64);
402    let mut edge_u: Vec<u32> = Vec::new();
403    let mut edge_v: Vec<u32> = Vec::new();
404    let mut edge_p: Vec<f64> = Vec::new();
405    let mut edge_obs_rows: Vec<&[usize]> = Vec::new();
406    let mut slots: HashMap<(u32, u32), u32> = HashMap::new();
407    for mechanism in model.mechanisms() {
408        let p = mechanism.probability();
409        if !(0.0..1.0).contains(&p) {
410            return Err(PrismError::InvalidParameter {
411                message: format!(
412                    "mechanism `{}` has probability {p}, outside [0, 1)",
413                    symptom_label(mechanism)
414                ),
415            });
416        }
417        if p == 0.0 {
418            continue;
419        }
420        let endpoints = match *mechanism.detectors() {
421            [] => continue,
422            [d] => (d as u32, BOUNDARY),
423            [d0, d1] => (d0 as u32, d1 as u32),
424            _ => {
425                return Err(PrismError::InvalidParameter {
426                    message: format!(
427                        "mechanism `{}` flips {} detectors; {decoding} decoding needs a                          graphlike model, apply `decompose_graphlike` first",
428                        symptom_label(mechanism),
429                        mechanism.detectors().len()
430                    ),
431                });
432            }
433        };
434        match slots.entry(endpoints) {
435            Entry::Occupied(slot) => {
436                let at = *slot.get() as usize;
437                if p > edge_p[at] {
438                    edge_p[at] = p;
439                    edge_obs_rows[at] = mechanism.observables();
440                }
441            }
442            Entry::Vacant(slot) => {
443                slot.insert(edge_u.len() as u32);
444                edge_u.push(endpoints.0);
445                edge_v.push(endpoints.1);
446                edge_p.push(p);
447                edge_obs_rows.push(mechanism.observables());
448            }
449        }
450    }
451
452    let mut edge_obs = vec![0u64; edge_u.len() * obs_words];
453    for (edge, row) in edge_obs_rows.iter().enumerate() {
454        for &observable in *row {
455            edge_obs[edge * obs_words + observable / 64] |= 1u64 << (observable % 64);
456        }
457    }
458    Ok(GraphEdges {
459        num_detectors: model.num_detectors(),
460        num_observables: model.num_observables(),
461        edge_u,
462        edge_v,
463        edge_p,
464        edge_obs,
465    })
466}
467
468impl ShotDecoder for UnionFindDecoder {
469    type Scratch = DecodeScratch;
470    type Failure = Stuck;
471
472    fn num_detectors(&self) -> usize {
473        self.num_detectors
474    }
475
476    fn num_observables(&self) -> usize {
477        self.num_observables
478    }
479
480    fn scratch(&self) -> DecodeScratch {
481        DecodeScratch::new(self)
482    }
483
484    fn decode_shot(
485        &self,
486        row: &[u64],
487        out_row: &mut [u64],
488        scratch: &mut DecodeScratch,
489    ) -> std::result::Result<(), Stuck> {
490        UnionFindDecoder::decode_shot(self, row, out_row, scratch)
491    }
492
493    fn failure_error(failure: Stuck, shot: usize) -> PrismError {
494        failure.into_error(shot)
495    }
496}
497
498/// Per-shot contract the batch driver runs a decoder through. A shot reads
499/// one shot-major detector row and XORs its prediction into a zeroed
500/// observable row.
501pub(super) trait ShotDecoder: Sync {
502    type Scratch;
503    type Failure: Send;
504    /// Batches at least this large decode on Rayon.
505    #[cfg(feature = "parallel")]
506    const PARALLEL_SHOT_THRESHOLD: usize = 1024;
507    /// Shots per Rayon task; each task builds one scratch.
508    #[cfg(feature = "parallel")]
509    const SHOT_CHUNK: usize = 256;
510
511    fn num_detectors(&self) -> usize;
512    fn num_observables(&self) -> usize;
513    fn scratch(&self) -> Self::Scratch;
514    fn decode_shot(
515        &self,
516        row: &[u64],
517        out_row: &mut [u64],
518        scratch: &mut Self::Scratch,
519    ) -> std::result::Result<(), Self::Failure>;
520    fn failure_error(failure: Self::Failure, shot: usize) -> PrismError;
521}
522
523pub(super) fn decode_batch<D: ShotDecoder>(
524    decoder: &D,
525    detectors: &PackedShots,
526) -> Result<PackedShots> {
527    let num_detectors = decoder.num_detectors();
528    let num_observables = decoder.num_observables();
529    if detectors.num_measurements() != num_detectors {
530        return Err(PrismError::InvalidParameter {
531            message: format!(
532                "detector shots carry {} measurements, the model has {} detectors",
533                detectors.num_measurements(),
534                num_detectors
535            ),
536        });
537    }
538    let num_shots = detectors.num_shots();
539    let m_words = num_detectors.div_ceil(64);
540    let transposed;
541    let rows: &[u64] = match detectors.layout() {
542        ShotLayout::ShotMajor => detectors.raw_data(),
543        ShotLayout::MeasMajor => {
544            transposed = detectors.clone().into_shot_major_data();
545            &transposed
546        }
547    };
548    let out_words = num_observables.div_ceil(64);
549    let mut out = vec![0u64; num_shots * out_words];
550
551    #[cfg(feature = "parallel")]
552    if num_shots >= D::PARALLEL_SHOT_THRESHOLD && out_words > 0 {
553        use rayon::prelude::*;
554        let failure = out
555            .par_chunks_mut(D::SHOT_CHUNK * out_words)
556            .enumerate()
557            .map_init(
558                || decoder.scratch(),
559                |scratch, (chunk, chunk_out)| {
560                    for (offset, shot_out) in chunk_out.chunks_mut(out_words).enumerate() {
561                        let shot = chunk * D::SHOT_CHUNK + offset;
562                        let row = &rows[shot * m_words..(shot + 1) * m_words];
563                        if let Err(failure) = decoder.decode_shot(row, shot_out, scratch) {
564                            return Some((shot, failure));
565                        }
566                    }
567                    None
568                },
569            )
570            .reduce(
571                || None,
572                |a, b| match (a, b) {
573                    (Some(a), Some(b)) => Some(if a.0 <= b.0 { a } else { b }),
574                    (a, b) => a.or(b),
575                },
576            );
577        if let Some((shot, failure)) = failure {
578            return Err(D::failure_error(failure, shot));
579        }
580        return Ok(PackedShots::from_shot_major(
581            out,
582            num_shots,
583            num_observables,
584        ));
585    }
586
587    let mut scratch = decoder.scratch();
588    for shot in 0..num_shots {
589        let row = &rows[shot * m_words..(shot + 1) * m_words];
590        let shot_out = &mut out[shot * out_words..(shot + 1) * out_words];
591        decoder
592            .decode_shot(row, shot_out, &mut scratch)
593            .map_err(|failure| D::failure_error(failure, shot))?;
594    }
595    Ok(PackedShots::from_shot_major(
596        out,
597        num_shots,
598        num_observables,
599    ))
600}
601
602pub(super) fn logical_error_rate(
603    predicted: &PackedShots,
604    observables: &PackedShots,
605) -> Result<f64> {
606    if observables.num_shots() != predicted.num_shots()
607        || observables.num_measurements() != predicted.num_measurements()
608    {
609        return Err(PrismError::InvalidParameter {
610            message: format!(
611                "observable records hold {} shots of {} observables, the prediction {} shots of {}",
612                observables.num_shots(),
613                observables.num_measurements(),
614                predicted.num_shots(),
615                predicted.num_measurements()
616            ),
617        });
618    }
619    let num_shots = predicted.num_shots();
620    if num_shots == 0 {
621        return Ok(0.0);
622    }
623    let words = predicted.num_measurements().div_ceil(64);
624    if words == 0 {
625        return Ok(0.0);
626    }
627    let transposed;
628    let actual: &[u64] = match observables.layout() {
629        ShotLayout::ShotMajor => observables.raw_data(),
630        ShotLayout::MeasMajor => {
631            transposed = observables.clone().into_shot_major_data();
632            &transposed
633        }
634    };
635    let tail = match predicted.num_measurements() % 64 {
636        0 => u64::MAX,
637        bits => (1u64 << bits) - 1,
638    };
639    let failures = predicted
640        .raw_data()
641        .chunks_exact(words)
642        .zip(actual.chunks_exact(words))
643        .filter(|(p, a)| {
644            let last = words - 1;
645            p[..last] != a[..last] || (p[last] ^ a[last]) & tail != 0
646        })
647        .count();
648    Ok(failures as f64 / num_shots as f64)
649}
650
651pub(super) struct Stuck {
652    pub(super) detector: u32,
653}
654
655impl Stuck {
656    pub(super) fn into_error(self, shot: usize) -> PrismError {
657        PrismError::InvalidParameter {
658            message: format!(
659                "shot {shot}: the detector component containing D{} has odd syndrome parity                  but no boundary edge, so the syndrome is impossible under the model",
660                self.detector
661            ),
662        }
663    }
664}
665
666/// Reusable per-shot decode state. Vertex and edge slots are validated by
667/// stamp comparison against the current shot or round, so nothing is cleared
668/// between shots and untouched slots cost nothing.
669pub(super) struct DecodeScratch {
670    stamp: u64,
671    shot_stamp: u64,
672    parent: Vec<u32>,
673    size: Vec<u32>,
674    parity: Vec<bool>,
675    boundary: Vec<bool>,
676    boundary_edge: Vec<u32>,
677    list_tail: Vec<u32>,
678    list_next: Vec<u32>,
679    vertex_stamp: Vec<u64>,
680    seen_stamp: Vec<u64>,
681    defect_stamp: Vec<u64>,
682    dfs_stamp: Vec<u64>,
683    peeled_stamp: Vec<u64>,
684    edge_stamp: Vec<u64>,
685    edge_growth: Vec<f64>,
686    edge_saturated: Vec<bool>,
687    touch_stamp: Vec<u64>,
688    touch_count: Vec<u8>,
689    defects: Vec<u32>,
690    active: Vec<u32>,
691    touched: Vec<u32>,
692    fused: Vec<u32>,
693    stack: Vec<u32>,
694    order: Vec<(u32, u32, u32)>,
695}
696
697impl DecodeScratch {
698    fn new(decoder: &UnionFindDecoder) -> Self {
699        let vertices = decoder.num_detectors;
700        let edges = decoder.edge_u.len();
701        Self {
702            stamp: 0,
703            shot_stamp: 0,
704            parent: vec![0; vertices],
705            size: vec![0; vertices],
706            parity: vec![false; vertices],
707            boundary: vec![false; vertices],
708            boundary_edge: vec![0; vertices],
709            list_tail: vec![0; vertices],
710            list_next: vec![0; vertices],
711            vertex_stamp: vec![0; vertices],
712            seen_stamp: vec![0; vertices],
713            defect_stamp: vec![0; vertices],
714            dfs_stamp: vec![0; vertices],
715            peeled_stamp: vec![0; vertices],
716            edge_stamp: vec![0; edges],
717            edge_growth: vec![0.0; edges],
718            edge_saturated: vec![false; edges],
719            touch_stamp: vec![0; edges],
720            touch_count: vec![0; edges],
721            defects: Vec::new(),
722            active: Vec::new(),
723            touched: Vec::new(),
724            fused: Vec::new(),
725            stack: Vec::new(),
726            order: Vec::new(),
727        }
728    }
729
730    fn activate(&mut self, v: u32) {
731        let at = v as usize;
732        if self.vertex_stamp[at] == self.shot_stamp {
733            return;
734        }
735        self.vertex_stamp[at] = self.shot_stamp;
736        self.parent[at] = v;
737        self.size[at] = 1;
738        self.parity[at] = false;
739        self.boundary[at] = false;
740        self.boundary_edge[at] = EDGE_NONE;
741        self.list_tail[at] = v;
742        self.list_next[at] = VERTEX_NONE;
743    }
744
745    fn find(&mut self, mut v: u32) -> u32 {
746        while self.parent[v as usize] != v {
747            let grand = self.parent[self.parent[v as usize] as usize];
748            self.parent[v as usize] = grand;
749            v = grand;
750        }
751        v
752    }
753
754    fn union(&mut self, a: u32, b: u32) {
755        let (big, small) = if self.size[a as usize] > self.size[b as usize]
756            || (self.size[a as usize] == self.size[b as usize] && a < b)
757        {
758            (a, b)
759        } else {
760            (b, a)
761        };
762        let (big_at, small_at) = (big as usize, small as usize);
763        self.parent[small_at] = big;
764        self.size[big_at] += self.size[small_at];
765        self.parity[big_at] ^= self.parity[small_at];
766        self.boundary[big_at] |= self.boundary[small_at];
767        self.boundary_edge[big_at] = self.boundary_edge[big_at].min(self.boundary_edge[small_at]);
768        self.list_next[self.list_tail[big_at] as usize] = small;
769        self.list_tail[big_at] = self.list_tail[small_at];
770    }
771}