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}