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}