paulistrings/channel/
mod.rs

1//! The [`Channel`] trait, its output buffer, and the built-in gates and noise channels.
2
3pub(crate) mod prepared;
4pub(crate) mod ptm;
5pub(crate) mod rotation;
6
7pub use ptm::PtmChannel;
8pub use rotation::PauliRotation;
9
10pub(crate) use prepared::Prepare;
11
12use crate::pauli_string::qubit_mask;
13use crate::pauli_sum::hash::Gf2Hash;
14use num_complex::Complex64;
15use prepared::{PrepareToken, Prepared};
16
17/// The terms one [`Channel::apply`] call emits, pushed with [`push`](Self::push).
18pub struct OutputBuffer<const W: usize> {
19    x: Vec<[u64; W]>,
20    z: Vec<[u64; W]>,
21    coeff: Vec<Complex64>,
22}
23
24impl<const W: usize> OutputBuffer<W> {
25    /// Append one term.
26    pub fn push(&mut self, x: [u64; W], z: [u64; W], coeff: Complex64) {
27        self.x.push(x);
28        self.z.push(z);
29        self.coeff.push(coeff);
30    }
31
32    pub(crate) fn new() -> Self {
33        Self { x: Vec::new(), z: Vec::new(), coeff: Vec::new() }
34    }
35
36    /// Forget every term, keeping the storage.
37    pub(crate) fn clear(&mut self) {
38        self.x.clear();
39        self.z.clear();
40        self.coeff.clear();
41    }
42
43    /// The terms in push order.
44    pub(crate) fn terms(
45        &self,
46    ) -> impl ExactSizeIterator<Item = ([u64; W], [u64; W], Complex64)> + '_ {
47        self.x.iter().zip(&self.z).zip(&self.coeff).map(|((&x, &z), &coeff)| (x, z, coeff))
48    }
49}
50
51/// Pack qubit indices into a [`Channel::support`] bitmask; order and duplicates do not matter.
52///
53/// # Panics
54///
55/// If a qubit is not below `64 ยท W`.
56pub fn support_mask<const W: usize>(qubits: &[usize]) -> [u64; W] {
57    qubit_mask(qubits.iter().copied(), 64 * W)
58}
59
60/// `(word, bit, 1 << bit)` of qubit `q`.
61fn qubit_loc(q: usize) -> (usize, usize, u64) {
62    let word = q / 64;
63    let bit = q % 64;
64    (word, bit, 1u64 << bit)
65}
66
67/// The packed single-qubit Pauli index `x | (z << 1)` (`I=0, X=1, Z=2, Y=3`) at `(word, bit)`.
68fn read_pauli<const W: usize>(x: &[u64; W], z: &[u64; W], word: usize, bit: usize) -> usize {
69    let x_bit = (x[word] >> bit) & 1;
70    let z_bit = (z[word] >> bit) & 1;
71    (x_bit | (z_bit << 1)) as usize
72}
73
74/// Overwrite the qubit at `(word, bit, mask)` with the packed Pauli index `p`.
75fn write_pauli<const W: usize>(
76    x: &mut [u64; W],
77    z: &mut [u64; W],
78    word: usize,
79    bit: usize,
80    mask: u64,
81    p: usize,
82) {
83    let x_bit = (p & 1) as u64;
84    let z_bit = ((p >> 1) & 1) as u64;
85    x[word] = (x[word] & !mask) | (x_bit << bit);
86    z[word] = (z[word] & !mask) | (z_bit << bit);
87}
88
89/// A gate or noise channel: maps a Pauli string to a small weighted sum of Pauli strings.
90///
91/// Built-ins: [`PtmChannel`] (every one- and two-qubit gate and noise channel) and [`PauliRotation`].
92/// A custom channel implements `support`, `apply` and `apply_adjoint`; the engine derives its per-layer form by probing `apply` on the at most 16 local basis Paulis.
93pub trait Channel<const W: usize>: Send + Sync {
94    /// Qubits this channel acts on, as a bitmask built with [`support_mask`].
95    ///
96    /// Outputs must differ from their input only at these qubits, with amplitudes that depend on the input only through its bits here; the engine's probe of `apply` is sound exactly then, and debug builds re-probe on an all-ones background to catch a violation.
97    fn support(&self) -> [u64; W];
98
99    /// Short name for the per-layer progress log.
100    ///
101    /// The default is the type name without path or generics, which is unhelpful for tuples, closures and boxes; override it there.
102    fn debug_name(&self) -> &'static str {
103        let full = core::any::type_name::<Self>();
104        let head = match full.find('<') {
105            Some(i) => &full[..i],
106            None => full,
107        };
108        match head.rfind("::") {
109            Some(i) => &head[i + 2..],
110            None => head,
111        }
112    }
113
114    /// Apply the channel to one input term, pushing its outputs to `out`.
115    fn apply(
116        &self,
117        input_x: &[u64; W],
118        input_z: &[u64; W],
119        coeff: Complex64,
120        out: &mut OutputBuffer<W>,
121    );
122
123    /// Apply the channel's adjoint to one input term; run by `Direction::Heisenberg`.
124    ///
125    /// A self-adjoint channel calls [`apply`](Self::apply).
126    fn apply_adjoint(
127        &self,
128        input_x: &[u64; W],
129        input_z: &[u64; W],
130        coeff: Complex64,
131        out: &mut OutputBuffer<W>,
132    );
133
134    /// The engine form for one layer: probed from `apply` unless a built-in knows its table.
135    /// Neither callable nor overridable outside the crate, since `PrepareToken` and `Prepared` cannot be named there.
136    #[doc(hidden)]
137    fn prepare_for_layer(
138        &self,
139        hash: &Gf2Hash<W>,
140        adjoint: bool,
141        _: PrepareToken,
142    ) -> Option<Prepared<W>> {
143        Prepared::derive_local(self, hash, adjoint)
144    }
145}
146
147#[cfg(test)]
148mod tests;