1use crate::quantum::{fxmul, Amp, StateVector, FRAC, ONE};
16
17#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct DensityMatrix {
20 pub n_qubits: u8,
21 pub d: usize,
23 m: Vec<Amp>,
24}
25
26impl DensityMatrix {
27
28 pub fn from_entries(m: Vec<Amp>) -> Option<DensityMatrix> {
32 let d = (m.len() as f64).sqrt() as usize;
33 if d * d != m.len() || !d.is_power_of_two() {
34 return None;
35 }
36 Some(DensityMatrix { n_qubits: d.trailing_zeros() as u8, d, m })
37 }
38
39 pub fn expect_pauli(&self, ops: &[(u8, Pauli)]) -> i64 {
42 let flip = pauli_flip(ops);
43 let mut acc: i128 = 0;
44 for j in 0..self.d {
45 acc += pauli_coeff(j, ops).mul(self.get(j, j ^ flip)).re as i128;
46 }
47 acc as i64
48 }
49
50 pub fn get(&self, r: usize, c: usize) -> Amp {
51 self.m[r * self.d + c]
52 }
53
54 pub fn from_pure(sv: &StateVector) -> DensityMatrix {
56 let d = sv.amps.len();
57 let mut m = vec![Amp::ZERO; d * d];
58 for r in 0..d {
59 for c in 0..d {
60 m[r * d + c] = sv.amps[r].mul(sv.amps[c].conj());
61 }
62 }
63 DensityMatrix { n_qubits: sv.n_qubits, d, m }
64 }
65
66 pub fn mixture(parts: &[(i64, DensityMatrix)]) -> Option<DensityMatrix> {
69 let first = parts.first()?;
70 let (n, d) = (first.1.n_qubits, first.1.d);
71 if parts.iter().any(|(_, p)| p.d != d) {
72 return None;
73 }
74 let mut m = vec![Amp::ZERO; d * d];
75 for (w, p) in parts {
76 for k in 0..d * d {
77 m[k] = m[k].add(Amp { re: fxmul(*w, p.m[k].re), im: fxmul(*w, p.m[k].im) });
78 }
79 }
80 Some(DensityMatrix { n_qubits: n, d, m })
81 }
82
83 pub fn trace(&self) -> Amp {
85 (0..self.d).fold(Amp::ZERO, |acc, i| acc.add(self.get(i, i)))
86 }
87
88 pub fn purity(&self) -> i64 {
92 let mut acc: i128 = 0;
93 for r in 0..self.d {
94 for c in 0..self.d {
95 let (a, b) = (self.get(r, c), self.get(c, r));
96 acc += (a.re as i128 * b.re as i128 - a.im as i128 * b.im as i128) >> FRAC;
97 }
98 }
99 acc as i64
100 }
101
102 pub fn partial_trace(&self, keep: &[u8]) -> DensityMatrix {
107 let mut keep: Vec<u8> = keep.to_vec();
108 keep.sort_unstable();
109 keep.dedup();
110 let traced: Vec<u8> = (0..self.n_qubits).filter(|q| !keep.contains(q)).collect();
111 let dk = 1usize << keep.len();
112 let dt = 1usize << traced.len();
113 let widen = |bits: usize, qs: &[u8]| -> usize {
114 qs.iter().enumerate().fold(0usize, |acc, (i, &q)| {
115 acc | (((bits >> i) & 1) << q)
116 })
117 };
118 let mut m = vec![Amp::ZERO; dk * dk];
119 for a in 0..dk {
120 for b in 0..dk {
121 let (ka, kb) = (widen(a, &keep), widen(b, &keep));
122 let mut sum = Amp::ZERO;
123 for t in 0..dt {
124 let tt = widen(t, &traced);
125 sum = sum.add(self.get(ka | tt, kb | tt));
126 }
127 m[a * dk + b] = sum;
128 }
129 }
130 DensityMatrix { n_qubits: keep.len() as u8, d: dk, m }
131 }
132
133 pub fn hash(&self) -> [u8; 32] {
135 let mut h = blake3::Hasher::new();
136 h.update(b"wai:density-matrix\x01");
137 h.update(&(self.n_qubits as u64).to_le_bytes());
138 for a in &self.m {
139 h.update(&a.re.to_le_bytes());
140 h.update(&a.im.to_le_bytes());
141 }
142 *h.finalize().as_bytes()
143 }
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub enum Pauli {
149 I,
150 X,
151 Y,
152 Z,
153}
154
155pub(crate) fn pauli_flip(ops: &[(u8, Pauli)]) -> usize {
157 let mut flip = 0usize;
158 for (q, p) in ops {
159 if matches!(p, Pauli::X | Pauli::Y) {
160 flip |= 1 << q;
161 }
162 }
163 flip
164}
165
166pub(crate) fn pauli_coeff(i: usize, ops: &[(u8, Pauli)]) -> Amp {
169 let mut sign = 1i64;
170 let mut i_pow = 0u32;
171 for (q, p) in ops {
172 let bit = (i >> q) & 1;
173 match p {
174 Pauli::I | Pauli::X => {}
175 Pauli::Z => {
176 if bit == 1 {
177 sign = -sign;
178 }
179 }
180 Pauli::Y => {
181 i_pow += 1;
182 if bit == 1 {
183 sign = -sign;
184 }
185 }
186 }
187 }
188 match i_pow % 4 {
189 0 => Amp { re: sign * ONE, im: 0 },
190 1 => Amp { re: 0, im: sign * ONE },
191 2 => Amp { re: -sign * ONE, im: 0 },
192 _ => Amp { re: 0, im: -sign * ONE },
193 }
194}
195
196pub fn expect_pauli(sv: &StateVector, ops: &[(u8, Pauli)]) -> i64 {
202 let flip = pauli_flip(ops);
203 let mut acc: i128 = 0;
204 for i in 0..sv.amps.len() {
205 let c = pauli_coeff(i, ops);
206 acc += sv.amps[i ^ flip].conj().mul(c.mul(sv.amps[i])).re as i128;
207 }
208 acc as i64
209}
210
211pub struct Chsh {
219 pub s: f64,
220 pub classical_bound: f64,
221 pub tsirelson_bound: f64,
222 pub zz_fx: i64,
224 pub xx_fx: i64,
225}
226
227pub fn chsh(sv: &StateVector) -> Option<Chsh> {
229 if sv.n_qubits != 2 {
230 return None;
231 }
232 let zz = expect_pauli(sv, &[(0, Pauli::Z), (1, Pauli::Z)]);
233 let xx = expect_pauli(sv, &[(0, Pauli::X), (1, Pauli::X)]);
234 let s = std::f64::consts::SQRT_2 * ((zz + xx) as f64 / ONE as f64);
235 Some(Chsh {
236 s,
237 classical_bound: 2.0,
238 tsirelson_bound: 2.0 * std::f64::consts::SQRT_2,
239 zz_fx: zz,
240 xx_fx: xx,
241 })
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::quantum::Circuit;
248
249 fn bell() -> StateVector {
250 let mut c = Circuit::new(2);
251 c.h(0).cx(0, 1);
252 c.simulate().unwrap()
253 }
254
255 #[test]
256 fn a_pure_state_is_pure_and_normalised() {
257 let dm = DensityMatrix::from_pure(&bell());
258 let tol = ONE / 10_000;
259 assert!((dm.trace().re - ONE).abs() < tol, "trace {}", dm.trace().re);
260 assert!(dm.trace().im.abs() < tol);
261 assert!((dm.purity() - ONE).abs() < tol, "purity {}", dm.purity());
262 }
263
264 #[test]
267 fn half_of_a_bell_pair_is_maximally_mixed() {
268 let dm = DensityMatrix::from_pure(&bell());
269 let red = dm.partial_trace(&[0]);
270 let tol = ONE / 10_000;
271 assert_eq!(red.d, 2);
272 assert!((red.trace().re - ONE).abs() < tol);
273 assert!((red.purity() - ONE / 2).abs() < tol, "purity {}", red.purity());
275 assert!(red.get(0, 1).re.abs() < tol && red.get(0, 1).im.abs() < tol);
276 assert!((red.get(0, 0).re - ONE / 2).abs() < tol);
277 assert!((red.get(1, 1).re - ONE / 2).abs() < tol);
278 }
279
280 #[test]
281 fn a_product_state_leaves_a_pure_subsystem() {
282 let mut c = Circuit::new(2);
283 c.h(0); let dm = DensityMatrix::from_pure(&c.simulate().unwrap());
285 let red = dm.partial_trace(&[0]);
286 let tol = ONE / 10_000;
287 assert!((red.purity() - ONE).abs() < tol, "a product state's part is pure: {}", red.purity());
288 }
289
290 #[test]
291 fn pauli_expectations_of_a_bell_state() {
292 let sv = bell();
293 let tol = ONE / 10_000;
294 assert!((expect_pauli(&sv, &[(0, Pauli::Z), (1, Pauli::Z)]) - ONE).abs() < tol);
296 assert!((expect_pauli(&sv, &[(0, Pauli::X), (1, Pauli::X)]) - ONE).abs() < tol);
297 assert!((expect_pauli(&sv, &[(0, Pauli::Y), (1, Pauli::Y)]) + ONE).abs() < tol);
298 assert!(expect_pauli(&sv, &[(0, Pauli::Z)]).abs() < tol);
299 assert!(expect_pauli(&sv, &[(1, Pauli::X)]).abs() < tol);
300 assert!((expect_pauli(&sv, &[(0, Pauli::I)]) - ONE).abs() < tol);
302 }
303
304 #[test]
308 fn chsh_violates_the_classical_bound_and_saturates_tsirelson() {
309 let r = chsh(&bell()).expect("two qubits");
310 assert!(r.s > r.classical_bound + 0.5, "S = {} must beat the classical 2", r.s);
311 assert!(r.s <= r.tsirelson_bound + 1e-6, "S = {} may not beat Tsirelson", r.s);
312 assert!((r.s - 2.0 * std::f64::consts::SQRT_2).abs() < 1e-3, "S = {} should reach 2√2", r.s);
313 }
314
315 #[test]
317 fn a_product_state_obeys_the_classical_bound() {
318 let mut c = Circuit::new(2);
319 c.h(0).h(1);
320 let r = chsh(&c.simulate().unwrap()).unwrap();
321 assert!(r.s <= r.classical_bound + 1e-6, "S = {} from a product state", r.s);
322 }
323
324 #[test]
330 fn mixing_a_state_with_itself_returns_it() {
331 let pure = DensityMatrix::from_pure(&bell());
332 let mixed = DensityMatrix::mixture(&[(ONE / 2, pure.clone()), (ONE / 2, pure.clone())]).unwrap();
333 let tol = ONE / 100_000;
334 for r in 0..pure.d {
335 for c in 0..pure.d {
336 let (a, b) = (pure.get(r, c), mixed.get(r, c));
337 assert!(
338 (a.re - b.re).abs() <= tol && (a.im - b.im).abs() <= tol,
339 "entry ({r},{c}) drifted: {a:?} vs {b:?}"
340 );
341 }
342 }
343 assert!((mixed.trace().re - ONE).abs() < ONE / 10_000, "a mixture is still a state");
344 }
345
346 #[test]
347 fn the_hash_identifies_the_mixture() {
348 let a = DensityMatrix::from_pure(&bell());
349 let b = DensityMatrix::from_pure(&bell());
350 assert_eq!(a.hash(), b.hash(), "same computation, same identity");
351 let mut c = Circuit::new(2);
352 c.h(0); assert_ne!(DensityMatrix::from_pure(&c.simulate().unwrap()).hash(), a.hash());
354 }
355}