1use crate::algorithms::four_d::{GraphNode4D, GraphProperties, TemporalEdge};
2
3#[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#[derive(Debug, Clone)]
32pub struct Mpo {
33 pub sites: Vec<MpoSite>,
34}
35
36fn 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
61pub fn svd_thin(a: &[f32], m: usize, n: usize) -> (Vec<f32>, Vec<f32>, Vec<f32>) {
64 if m < n {
65 let at = transpose(a, m, n);
67 let (v, sigma, ut) = svd_thin_tall(&at, n, m);
68 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
76fn 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 let at = transpose(a, m, n);
82 let mut c = matmul(&at, a, n, m, n);
83
84 let mut v = vec![0.0f32; n * n];
86 for i in 0..n {
87 v[i * n + i] = 1.0;
88 }
89
90 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 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 c[p * n + r] = c[r * n + p];
121 c[q * n + r] = c[r * n + q];
122 }
123 }
124
125 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 let mut sigma: Vec<f32> = (0..n).map(|i| c[i * n + i].max(0.0).sqrt()).collect();
142
143 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 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
185pub 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
216fn pad_factors(factors: &mut Vec<usize>, target_len: usize) {
218 while factors.len() < target_len {
219 factors.insert(0, 1);
220 }
221}
222
223pub 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 let phys = d_out * d_in; 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 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; 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 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 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 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
369pub 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 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 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 let mut bond = vec![1.0f32]; 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
432pub 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
457pub 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
467pub 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
518fn 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
666fn 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
702pub 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 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 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 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 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}