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