Skip to main content

geographdb_core/algorithms/
mpo.rs

1use crate::algorithms::four_d::{GraphNode4D, GraphProperties, TemporalEdge};
2
3/// Matrix Product Operator (MPO) for weight matrix compression.
4///
5/// An MPO represents a linear map W: ℝ^n_in → ℝ^n_out as a chain of rank-4
6/// tensors. Each site has shape [chi_left, d_out, d_in, chi_right].
7/// Forward pass costs O(n · d² · χ³) instead of O(n_out · n_in).
8///
9/// Construction: reshape W as a rank-2k tensor with interleaved output/input
10/// indices, then decompose left-to-right via sequential SVD.
11/// One site in an MPO chain. Data is row-major [chi_left, d_out, d_in, chi_right].
12#[derive(Debug, Clone)]
13pub struct MpoSite {
14    pub chi_left: usize,
15    pub d_out: usize,
16    pub d_in: usize,
17    pub chi_right: usize,
18    pub data: Vec<f32>,
19}
20
21impl MpoSite {
22    fn get(&self, il: usize, io: usize, ii: usize, ir: usize) -> f32 {
23        self.data[il * self.d_out * self.d_in * self.chi_right
24            + io * self.d_in * self.chi_right
25            + ii * self.chi_right
26            + ir]
27    }
28}
29
30/// A Matrix Product Operator representing a compressed weight matrix.
31#[derive(Debug, Clone)]
32pub struct Mpo {
33    pub sites: Vec<MpoSite>,
34}
35
36// ── Linear algebra helpers ────────────────────────────────────────────────────
37
38fn matmul(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
39    let mut c = vec![0.0f32; m * n];
40    for i in 0..m {
41        for p in 0..k {
42            let aip = a[i * k + p];
43            for j in 0..n {
44                c[i * n + j] += aip * b[p * n + j];
45            }
46        }
47    }
48    c
49}
50
51fn transpose(a: &[f32], m: usize, n: usize) -> Vec<f32> {
52    let mut t = vec![0.0f32; n * m];
53    for i in 0..m {
54        for j in 0..n {
55            t[j * m + i] = a[i * n + j];
56        }
57    }
58    t
59}
60
61/// Thin SVD of A (m×n). Returns (U m×k, sigma k, Vt k×n) where k = min(m,n).
62/// Singular values are descending. U columns and Vt rows are orthonormal.
63pub fn svd_thin(a: &[f32], m: usize, n: usize) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
64    if m < n {
65        // Work on A^T (n×m, n >= m), then swap back.
66        let at = transpose(a, m, n);
67        let (v, sigma, ut) = svd_thin_tall(&at, n, m);
68        // A = U * S * Vt  where U = Vt_t^T (m×m) and Vt = U_t^T (m×n)
69        let u = transpose(&ut, m, m);
70        let vt = transpose(&v, n, m);
71        return (u, sigma, vt);
72    }
73    svd_thin_tall(a, m, n)
74}
75
76/// Thin SVD of A (m×n, m ≥ n) via Jacobi iteration on A^T A.
77fn svd_thin_tall(a: &[f32], m: usize, n: usize) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
78    assert!(m >= n, "svd_thin_tall requires m >= n, got m={m} n={n}");
79
80    // C = A^T · A  (n×n symmetric PSD)
81    let at = transpose(a, m, n);
82    let mut c = matmul(&at, a, n, m, n);
83
84    // One-sided Jacobi: accumulate eigenvectors in v, converge diagonal of c to eigenvalues.
85    let mut v = vec![0.0f32; n * n];
86    for i in 0..n {
87        v[i * n + i] = 1.0;
88    }
89
90    // Jacobi sweep iteration
91    for _ in 0..100 {
92        let mut converged = true;
93        for p in 0..n {
94            for q in (p + 1)..n {
95                let cpq = c[p * n + q];
96                if cpq.abs() > 1e-15 {
97                    converged = false;
98                    let cpp = c[p * n + p];
99                    let cqq = c[q * n + q];
100                    let theta = (cqq - cpp) / (2.0 * cpq);
101                    let t = 1.0 / (theta.abs() + (1.0 + theta * theta).sqrt());
102                    let t = if theta < 0.0 { -t } else { t };
103                    let cos = 1.0 / (1.0 + t * t).sqrt();
104                    let sin = t * cos;
105                    let tau = sin / (1.0 + cos);
106
107                    // Update symmetric c elements
108                    c[p * n + p] = cpp - t * cpq;
109                    c[q * n + q] = cqq + t * cpq;
110                    c[p * n + q] = 0.0;
111                    c[q * n + p] = 0.0;
112
113                    for r in 0..n {
114                        if r != p && r != q {
115                            let crp = c[r * n + p];
116                            let crq = c[r * n + q];
117                            c[r * n + p] = crp - sin * (crq + tau * crp);
118                            c[r * n + q] = crq + sin * (crp - tau * crq);
119                            // Symmetrize
120                            c[p * n + r] = c[r * n + p];
121                            c[q * n + r] = c[r * n + q];
122                        }
123                    }
124
125                    // Update V matrix (right singular vectors/eigenvectors)
126                    for i in 0..n {
127                        let vip = v[i * n + p];
128                        let viq = v[i * n + q];
129                        v[i * n + p] = vip - sin * (viq + tau * vip);
130                        v[i * n + q] = viq + sin * (vip - tau * viq);
131                    }
132                }
133            }
134        }
135        if converged {
136            break;
137        }
138    }
139
140    // Eigenvalues sit on the diagonal of c.
141    let mut sigma: Vec<f32> = (0..n).map(|i| c[i * n + i].max(0.0).sqrt()).collect();
142
143    // Sort descending.
144    let mut order: Vec<usize> = (0..n).collect();
145    order.sort_by(|&a, &b| sigma[b].partial_cmp(&sigma[a]).unwrap());
146    sigma = order.iter().map(|&i| sigma[i]).collect();
147    let mut v_sorted = vec![0.0f32; n * n];
148    for (new_col, &old_col) in order.iter().enumerate() {
149        for row in 0..n {
150            v_sorted[row * n + new_col] = v[row * n + old_col];
151        }
152    }
153
154    // U = A · V · diag(1/sigma)
155    let av = matmul(a, &v_sorted, m, n, n);
156    let mut u = vec![0.0f32; m * n];
157    for col in 0..n {
158        let inv_s = if sigma[col] > 1e-10 {
159            1.0 / sigma[col]
160        } else {
161            0.0
162        };
163        for row in 0..m {
164            u[row * n + col] = av[row * n + col] * inv_s;
165        }
166    }
167
168    let vt = transpose(&v_sorted, n, n);
169    (u, sigma, vt)
170}
171
172fn nth_root(n: usize, k: usize) -> usize {
173    if k == 1 {
174        return n;
175    }
176    let r = (n as f64).powf(1.0 / k as f64).round() as usize;
177    for candidate in r.saturating_sub(2)..=r + 2 {
178        if candidate.pow(k as u32) == n {
179            return candidate;
180        }
181    }
182    r
183}
184
185/// Factor `d` into a list of small integer factors (each ≥ 2).
186///
187/// Prefers 4, then 2, then odd factors. Returns `vec![d]` for primes.
188pub fn factor_dimension(d: usize) -> Vec<usize> {
189    if d < 2 {
190        return vec![d];
191    }
192    let mut remaining = d;
193    let mut factors = Vec::new();
194    while remaining.is_multiple_of(4) {
195        factors.push(4);
196        remaining /= 4;
197    }
198    while remaining.is_multiple_of(2) {
199        factors.push(2);
200        remaining /= 2;
201    }
202    let mut odd = 3;
203    while odd * odd <= remaining {
204        while remaining.is_multiple_of(odd) {
205            factors.push(odd);
206            remaining /= odd;
207        }
208        odd += 2;
209    }
210    if remaining > 1 {
211        factors.push(remaining);
212    }
213    factors
214}
215
216/// Pad a factorization to exactly `target_len` elements by prepending 1s.
217fn pad_factors(factors: &mut Vec<usize>, target_len: usize) {
218    while factors.len() < target_len {
219        factors.insert(0, 1);
220    }
221}
222
223// ── MPO construction ──────────────────────────────────────────────────────────
224
225/// Compress a dense weight matrix into an MPO via sequential SVD.
226///
227/// `weights`: row-major n_out × n_in matrix.
228/// `n_sites`: number of sites. Requires n_in = d_in^n_sites, n_out = d_out^n_sites.
229/// `chi_max`: maximum bond dimension.
230pub fn compress_matrix_to_mpo(
231    weights: &[f32],
232    n_out: usize,
233    n_in: usize,
234    n_sites: usize,
235    chi_max: usize,
236) -> Mpo {
237    assert_eq!(weights.len(), n_out * n_in);
238    assert!(n_sites >= 1);
239    let d_in = nth_root(n_in, n_sites);
240    let d_out = nth_root(n_out, n_sites);
241    assert_eq!(d_in.pow(n_sites as u32), n_in);
242    assert_eq!(d_out.pow(n_sites as u32), n_out);
243
244    // Reshape W (n_out × n_in) into W_T with interleaved indices:
245    // W_T[io_0, ii_0, io_1, ii_1, ..., io_{n-1}, ii_{n-1}]
246    let phys = d_out * d_in; // local Hilbert space size per site
247    let do_strides: Vec<usize> = (0..n_sites)
248        .map(|k| d_out.pow((n_sites - 1 - k) as u32))
249        .collect();
250    let di_strides: Vec<usize> = (0..n_sites)
251        .map(|k| d_in.pow((n_sites - 1 - k) as u32))
252        .collect();
253
254    let mut w_t = vec![0.0f32; n_out * n_in];
255    for row in 0..n_out {
256        for col in 0..n_in {
257            let mut r = row;
258            let mut c = col;
259            let mut interleaved = 0usize;
260            let mut pair_stride = phys.pow((n_sites - 1) as u32);
261            for k in 0..n_sites {
262                let io_k = r / do_strides[k];
263                r %= do_strides[k];
264                let ii_k = c / di_strides[k];
265                c %= di_strides[k];
266                interleaved += (io_k * d_in + ii_k) * pair_stride;
267                if k + 1 < n_sites {
268                    pair_stride /= phys;
269                }
270            }
271            w_t[interleaved] = weights[row * n_in + col];
272        }
273    }
274
275    // Sequential left-to-right SVD decomposition.
276    // At step k: current has shape (chi_left, phys^(n_sites-k))
277    let mut current = w_t;
278    let mut chi_left = 1usize;
279    let mut sites = Vec::with_capacity(n_sites);
280
281    for k in 0..n_sites {
282        let total_right = phys.pow((n_sites - k) as u32);
283        let m_svd = chi_left * phys;
284        let n_svd = total_right / phys; // = phys^(n_sites-k-1)
285
286        // Unfold: (chi_left, phys, n_svd) → (chi_left*phys, n_svd)
287        let mut unfolded = vec![0.0f32; m_svd * n_svd];
288        for il in 0..chi_left {
289            for ip in 0..phys {
290                let row = il * phys + ip;
291                for col in 0..n_svd {
292                    unfolded[row * n_svd + col] = current[il * total_right + ip * n_svd + col];
293                }
294            }
295        }
296
297        // SVD: always orient so m >= n for svd_thin.
298        let (u, sigma, vt, k_svd) = if m_svd >= n_svd {
299            let (u, s, vt) = svd_thin(&unfolded, m_svd, n_svd);
300            let k = s.len();
301            (u, s, vt, k)
302        } else {
303            let ut = transpose(&unfolded, m_svd, n_svd);
304            let (v, s, ut2) = svd_thin(&ut, n_svd, m_svd);
305            let k = s.len();
306            let u2 = transpose(&ut2, m_svd, m_svd);
307            let vt2 = transpose(&v, n_svd, m_svd);
308            (u2, s, vt2, k)
309        };
310
311        const SVD_EPS: f32 = 1e-9;
312        let chi_new = sigma
313            .iter()
314            .filter(|&&s| s > SVD_EPS)
315            .count()
316            .min(chi_max)
317            .max(1);
318        let chi_right = if k == n_sites - 1 { 1 } else { chi_new };
319
320        // Site tensor: M[il, io, ii, ir] = U[il*phys + io*d_in + ii, ir] * sigma[ir]
321        let mut data = vec![0.0f32; chi_left * d_out * d_in * chi_right];
322        for il in 0..chi_left {
323            for io in 0..d_out {
324                for ii in 0..d_in {
325                    let u_row = il * phys + io * d_in + ii;
326                    for ir in 0..chi_right {
327                        let u_val = if ir < k_svd {
328                            u[u_row * k_svd + ir]
329                        } else {
330                            0.0
331                        };
332                        let s_val = if ir < k_svd { sigma[ir] } else { 0.0 };
333                        data[il * d_out * d_in * chi_right
334                            + io * d_in * chi_right
335                            + ii * chi_right
336                            + ir] = u_val * s_val;
337                    }
338                }
339            }
340        }
341        sites.push(MpoSite {
342            chi_left,
343            d_out,
344            d_in,
345            chi_right,
346            data,
347        });
348
349        // Remainder: Vt[:chi_right, :] becomes current (chi_right × n_svd).
350        if k < n_sites - 1 {
351            current = (0..chi_right * n_svd)
352                .map(|idx| {
353                    let ir = idx / n_svd;
354                    let col = idx % n_svd;
355                    if ir < k_svd {
356                        vt[ir * n_svd + col]
357                    } else {
358                        0.0
359                    }
360                })
361                .collect();
362        }
363        chi_left = chi_right;
364    }
365
366    Mpo { sites }
367}
368
369// ── MPO forward pass ──────────────────────────────────────────────────────────
370
371/// Apply the MPO as a forward pass: y = W_mpo · x.
372pub fn mpo_apply(mpo: &Mpo, x: &[f32]) -> Vec<f32> {
373    let n_sites = mpo.sites.len();
374    if n_sites == 0 {
375        return Vec::new();
376    }
377
378    let d_out_dims: Vec<usize> = mpo.sites.iter().map(|s| s.d_out).collect();
379    let d_in_dims: Vec<usize> = mpo.sites.iter().map(|s| s.d_in).collect();
380    let n_out: usize = d_out_dims.iter().product();
381    let n_in: usize = d_in_dims.iter().product();
382    assert_eq!(x.len(), n_in);
383
384    // Strides for decoding flat output/input indices into per-site indices.
385    let out_strides: Vec<usize> = {
386        let mut s = vec![1usize; n_sites];
387        for k in (0..n_sites - 1).rev() {
388            s[k] = s[k + 1] * d_out_dims[k + 1];
389        }
390        s
391    };
392    let in_strides: Vec<usize> = {
393        let mut s = vec![1usize; n_sites];
394        for k in (0..n_sites - 1).rev() {
395            s[k] = s[k + 1] * d_in_dims[k + 1];
396        }
397        s
398    };
399
400    let mut y = vec![0.0f32; n_out];
401
402    // y[io_0,...,io_{n-1}] = Σ_{ii, bonds} [∏_k M_k[bond_{k-1}, io_k, ii_k, bond_k]] · x[ii]
403    // Contract bond chain for each (out_idx, in_idx) pair.
404    for (out_idx, y_out) in y.iter_mut().enumerate() {
405        let io: Vec<usize> = (0..n_sites)
406            .map(|k| (out_idx / out_strides[k]) % d_out_dims[k])
407            .collect();
408        for (in_idx, &x_in) in x.iter().enumerate() {
409            let ii: Vec<usize> = (0..n_sites)
410                .map(|k| (in_idx / in_strides[k]) % d_in_dims[k])
411                .collect();
412            // Trace bond chain.
413            let mut bond = vec![1.0f32]; // chi=1 left boundary
414            for k in 0..n_sites {
415                let site = &mpo.sites[k];
416                let chi_r = site.chi_right;
417                let mut next = vec![0.0f32; chi_r];
418                for (il, &bond_val) in bond.iter().enumerate() {
419                    for (ir, next_val) in next.iter_mut().enumerate() {
420                        *next_val += bond_val * site.get(il, io[k], ii[k], ir);
421                    }
422                }
423                bond = next;
424            }
425            *y_out += bond[0] * x_in;
426        }
427    }
428
429    y
430}
431
432// ── utilities ─────────────────────────────────────────────────────────────────
433
434/// Frobenius reconstruction error: ‖W_mpo − W‖_F / ‖W‖_F.
435pub fn mpo_reconstruction_error(mpo: &Mpo, original: &[f32], n_out: usize, n_in: usize) -> f32 {
436    let mut diff_sq = 0.0f32;
437    let mut norm_sq = 0.0f32;
438
439    let mut e_j = vec![0.0f32; n_in];
440    for j in 0..n_in {
441        e_j[j] = 1.0;
442        let col = mpo_apply(mpo, &e_j);
443        e_j[j] = 0.0;
444        for i in 0..n_out {
445            let d = col[i] - original[i * n_in + j];
446            diff_sq += d * d;
447            norm_sq += original[i * n_in + j] * original[i * n_in + j];
448        }
449    }
450
451    if norm_sq < 1e-10 {
452        return 0.0;
453    }
454    (diff_sq / norm_sq).sqrt()
455}
456
457/// Compression ratio: mpo parameter count / (n_out * n_in).
458pub fn mpo_compression_ratio(mpo: &Mpo, n_out: usize, n_in: usize) -> f32 {
459    let mpo_params: usize = mpo
460        .sites
461        .iter()
462        .map(|s| s.chi_left * s.d_out * s.d_in * s.chi_right)
463        .sum();
464    mpo_params as f32 / (n_out * n_in) as f32
465}
466
467/// Compress a dense weight matrix into an MPO with automatic dimension handling.
468///
469/// Unlike [`compress_matrix_to_mpo`], this function accepts *any* `n_out × n_in`
470/// matrix. It factors each dimension into a product of small integers, pads to
471/// equal site counts with trivial (dim-1) sites, zero-pads the matrix to the
472/// nearest compatible shape, compresses, then trims the output.
473///
474/// `chi_max` controls the maximum bond dimension (compression quality).
475pub fn compress_matrix_to_mpo_auto(
476    weights: &[f32],
477    n_out: usize,
478    n_in: usize,
479    chi_max: usize,
480) -> Mpo {
481    assert_eq!(weights.len(), n_out * n_in);
482    assert!(n_out >= 1 && n_in >= 1);
483
484    let mut out_factors = factor_dimension(n_out);
485    let mut in_factors = factor_dimension(n_in);
486
487    let n_sites = out_factors.len().max(in_factors.len());
488    pad_factors(&mut out_factors, n_sites);
489    pad_factors(&mut in_factors, n_sites);
490
491    let padded_out: usize = out_factors.iter().product();
492    let padded_in: usize = in_factors.iter().product();
493
494    let padded_weights = if padded_out == n_out && padded_in == n_in {
495        weights.to_vec()
496    } else {
497        let mut padded = vec![0.0f32; padded_out * padded_in];
498        for r in 0..n_out {
499            let src = &weights[r * n_in..(r + 1) * n_in];
500            let dst = &mut padded[r * padded_in..(r + 1) * padded_in];
501            dst[..n_in].copy_from_slice(src);
502        }
503        padded
504    };
505
506    let mpo = compress_variable_mpo(
507        &padded_weights,
508        padded_out,
509        padded_in,
510        &out_factors,
511        &in_factors,
512        chi_max,
513    );
514
515    trim_mpo(&mpo, n_out, n_in, padded_out, padded_in)
516}
517
518/// Internal: compress with variable per-site dimensions.
519fn compress_variable_mpo(
520    weights: &[f32],
521    n_out: usize,
522    n_in: usize,
523    d_out_dims: &[usize],
524    d_in_dims: &[usize],
525    chi_max: usize,
526) -> Mpo {
527    let n_sites = d_out_dims.len();
528    assert_eq!(d_in_dims.len(), n_sites);
529    assert_eq!(weights.len(), n_out * n_in);
530
531    let phys_dims: Vec<usize> = (0..n_sites).map(|k| d_out_dims[k] * d_in_dims[k]).collect();
532
533    let do_strides: Vec<usize> = {
534        let mut s = vec![0usize; n_sites];
535        let mut stride = 1;
536        for k in (0..n_sites).rev() {
537            s[k] = stride;
538            stride *= d_out_dims[k];
539        }
540        s
541    };
542    let di_strides: Vec<usize> = {
543        let mut s = vec![0usize; n_sites];
544        let mut stride = 1;
545        for k in (0..n_sites).rev() {
546            s[k] = stride;
547            stride *= d_in_dims[k];
548        }
549        s
550    };
551
552    let pair_strides: Vec<usize> = {
553        let mut s = vec![0usize; n_sites];
554        let mut stride = 1;
555        for k in (0..n_sites).rev() {
556            s[k] = stride;
557            stride *= phys_dims[k];
558        }
559        s
560    };
561
562    let mut w_t = vec![0.0f32; n_out * n_in];
563    for row in 0..n_out {
564        for col in 0..n_in {
565            let mut interleaved = 0usize;
566            for k in 0..n_sites {
567                let io_k = (row / do_strides[k]) % d_out_dims[k];
568                let ii_k = (col / di_strides[k]) % d_in_dims[k];
569                interleaved += (io_k * d_in_dims[k] + ii_k) * pair_strides[k];
570            }
571            w_t[interleaved] = weights[row * n_in + col];
572        }
573    }
574
575    let mut current = w_t;
576    let mut chi_left = 1usize;
577    let mut sites = Vec::with_capacity(n_sites);
578
579    for k in 0..n_sites {
580        let phys_k = phys_dims[k];
581        let d_out_k = d_out_dims[k];
582        let d_in_k = d_in_dims[k];
583        let total_right: usize = phys_dims[k..].iter().product();
584        let n_svd = total_right / phys_k;
585        let m_svd = chi_left * phys_k;
586
587        let mut unfolded = vec![0.0f32; m_svd * n_svd];
588        for il in 0..chi_left {
589            for ip in 0..phys_k {
590                let row = il * phys_k + ip;
591                for col in 0..n_svd {
592                    unfolded[row * n_svd + col] = current[il * total_right + ip * n_svd + col];
593                }
594            }
595        }
596
597        let (u, sigma, vt, k_svd) = if m_svd >= n_svd {
598            let (u, s, vt) = svd_thin(&unfolded, m_svd, n_svd);
599            let k = s.len();
600            (u, s, vt, k)
601        } else {
602            let ut = transpose(&unfolded, m_svd, n_svd);
603            let (v, s, ut2) = svd_thin(&ut, n_svd, m_svd);
604            let k = s.len();
605            let u2 = transpose(&ut2, m_svd, m_svd);
606            let vt2 = transpose(&v, n_svd, m_svd);
607            (u2, s, vt2, k)
608        };
609
610        const SVD_EPS: f32 = 1e-9;
611        let chi_new = sigma
612            .iter()
613            .filter(|&&s| s > SVD_EPS)
614            .count()
615            .min(chi_max)
616            .max(1);
617        let chi_right = if k == n_sites - 1 { 1 } else { chi_new };
618
619        let mut data = vec![0.0f32; chi_left * d_out_k * d_in_k * chi_right];
620        for il in 0..chi_left {
621            for io in 0..d_out_k {
622                for ii in 0..d_in_k {
623                    let u_row = il * phys_k + io * d_in_k + ii;
624                    for ir in 0..chi_right {
625                        let u_val = if ir < k_svd {
626                            u[u_row * k_svd + ir]
627                        } else {
628                            0.0
629                        };
630                        let s_val = if ir < k_svd { sigma[ir] } else { 0.0 };
631                        data[il * d_out_k * d_in_k * chi_right
632                            + io * d_in_k * chi_right
633                            + ii * chi_right
634                            + ir] = u_val * s_val;
635                    }
636                }
637            }
638        }
639        sites.push(MpoSite {
640            chi_left,
641            d_out: d_out_k,
642            d_in: d_in_k,
643            chi_right,
644            data,
645        });
646
647        if k < n_sites - 1 {
648            current = (0..chi_right * n_svd)
649                .map(|idx| {
650                    let ir = idx / n_svd;
651                    let col = idx % n_svd;
652                    if ir < k_svd {
653                        vt[ir * n_svd + col]
654                    } else {
655                        0.0
656                    }
657                })
658                .collect();
659        }
660        chi_left = chi_right;
661    }
662
663    Mpo { sites }
664}
665
666/// Trim an MPO that was built from a zero-padded matrix.
667///
668/// Sites with `d_in == 1` or `d_out == 1` (trivial padding sites) are removed.
669/// The remaining sites are re-chained with correct bond dimensions.
670fn trim_mpo(mpo: &Mpo, orig_out: usize, orig_in: usize, pad_out: usize, pad_in: usize) -> Mpo {
671    if orig_out == pad_out && orig_in == pad_in {
672        return mpo.clone();
673    }
674
675    let mut kept: Vec<MpoSite> = mpo
676        .sites
677        .iter()
678        .filter(|s| s.d_out > 1 || s.d_in > 1)
679        .cloned()
680        .collect();
681
682    if kept.is_empty() {
683        kept = vec![MpoSite {
684            chi_left: 1,
685            d_out: orig_out,
686            d_in: orig_in,
687            chi_right: 1,
688            data: vec![0.0; orig_out * orig_in],
689        }];
690    }
691
692    kept[0].chi_left = 1;
693    let last = kept.last_mut().unwrap();
694    if last.chi_right != 1 {
695        last.chi_right = 1;
696        last.data.truncate(last.chi_left * last.d_out * last.d_in);
697    }
698
699    Mpo { sites: kept }
700}
701
702/// Convert an MPO to a chain of `GraphNode4D` nodes compatible with the MPS graph infrastructure.
703///
704/// Each site is stored as a graph node with its tensor data. Sites are linked by
705/// `TemporalEdge`s whose `weight` encodes the bond dimension. This enables MPO
706/// sites to participate in the same spatial-temporal graph queries as MPS states.
707pub fn mpo_to_graph_nodes(mpo: &Mpo) -> Vec<GraphNode4D> {
708    mpo.sites
709        .iter()
710        .enumerate()
711        .map(|(i, site)| {
712            let mut props = GraphProperties::new();
713            props.insert(
714                "shape".into(),
715                serde_json::Value::Array(vec![
716                    serde_json::Value::from(site.chi_left as u64),
717                    serde_json::Value::from(site.d_out as u64),
718                    serde_json::Value::from(site.d_in as u64),
719                    serde_json::Value::from(site.chi_right as u64),
720                ]),
721            );
722            props.insert(
723                "mpo_kind".into(),
724                serde_json::Value::String("mpo_site".into()),
725            );
726            let data_json = serde_json::Value::Array(
727                site.data
728                    .iter()
729                    .map(|&x| serde_json::Value::from(x as f64))
730                    .collect(),
731            );
732            props.insert("data".into(), data_json);
733
734            let successors = if i + 1 < mpo.sites.len() {
735                vec![TemporalEdge {
736                    dst: (i + 1) as u64,
737                    weight: site.chi_right as f32,
738                    begin_ts: 0,
739                    end_ts: 1,
740                }]
741            } else {
742                vec![]
743            };
744
745            GraphNode4D {
746                id: i as u64,
747                x: i as f32,
748                y: 0.0,
749                z: 0.0,
750                begin_ts: 0,
751                end_ts: 1,
752                properties: props,
753                successors,
754            }
755        })
756        .collect()
757}
758
759#[cfg(test)]
760mod tests {
761    use super::*;
762
763    fn identity_matrix(n: usize) -> Vec<f32> {
764        let mut m = vec![0.0f32; n * n];
765        for i in 0..n {
766            m[i * n + i] = 1.0;
767        }
768        m
769    }
770
771    fn outer_product(u: &[f32], v: &[f32]) -> Vec<f32> {
772        let mut m = vec![0.0f32; u.len() * v.len()];
773        for (i, &ui) in u.iter().enumerate() {
774            for (j, &vj) in v.iter().enumerate() {
775                m[i * v.len() + j] = ui * vj;
776            }
777        }
778        m
779    }
780
781    fn norm(v: &[f32]) -> f32 {
782        v.iter().map(|x| x * x).sum::<f32>().sqrt()
783    }
784
785    #[test]
786    fn test_svd_thin_orthogonality() {
787        let a = [
788            1.0f32, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0,
789            0.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0,
790        ];
791        let (u, _sigma, vt) = svd_thin(&a, 4, 6);
792
793        // U^T U ≈ I  (u is 4×4, checking inner products of columns)
794        for i in 0..4 {
795            for j in 0..4 {
796                let dot: f32 = (0..4).map(|k| u[k * 4 + i] * u[k * 4 + j]).sum();
797                let expected = if i == j { 1.0 } else { 0.0 };
798                assert!(
799                    (dot - expected).abs() < 1e-3,
800                    "U^T U [{i},{j}] = {dot:.6}, expected {expected}"
801                );
802            }
803        }
804        // Vt Vt^T ≈ I
805        for i in 0..4 {
806            for j in 0..4 {
807                let dot: f32 = (0..6).map(|k| vt[i * 6 + k] * vt[j * 6 + k]).sum();
808                let expected = if i == j { 1.0 } else { 0.0 };
809                assert!(
810                    (dot - expected).abs() < 1e-3,
811                    "Vt Vt^T [{i},{j}] = {dot:.6}, expected {expected}"
812                );
813            }
814        }
815    }
816
817    #[test]
818    fn test_compress_identity_4x4_exact() {
819        let w = identity_matrix(4);
820        let mpo = compress_matrix_to_mpo(&w, 4, 4, 2, 4);
821        let err = mpo_reconstruction_error(&mpo, &w, 4, 4);
822        assert!(
823            err < 1e-3,
824            "identity matrix MPO reconstruction error {err:.6} should be < 1e-3"
825        );
826    }
827
828    #[test]
829    fn test_compress_rank1_chi1_exact() {
830        let u = [1.0f32, 2.0, 2.0, 4.0];
831        let v = [1.0f32, 1.0, 1.0, 1.0];
832        let w = outer_product(&u, &v);
833        let mpo = compress_matrix_to_mpo(&w, 4, 4, 2, 1);
834        let err = mpo_reconstruction_error(&mpo, &w, 4, 4);
835        assert!(
836            err < 1e-3,
837            "rank-1 MPO reconstruction error {err:.6} should be < 1e-3 with chi_max=1"
838        );
839    }
840
841    #[test]
842    fn test_mpo_apply_identity() {
843        let w = identity_matrix(4);
844        let mpo = compress_matrix_to_mpo(&w, 4, 4, 2, 4);
845        let x = [1.0f32, 0.0, 0.0, 0.0];
846        let y = mpo_apply(&mpo, &x);
847        assert_eq!(y.len(), 4);
848        assert!(
849            (y[0] - 1.0).abs() < 1e-3 && y[1].abs() < 1e-3,
850            "identity MPO on e_0 should return e_0, got {y:?}"
851        );
852    }
853
854    #[test]
855    fn test_mpo_compression_ratio() {
856        let w: Vec<f32> = (0..256).map(|i| i as f32 * 0.01).collect();
857        let mpo = compress_matrix_to_mpo(&w, 16, 16, 2, 1);
858        let ratio = mpo_compression_ratio(&mpo, 16, 16);
859        assert!(
860            ratio < 0.5,
861            "chi_max=1 on 16×16 should give ratio < 0.5, got {ratio:.4}"
862        );
863    }
864
865    #[test]
866    fn test_reconstruction_error_decreases_with_chi() {
867        let w: Vec<f32> = (0..256).map(|i| ((i * 7 + 3) % 17) as f32 - 8.0).collect();
868        let mpo1 = compress_matrix_to_mpo(&w, 16, 16, 2, 1);
869        let mpo4 = compress_matrix_to_mpo(&w, 16, 16, 2, 4);
870        let err1 = mpo_reconstruction_error(&mpo1, &w, 16, 16);
871        let err4 = mpo_reconstruction_error(&mpo4, &w, 16, 16);
872        assert!(
873            err4 < err1,
874            "chi=4 error {err4:.4} should be less than chi=1 error {err1:.4}"
875        );
876    }
877
878    #[test]
879    fn test_factor_dimension_powers_of_4() {
880        let factors = factor_dimension(16);
881        assert_eq!(factors, vec![4, 4], "16 = 4×2");
882        let factors64 = factor_dimension(64);
883        assert_eq!(factors64, vec![4, 4, 4], "64 = 4³");
884    }
885
886    #[test]
887    fn test_factor_dimension_non_power() {
888        let factors = factor_dimension(6);
889        assert!(
890            factors.iter().product::<usize>() == 6,
891            "factors must multiply to 6, got {factors:?}"
892        );
893        assert!(factors.iter().all(|&d| d >= 2), "all factors >= 2");
894    }
895
896    #[test]
897    fn test_factor_dimension_prime() {
898        let factors = factor_dimension(7);
899        assert_eq!(factors, vec![7], "prime number factors to itself");
900    }
901
902    #[test]
903    fn test_compress_auto_6x6() {
904        let w: Vec<f32> = (0..36).map(|i| i as f32 * 0.1 - 1.8).collect();
905        let mpo = compress_matrix_to_mpo_auto(&w, 6, 6, 4);
906        assert_eq!(mpo.sites.len(), 2);
907        let err = mpo_reconstruction_error(&mpo, &w, 6, 6);
908        assert!(
909            err < 0.3,
910            "6×6 auto MPO error {err:.4} should be < 0.3 with chi=4"
911        );
912    }
913
914    #[test]
915    fn test_compress_auto_12x8() {
916        let w: Vec<f32> = (0..96)
917            .map(|i| (((i * 7 + 3) % 13) as f32 - 6.0) * 0.5)
918            .collect();
919        let mpo = compress_matrix_to_mpo_auto(&w, 12, 8, 8);
920        assert!(!mpo.sites.is_empty());
921        let err = mpo_reconstruction_error(&mpo, &w, 12, 8);
922        assert!(
923            err < 0.3,
924            "12×8 auto MPO error {err:.4} should be < 0.3 with chi=8"
925        );
926    }
927
928    #[test]
929    fn test_compress_auto_identity_non_power() {
930        let w = identity_matrix(6);
931        let mpo = compress_matrix_to_mpo_auto(&w, 6, 6, 4);
932        let err = mpo_reconstruction_error(&mpo, &w, 6, 6);
933        assert!(err < 0.1, "6×6 identity MPO error {err:.4} should be < 0.1");
934    }
935
936    #[test]
937    fn test_mpo_to_graph_nodes_chain() {
938        let w = identity_matrix(4);
939        let mpo = compress_matrix_to_mpo(&w, 4, 4, 2, 4);
940        let nodes = mpo_to_graph_nodes(&mpo);
941        assert_eq!(nodes.len(), 2, "2-site MPO → 2 graph nodes");
942        assert_eq!(nodes[0].successors.len(), 1, "site 0 links to site 1");
943        assert_eq!(nodes[0].successors[0].dst, 1);
944        assert_eq!(
945            nodes[0].successors[0].weight as usize,
946            mpo.sites[0].chi_right
947        );
948    }
949
950    #[test]
951    fn test_auto_mpo_apply_matches_dense() {
952        let w: Vec<f32> = (0..36).map(|i| ((i * 7 + 3) % 11) as f32 - 5.0).collect();
953        let mpo = compress_matrix_to_mpo_auto(&w, 6, 6, 8);
954        let x: Vec<f32> = vec![1.0, -1.0, 0.5, -0.5, 2.0, -2.0];
955        let y_mpo = mpo_apply(&mpo, &x);
956        let y_dense: Vec<f32> = (0..6)
957            .map(|i| (0..6).map(|j| w[i * 6 + j] * x[j]).sum())
958            .collect();
959        let rel_err = {
960            let diff: f32 = y_mpo
961                .iter()
962                .zip(y_dense.iter())
963                .map(|(a, b)| (a - b).powi(2))
964                .sum();
965            let norm: f32 = y_dense.iter().map(|v| v.powi(2)).sum();
966            (diff / (norm + 1e-8)).sqrt()
967        };
968        assert!(
969            rel_err < 0.3,
970            "auto MPO apply rel_err {rel_err:.4} should be < 0.3"
971        );
972    }
973
974    #[test]
975    fn test_mpo_apply_random_close() {
976        let w: Vec<f32> = (0..256).map(|i| ((i * 7 + 3) % 17) as f32 - 8.0).collect();
977
978        // 1. With chi_max=16 (exact reconstruction because it is full rank)
979        let mpo_exact = compress_matrix_to_mpo(&w, 16, 16, 2, 16);
980        let x: Vec<f32> = (0..16).map(|i| (i as f32 + 1.0) / 16.0).collect();
981        let y_dense: Vec<f32> = (0..16)
982            .map(|i| (0..16).map(|j| w[i * 16 + j] * x[j]).sum())
983            .collect();
984        let y_mpo_exact = mpo_apply(&mpo_exact, &x);
985        let diff_exact: Vec<f32> = y_dense
986            .iter()
987            .zip(y_mpo_exact.iter())
988            .map(|(a, b)| a - b)
989            .collect();
990        let rel_err_exact = norm(&diff_exact) / (norm(&y_dense) + 1e-8);
991        assert!(
992            rel_err_exact < 1e-3,
993            "full-rank MPO reconstruction error {rel_err_exact:.6} should be < 1e-3"
994        );
995
996        // 2. With chi_max=4 (truncated reconstruction, error is bounded by theoretical rank limits)
997        let mpo_trunc = compress_matrix_to_mpo(&w, 16, 16, 2, 4);
998        let y_mpo_trunc = mpo_apply(&mpo_trunc, &x);
999        let diff_trunc: Vec<f32> = y_dense
1000            .iter()
1001            .zip(y_mpo_trunc.iter())
1002            .map(|(a, b)| a - b)
1003            .collect();
1004        let rel_err_trunc = norm(&diff_trunc) / (norm(&y_dense) + 1e-8);
1005        assert!(
1006            rel_err_trunc < 0.70,
1007            "truncated MPO reconstruction error {rel_err_trunc:.4} should be < 0.70"
1008        );
1009    }
1010}