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}