Skip to main content

outram_foam_basic_lib/interface/
one_dimensional_meshing.rs

1use crate::mesh::{BoundaryPatch, FvMesh, FvMeshBuilder, MeshError, PatchKind};
2use crate::primitives::Vector3;
3use uom::si::area::square_meter;
4use uom::si::f64::*;
5use uom::si::length::meter;
6
7/// Creates a uniform 1-D finite-volume mesh along the x-axis.
8///
9/// Produces `number_of_cells` equal-width cells spanning x ∈ \[0, `l`\] with a
10/// constant cross-sectional area of `xs_area`.  All geometry is aligned with
11/// the x-axis; y and z components are zero everywhere.
12///
13/// ## Layout
14/// ```text
15/// |  cell 0  |  cell 1  |  …  |  cell n-1  |
16/// ^          ^          ^     ^             ^
17/// left       i-face 0   …   i-face n-2    right
18/// (patch)                                 (patch)
19/// ```
20///
21/// Face ordering follows the OpenFOAM convention:
22/// - `[0, n-1)` — internal faces (face `i` separates cell `i` from cell `i+1`)
23/// - face `n-1` — `"right"` boundary at x = `l`  (outward normal = +x)
24/// - face `n`   — `"left"`  boundary at x = 0   (outward normal = −x)
25///
26/// Both patches are typed [`PatchKind::Patch`] (generic).  Replace them via
27/// [`FvMesh::patches`] if you need `Wall`, `Cyclic`, etc.
28///
29/// ## Parameters
30/// - `l`               — total pipe length \[m\]
31/// - `xs_area`         — constant cross-sectional area \[m²\]
32/// - `number_of_cells` — number of cells; must be ≥ 1
33///
34/// ## Errors
35/// Returns `Err` if `number_of_cells < 1`.
36///
37/// ## Example
38/// ```rust
39/// use uom::si::f64::*;
40/// use uom::si::length::meter;
41/// use uom::si::area::square_meter;
42/// use outram_foam_basic_lib::interface::one_dimensional_meshing::create_one_d_mesh;
43///
44/// let mesh = create_one_d_mesh(
45///     Length::new::<meter>(1.0),
46///     Area::new::<square_meter>(0.01),
47///     10,
48/// ).unwrap();
49///
50/// assert_eq!(mesh.n_cells, 10);
51/// assert_eq!(mesh.n_internal_faces, 9);
52/// assert_eq!(mesh.n_faces, 11);
53/// ```
54pub fn create_one_d_mesh(
55    l: Length,
56    xs_area: Area,
57    number_of_cells: i64,
58) -> Result<FvMesh, MeshError> {
59    if number_of_cells < 1 {
60        return Err(MeshError::NonPositiveCellCount {
61            got: number_of_cells,
62        });
63    }
64    let n = number_of_cells as usize;
65    let l_m = l.get::<meter>();
66    let a_m2 = xs_area.get::<square_meter>();
67    let dx = l_m / n as f64;
68
69    // ── Topology ──────────────────────────────────────────────────────────────
70    // Face indices:
71    //   [0, n-1)   internal — face i separates cell i from cell i+1
72    //   n-1        right boundary, owned by cell n-1
73    //   n          left  boundary, owned by cell 0
74    let n_internal = n - 1;
75    let n_faces = n + 1;
76
77    let mut owner = Vec::with_capacity(n_faces);
78    let mut neighbour = Vec::with_capacity(n_internal);
79    for i in 0..n_internal {
80        owner.push(i);
81        neighbour.push(i + 1);
82    }
83    owner.push(n - 1); // right boundary
84    owner.push(0); // left boundary
85
86    // ── Geometry ──────────────────────────────────────────────────────────────
87    let cell_volumes: Vec<f64> = vec![dx * a_m2; n];
88    let cell_centres: Vec<Vector3> = (0..n)
89        .map(|i| Vector3::new((i as f64 + 0.5) * dx, 0.0, 0.0))
90        .collect();
91
92    let mut face_centres = Vec::with_capacity(n_faces);
93    let mut face_area_vectors = Vec::with_capacity(n_faces);
94
95    // Internal faces: pointing +x (owner → neighbour)
96    for i in 0..n_internal {
97        face_centres.push(Vector3::new((i as f64 + 1.0) * dx, 0.0, 0.0));
98        face_area_vectors.push(Vector3::new(a_m2, 0.0, 0.0));
99    }
100    // Right boundary: outward from cell n-1, pointing +x
101    face_centres.push(Vector3::new(l_m, 0.0, 0.0));
102    face_area_vectors.push(Vector3::new(a_m2, 0.0, 0.0));
103    // Left boundary: outward from cell 0, pointing -x
104    face_centres.push(Vector3::new(0.0, 0.0, 0.0));
105    face_area_vectors.push(Vector3::new(-a_m2, 0.0, 0.0));
106
107    // ── Patches ───────────────────────────────────────────────────────────────
108    // Must cover [n_internal, n_faces) without gaps.
109    let patches = vec![
110        BoundaryPatch::new("right", n_internal, 1, PatchKind::Patch),
111        BoundaryPatch::new("left", n_internal + 1, 1, PatchKind::Patch),
112    ];
113
114    FvMeshBuilder::new()
115        .n_cells(n)
116        .n_internal_faces(n_internal)
117        .owner(owner)
118        .neighbour(neighbour)
119        .patches(patches)
120        .cell_volumes(cell_volumes)
121        .cell_centres(cell_centres)
122        .face_area_vectors(face_area_vectors)
123        .face_centres(face_centres)
124        .build()
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use uom::si::area::square_meter;
131    use uom::si::length::meter;
132
133    #[test]
134    fn single_cell_mesh_is_valid() {
135        let mesh = create_one_d_mesh(
136            Length::new::<meter>(1.0),
137            Area::new::<square_meter>(0.01),
138            1,
139        )
140        .unwrap();
141        assert_eq!(mesh.n_cells, 1);
142        assert_eq!(mesh.n_internal_faces, 0);
143        assert_eq!(mesh.n_faces, 2);
144        assert_eq!(mesh.neighbour.len(), 0);
145        mesh.validate().unwrap();
146    }
147
148    #[test]
149    fn ten_cell_mesh_topology() {
150        let mesh = create_one_d_mesh(
151            Length::new::<meter>(1.0),
152            Area::new::<square_meter>(0.05),
153            10,
154        )
155        .unwrap();
156        assert_eq!(mesh.n_cells, 10);
157        assert_eq!(mesh.n_internal_faces, 9);
158        assert_eq!(mesh.n_faces, 11);
159        assert_eq!(mesh.owner.len(), 11);
160        assert_eq!(mesh.neighbour.len(), 9);
161        mesh.validate().unwrap();
162    }
163
164    #[test]
165    fn cell_volumes_are_uniform() {
166        let l = 2.0_f64;
167        let a = 0.03_f64;
168        let n = 5_i64;
169        let mesh =
170            create_one_d_mesh(Length::new::<meter>(l), Area::new::<square_meter>(a), n).unwrap();
171        let expected = l * a / n as f64;
172        for &v in &mesh.cell_volumes {
173            assert!((v - expected).abs() < 1e-14, "volume {v} != {expected}");
174        }
175    }
176
177    #[test]
178    fn cell_centres_are_midpoints() {
179        let n = 4_i64;
180        let dx = 1.0 / n as f64;
181        let mesh = create_one_d_mesh(Length::new::<meter>(1.0), Area::new::<square_meter>(1.0), n)
182            .unwrap();
183        for (i, c) in mesh.cell_centres.iter().enumerate() {
184            let expected_x = (i as f64 + 0.5) * dx;
185            assert!((c.x - expected_x).abs() < 1e-14);
186            assert_eq!(c.y, 0.0);
187            assert_eq!(c.z, 0.0);
188        }
189    }
190
191    #[test]
192    fn boundary_patches_named_correctly() {
193        let mesh = create_one_d_mesh(Length::new::<meter>(1.0), Area::new::<square_meter>(1.0), 3)
194            .unwrap();
195        assert_eq!(mesh.patches[0].name, "right");
196        assert_eq!(mesh.patches[1].name, "left");
197    }
198
199    #[test]
200    fn left_face_area_vector_points_negative_x() {
201        let a = 0.1_f64;
202        let mesh =
203            create_one_d_mesh(Length::new::<meter>(1.0), Area::new::<square_meter>(a), 3).unwrap();
204        // left boundary is the last face
205        let left_sf = mesh.face_area_vectors.last().unwrap();
206        assert!((left_sf.x + a).abs() < 1e-14);
207        assert_eq!(left_sf.y, 0.0);
208    }
209
210    #[test]
211    fn zero_cells_returns_error() {
212        assert!(
213            create_one_d_mesh(Length::new::<meter>(1.0), Area::new::<square_meter>(1.0), 0,)
214                .is_err()
215        );
216    }
217}