Skip to main content

outram_foam_basic_lib/fv_operators/
adjust_phi.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 crate::fields::boundary::bc::BoundaryCondition;
23use crate::fields::surface_field::SurfaceScalarField;
24use crate::fields::vol_field::VolVectorField;
25
26/// Adjust face fluxes to satisfy global mass balance.
27///
28/// Sums all boundary fluxes.  Any imbalance (net outflow ≠ 0 in a closed
29/// domain) is distributed uniformly over **adjustable** boundary faces — those
30/// whose corresponding velocity BC is `ZeroGradient` (i.e. free-stream /
31/// outlet faces where the flux is not prescribed).  Fixed-value faces are left
32/// untouched.
33///
34/// This matches `adjustPhi(phi, U, p)` in OpenFOAM
35/// (`src/finiteVolume/cfdTools/general/adjustPhi/adjustPhi.C`).
36///
37/// # Returns
38/// `true` if any adjustment was made, `false` if already balanced.
39pub fn adjust_phi(phi: &mut SurfaceScalarField, u: &VolVectorField) -> bool {
40    let mesh = &phi.mesh;
41
42    // 1. Count adjustable boundary faces and sum all boundary fluxes.
43    let mut total_flux = 0.0_f64;
44    let mut adjustable_n = 0usize;
45
46    for (pi, patch) in mesh.patches.iter().enumerate() {
47        for fi in 0..patch.size {
48            total_flux += phi.boundary[pi].values[fi];
49            if matches!(u.boundary[pi].bc, BoundaryCondition::ZeroGradient) {
50                adjustable_n += 1;
51            }
52        }
53    }
54
55    if adjustable_n == 0 || total_flux.abs() < 1e-30 {
56        return false;
57    }
58
59    // 2. Distribute correction over adjustable faces.
60    let correction = -total_flux / adjustable_n as f64;
61    for (pi, patch) in mesh.patches.iter().enumerate() {
62        if matches!(u.boundary[pi].bc, BoundaryCondition::ZeroGradient) {
63            for fi in 0..patch.size {
64                phi.boundary[pi].values[fi] += correction;
65            }
66        }
67    }
68    true
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::fields::boundary::bc::{BoundaryCondition, PatchField};
75    use crate::fields::field::Field;
76    use crate::fields::vol_field::VolVectorField;
77    use crate::mesh::fv_mesh::{BoundaryPatch, FvMeshBuilder, PatchKind};
78    use crate::primitives::Vector3;
79    use std::sync::Arc;
80
81    fn two_cell_mesh() -> Arc<crate::mesh::fv_mesh::FvMesh> {
82        Arc::new(
83            FvMeshBuilder::new()
84                .n_cells(2)
85                .n_internal_faces(1)
86                .owner(vec![0, 1, 0])
87                .neighbour(vec![1])
88                .patches(vec![
89                    BoundaryPatch::new("right", 1, 1, PatchKind::Wall),
90                    BoundaryPatch::new("left", 2, 1, PatchKind::Wall),
91                ])
92                .cell_volumes(vec![1.0, 1.0])
93                .cell_centres(vec![
94                    Vector3::new(0.25, 0.0, 0.0),
95                    Vector3::new(0.75, 0.0, 0.0),
96                ])
97                .face_area_vectors(vec![
98                    Vector3::new(1.0, 0.0, 0.0),
99                    Vector3::new(1.0, 0.0, 0.0),
100                    Vector3::new(-1.0, 0.0, 0.0),
101                ])
102                .face_centres(vec![
103                    Vector3::new(0.5, 0.0, 0.0),
104                    Vector3::new(1.0, 0.0, 0.0),
105                    Vector3::new(0.0, 0.0, 0.0),
106                ])
107                .build()
108                .unwrap(),
109        )
110    }
111
112    #[test]
113    fn balances_net_outflow() {
114        let m = two_cell_mesh();
115        // Inlet (left) fixed at phi=-1, outlet (right) ZeroGradient at phi=0.5 (imbalanced)
116        let bnd = vec![
117            PatchField {
118                bc: BoundaryCondition::ZeroGradient,
119                values: Field::new(vec![0.5]),
120            },
121            PatchField {
122                bc: BoundaryCondition::ZeroGradient,
123                values: Field::new(vec![-1.0]),
124            },
125        ];
126        let u_bnd = vec![
127            crate::fields::boundary::bc::PatchField {
128                bc: BoundaryCondition::ZeroGradient,
129                values: Field::new(vec![Vector3::ZERO]),
130            },
131            crate::fields::boundary::bc::PatchField {
132                bc: BoundaryCondition::FixedValue(Vector3::new(-1.0, 0.0, 0.0)),
133                values: Field::new(vec![Vector3::new(-1.0, 0.0, 0.0)]),
134            },
135        ];
136        let u = VolVectorField::new("U", m.clone(), Field::new(vec![Vector3::ZERO; 2]), u_bnd);
137        let mut phi = SurfaceScalarField::new("phi", m.clone(), Field::new(vec![0.0]), bnd);
138
139        let adjusted = adjust_phi(&mut phi, &u);
140        assert!(adjusted);
141        // net = 0.5 + (-1) = -0.5 → correction = 0.5 → outlet becomes 0.5+0.5=1.0
142        let net: f64 = phi
143            .boundary
144            .iter()
145            .flat_map(|p| p.values.iter().cloned())
146            .sum();
147        assert!(net.abs() < 1e-12, "net flux = {net}");
148    }
149
150    #[test]
151    fn no_adjustment_when_balanced() {
152        let m = two_cell_mesh();
153        let bnd = vec![
154            PatchField {
155                bc: BoundaryCondition::ZeroGradient,
156                values: Field::new(vec![1.0]),
157            },
158            PatchField {
159                bc: BoundaryCondition::ZeroGradient,
160                values: Field::new(vec![-1.0]),
161            },
162        ];
163        let u_bnd = vec![
164            PatchField {
165                bc: BoundaryCondition::ZeroGradient,
166                values: Field::new(vec![Vector3::ZERO]),
167            },
168            PatchField {
169                bc: BoundaryCondition::ZeroGradient,
170                values: Field::new(vec![Vector3::ZERO]),
171            },
172        ];
173        let u = VolVectorField::new("U", m.clone(), Field::new(vec![Vector3::ZERO; 2]), u_bnd);
174        let mut phi = SurfaceScalarField::new("phi", m.clone(), Field::new(vec![0.0]), bnd);
175        let adjusted = adjust_phi(&mut phi, &u);
176        assert!(!adjusted);
177    }
178}