Skip to main content

oxiphysics_core/tensor/
decomposition.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6use super::operations::*;
7use super::types::*;
8
9/// Right polar decomposition of a non-singular 3×3 tensor F = R U.
10///
11/// Uses iterative algorithm: R_new = (R + R^{-T})/2 normalised.
12/// Returns `(R, U)` where R is proper orthogonal and U is symmetric positive-definite.
13#[allow(dead_code)]
14pub fn polar_decompose_right(f: &Tensor2) -> Option<(Tensor2, Tensor2)> {
15    let mut r = Tensor2 { data: f.data };
16    for _ in 0..200 {
17        let r_inv_t = r.inverse()?.transpose();
18        let r_new_data: [[f64; 3]; 3] = {
19            let mut d = [[0.0f64; 3]; 3];
20            for i in 0..3 {
21                for j in 0..3 {
22                    d[i][j] = 0.5 * (r.data[i][j] + r_inv_t.data[i][j]);
23                }
24            }
25            d
26        };
27        let r_new = Tensor2 { data: r_new_data };
28        let diff_norm: f64 = (0..3)
29            .flat_map(|i| (0..3).map(move |j| (r_new.data[i][j] - r.data[i][j]).powi(2)))
30            .sum::<f64>()
31            .sqrt();
32        r = r_new;
33        if diff_norm < 1e-12 {
34            break;
35        }
36    }
37    let rt = r.transpose();
38    let u = rt.dot(f);
39    Some((r, u))
40}
41/// Approximate matrix logarithm of a tensor close to identity: ln(I + X) ≈ X - X²/2 + X³/3.
42///
43/// Only accurate for ||X|| < 0.5 (small deformation regime).
44#[allow(dead_code)]
45pub fn log_tensor_approx(f: &Tensor2) -> Tensor2 {
46    let id = Tensor2::identity();
47    let x = f.sub(&id);
48    let x2 = x.dot(&x);
49    let x3 = x2.dot(&x);
50    let term1 = Tensor2 { data: x.data };
51    let term2 = x2.scale(0.5);
52    let term3 = x3.scale(1.0 / 3.0);
53    term1.sub(&term2).add(&term3)
54}
55/// Approximate matrix exponential via Padé (2,2) approximation.
56///
57/// exp(A) ≈ (I - A/2 + A²/12)^{-1} (I + A/2 + A²/12).
58#[allow(dead_code)]
59pub fn exp_tensor_pade(a: &Tensor2) -> Option<Tensor2> {
60    let id = Tensor2::identity();
61    let a2 = a.dot(a);
62    let a_half = a.scale(0.5);
63    let a2_12 = a2.scale(1.0 / 12.0);
64    let p = id.add(&a_half).add(&a2_12);
65    let q = id.sub(&a_half).add(&a2_12);
66    let q_inv = q.inverse()?;
67    Some(q_inv.dot(&p))
68}
69/// Einstein summation over a pair of DenseTensors with explicit free and contracted indices.
70///
71/// `free_a`: list of modes of `a` that appear in the output.
72/// `free_b`: list of modes of `b` that appear in the output.
73/// `contract_a`: list of modes of `a` to contract.
74/// `contract_b`: list of modes of `b` to contract (must pair with `contract_a`).
75///
76/// Output shape = \[a.shape\[free_a\[0\]\], a.shape\[free_a\[1\]\], ..., b.shape\[free_b\[0\]\], ...\].
77#[allow(dead_code)]
78#[allow(clippy::too_many_arguments)]
79pub fn general_einsum(
80    a: &DenseTensor,
81    free_a: &[usize],
82    contract_a: &[usize],
83    b: &DenseTensor,
84    free_b: &[usize],
85    contract_b: &[usize],
86) -> DenseTensor {
87    assert_eq!(contract_a.len(), contract_b.len());
88    for (&ca, &cb) in contract_a.iter().zip(contract_b.iter()) {
89        assert_eq!(a.shape[ca], b.shape[cb], "contracted dimensions must match");
90    }
91    let mut out_shape: Vec<usize> = free_a.iter().map(|&m| a.shape[m]).collect();
92    out_shape.extend(free_b.iter().map(|&m| b.shape[m]));
93    let n_out: usize = if out_shape.is_empty() {
94        1
95    } else {
96        out_shape.iter().product()
97    };
98    let mut out_data = vec![0.0f64; n_out];
99    let out_rank = out_shape.len();
100    let mut out_strides = vec![1usize; out_rank.max(1)];
101    for k in (0..out_rank.saturating_sub(1)).rev() {
102        out_strides[k] = out_strides[k + 1] * out_shape[k + 1];
103    }
104    let strides_a = a.strides();
105    let strides_b = b.strides();
106    for fa in 0..a.data.len() {
107        let mut tmp = fa;
108        let mut midx_a = vec![0usize; a.shape.len()];
109        for k in 0..a.shape.len() {
110            midx_a[k] = tmp / strides_a[k];
111            tmp %= strides_a[k];
112        }
113        for fb in 0..b.data.len() {
114            let mut tmp2 = fb;
115            let mut midx_b = vec![0usize; b.shape.len()];
116            for k in 0..b.shape.len() {
117                midx_b[k] = tmp2 / strides_b[k];
118                tmp2 %= strides_b[k];
119            }
120            let contracted = contract_a
121                .iter()
122                .zip(contract_b.iter())
123                .all(|(&ca, &cb)| midx_a[ca] == midx_b[cb]);
124            if !contracted {
125                continue;
126            }
127            let mut oidx = vec![0usize; out_rank];
128            let mut oi = 0;
129            for &ma in free_a.iter() {
130                oidx[oi] = midx_a[ma];
131                oi += 1;
132            }
133            for &mb in free_b.iter() {
134                oidx[oi] = midx_b[mb];
135                oi += 1;
136            }
137            let fo: usize = if out_rank == 0 {
138                0
139            } else {
140                oidx.iter()
141                    .zip(out_strides.iter())
142                    .map(|(&i, &s)| i * s)
143                    .sum()
144            };
145            out_data[fo] += a.data[fa] * b.data[fb];
146        }
147    }
148    DenseTensor {
149        shape: out_shape,
150        data: out_data,
151    }
152}
153/// Reconstruct a DenseTensor from a CP decomposition.
154#[allow(dead_code)]
155pub fn cp_reconstruct(cp: &CpDecomposition) -> DenseTensor {
156    let r = cp.lambdas.len();
157    let n0 = cp.a.len();
158    let n1 = cp.b.len();
159    let n2 = cp.c.len();
160    let mut out = DenseTensor::zeros(&[n0, n1, n2]);
161    for rr in 0..r {
162        let lam = cp.lambdas[rr];
163        for i in 0..n0 {
164            for j in 0..n1 {
165                for k in 0..n2 {
166                    let v = out.get(&[i, j, k]) + lam * cp.a[i][rr] * cp.b[j][rr] * cp.c[k][rr];
167                    out.set(&[i, j, k], v);
168                }
169            }
170        }
171    }
172    out
173}
174/// Relative reconstruction error: ||T - T_approx||_F / ||T||_F.
175#[allow(dead_code)]
176pub fn cp_relative_error(original: &DenseTensor, cp: &CpDecomposition) -> f64 {
177    let recon = cp_reconstruct(cp);
178    let diff_norm = original.sub_tensor(&recon).frobenius_norm();
179    let orig_norm = original.frobenius_norm();
180    if orig_norm < 1e-30 {
181        return diff_norm;
182    }
183    diff_norm / orig_norm
184}
185/// Compute the 4th-order stiffness tensor for Neo-Hookean material at deformation F.
186///
187/// C_ijkl = lambda delta_ij delta_kl + mu (delta_ik delta_jl + delta_il delta_jk - delta_ij delta_kl/3)
188/// (linearised tangent at identity).
189#[allow(dead_code)]
190pub fn neo_hookean_stiffness(lambda: f64, mu: f64) -> Tensor4 {
191    Tensor4::isotropic(lambda, mu)
192}
193/// Compute Cauchy stress from linear elasticity: sigma = C : epsilon.
194#[allow(dead_code)]
195pub fn cauchy_stress_linear(c: &Tensor4, epsilon: &Tensor2) -> Tensor2 {
196    c.double_contract_2(epsilon)
197}
198/// Compute the engineering Young's modulus E and Poisson's ratio nu from Lame parameters.
199#[allow(dead_code)]
200pub fn lame_to_young_poisson(lambda: f64, mu: f64) -> (f64, f64) {
201    let e = mu * (3.0 * lambda + 2.0 * mu) / (lambda + mu);
202    let nu = lambda / (2.0 * (lambda + mu));
203    (e, nu)
204}
205/// Compute Lame parameters from Young's modulus E and Poisson's ratio nu.
206#[allow(dead_code)]
207pub fn young_poisson_to_lame(e: f64, nu: f64) -> (f64, f64) {
208    let lambda = e * nu / ((1.0 + nu) * (1.0 - 2.0 * nu));
209    let mu = e / (2.0 * (1.0 + nu));
210    (lambda, mu)
211}
212/// Compute the bulk modulus K from Lame parameters.
213#[allow(dead_code)]
214pub fn bulk_modulus(lambda: f64, mu: f64) -> f64 {
215    lambda + 2.0 * mu / 3.0
216}
217/// Compute the shear modulus (= mu).
218#[allow(dead_code)]
219pub fn shear_modulus(mu: f64) -> f64 {
220    mu
221}
222/// Build a transversely isotropic stiffness tensor given five elastic constants.
223///
224/// Symmetry axis = e_3 (index 2).
225/// C_1111 = C_2222 = c11, C_3333 = c33,
226/// C_1122 = c12, C_1133 = C_2233 = c13,
227/// C_2323 = C_1313 = c44, C_1212 = (c11-c12)/2.
228#[allow(dead_code)]
229#[allow(clippy::too_many_arguments)]
230pub fn transversely_isotropic_stiffness(
231    c11: f64,
232    c33: f64,
233    c12: f64,
234    c13: f64,
235    c44: f64,
236) -> Tensor4 {
237    let c66 = 0.5 * (c11 - c12);
238    let mut t = Tensor4::zero();
239    let mut set = |i: usize, j: usize, k: usize, l: usize, v: f64| {
240        t.data[i][j][k][l] = v;
241        t.data[j][i][k][l] = v;
242        t.data[i][j][l][k] = v;
243        t.data[j][i][l][k] = v;
244        t.data[k][l][i][j] = v;
245        t.data[l][k][i][j] = v;
246        t.data[k][l][j][i] = v;
247        t.data[l][k][j][i] = v;
248    };
249    set(0, 0, 0, 0, c11);
250    set(1, 1, 1, 1, c11);
251    set(2, 2, 2, 2, c33);
252    set(0, 0, 1, 1, c12);
253    set(0, 0, 2, 2, c13);
254    set(1, 1, 2, 2, c13);
255    set(1, 2, 1, 2, c44);
256    set(0, 2, 0, 2, c44);
257    set(0, 1, 0, 1, c66);
258    let _ = c66;
259    t
260}
261/// Harmonic mean of two stiffness tensors (Reuss bound):
262/// C_Reuss = (C_a^{-1} + C_b^{-1})^{-1} / 2  (in Voigt/Kelvin form).
263#[allow(dead_code)]
264pub fn reuss_average(c_a: &Tensor4, c_b: &Tensor4) -> Option<Tensor4> {
265    let ma = KelvinTensor::from_tensor4(c_a);
266    let mb = KelvinTensor::from_tensor4(c_b);
267    let sa = invert_6x6(&ma)?;
268    let sb = invert_6x6(&mb)?;
269    let mut sc = [[0.0f64; 6]; 6];
270    for i in 0..6 {
271        for j in 0..6 {
272            sc[i][j] = 0.5 * (sa[i][j] + sb[i][j]);
273        }
274    }
275    let mc = invert_6x6(&sc)?;
276    Some(KelvinTensor::to_tensor4(&mc))
277}
278/// Arithmetic mean of two stiffness tensors (Voigt bound):
279/// C_Voigt = (C_a + C_b) / 2.
280#[allow(dead_code)]
281pub fn voigt_average(c_a: &Tensor4, c_b: &Tensor4) -> Tensor4 {
282    let a = c_a.add(c_b);
283    a.scale(0.5)
284}
285/// Hill average: arithmetic mean of Voigt and Reuss bounds.
286#[allow(dead_code)]
287pub fn hill_average(c_a: &Tensor4, c_b: &Tensor4) -> Option<Tensor4> {
288    let cv = voigt_average(c_a, c_b);
289    let cr = reuss_average(c_a, c_b)?;
290    Some(cv.add(&cr).scale(0.5))
291}
292#[cfg(test)]
293mod tests_extended {
294    use super::*;
295    fn approx(a: f64, b: f64) -> bool {
296        (a - b).abs() < 1e-10
297    }
298    #[test]
299    fn test_bilinear_form_identity() {
300        let id = Tensor2::identity();
301        let v = [1.0, 2.0, 3.0];
302        let w = [4.0, 5.0, 6.0];
303        let bf = bilinear_form(&id, &v, &w);
304        assert!(approx(bf, 1.0 * 4.0 + 2.0 * 5.0 + 3.0 * 6.0));
305    }
306    #[test]
307    fn test_quadratic_form_identity() {
308        let id = Tensor2::identity();
309        let v = [3.0, 4.0, 0.0];
310        let q = quadratic_form(&id, &v);
311        assert!(approx(q, 25.0));
312    }
313    #[test]
314    fn test_quadratic_form_zero_tensor() {
315        let z = Tensor2::zero();
316        let v = [1.0, 2.0, 3.0];
317        assert!(approx(quadratic_form(&z, &v), 0.0));
318    }
319    #[test]
320    fn test_bilinear_form_diagonal() {
321        let mut d = Tensor2::zero();
322        d.data[0][0] = 2.0;
323        d.data[1][1] = 3.0;
324        d.data[2][2] = 5.0;
325        let v = [1.0, 1.0, 1.0];
326        let w = [1.0, 1.0, 1.0];
327        assert!(approx(bilinear_form(&d, &v, &w), 10.0));
328    }
329    #[test]
330    fn test_vec_outer_rank() {
331        let a = [1.0, 0.0, 0.0];
332        let b = [0.0, 1.0, 0.0];
333        let t = vec_outer(&a, &b);
334        assert!(approx(t.data[0][1], 1.0));
335        assert!(approx(t.data[1][0], 0.0));
336    }
337    #[test]
338    fn test_vec_outer_self_is_symmetric_for_same_vec() {
339        let v = [1.0, 2.0, 3.0];
340        let t = vec_outer(&v, &v);
341        assert!(t.is_symmetric(1e-12));
342    }
343    #[test]
344    fn test_tensor_outer_shape() {
345        let a = DenseTensor::zeros(&[2, 3]);
346        let b = DenseTensor::zeros(&[4, 5]);
347        let out = tensor_outer(&a, &b);
348        assert_eq!(out.shape, vec![2, 3, 4, 5]);
349    }
350    #[test]
351    fn test_tensor_outer_values() {
352        let mut a = DenseTensor::zeros(&[2]);
353        a.set(&[0], 2.0);
354        a.set(&[1], 3.0);
355        let mut b = DenseTensor::zeros(&[2]);
356        b.set(&[0], 5.0);
357        b.set(&[1], 7.0);
358        let out = tensor_outer(&a, &b);
359        assert!(approx(out.get(&[0, 0]), 10.0));
360        assert!(approx(out.get(&[1, 1]), 21.0));
361        assert!(approx(out.get(&[0, 1]), 14.0));
362    }
363    #[test]
364    fn test_symmetrize_tensor_rank2() {
365        let mut t = DenseTensor::zeros(&[3, 3]);
366        t.set(&[0, 1], 2.0);
367        t.set(&[1, 0], 4.0);
368        let s = symmetrize_tensor(&t);
369        assert!(approx(s.get(&[0, 1]), 3.0));
370        assert!(approx(s.get(&[1, 0]), 3.0));
371    }
372    #[test]
373    fn test_symmetrize_tensor_already_sym() {
374        let mut t = DenseTensor::zeros(&[2, 2]);
375        t.set(&[0, 0], 1.0);
376        t.set(&[0, 1], 3.0);
377        t.set(&[1, 0], 3.0);
378        t.set(&[1, 1], 2.0);
379        let s = symmetrize_tensor(&t);
380        assert!(approx(s.get(&[0, 1]), 3.0));
381    }
382    #[test]
383    fn test_dense_tensor_scale() {
384        let t = DenseTensor::from_data(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
385        let s = t.scale(2.0);
386        assert!(approx(s.get(&[0, 0]), 2.0));
387        assert!(approx(s.get(&[1, 1]), 8.0));
388    }
389    #[test]
390    fn test_dense_tensor_add_sub() {
391        let a = DenseTensor::from_data(&[3], vec![1.0, 2.0, 3.0]);
392        let b = DenseTensor::from_data(&[3], vec![4.0, 5.0, 6.0]);
393        let c = a.add_tensor(&b);
394        assert!(approx(c.get(&[2]), 9.0));
395        let d = c.sub_tensor(&a);
396        assert!(approx(d.get(&[0]), 4.0));
397    }
398    #[test]
399    fn test_dense_tensor_sum() {
400        let t = DenseTensor::from_data(&[4], vec![1.0, 2.0, 3.0, 4.0]);
401        assert!(approx(t.sum(), 10.0));
402    }
403    #[test]
404    fn test_dense_tensor_max_abs() {
405        let t = DenseTensor::from_data(&[3], vec![-5.0, 3.0, 1.0]);
406        assert!(approx(t.max_abs(), 5.0));
407    }
408    #[test]
409    fn test_fourth_order_symmetric_identity_symmetry() {
410        let is4 = fourth_order_symmetric_identity();
411        assert!(is4.has_minor_symmetry(1e-12));
412        assert!(is4.has_major_symmetry(1e-12));
413    }
414    #[test]
415    fn test_fourth_order_skew_identity_antisymmetry() {
416        let ia4 = fourth_order_skew_identity();
417        let result = ia4.double_contract_4(&ia4);
418        for i in 0..3 {
419            for j in 0..3 {
420                for k in 0..3 {
421                    for l in 0..3 {
422                        assert!(
423                            (result.data[i][j][k][l] - ia4.data[i][j][k][l]).abs() < 1e-10,
424                            "I^A :: I^A != I^A at [{i}][{j}][{k}][{l}]"
425                        );
426                    }
427                }
428            }
429        }
430    }
431    #[test]
432    fn test_fourth_order_is_plus_ia_equals_identity() {
433        let is4 = fourth_order_symmetric_identity();
434        let ia4 = fourth_order_skew_identity();
435        let id4 = Tensor4::identity();
436        for i in 0..3 {
437            for j in 0..3 {
438                for k in 0..3 {
439                    for l in 0..3 {
440                        assert!(
441                            (is4.data[i][j][k][l] + ia4.data[i][j][k][l] - id4.data[i][j][k][l])
442                                .abs()
443                                < 1e-12
444                        );
445                    }
446                }
447            }
448        }
449    }
450    #[test]
451    fn test_elasticity_stress_isotropic() {
452        let c = Tensor4::isotropic(1.0, 1.0);
453        let eps = Tensor2::identity().scale(1.0 / 3.0);
454        let sigma = elasticity_stress(&c, &eps);
455        let s_diag = sigma.data[0][0];
456        let expected = 1.0 * 1.0 + 2.0 * 1.0 / 3.0;
457        assert!(
458            (s_diag - expected).abs() < 1e-10,
459            "s_diag={s_diag}, expected={expected}"
460        );
461    }
462    #[test]
463    fn test_lame_to_young_poisson_round_trip() {
464        let (lambda, mu) = (1.0e10, 1.5e10);
465        let (e, nu) = lame_to_young_poisson(lambda, mu);
466        let (lambda2, mu2) = young_poisson_to_lame(e, nu);
467        assert!((lambda - lambda2).abs() / lambda < 1e-10);
468        assert!((mu - mu2).abs() / mu < 1e-10);
469    }
470    #[test]
471    fn test_young_poisson_steel() {
472        let (lambda, mu) = young_poisson_to_lame(200e9, 0.3);
473        assert!(lambda > 0.0 && mu > 0.0);
474        let k = bulk_modulus(lambda, mu);
475        let k_expected = 200e9 / (3.0 * (1.0 - 2.0 * 0.3));
476        assert!((k - k_expected).abs() / k_expected < 1e-8);
477    }
478    #[test]
479    fn test_invert_6x6_identity() {
480        let id6: [[f64; 6]; 6] = {
481            let mut m = [[0.0f64; 6]; 6];
482            for i in 0..6 {
483                m[i][i] = 1.0;
484            }
485            m
486        };
487        let inv = invert_6x6(&id6).unwrap();
488        for i in 0..6 {
489            for j in 0..6 {
490                let expected = if i == j { 1.0 } else { 0.0 };
491                assert!((inv[i][j] - expected).abs() < 1e-10);
492            }
493        }
494    }
495    #[test]
496    fn test_invert_6x6_diagonal() {
497        let mut m = [[0.0f64; 6]; 6];
498        for i in 0..6 {
499            m[i][i] = (i + 1) as f64;
500        }
501        let inv = invert_6x6(&m).unwrap();
502        for i in 0..6 {
503            assert!((inv[i][i] - 1.0 / (i + 1) as f64).abs() < 1e-10);
504        }
505    }
506    #[test]
507    fn test_compliance_round_trip_isotropic() {
508        let c = Tensor4::isotropic(1.0e10, 1.5e10);
509        let s_mat = compliance_from_stiffness(&c).unwrap();
510        let c2 = KelvinTensor::to_tensor4(&s_mat);
511        let c_kelvin = KelvinTensor::from_tensor4(&c);
512        for i in 0..6 {
513            let mut row_sum = 0.0f64;
514            for k in 0..6 {
515                row_sum += c_kelvin[i][k] * s_mat[k][i];
516            }
517            assert!((row_sum - 1.0).abs() < 1e-6, "diagonal[{i}]={row_sum}");
518        }
519        let _ = c2;
520    }
521    #[test]
522    fn test_polar_decompose_identity() {
523        let f = Tensor2::identity();
524        let (r, u) = polar_decompose_right(&f).unwrap();
525        for i in 0..3 {
526            for j in 0..3 {
527                let expected = if i == j { 1.0 } else { 0.0 };
528                assert!((r.data[i][j] - expected).abs() < 1e-8);
529                assert!((u.data[i][j] - expected).abs() < 1e-8);
530            }
531        }
532    }
533    #[test]
534    fn test_polar_decompose_r_is_orthogonal() {
535        let f = Tensor2::new([[1.2, 0.3, 0.0], [0.1, 0.9, 0.0], [0.0, 0.0, 1.0]]);
536        let (r, _u) = polar_decompose_right(&f).unwrap();
537        let rrt = r.dot(&r.transpose());
538        for i in 0..3 {
539            for j in 0..3 {
540                let expected = if i == j { 1.0 } else { 0.0 };
541                assert!(
542                    (rrt.data[i][j] - expected).abs() < 1e-7,
543                    "R R^T != I at [{i}][{j}]: got {}",
544                    rrt.data[i][j]
545                );
546            }
547        }
548    }
549    #[test]
550    fn test_log_tensor_approx_near_identity() {
551        let epsilon = 0.01;
552        let mut a = Tensor2::zero();
553        a.data[0][1] = 1.0;
554        a.data[1][0] = 1.0;
555        let fa = Tensor2::identity().add(&a.scale(epsilon));
556        let log_fa = log_tensor_approx(&fa);
557        assert!(
558            (log_fa.data[0][1] - epsilon).abs() < 1e-4,
559            "log_fa[0][1]={}",
560            log_fa.data[0][1]
561        );
562    }
563    #[test]
564    fn test_exp_tensor_pade_identity() {
565        let z = Tensor2::zero();
566        let e = exp_tensor_pade(&z).unwrap();
567        for i in 0..3 {
568            for j in 0..3 {
569                let expected = if i == j { 1.0 } else { 0.0 };
570                assert!(
571                    (e.data[i][j] - expected).abs() < 1e-10,
572                    "exp(0) != I at [{i}][{j}]"
573                );
574            }
575        }
576    }
577    #[test]
578    fn test_eshelby_sphere_symmetry() {
579        let s = eshelby_sphere(0.3);
580        assert!(s.has_minor_symmetry(1e-12));
581    }
582    #[test]
583    fn test_eshelby_sphere_nu_zero() {
584        let s = eshelby_sphere(0.0);
585        assert!((s.data[0][0][0][0] - 7.0 / 15.0).abs() < 1e-10);
586        assert!((s.data[0][0][1][1] - (-1.0 / 15.0)).abs() < 1e-10);
587        assert!((s.data[0][1][0][1] - 4.0 / 15.0).abs() < 1e-10);
588    }
589    #[test]
590    fn test_transversely_isotropic_major_symmetry() {
591        let c = transversely_isotropic_stiffness(200.0, 180.0, 60.0, 65.0, 75.0);
592        assert!(c.has_major_symmetry(1e-10));
593    }
594    #[test]
595    fn test_transversely_isotropic_minor_symmetry() {
596        let c = transversely_isotropic_stiffness(200.0, 180.0, 60.0, 65.0, 75.0);
597        assert!(c.has_minor_symmetry(1e-10));
598    }
599    #[test]
600    fn test_voigt_average_is_arithmetic_mean() {
601        let c1 = Tensor4::isotropic(1.0, 1.0);
602        let c2 = Tensor4::isotropic(3.0, 3.0);
603        let cv = voigt_average(&c1, &c2);
604        let expected = Tensor4::isotropic(2.0, 2.0);
605        for i in 0..3 {
606            for j in 0..3 {
607                for k in 0..3 {
608                    for l in 0..3 {
609                        assert!((cv.data[i][j][k][l] - expected.data[i][j][k][l]).abs() < 1e-10);
610                    }
611                }
612            }
613        }
614    }
615    #[test]
616    fn test_reuss_average_symmetric_matrices() {
617        let c1 = Tensor4::isotropic(1.0, 1.0);
618        let c2 = Tensor4::isotropic(1.0, 1.0);
619        let cr = reuss_average(&c1, &c2).unwrap();
620        for i in 0..3 {
621            for j in 0..3 {
622                for k in 0..3 {
623                    for l in 0..3 {
624                        assert!(
625                            (cr.data[i][j][k][l] - c1.data[i][j][k][l]).abs() < 1e-4,
626                            "[{i}][{j}][{k}][{l}]: cr={}, c1={}",
627                            cr.data[i][j][k][l],
628                            c1.data[i][j][k][l]
629                        );
630                    }
631                }
632            }
633        }
634    }
635    #[test]
636    fn test_cp_reconstruct_zero_lambdas() {
637        let cp = CpDecomposition {
638            lambdas: vec![0.0],
639            a: vec![vec![1.0], vec![0.0], vec![0.0]],
640            b: vec![vec![1.0], vec![0.0], vec![0.0]],
641            c: vec![vec![1.0], vec![0.0], vec![0.0]],
642        };
643        let recon = cp_reconstruct(&cp);
644        assert!(recon.frobenius_norm() < 1e-12);
645    }
646    #[test]
647    fn test_cp_relative_error_exact_reconstruction() {
648        let t = DenseTensor::from_data(&[2, 2, 2], vec![1.0, 2.0, 2.0, 4.0, 2.0, 4.0, 4.0, 8.0]);
649        let cp = cp_als(&t, 1, 50, 1e-10);
650        let err = cp_relative_error(&t, &cp);
651        assert!(err < 0.01, "relative error too large: {err}");
652    }
653    #[test]
654    fn test_tucker_reconstruction_error_full_rank() {
655        let shape = [3, 3, 3];
656        let n: usize = shape.iter().product();
657        let data: Vec<f64> = (0..n).map(|x| x as f64).collect();
658        let t = DenseTensor::from_data(&shape, data);
659        let td = tucker_hosvd(&t, shape);
660        let err = td.relative_error(&t);
661        assert!(err < 1e-10, "full-rank Tucker error: {err}");
662    }
663    #[test]
664    fn test_tt_to_dense_shape() {
665        let t = DenseTensor::from_data(&[2, 3], (0..6).map(|x| x as f64).collect());
666        let tt = TensorTrain::from_dense(&t, 6, 1e-14);
667        let recon = tt.to_dense();
668        assert_eq!(recon.shape, t.shape);
669    }
670    #[test]
671    fn test_tt_scale() {
672        let t = DenseTensor::from_data(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
673        let tt = TensorTrain::from_dense(&t, 4, 1e-14);
674        let tt2 = tt.scale(2.0);
675        let recon = tt2.to_dense();
676        assert!((recon.get(&[0, 0]) - 2.0).abs() < 1e-6);
677    }
678    #[test]
679    fn test_tt_dot_product_self() {
680        let t = DenseTensor::from_data(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
681        let tt = TensorTrain::from_dense(&t, 4, 1e-14);
682        let dot = tt.dot_product(&tt);
683        let expected = t.data.iter().map(|&x| x * x).sum::<f64>();
684        assert!((dot - expected).abs() / expected < 1e-6);
685    }
686    #[test]
687    fn test_general_einsum_matmul() {
688        let a = DenseTensor::from_data(&[2, 2], vec![1.0, 2.0, 3.0, 4.0]);
689        let b = DenseTensor::from_data(&[2, 2], vec![5.0, 6.0, 7.0, 8.0]);
690        let c = general_einsum(&a, &[0], &[1], &b, &[1], &[0]);
691        assert!((c.get(&[0, 0]) - 19.0).abs() < 1e-10);
692        assert!((c.get(&[0, 1]) - 22.0).abs() < 1e-10);
693        assert!((c.get(&[1, 0]) - 43.0).abs() < 1e-10);
694        assert!((c.get(&[1, 1]) - 50.0).abs() < 1e-10);
695    }
696    #[test]
697    fn test_general_einsum_inner_product() {
698        let a = DenseTensor::from_data(&[3], vec![1.0, 2.0, 3.0]);
699        let b = DenseTensor::from_data(&[3], vec![4.0, 5.0, 6.0]);
700        let c = general_einsum(&a, &[], &[0], &b, &[], &[0]);
701        assert!((c.get(&[0]) - 32.0).abs() < 1e-10);
702    }
703    #[test]
704    fn test_general_einsum_outer_product() {
705        let a = DenseTensor::from_data(&[2], vec![2.0, 3.0]);
706        let b = DenseTensor::from_data(&[2], vec![5.0, 7.0]);
707        let c = general_einsum(&a, &[0], &[], &b, &[0], &[]);
708        assert!((c.get(&[0, 0]) - 10.0).abs() < 1e-10);
709        assert!((c.get(&[1, 1]) - 21.0).abs() < 1e-10);
710    }
711    #[test]
712    fn test_is_symmetric_modes_identity_matrix() {
713        let mut t = DenseTensor::zeros(&[3, 3]);
714        for i in 0..3 {
715            t.set(&[i, i], 1.0);
716        }
717        assert!(is_symmetric_modes(&t, 0, 1, 1e-12));
718    }
719    #[test]
720    fn test_is_symmetric_modes_antisymmetric() {
721        let mut t = DenseTensor::zeros(&[2, 2]);
722        t.set(&[0, 1], 1.0);
723        t.set(&[1, 0], -1.0);
724        assert!(!is_symmetric_modes(&t, 0, 1, 1e-12));
725    }
726    #[test]
727    fn test_neo_hookean_stiffness_equals_isotropic() {
728        let c1 = neo_hookean_stiffness(2.0, 3.0);
729        let c2 = Tensor4::isotropic(2.0, 3.0);
730        for i in 0..3 {
731            for j in 0..3 {
732                for k in 0..3 {
733                    for l in 0..3 {
734                        assert!((c1.data[i][j][k][l] - c2.data[i][j][k][l]).abs() < 1e-12);
735                    }
736                }
737            }
738        }
739    }
740    #[test]
741    fn test_cauchy_stress_linear_zero_strain() {
742        let c = Tensor4::isotropic(1.0, 1.0);
743        let eps = Tensor2::zero();
744        let sigma = cauchy_stress_linear(&c, &eps);
745        for i in 0..3 {
746            for j in 0..3 {
747                assert!(approx(sigma.data[i][j], 0.0));
748            }
749        }
750    }
751}