Skip to main content

outram_foam_basic_lib/primitives/
symm_tensor.rs

1// SPDX-License-Identifier: GPL-3.0-only
2// Copyright (C) 2026 OUTRAM PARK contributors
3// Derived from OpenFOAM (www.openfoam.com)
4// Copyright (C) 2004-2023 OpenFOAM Foundation
5// Copyright (C) 2016-2023 OpenCFD Ltd.
6//
7// This file is part of OUTRAM PARK.
8//
9// OUTRAM PARK is free software: you can redistribute it and/or modify it
10// under the terms of the GNU General Public License as published by the
11// Free Software Foundation, either version 3 of the License, or (at your
12// option) any later version.
13//
14// OUTRAM PARK is distributed in the hope that it will be useful, but
15// WITHOUT ANY WARRANTY; without even the implied warranty of
16// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17// General Public License for more details.
18//
19// You should have received a copy of the GNU General Public License along
20// with OUTRAM PARK.  If not, see <https://www.gnu.org/licenses/>.
21
22use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Neg, Sub, SubAssign};
23
24use super::spherical_tensor::SphericalTensor;
25use super::vector::Vector3;
26
27/// Symmetric 3×3 tensor stored in upper-triangle order: xx, xy, xz, yy, yz, zz.
28/// Maps to `Foam::symmTensor` (`Foam::SymmTensor<scalar>`).
29#[derive(Debug, Clone, Copy, PartialEq, Default)]
30pub struct SymmTensor {
31    pub xx: f64,
32    pub xy: f64,
33    pub xz: f64,
34    pub yy: f64,
35    pub yz: f64,
36    pub zz: f64,
37}
38
39impl SymmTensor {
40    pub const ZERO: Self = Self {
41        xx: 0.0,
42        xy: 0.0,
43        xz: 0.0,
44        yy: 0.0,
45        yz: 0.0,
46        zz: 0.0,
47    };
48    pub const IDENTITY: Self = Self {
49        xx: 1.0,
50        xy: 0.0,
51        xz: 0.0,
52        yy: 1.0,
53        yz: 0.0,
54        zz: 1.0,
55    };
56
57    #[inline]
58    pub fn new(xx: f64, xy: f64, xz: f64, yy: f64, yz: f64, zz: f64) -> Self {
59        Self {
60            xx,
61            xy,
62            xz,
63            yy,
64            yz,
65            zz,
66        }
67    }
68
69    /// Construct from diagonal only (off-diagonal = 0).
70    #[inline]
71    pub fn from_diag(xx: f64, yy: f64, zz: f64) -> Self {
72        Self {
73            xx,
74            xy: 0.0,
75            xz: 0.0,
76            yy,
77            yz: 0.0,
78            zz,
79        }
80    }
81
82    /// Row vectors (yx = xy, zx = xz, zy = yz because symmetric)
83    #[inline]
84    pub fn row_x(self) -> Vector3 {
85        Vector3::new(self.xx, self.xy, self.xz)
86    }
87    #[inline]
88    pub fn row_y(self) -> Vector3 {
89        Vector3::new(self.xy, self.yy, self.yz)
90    }
91    #[inline]
92    pub fn row_z(self) -> Vector3 {
93        Vector3::new(self.xz, self.yz, self.zz)
94    }
95
96    /// Diagonal as a vector
97    #[inline]
98    pub fn diag(self) -> Vector3 {
99        Vector3::new(self.xx, self.yy, self.zz)
100    }
101
102    /// Trace: xx + yy + zz
103    #[inline]
104    pub fn tr(self) -> f64 {
105        self.xx + self.yy + self.zz
106    }
107
108    /// Spherical (isotropic) part: (tr/3) * I
109    #[inline]
110    pub fn sph(self) -> SphericalTensor {
111        SphericalTensor::new(self.tr() / 3.0)
112    }
113
114    /// Deviatoric part: self - (tr/3)*I
115    #[inline]
116    pub fn dev(self) -> Self {
117        self - self.sph()
118    }
119
120    /// Two-thirds deviatoric part: self - (2*tr/3)*I
121    #[inline]
122    pub fn dev2(self) -> Self {
123        self - 2.0 * self.sph()
124    }
125
126    /// Determinant
127    #[inline]
128    pub fn det(self) -> f64 {
129        let Self {
130            xx,
131            xy,
132            xz,
133            yy,
134            yz,
135            zz,
136        } = self;
137        xx * yy * zz + xy * yz * xz + xz * xy * yz - xx * yz * yz - xy * xy * zz - xz * yy * xz
138    }
139
140    /// Adjunct (= cofactor matrix, same as adjunct because symmetric)
141    #[inline]
142    pub fn adjunct(self) -> Self {
143        let Self {
144            xx,
145            xy,
146            xz,
147            yy,
148            yz,
149            zz,
150        } = self;
151        Self {
152            xx: yy * zz - yz * yz,
153            xy: xz * yz - xy * zz,
154            xz: xy * yz - xz * yy,
155            yy: xx * zz - xz * xz,
156            yz: xy * xz - xx * yz,
157            zz: xx * yy - xy * xy,
158        }
159    }
160
161    /// Inverse = adjunct / det. Panics if singular in debug builds.
162    #[inline]
163    pub fn inv(self) -> Self {
164        let d = self.det();
165        debug_assert!(d.abs() > 0.0, "SymmTensor is singular");
166        self.adjunct() / d
167    }
168
169    /// Inverse with fallback: returns ZERO if nearly singular.
170    pub fn safe_inv(self) -> Self {
171        use super::scalar::SMALL;
172        let diag_sqr = self.xx * self.xx + self.yy * self.yy + self.zz * self.zz;
173        let threshold = SMALL * diag_sqr;
174        let mut work = self;
175        let small_xx = self.xx * self.xx < threshold;
176        let small_yy = self.yy * self.yy < threshold;
177        let small_zz = self.zz * self.zz < threshold;
178        if small_xx {
179            work.xx += 1.0;
180        }
181        if small_yy {
182            work.yy += 1.0;
183        }
184        if small_zz {
185            work.zz += 1.0;
186        }
187        let d = work.det();
188        if d.abs() < super::scalar::ROOT_VSMALL {
189            return Self::ZERO;
190        }
191        let mut result = work.adjunct() / d;
192        if small_xx {
193            result.xx -= 1.0;
194        }
195        if small_yy {
196            result.yy -= 1.0;
197        }
198        if small_zz {
199            result.zz -= 1.0;
200        }
201        result
202    }
203
204    /// Frobenius norm squared (off-diagonal counted twice, matching OpenFOAM)
205    #[inline]
206    pub fn mag_sqr(self) -> f64 {
207        let Self {
208            xx,
209            xy,
210            xz,
211            yy,
212            yz,
213            zz,
214        } = self;
215        xx * xx + 2.0 * xy * xy + 2.0 * xz * xz + yy * yy + 2.0 * yz * yz + zz * zz
216    }
217
218    #[inline]
219    pub fn mag(self) -> f64 {
220        self.mag_sqr().sqrt()
221    }
222
223    /// Sum of squared diagonal entries (not Frobenius)
224    #[inline]
225    pub fn diag_sqr(self) -> f64 {
226        self.xx * self.xx + self.yy * self.yy + self.zz * self.zz
227    }
228
229    /// Self² as a SymmTensor (S·S where both factors are symmetric)
230    #[inline]
231    pub fn inner_sqr(self) -> Self {
232        let Self {
233            xx,
234            xy,
235            xz,
236            yy,
237            yz,
238            zz,
239        } = self;
240        Self {
241            xx: xx * xx + xy * xy + xz * xz,
242            xy: xx * xy + xy * yy + xz * yz,
243            xz: xx * xz + xy * yz + xz * zz,
244            yy: xy * xy + yy * yy + yz * yz,
245            yz: xy * xz + yy * yz + yz * zz,
246            zz: xz * xz + yz * yz + zz * zz,
247        }
248    }
249
250    /// Double contraction (Frobenius inner product). C++ `operator&&`.
251    /// Note: off-diagonal elements contribute twice because the tensor is symmetric.
252    #[inline]
253    pub fn double_inner(self, rhs: Self) -> f64 {
254        self.xx * rhs.xx
255            + 2.0 * self.xy * rhs.xy
256            + 2.0 * self.xz * rhs.xz
257            + self.yy * rhs.yy
258            + 2.0 * self.yz * rhs.yz
259            + self.zz * rhs.zz
260    }
261
262    /// Matrix multiply SymmTensor·Vector → Vector. C++ `operator&(SymmTensor, Vector)`.
263    #[inline]
264    pub fn mat_vec(self, v: Vector3) -> Vector3 {
265        Vector3::new(
266            self.xx * v.x + self.xy * v.y + self.xz * v.z,
267            self.xy * v.x + self.yy * v.y + self.yz * v.z,
268            self.xz * v.x + self.yz * v.y + self.zz * v.z,
269        )
270    }
271
272    /// Hodge dual: returns the axial vector. C++ `operator*(SymmTensor)`.
273    #[inline]
274    pub fn hodge_dual(self) -> Vector3 {
275        Vector3::new(self.yz, -self.xz, self.xy)
276    }
277
278    /// Outer (dyadic) product of a vector with itself: v ⊗ v → SymmTensor.
279    /// C++ `sqr(Vector)`.
280    #[inline]
281    pub fn from_outer(v: Vector3) -> Self {
282        Self {
283            xx: v.x * v.x,
284            xy: v.x * v.y,
285            xz: v.x * v.z,
286            yy: v.y * v.y,
287            yz: v.y * v.z,
288            zz: v.z * v.z,
289        }
290    }
291
292    /// First invariant: trace
293    #[inline]
294    pub fn invariant_i(self) -> f64 {
295        self.tr()
296    }
297
298    /// Second invariant: (xx*yy + yy*zz + xx*zz) - (xy² + yz² + xz²)
299    #[inline]
300    pub fn invariant_ii(self) -> f64 {
301        self.xx * self.yy + self.yy * self.zz + self.xx * self.zz
302            - self.xy * self.xy
303            - self.yz * self.yz
304            - self.xz * self.xz
305    }
306
307    /// Third invariant: determinant
308    #[inline]
309    pub fn invariant_iii(self) -> f64 {
310        self.det()
311    }
312
313    /// Linear interpolation
314    #[inline]
315    pub fn lerp(a: Self, b: Self, t: f64) -> Self {
316        let ot = 1.0 - t;
317        Self {
318            xx: ot * a.xx + t * b.xx,
319            xy: ot * a.xy + t * b.xy,
320            xz: ot * a.xz + t * b.xz,
321            yy: ot * a.yy + t * b.yy,
322            yz: ot * a.yz + t * b.yz,
323            zz: ot * a.zz + t * b.zz,
324        }
325    }
326
327    /// True if the tensor is (approximately) the identity.
328    pub fn is_identity(self, tol: f64) -> bool {
329        (self.xx - 1.0).abs() < tol
330            && (self.yy - 1.0).abs() < tol
331            && (self.zz - 1.0).abs() < tol
332            && self.xy.abs() < tol
333            && self.xz.abs() < tol
334            && self.yz.abs() < tol
335    }
336}
337
338// --- Conversions ---
339
340impl From<SphericalTensor> for SymmTensor {
341    #[inline]
342    fn from(st: SphericalTensor) -> Self {
343        Self {
344            xx: st.ii,
345            xy: 0.0,
346            xz: 0.0,
347            yy: st.ii,
348            yz: 0.0,
349            zz: st.ii,
350        }
351    }
352}
353
354// --- Arithmetic operators ---
355
356impl Neg for SymmTensor {
357    type Output = Self;
358    #[inline]
359    fn neg(self) -> Self {
360        Self {
361            xx: -self.xx,
362            xy: -self.xy,
363            xz: -self.xz,
364            yy: -self.yy,
365            yz: -self.yz,
366            zz: -self.zz,
367        }
368    }
369}
370
371impl Add for SymmTensor {
372    type Output = Self;
373    #[inline]
374    fn add(self, r: Self) -> Self {
375        Self {
376            xx: self.xx + r.xx,
377            xy: self.xy + r.xy,
378            xz: self.xz + r.xz,
379            yy: self.yy + r.yy,
380            yz: self.yz + r.yz,
381            zz: self.zz + r.zz,
382        }
383    }
384}
385
386impl Sub for SymmTensor {
387    type Output = Self;
388    #[inline]
389    fn sub(self, r: Self) -> Self {
390        Self {
391            xx: self.xx - r.xx,
392            xy: self.xy - r.xy,
393            xz: self.xz - r.xz,
394            yy: self.yy - r.yy,
395            yz: self.yz - r.yz,
396            zz: self.zz - r.zz,
397        }
398    }
399}
400
401impl Mul<f64> for SymmTensor {
402    type Output = Self;
403    #[inline]
404    fn mul(self, s: f64) -> Self {
405        Self {
406            xx: self.xx * s,
407            xy: self.xy * s,
408            xz: self.xz * s,
409            yy: self.yy * s,
410            yz: self.yz * s,
411            zz: self.zz * s,
412        }
413    }
414}
415
416impl Mul<SymmTensor> for f64 {
417    type Output = SymmTensor;
418    #[inline]
419    fn mul(self, st: SymmTensor) -> SymmTensor {
420        st * self
421    }
422}
423
424impl Div<f64> for SymmTensor {
425    type Output = Self;
426    #[inline]
427    fn div(self, s: f64) -> Self {
428        Self {
429            xx: self.xx / s,
430            xy: self.xy / s,
431            xz: self.xz / s,
432            yy: self.yy / s,
433            yz: self.yz / s,
434            zz: self.zz / s,
435        }
436    }
437}
438
439impl AddAssign for SymmTensor {
440    #[inline]
441    fn add_assign(&mut self, r: Self) {
442        *self = *self + r;
443    }
444}
445
446impl SubAssign for SymmTensor {
447    #[inline]
448    fn sub_assign(&mut self, r: Self) {
449        *self = *self - r;
450    }
451}
452
453impl MulAssign<f64> for SymmTensor {
454    #[inline]
455    fn mul_assign(&mut self, s: f64) {
456        *self = *self * s;
457    }
458}
459
460// SphericalTensor ± SymmTensor → SymmTensor
461
462impl Add<SymmTensor> for SphericalTensor {
463    type Output = SymmTensor;
464    #[inline]
465    fn add(self, st: SymmTensor) -> SymmTensor {
466        SymmTensor {
467            xx: self.ii + st.xx,
468            xy: st.xy,
469            xz: st.xz,
470            yy: self.ii + st.yy,
471            yz: st.yz,
472            zz: self.ii + st.zz,
473        }
474    }
475}
476
477impl Add<SphericalTensor> for SymmTensor {
478    type Output = Self;
479    #[inline]
480    fn add(self, spt: SphericalTensor) -> Self {
481        spt + self
482    }
483}
484
485impl Sub<SymmTensor> for SphericalTensor {
486    type Output = SymmTensor;
487    #[inline]
488    fn sub(self, st: SymmTensor) -> SymmTensor {
489        SymmTensor {
490            xx: self.ii - st.xx,
491            xy: -st.xy,
492            xz: -st.xz,
493            yy: self.ii - st.yy,
494            yz: -st.yz,
495            zz: self.ii - st.zz,
496        }
497    }
498}
499
500impl Sub<SphericalTensor> for SymmTensor {
501    type Output = Self;
502    #[inline]
503    fn sub(self, spt: SphericalTensor) -> Self {
504        Self {
505            xx: self.xx - spt.ii,
506            xy: self.xy,
507            xz: self.xz,
508            yy: self.yy - spt.ii,
509            yz: self.yz,
510            zz: self.zz - spt.ii,
511        }
512    }
513}
514
515// SphericalTensor & SymmTensor (inner product) → SymmTensor (scalar times self)
516impl Mul<SymmTensor> for SphericalTensor {
517    type Output = SymmTensor;
518    #[inline]
519    fn mul(self, st: SymmTensor) -> SymmTensor {
520        st * self.ii
521    }
522}
523
524impl Mul<SphericalTensor> for SymmTensor {
525    type Output = Self;
526    #[inline]
527    fn mul(self, spt: SphericalTensor) -> Self {
528        self * spt.ii
529    }
530}
531
532// --- Free functions ---
533
534#[inline]
535pub fn tr(st: SymmTensor) -> f64 {
536    st.tr()
537}
538
539#[inline]
540pub fn det(st: SymmTensor) -> f64 {
541    st.det()
542}
543
544#[inline]
545pub fn inv(st: SymmTensor) -> SymmTensor {
546    st.inv()
547}
548
549#[inline]
550pub fn dev(st: SymmTensor) -> SymmTensor {
551    st.dev()
552}
553
554#[inline]
555pub fn dev2(st: SymmTensor) -> SymmTensor {
556    st.dev2()
557}
558
559/// Symmetric part of a SymmTensor is itself.
560#[inline]
561pub fn symm(st: SymmTensor) -> SymmTensor {
562    st
563}
564
565/// Twice the symmetric part of a SymmTensor.
566#[inline]
567pub fn two_symm(st: SymmTensor) -> SymmTensor {
568    st * 2.0
569}
570
571/// dev(symm(st)) — deviatoric of symmetric part (same as dev for SymmTensor).
572#[inline]
573pub fn dev_symm(st: SymmTensor) -> SymmTensor {
574    st.dev()
575}
576
577/// dev(2*symm(st))
578#[inline]
579pub fn dev_two_symm(st: SymmTensor) -> SymmTensor {
580    st.dev2()
581}
582
583/// Outer (dyadic) product v ⊗ v as a SymmTensor. C++ `sqr(Vector)`.
584#[inline]
585pub fn sqr(v: Vector3) -> SymmTensor {
586    SymmTensor::from_outer(v)
587}
588
589#[inline]
590pub fn mag_sqr(st: SymmTensor) -> f64 {
591    st.mag_sqr()
592}
593
594#[inline]
595pub fn lerp(a: SymmTensor, b: SymmTensor, t: f64) -> SymmTensor {
596    SymmTensor::lerp(a, b, t)
597}
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602
603    #[test]
604    fn identity_properties() {
605        let i = SymmTensor::IDENTITY;
606        assert_eq!(i.tr(), 3.0);
607        assert_eq!(i.det(), 1.0);
608        let inv = i.inv();
609        assert!((inv.xx - 1.0).abs() < 1e-14);
610        assert!((inv.yy - 1.0).abs() < 1e-14);
611        assert!((inv.zz - 1.0).abs() < 1e-14);
612    }
613
614    #[test]
615    fn dev_removes_spherical() {
616        let st = SymmTensor::new(4.0, 0.0, 0.0, 2.0, 0.0, 3.0);
617        let d = st.dev();
618        assert!((d.tr()).abs() < 1e-14);
619    }
620
621    #[test]
622    fn from_outer_is_symmetric() {
623        let v = Vector3::new(1.0, 2.0, 3.0);
624        let s = SymmTensor::from_outer(v);
625        assert_eq!(s.xx, 1.0);
626        assert_eq!(s.xy, 2.0);
627        assert_eq!(s.xz, 3.0);
628        assert_eq!(s.yy, 4.0);
629        assert_eq!(s.yz, 6.0);
630        assert_eq!(s.zz, 9.0);
631    }
632
633    #[test]
634    fn mat_vec() {
635        let i = SymmTensor::IDENTITY;
636        let v = Vector3::new(1.0, 2.0, 3.0);
637        assert_eq!(i.mat_vec(v), v);
638    }
639
640    #[test]
641    fn dev2_regression() {
642        // T = diag(6, 3, 3), tr = 12 → (2/3)*tr = 8
643        // dev2 = T − 8·I = diag(6-8, 3-8, 3-8) = diag(-2, -5, -5)
644        let t = SymmTensor::from_diag(6.0, 3.0, 3.0);
645        let d = t.dev2();
646        assert!((d.xx - (-2.0)).abs() < 1e-14, "xx={}", d.xx);
647        assert!((d.yy - (-5.0)).abs() < 1e-14, "yy={}", d.yy);
648        assert!((d.zz - (-5.0)).abs() < 1e-14, "zz={}", d.zz);
649        assert!(d.xy.abs() < 1e-14);
650        assert!(d.xz.abs() < 1e-14);
651        assert!(d.yz.abs() < 1e-14);
652        // Confirm dev2 is NOT trace-free (tr(dev2) = tr(T) - 2·tr(T) = -tr)
653        assert!((d.tr() - (-12.0)).abs() < 1e-14, "tr(dev2)={}", d.tr());
654    }
655
656    #[test]
657    fn inv_roundtrip_is_identity() {
658        // T · T⁻¹ == I for a well-conditioned symmetric tensor
659        use crate::primitives::Tensor;
660        let t = SymmTensor::new(4.0, 2.0, 1.0, 3.0, 0.5, 2.0);
661        let product = Tensor::from(t).mat_mul(Tensor::from(t.inv()));
662        assert!(product.is_identity(1e-12), "T·T⁻¹ is not identity");
663    }
664}