Skip to main content

outram_foam_basic_lib/fields/
vol_field_algebra.rs

1// SPDX-License-Identifier: GPL-3.0-only
2// Copyright (C) 2026 OUTRAM PARK contributors
3// Derived from OpenFOAM (www.openfoam.com)
4//   src/OpenFOAM/fields/GeometricFields/GeometricField/GeometricFieldFunctions.C
5//   src/OpenFOAM/primitives/Tensor/TensorI.H, SymmTensor/SymmTensorI.H
6//   @ commit 481094fd (OpenFOAM v2606)
7// Copyright (C) 2004-2023 OpenFOAM Foundation
8// Copyright (C) 2016-2023 OpenCFD Ltd.
9//
10// This file is part of OUTRAM PARK.
11//
12// OUTRAM PARK is free software: you can redistribute it and/or modify it
13// under the terms of the GNU General Public License as published by the
14// Free Software Foundation, either version 3 of the License, or (at your
15// option) any later version.
16//
17// OUTRAM PARK is distributed in the hope that it will be useful, but
18// WITHOUT ANY WARRANTY; without even the implied warranty of
19// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
20// General Public License for more details.
21//
22// You should have received a copy of the GNU General Public License along
23// with OUTRAM PARK.  If not, see <https://www.gnu.org/licenses/>.
24
25//! Field-level (`GeometricField`) tensor algebra.
26//!
27//! Thin per-element wrappers that lift the primitive `Tensor` / `SymmTensor`
28//! operations (`tr`, `symm`, `twoSymm`, `dev`, `dev2` — defined in
29//! `crate::primitives`) to whole volume fields, applying the operation to the
30//! internal field and every boundary patch and returning the correctly-ranked
31//! output field.
32//!
33//! These belong here (with the field types) rather than in the FV operator
34//! layer because they are pure algebra — no mesh metrics, no interpolation.
35//! They are the field-level counterparts a `solidDisplacementFoam`-style stress
36//! update needs, e.g. `sigma = mu*twoSymm(grad(D)) + lambda*tr(grad(D))*I`.
37//!
38//! Rank map (OpenFOAM convention):
39//!
40//! - `tr`        : tensor / symmTensor → scalar
41//! - `symm`      : tensor → symmTensor  (0.5·(T + Tᵀ))
42//! - `two_symm`  : tensor / symmTensor → symmTensor  (T + Tᵀ)
43//! - `dev`       : tensor / symmTensor → same rank   (T − (tr/3)·I)
44//! - `dev2`      : tensor / symmTensor → same rank   (T − (2·tr/3)·I)
45//!
46//! The output boundary patches are set zero-gradient (values carry the mapped
47//! result); the operation is applied element-wise so no BC evaluation occurs.
48
49use crate::fields::boundary::bc::{BoundaryCondition, PatchField};
50use crate::fields::vol_field::{VolField, VolScalarField, VolSymmTensorField, VolTensorField};
51
52/// Map a `VolField<T>` to a `VolField<U>` by applying `f` element-wise to the
53/// internal field and to every boundary-patch value.  Output patches are
54/// zero-gradient (the mapped values are stored directly).
55fn map_vol_field<T, U, F>(vol: &VolField<T>, f: F) -> VolField<U>
56where
57    T: Clone,
58    U: Clone + Default,
59    F: Fn(&T) -> U,
60{
61    let internal = vol.internal.map(&f);
62    let boundary = vol
63        .boundary
64        .iter()
65        .map(|pf| PatchField {
66            bc: BoundaryCondition::ZeroGradient,
67            values: pf.values.map(&f),
68        })
69        .collect();
70    VolField::new(vol.name.clone(), vol.mesh.clone(), internal, boundary)
71}
72
73// ── Trace ──────────────────────────────────────────────────────────────────────
74
75/// `tr(T)` of a tensor field → scalar field: `tr = T_xx + T_yy + T_zz`.
76pub fn tr(vol: &VolTensorField) -> VolScalarField {
77    map_vol_field(vol, |t| t.tr())
78}
79
80/// `tr(S)` of a symmetric-tensor field → scalar field.
81pub fn tr_of_symm(vol: &VolSymmTensorField) -> VolScalarField {
82    map_vol_field(vol, |s| s.tr())
83}
84
85// ── Symmetric part ─────────────────────────────────────────────────────────────
86
87/// `symm(T) = 0.5·(T + Tᵀ)` of a tensor field → symmetric-tensor field.
88pub fn symm(vol: &VolTensorField) -> VolSymmTensorField {
89    map_vol_field(vol, |t| t.symm())
90}
91
92/// `twoSymm(T) = T + Tᵀ` of a tensor field → symmetric-tensor field.
93pub fn two_symm(vol: &VolTensorField) -> VolSymmTensorField {
94    map_vol_field(vol, |t| t.two_symm())
95}
96
97/// `twoSymm(S) = 2·S` of a symmetric-tensor field → symmetric-tensor field.
98pub fn two_symm_of_symm(vol: &VolSymmTensorField) -> VolSymmTensorField {
99    map_vol_field(vol, |s| *s * 2.0)
100}
101
102// ── Deviatoric part ────────────────────────────────────────────────────────────
103
104/// `dev(T) = T − (tr(T)/3)·I` of a tensor field → tensor field (trace-free).
105pub fn dev(vol: &VolTensorField) -> VolTensorField {
106    map_vol_field(vol, |t| t.dev())
107}
108
109/// `dev2(T) = T − (2·tr(T)/3)·I` of a tensor field → tensor field.
110pub fn dev2(vol: &VolTensorField) -> VolTensorField {
111    map_vol_field(vol, |t| t.dev2())
112}
113
114/// `dev(S) = S − (tr(S)/3)·I` of a symmetric-tensor field → symmetric-tensor field.
115pub fn dev_of_symm(vol: &VolSymmTensorField) -> VolSymmTensorField {
116    map_vol_field(vol, |s| s.dev())
117}
118
119/// `dev2(S) = S − (2·tr(S)/3)·I` of a symmetric-tensor field → symmetric-tensor field.
120pub fn dev2_of_symm(vol: &VolSymmTensorField) -> VolSymmTensorField {
121    map_vol_field(vol, |s| s.dev2())
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::fields::field::Field;
128    use crate::mesh::fv_mesh::{BoundaryPatch, FvMeshBuilder, PatchKind};
129    use crate::primitives::{SymmTensor, Tensor, Vector3};
130    use std::sync::Arc;
131
132    fn unit_mesh() -> Arc<crate::mesh::fv_mesh::FvMesh> {
133        Arc::new(
134            FvMeshBuilder::new()
135                .n_cells(2)
136                .n_internal_faces(1)
137                .owner(vec![0, 1, 0])
138                .neighbour(vec![1])
139                .patches(vec![
140                    BoundaryPatch::new("right", 1, 1, PatchKind::Wall),
141                    BoundaryPatch::new("left", 2, 1, PatchKind::Wall),
142                ])
143                .cell_volumes(vec![0.5, 0.5])
144                .cell_centres(vec![
145                    Vector3::new(0.25, 0.0, 0.0),
146                    Vector3::new(0.75, 0.0, 0.0),
147                ])
148                .face_area_vectors(vec![
149                    Vector3::new(1.0, 0.0, 0.0),
150                    Vector3::new(1.0, 0.0, 0.0),
151                    Vector3::new(-1.0, 0.0, 0.0),
152                ])
153                .face_centres(vec![
154                    Vector3::new(0.5, 0.0, 0.0),
155                    Vector3::new(1.0, 0.0, 0.0),
156                    Vector3::new(0.0, 0.0, 0.0),
157                ])
158                .build()
159                .unwrap(),
160        )
161    }
162
163    fn tensor_field(m: Arc<crate::mesh::fv_mesh::FvMesh>, t: Tensor) -> VolTensorField {
164        // Store `t` on the boundary too so the boundary-mapping is exercised.
165        let boundary = m
166            .patches
167            .iter()
168            .map(|p| PatchField::fixed_value_tensor(p.size, t))
169            .collect();
170        VolTensorField::new("T", m.clone(), Field::uniform(m.n_cells, t), boundary)
171    }
172
173    /// V&V — methodology: for T = [[1,2,3],[4,5,6],[7,8,9]] the primitive
174    /// identities must hold field-wide (per element, so identical in every cell):
175    /// `tr = 15`; `symm` and `two_symm` recover the primitive results;
176    /// `symm(T) + skew(T) == T`; `dev` is trace-free; `dev2` has `tr = −tr(T)`.
177    ///
178    /// Result (measured 2026-07-15): tr = 15; symm.xy = 3 (=(2+4)/2);
179    /// two_symm.xy = 6; dev trace < 1e-13; dev2 trace = −15 (< 1e-13 error).
180    #[test]
181    fn field_algebra_matches_primitive() {
182        let m = unit_mesh();
183        let t = Tensor::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0);
184        let vt = tensor_field(m.clone(), t);
185
186        let tr_f = tr(&vt);
187        assert!((tr_f.internal[0] - 15.0).abs() < 1e-13);
188        // boundary values also mapped
189        assert!((tr_f.boundary[0].values[0] - 15.0).abs() < 1e-13);
190
191        let s = symm(&vt);
192        assert!((s.internal[0].xy - 3.0).abs() < 1e-13); // (2+4)/2
193        assert!((s.internal[0].xz - 5.0).abs() < 1e-13); // (3+7)/2
194        assert!((s.internal[0].yz - 7.0).abs() < 1e-13); // (6+8)/2
195
196        let ts = two_symm(&vt);
197        assert!((ts.internal[0].xy - 6.0).abs() < 1e-13);
198        assert!((ts.internal[0].xx - 2.0).abs() < 1e-13);
199
200        let d = dev(&vt);
201        assert!(d.internal[0].tr().abs() < 1e-13, "dev not trace-free");
202
203        let d2 = dev2(&vt);
204        assert!((d2.internal[0].tr() - (-15.0)).abs() < 1e-13);
205    }
206
207    /// V&V — methodology: symmetric-tensor variants. For
208    /// S = symm[xx=2, xy=1, xz=0, yy=4, yz=0, zz=6] (tr = 12):
209    /// `tr_of_symm = 12`; `two_symm_of_symm = 2·S`; `dev_of_symm` trace-free;
210    /// `dev2_of_symm` trace = −12.
211    ///
212    /// Result (measured 2026-07-15): tr = 12; two_symm.xx = 4; dev trace < 1e-13;
213    /// dev2 trace = −12 (< 1e-13 error).
214    #[test]
215    fn symm_field_algebra_matches_primitive() {
216        let m = unit_mesh();
217        let s = SymmTensor::new(2.0, 1.0, 0.0, 4.0, 0.0, 6.0);
218        let boundary = m
219            .patches
220            .iter()
221            .map(|p| PatchField::zero_gradient_symm_tensor(p.size))
222            .collect();
223        let vs = VolSymmTensorField::new("S", m.clone(), Field::uniform(m.n_cells, s), boundary);
224
225        assert!((tr_of_symm(&vs).internal[0] - 12.0).abs() < 1e-13);
226
227        let ts = two_symm_of_symm(&vs);
228        assert!((ts.internal[0].xx - 4.0).abs() < 1e-13);
229        assert!((ts.internal[0].zz - 12.0).abs() < 1e-13);
230
231        assert!(dev_of_symm(&vs).internal[0].tr().abs() < 1e-13);
232        assert!((dev2_of_symm(&vs).internal[0].tr() - (-12.0)).abs() < 1e-13);
233    }
234}