dreamwell_intelligence/
density_matrix.rs1use crate::complex::Complex;
8
9const PHI: f32 = 1.618033988;
11
12fn splitmix64_f32(seed: u64) -> f32 {
14 let mut z = seed.wrapping_add(0x9e3779b97f4a7c15);
15 z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
16 z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
17 z = z ^ (z >> 31);
18 (z as f32) / (u64::MAX as f32)
19}
20
21#[derive(Clone)]
24pub struct DensityMatrixN {
25 pub dim: usize,
26 pub entries: Vec<Complex>,
27 pub(crate) scratch_a: Vec<Complex>,
29 #[allow(dead_code)]
30 pub(crate) scratch_b: Vec<Complex>,
31}
32
33impl DensityMatrixN {
34 fn with_scratch(dim: usize, entries: Vec<Complex>) -> Self {
35 let n2 = dim * dim;
36 Self {
37 dim,
38 entries,
39 scratch_a: vec![Complex::ZERO; n2],
40 scratch_b: vec![Complex::ZERO; n2],
41 }
42 }
43
44 pub fn maximally_mixed(dim: usize) -> Self {
46 let mut entries = vec![Complex::ZERO; dim * dim];
47 let p = 1.0 / dim as f32;
48 for k in 0..dim {
49 entries[k * dim + k] = Complex::new(p, 0.0);
50 }
51 Self::with_scratch(dim, entries)
52 }
53
54 pub fn pure_state(k: usize, dim: usize) -> Self {
56 let mut entries = vec![Complex::ZERO; dim * dim];
57 entries[k * dim + k] = Complex::ONE;
58 Self::with_scratch(dim, entries)
59 }
60
61 pub fn equal_superposition(dim: usize) -> Self {
63 let amp = 1.0 / (dim as f32).sqrt();
64 let val = amp * amp;
65 let mut entries = vec![Complex::ZERO; dim * dim];
66 for i in 0..dim {
67 for j in 0..dim {
68 entries[i * dim + j] = Complex::new(val, 0.0);
69 }
70 }
71 Self::with_scratch(dim, entries)
72 }
73
74 pub fn populations(&self) -> Vec<f32> {
76 (0..self.dim).map(|k| self.entries[k * self.dim + k].re).collect()
77 }
78
79 pub fn coherence_magnitude(&self) -> f32 {
81 let mut sum = 0.0f32;
82 for i in 0..self.dim {
83 for j in (i + 1)..self.dim {
84 sum += self.entries[i * self.dim + j].norm();
85 }
86 }
87 sum
88 }
89
90 pub fn purity(&self) -> f32 {
92 let mut sum = 0.0f32;
93 for e in &self.entries {
94 sum += e.norm_sq();
95 }
96 sum
97 }
98
99 pub fn von_neumann_entropy(&self) -> f32 {
103 let d = self.dim;
104 let mut work = self.entries.clone(); let mut eigenvalues = vec![0.0f32; d];
107 dreamwell_math::eigen::eigenvalues_hermitian(&mut work, &mut eigenvalues, d, 50, 1e-8);
108 dreamwell_math::eigen::von_neumann_entropy(&eigenvalues)
109 }
110
111 pub fn free_energy(&self, energies: &[f32]) -> f32 {
117 let pops = self.populations();
118 let expected_h: f32 = pops.iter().zip(energies.iter()).map(|(p, e)| p * e).sum();
119 let temperature = 1.0 / (1.0 + PHI * self.coherence_magnitude());
120 let entropy = self.von_neumann_entropy();
121 expected_h - temperature * entropy
122 }
123
124 pub fn dephase(&mut self, epsilon: f32) {
126 let retain = (1.0 - epsilon).max(0.0);
127 for i in 0..self.dim {
128 for j in 0..self.dim {
129 if i != j {
130 self.entries[i * self.dim + j] = self.entries[i * self.dim + j].scale(retain);
131 }
132 }
133 }
134 }
135
136 pub fn couple_dephase(&mut self, other: &DensityMatrixN, strength: f32) {
138 let other_coh = other.coherence_magnitude();
139 let retain = (1.0 - strength * (1.0 - other_coh.min(1.0))).max(0.0);
140 for i in 0..self.dim {
141 for j in 0..self.dim {
142 if i != j {
143 self.entries[i * self.dim + j] = self.entries[i * self.dim + j].scale(retain);
144 }
145 }
146 }
147 }
148
149 pub fn evolve(&mut self, unitary: &[Complex]) {
160 let d = self.dim;
161 dreamwell_math::linalg::cgemm(unitary, &self.entries, &mut self.scratch_a, d, d, d);
167 dreamwell_math::linalg::cgemm_conj_transpose_b(&self.scratch_a, unitary, &mut self.entries, d, d, d);
168 }
169
170 pub fn hamiltonian_unitary(h: &[f32], dim: usize, dt: f32) -> Vec<Complex> {
173 let n2 = dim * dim;
174 let mut u = vec![Complex::ZERO; n2];
175 let mut sa = vec![Complex::ZERO; n2];
176 let mut sb = vec![Complex::ZERO; n2];
177 dreamwell_math::matrix_exp::expm_skew_hermitian(h, dt, &mut u, &mut sa, &mut sb, dim);
178 u
179 }
180
181 pub fn born_sample(&self, seed: u64) -> usize {
183 let r = splitmix64_f32(seed);
184 let mut cumulative = 0.0f32;
185 for k in 0..self.dim {
186 cumulative += self.entries[k * self.dim + k].re.max(0.0);
187 if r < cumulative {
188 return k;
189 }
190 }
191 self.dim - 1
192 }
193
194 pub fn trace(&self) -> f32 {
196 (0..self.dim).map(|k| self.entries[k * self.dim + k].re).sum()
197 }
198}
199
200fn gpu_linalg_lock() -> Option<std::sync::MutexGuard<'static, Option<dreamwell_math::gpu_linalg::GpuLinalgContext>>> {
205 use std::sync::{Mutex, OnceLock};
206 static GPU_CTX: OnceLock<Mutex<Option<dreamwell_math::gpu_linalg::GpuLinalgContext>>> = OnceLock::new();
207 let mtx = GPU_CTX.get_or_init(|| {
208 Mutex::new(dreamwell_math::gpu_linalg::GpuLinalgContext::new(256))
211 });
212 mtx.lock().ok()
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn pure_state_valid() {
221 let rho = DensityMatrixN::pure_state(0, 5);
222 assert!((rho.trace() - 1.0).abs() < 1e-6);
223 assert!((rho.purity() - 1.0).abs() < 1e-6);
224 assert_eq!(rho.populations()[0], 1.0);
225 }
226
227 #[test]
228 fn equal_superposition_valid() {
229 let rho = DensityMatrixN::equal_superposition(5);
230 assert!((rho.trace() - 1.0).abs() < 1e-6);
231 let pops = rho.populations();
232 for &p in &pops {
233 assert!((p - 0.2).abs() < 1e-6);
234 }
235 }
236
237 #[test]
238 fn dephasing_reduces_coherence() {
239 let mut rho = DensityMatrixN::equal_superposition(5);
240 let coh_before = rho.coherence_magnitude();
241 rho.dephase(0.5);
242 let coh_after = rho.coherence_magnitude();
243 assert!(coh_after < coh_before, "Dephasing should reduce coherence");
244 }
245
246 #[test]
247 fn maximally_mixed_min_purity() {
248 let rho = DensityMatrixN::maximally_mixed(5);
249 assert!((rho.purity() - 0.2).abs() < 1e-6);
250 }
251
252 #[test]
253 fn free_energy_computable() {
254 let rho = DensityMatrixN::equal_superposition(5);
255 let energies = vec![0.2, 0.3, 0.1, 0.15, 0.25];
256 let f = rho.free_energy(&energies);
257 assert!(f.is_finite());
258 }
259
260 #[test]
261 fn born_sample_deterministic() {
262 let rho = DensityMatrixN::pure_state(2, 5);
263 assert_eq!(rho.born_sample(42), 2);
264 assert_eq!(rho.born_sample(42), 2);
265 }
266
267 #[test]
268 fn evolve_preserves_trace() {
269 let mut rho = DensityMatrixN::equal_superposition(4);
270 let h = vec![0.1; 16];
271 let u = DensityMatrixN::hamiltonian_unitary(&h, 4, 0.01);
272 rho.evolve(&u);
273 assert!((rho.trace() - 1.0).abs() < 0.1, "trace={}", rho.trace());
274 }
275
276 #[test]
277 fn pure_state_entropy_is_zero() {
278 let rho = DensityMatrixN::pure_state(0, 5);
279 let s = rho.von_neumann_entropy();
280 assert!(s.abs() < 0.1, "pure state entropy should be ~0, got {s}");
281 }
282
283 #[test]
284 fn maximally_mixed_entropy_is_maximal() {
285 let rho = DensityMatrixN::maximally_mixed(5);
286 let s = rho.von_neumann_entropy();
287 let expected = (5.0f32).ln(); assert!(
289 (s - expected).abs() < 0.2,
290 "maximally mixed entropy should be ~{expected}, got {s}"
291 );
292 }
293
294 #[test]
295 fn evolve_at_dim16_preserves_trace() {
296 let dim = 16;
297 let mut rho = DensityMatrixN::equal_superposition(dim);
298 let mut h = vec![0.0f32; dim * dim];
299 for i in 0..dim {
300 h[i * dim + i] = (i as f32) * 0.1;
301 }
302 h[1] = 0.2;
303 h[dim] = 0.2; let u = DensityMatrixN::hamiltonian_unitary(&h, dim, 0.1);
305 rho.evolve(&u);
306 assert!(
307 (rho.trace() - 1.0).abs() < 0.15,
308 "dim=16 trace should be ~1.0, got {}",
309 rho.trace()
310 );
311 }
312}