Skip to main content

outram_foam_basic_lib/io/
case.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//! Whole-**case** reader (`system/`, `constant/polyMesh`, a time directory).
23//!
24//! [`FoamCase::read`] loads an OpenFOAM case directory into memory:
25//!
26//! - `system/controlDict`, `system/fvSchemes`, `system/fvSolution` (and any
27//!   other `system/` dictionaries) as [`FoamFile`] dictionaries;
28//! - `constant/polyMesh` as a [`PolyMesh`] (plus a derived [`FvMesh`]);
29//! - the fields in a time directory (default `0/`) as [`CaseField`]s,
30//!   dispatched by their `FoamFile` `class` (`volScalarField` /
31//!   `volVectorField`).
32//!
33//! Field classes other than scalar/vector volume fields are **skipped** and
34//! their file names recorded in [`FoamCase::skipped_fields`] (honest partial
35//! coverage — nothing is silently lost). [`FoamCase::write`] is a best-effort
36//! counterpart that re-emits the system dictionaries, the mesh, and the
37//! scalar/vector fields.
38
39use std::path::{Path, PathBuf};
40use std::sync::Arc;
41
42use super::dict::FoamFile;
43use super::field::{
44    read_vol_scalar_field, read_vol_vector_field, write_vol_scalar_field, write_vol_vector_field,
45    Dimensions,
46};
47use super::poly_mesh::PolyMesh;
48use super::IoError;
49use crate::fields::{VolScalarField, VolVectorField};
50use crate::mesh::FvMesh;
51
52/// A field loaded from a time directory, tagged by its value type.
53///
54/// An enum (not a trait object) so callers get exhaustive `match` handling and
55/// the field lives inline.
56#[derive(Debug, Clone)]
57pub enum CaseField {
58    /// A `volScalarField` and its dimensions.
59    Scalar(VolScalarField, Dimensions),
60    /// A `volVectorField` and its dimensions.
61    Vector(VolVectorField, Dimensions),
62}
63
64impl CaseField {
65    /// The field name (its `object`).
66    pub fn name(&self) -> &str {
67        match self {
68            CaseField::Scalar(f, _) => &f.name,
69            CaseField::Vector(f, _) => &f.name,
70        }
71    }
72}
73
74/// An in-memory OpenFOAM case.
75#[derive(Debug, Clone)]
76pub struct FoamCase {
77    /// The case root directory.
78    pub root: PathBuf,
79    /// `system/` dictionaries, keyed by file name (`controlDict`, …).
80    pub system: Vec<(String, FoamFile)>,
81    /// The connectivity-carrying mesh from `constant/polyMesh`, if present.
82    pub poly_mesh: Option<PolyMesh>,
83    /// The geometry-carrying FV mesh derived from `poly_mesh`, if present.
84    pub mesh: Option<Arc<FvMesh>>,
85    /// The time directory that was read (e.g. `"0"`).
86    pub time_dir: String,
87    /// Fields loaded from the time directory.
88    pub fields: Vec<CaseField>,
89    /// Field files skipped because their `class` is not a scalar/vector volume
90    /// field (`(file_name, class)`).
91    pub skipped_fields: Vec<(String, String)>,
92}
93
94impl FoamCase {
95    /// Read a case from `root`, using time directory `"0"`.
96    pub fn read(root: impl AsRef<Path>) -> Result<Self, IoError> {
97        Self::read_time(root, "0")
98    }
99
100    /// Read a case from `root`, using the given `time_dir` (e.g. `"0"`).
101    pub fn read_time(root: impl AsRef<Path>, time_dir: &str) -> Result<Self, IoError> {
102        let root = root.as_ref().to_path_buf();
103
104        // system/ dictionaries.
105        let mut system = Vec::new();
106        let sys_dir = root.join("system");
107        if sys_dir.is_dir() {
108            let mut names = list_files(&sys_dir)?;
109            names.sort();
110            for name in names {
111                let ff = FoamFile::read(sys_dir.join(&name))?;
112                system.push((name, ff));
113            }
114        }
115
116        // constant/polyMesh.
117        let poly_dir = root.join("constant").join("polyMesh");
118        let (poly_mesh, mesh) = if poly_dir.join("points").is_file() {
119            let pm = PolyMesh::read(&poly_dir)?;
120            let fv = Arc::new(pm.to_fv_mesh()?);
121            (Some(pm), Some(fv))
122        } else {
123            (None, None)
124        };
125
126        // time-directory fields (need a mesh to bind boundary patches).
127        let mut fields = Vec::new();
128        let mut skipped_fields = Vec::new();
129        let t_dir = root.join(time_dir);
130        if let Some(mesh) = &mesh {
131            if t_dir.is_dir() {
132                let mut names = list_files(&t_dir)?;
133                names.sort();
134                for name in names {
135                    let path = t_dir.join(&name);
136                    match peek_class(&path)?.as_deref() {
137                        Some("volScalarField") => {
138                            let (f, d) = read_vol_scalar_field(&path, Arc::clone(mesh))?;
139                            fields.push(CaseField::Scalar(f, d));
140                        }
141                        Some("volVectorField") => {
142                            let (f, d) = read_vol_vector_field(&path, Arc::clone(mesh))?;
143                            fields.push(CaseField::Vector(f, d));
144                        }
145                        Some(other) => skipped_fields.push((name, other.to_string())),
146                        None => skipped_fields.push((name, "<no class>".to_string())),
147                    }
148                }
149            }
150        }
151
152        Ok(Self {
153            root,
154            system,
155            poly_mesh,
156            mesh,
157            time_dir: time_dir.to_string(),
158            fields,
159            skipped_fields,
160        })
161    }
162
163    /// Look up a `system/` dictionary by file name.
164    pub fn system_dict(&self, name: &str) -> Option<&FoamFile> {
165        self.system.iter().find(|(n, _)| n == name).map(|(_, f)| f)
166    }
167
168    /// Best-effort write of the case back to `root`: `system/` dictionaries,
169    /// `constant/polyMesh`, and the scalar/vector fields into the time
170    /// directory. Skipped (unsupported-class) fields are not re-emitted.
171    pub fn write(&self, root: impl AsRef<Path>) -> Result<(), IoError> {
172        let root = root.as_ref();
173
174        let sys_dir = root.join("system");
175        create_dir(&sys_dir)?;
176        for (name, ff) in &self.system {
177            ff.write(sys_dir.join(name))?;
178        }
179
180        if let Some(pm) = &self.poly_mesh {
181            pm.write(root.join("constant").join("polyMesh"))?;
182        }
183
184        let t_dir = root.join(&self.time_dir);
185        if !self.fields.is_empty() {
186            create_dir(&t_dir)?;
187        }
188        for field in &self.fields {
189            match field {
190                CaseField::Scalar(f, d) => write_vol_scalar_field(t_dir.join(&f.name), f, *d)?,
191                CaseField::Vector(f, d) => write_vol_vector_field(t_dir.join(&f.name), f, *d)?,
192            }
193        }
194        Ok(())
195    }
196}
197
198// ── helpers ─────────────────────────────────────────────────────────────────
199
200fn create_dir(dir: &Path) -> Result<(), IoError> {
201    std::fs::create_dir_all(dir).map_err(|e| IoError::io(dir.display().to_string(), e))
202}
203
204/// List regular-file names (not sub-directories) in `dir`.
205fn list_files(dir: &Path) -> Result<Vec<String>, IoError> {
206    let mut out = Vec::new();
207    let rd = std::fs::read_dir(dir).map_err(|e| IoError::io(dir.display().to_string(), e))?;
208    for entry in rd {
209        let entry = entry.map_err(|e| IoError::io(dir.display().to_string(), e))?;
210        let path = entry.path();
211        if path.is_file() {
212            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
213                out.push(name.to_string());
214            }
215        }
216    }
217    Ok(out)
218}
219
220/// Read just the `FoamFile` `class` field from a file, without parsing the
221/// whole body.
222fn peek_class(path: &Path) -> Result<Option<String>, IoError> {
223    let text =
224        std::fs::read_to_string(path).map_err(|e| IoError::io(path.display().to_string(), e))?;
225    let tokens = super::dict::tokenize(&text);
226    // Find the FoamFile block and its `class` entry.
227    let mut i = 0;
228    while i < tokens.len() {
229        if tokens[i].text == "class" {
230            if let Some(v) = tokens.get(i + 1) {
231                return Ok(Some(v.text.clone()));
232            }
233        }
234        i += 1;
235    }
236    Ok(None)
237}