paulistrings/channel/
mod.rs

1//! [`Channel<W>`] — unified abstraction for gates and noise.
2//!
3//! Every operation on a [`PauliSum`] (Clifford gate, Pauli rotation,
4//! arbitrary unitary, noise channel) maps a single Pauli string to a small
5//! weighted sum of Pauli strings. The trait formalizes that mapping; the
6//! engine consumes it via the sort-merge pipeline (see [`engine`]).
7//!
8//! Built-ins in this module:
9//!
10//! - [`Clifford1Q`], [`Clifford2Q`] — table-driven Clifford gates with
11//!   `MAX_FANOUT = 1`.
12//! - [`PauliRotation`] — `exp(-i·θ·P/2)` with `MAX_FANOUT = 2` (commuting
13//!   inputs collapse to fanout-1 at runtime).
14//! - [`GeneralUnitary1Q`], [`GeneralUnitary2Q`] — generic unitaries stored
15//!   as Pauli-expansion tables.
16//! - [`Depolarizing`], [`Dephasing`] — coefficient-rescaling noise
17//!   (`MAX_FANOUT = 1`).
18//! - [`AmplitudeDamping`] — the one built-in with `MAX_FANOUT = 2`.
19//! - [`IdentityChannel`] — pass-through, used in tests and as a neutral
20//!   composition element.
21//!
22//! See design doc §6.
23//!
24//! # Implementing a custom channel
25//!
26//! Implement the trait directly; the engine treats your type as just another
27//! `Box<dyn `[`Channel<W>`]`>` inside a [`Circuit`]. Three required methods
28//! plus an optional [`Channel::apply_adjoint`] override.
29//!
30//! ```
31//! use paulistrings::{Channel, OutputBuffer};
32//! use num_complex::Complex64;
33//!
34//! /// Multiplies every input coefficient by a complex factor, with no
35//! /// support and `MAX_FANOUT = 1`.
36//! struct GlobalPhase {
37//!     support: [u32; 0],
38//!     factor: Complex64,
39//! }
40//!
41//! impl<const W: usize> Channel<W> for GlobalPhase {
42//!     fn max_fanout(&self) -> usize { 1 }
43//!     fn support(&self) -> &[u32] { &self.support }
44//!     fn apply(
45//!         &self,
46//!         input_x: &[u64; W],
47//!         input_z: &[u64; W],
48//!         coeff: Complex64,
49//!         out: &mut OutputBuffer<'_, W>,
50//!     ) {
51//!         out.push(*input_x, *input_z, coeff * self.factor);
52//!     }
53//! }
54//!
55//! let ch = GlobalPhase {
56//!     support: [],
57//!     factor: Complex64::new(0.0, 1.0),
58//! };
59//! let _: Box<dyn Channel<1>> = Box::new(ch);
60//! ```
61//!
62//! [`PauliSum`]: crate::PauliSum
63//! [`engine`]: crate::engine
64//! [`Circuit`]: crate::Circuit
65
66#![allow(unused)]
67
68pub mod clifford;
69pub mod identity;
70pub mod noise;
71pub mod rotation;
72pub mod unitary;
73
74pub use clifford::{Clifford1Q, Clifford2Q};
75pub use identity::IdentityChannel;
76pub use noise::{AmplitudeDamping, Dephasing, Depolarizing};
77pub use rotation::PauliRotation;
78pub use unitary::{GeneralUnitary1Q, GeneralUnitary2Q};
79
80use num_complex::Complex64;
81
82/// Pre-allocated, fixed-capacity SoA scratch buffer for channel outputs.
83///
84/// Sized by the engine to `n_in · channel.max_fanout()` so that `apply` can
85/// write without dynamic growth. Required for GPU correctness and for CPU
86/// hot-loop performance. Channel impls write via [`OutputBuffer::push`].
87pub struct OutputBuffer<'a, const W: usize> {
88    /// X-part column. Length equals the buffer's capacity.
89    pub x: &'a mut [[u64; W]],
90    /// Z-part column. Length equals the buffer's capacity.
91    pub z: &'a mut [[u64; W]],
92    /// Coefficient column. Length equals the buffer's capacity.
93    pub coeff: &'a mut [Complex64],
94    /// Cursor into the slices; `apply` writes at `len` and advances.
95    pub len: &'a mut usize,
96}
97
98impl<'a, const W: usize> OutputBuffer<'a, W> {
99    /// Append one term to the buffer at the current cursor.
100    ///
101    /// Capacity is `self.x.len()`; the engine sizes the slices to
102    /// `channel.max_fanout()` per input term, so a `Channel::apply` body
103    /// must not push more than its declared `max_fanout`. Out-of-range
104    /// writes are caught by slice bounds-checking (and, in debug builds,
105    /// by an explicit assertion with a clearer message).
106    #[inline]
107    pub fn push(&mut self, x: [u64; W], z: [u64; W], c: Complex64) {
108        debug_assert!(
109            *self.len < self.x.len(),
110            "OutputBuffer overflow: {} pushes into a buffer of capacity {}",
111            *self.len + 1,
112            self.x.len()
113        );
114        let i = *self.len;
115        self.x[i] = x;
116        self.z[i] = z;
117        self.coeff[i] = c;
118        *self.len = i + 1;
119    }
120
121    /// Reset the cursor to zero so the same backing storage can be reused
122    /// for the next input term without reallocation.
123    #[inline]
124    pub fn clear(&mut self) {
125        *self.len = 0;
126    }
127}
128
129/// Anything that maps a Pauli string to a small weighted sum of Pauli strings.
130///
131/// [`Channel::max_fanout`] is a method (not an associated `const`) so the
132/// trait stays `dyn`-compatible — [`Circuit`](crate::Circuit) stores
133/// `Box<dyn Channel<W>>` to keep the channel set open for user extensions.
134/// For built-in channels the returned value is a compile-time constant so
135/// the engine still gets constant-folded buffer sizing once the concrete
136/// type is in hand.
137///
138/// See the [module-level docs](self) for an `impl Channel` example.
139pub trait Channel<const W: usize>: Send + Sync {
140    /// Maximum number of output terms produced per input term. Used by the
141    /// engine to size the scratch buffer up-front.
142    fn max_fanout(&self) -> usize;
143
144    /// Qubits this channel acts on. Outputs differ from inputs only at these
145    /// bit positions; the engine uses this for bucket layout (§5).
146    fn support(&self) -> &[u32];
147
148    /// Apply the channel to a single input term, writing outputs to `out`.
149    fn apply(
150        &self,
151        input_x: &[u64; W],
152        input_z: &[u64; W],
153        coeff: Complex64,
154        out: &mut OutputBuffer<'_, W>,
155    );
156
157    /// Apply the channel's *adjoint* to a single input term, writing outputs
158    /// to `out`. Used by the engine in `Direction::Heisenberg` mode for
159    /// backpropagating observables.
160    ///
161    /// The default implementation is `self.apply(...)`, i.e. assumes the
162    /// channel is self-adjoint. Channels that are not self-adjoint
163    /// (`PauliRotation`, `Clifford1Q::s`) override this. The design doc
164    /// (§8) does not pin down a mechanism; this is the v0.1 convention.
165    fn apply_adjoint(
166        &self,
167        input_x: &[u64; W],
168        input_z: &[u64; W],
169        coeff: Complex64,
170        out: &mut OutputBuffer<'_, W>,
171    ) {
172        self.apply(input_x, input_z, coeff, out);
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[allow(clippy::type_complexity)]
181    fn alloc_bufs<const W: usize>(
182        n: usize,
183    ) -> (Vec<[u64; W]>, Vec<[u64; W]>, Vec<Complex64>, usize) {
184        (
185            vec![[0u64; W]; n],
186            vec![[0u64; W]; n],
187            vec![Complex64::new(0.0, 0.0); n],
188            0usize,
189        )
190    }
191
192    #[test]
193    fn push_writes_at_cursor_w1() {
194        let (mut x, mut z, mut c, mut len) = alloc_bufs::<1>(4);
195        {
196            let mut buf = OutputBuffer::<1> {
197                x: &mut x,
198                z: &mut z,
199                coeff: &mut c,
200                len: &mut len,
201            };
202            buf.push([0xAA], [0xBB], Complex64::new(1.0, 2.0));
203            buf.push([0xCC], [0xDD], Complex64::new(3.0, 4.0));
204            assert_eq!(*buf.len, 2);
205        }
206        assert_eq!(x[0], [0xAA]);
207        assert_eq!(z[0], [0xBB]);
208        assert_eq!(c[0], Complex64::new(1.0, 2.0));
209        assert_eq!(x[1], [0xCC]);
210        assert_eq!(z[1], [0xDD]);
211        assert_eq!(c[1], Complex64::new(3.0, 4.0));
212        // remaining slots untouched
213        assert_eq!(x[2], [0]);
214        assert_eq!(x[3], [0]);
215        assert_eq!(c[3], Complex64::new(0.0, 0.0));
216    }
217
218    #[test]
219    fn push_writes_at_cursor_w2() {
220        let (mut x, mut z, mut c, mut len) = alloc_bufs::<2>(3);
221        {
222            let mut buf = OutputBuffer::<2> {
223                x: &mut x,
224                z: &mut z,
225                coeff: &mut c,
226                len: &mut len,
227            };
228            buf.push([0x11, 0x22], [0x33, 0x44], Complex64::new(5.0, 6.0));
229            assert_eq!(*buf.len, 1);
230        }
231        assert_eq!(x[0], [0x11, 0x22]);
232        assert_eq!(z[0], [0x33, 0x44]);
233        assert_eq!(c[0], Complex64::new(5.0, 6.0));
234    }
235
236    #[test]
237    #[should_panic]
238    fn push_panics_when_full() {
239        let (mut x, mut z, mut c, mut len) = alloc_bufs::<1>(2);
240        let mut buf = OutputBuffer::<1> {
241            x: &mut x,
242            z: &mut z,
243            coeff: &mut c,
244            len: &mut len,
245        };
246        buf.push([0; 1], [0; 1], Complex64::new(1.0, 0.0));
247        buf.push([0; 1], [0; 1], Complex64::new(1.0, 0.0));
248        buf.push([0; 1], [0; 1], Complex64::new(1.0, 0.0));
249    }
250
251    #[test]
252    fn clear_resets_cursor() {
253        let (mut x, mut z, mut c, mut len) = alloc_bufs::<1>(4);
254        {
255            let mut buf = OutputBuffer::<1> {
256                x: &mut x,
257                z: &mut z,
258                coeff: &mut c,
259                len: &mut len,
260            };
261            buf.push([0xAA], [0xBB], Complex64::new(1.0, 0.0));
262            buf.push([0xCC], [0xDD], Complex64::new(2.0, 0.0));
263            assert_eq!(*buf.len, 2);
264            buf.clear();
265            assert_eq!(*buf.len, 0);
266            buf.push([0xEE], [0xFF], Complex64::new(3.0, 0.0));
267            assert_eq!(*buf.len, 1);
268        }
269        // The post-clear push lands at slot 0, overwriting the prior contents.
270        assert_eq!(x[0], [0xEE]);
271        assert_eq!(z[0], [0xFF]);
272        assert_eq!(c[0], Complex64::new(3.0, 0.0));
273        // Slot 1 was written before the clear and is left as-is.
274        assert_eq!(x[1], [0xCC]);
275        assert_eq!(z[1], [0xDD]);
276    }
277
278    #[test]
279    fn reuse_does_not_grow_backing_vecs() {
280        let cap = 4;
281        let mut x: Vec<[u64; 1]> = vec![[0u64; 1]; cap];
282        let mut z: Vec<[u64; 1]> = vec![[0u64; 1]; cap];
283        let mut c: Vec<Complex64> = vec![Complex64::new(0.0, 0.0); cap];
284        assert_eq!(x.capacity(), cap);
285        assert_eq!(z.capacity(), cap);
286        assert_eq!(c.capacity(), cap);
287        let mut len = 0usize;
288        for i in 0..100u64 {
289            len = 0;
290            let mut buf = OutputBuffer::<1> {
291                x: &mut x,
292                z: &mut z,
293                coeff: &mut c,
294                len: &mut len,
295            };
296            buf.push([i], [0], Complex64::new(i as f64, 0.0));
297            buf.push([i + 1], [0], Complex64::new((i + 1) as f64, 0.0));
298        }
299        assert_eq!(x.capacity(), cap);
300        assert_eq!(z.capacity(), cap);
301        assert_eq!(c.capacity(), cap);
302    }
303}