Skip to main content

oxiphysics_io/openfoam/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#[allow(unused_imports)]
6use super::functions::*;
7use super::functions::{foam_header, parse_dict_tokens, strip_foam_comments, tokenise_foam};
8#[allow(unused_imports)]
9use super::functions_2::*;
10
11/// A single boundary condition entry for a `FoamField`.
12pub struct FoamBc {
13    /// Patch name matching an entry in the boundary file.
14    pub patch_name: String,
15    /// Boundary condition type: "zeroGradient", "fixedValue", "noSlip", etc.
16    pub bc_type: String,
17    /// Optional value string used with "fixedValue" conditions.
18    pub value: Option<String>,
19}
20/// Common boundary condition constructors.
21#[allow(dead_code)]
22impl FoamBc {
23    /// Zero-gradient (Neumann) condition.
24    pub fn zero_gradient(patch: &str) -> Self {
25        FoamBc {
26            patch_name: patch.to_string(),
27            bc_type: "zeroGradient".to_string(),
28            value: None,
29        }
30    }
31    /// Fixed scalar value (Dirichlet) condition.
32    pub fn fixed_scalar(patch: &str, val: f64) -> Self {
33        FoamBc {
34            patch_name: patch.to_string(),
35            bc_type: "fixedValue".to_string(),
36            value: Some(format!("uniform {}", val)),
37        }
38    }
39    /// Fixed vector value (Dirichlet) condition.
40    pub fn fixed_vector(patch: &str, val: [f64; 3]) -> Self {
41        FoamBc {
42            patch_name: patch.to_string(),
43            bc_type: "fixedValue".to_string(),
44            value: Some(format!("uniform ({} {} {})", val[0], val[1], val[2])),
45        }
46    }
47    /// No-slip wall condition.
48    pub fn no_slip(patch: &str) -> Self {
49        FoamBc {
50            patch_name: patch.to_string(),
51            bc_type: "noSlip".to_string(),
52            value: None,
53        }
54    }
55    /// Symmetry condition.
56    pub fn symmetry(patch: &str) -> Self {
57        FoamBc {
58            patch_name: patch.to_string(),
59            bc_type: "symmetry".to_string(),
60            value: None,
61        }
62    }
63    /// Inlet-outlet condition with a given inlet value.
64    pub fn inlet_outlet_scalar(patch: &str, inlet_val: f64) -> Self {
65        FoamBc {
66            patch_name: patch.to_string(),
67            bc_type: "inletOutlet".to_string(),
68            value: Some(format!("uniform {}", inlet_val)),
69        }
70    }
71    /// Pressure inlet-outlet condition.
72    pub fn pressure_inlet_outlet(patch: &str, val: f64) -> Self {
73        FoamBc {
74            patch_name: patch.to_string(),
75            bc_type: "totalPressure".to_string(),
76            value: Some(format!("uniform {}", val)),
77        }
78    }
79    /// Empty condition (for 2D cases).
80    pub fn empty(patch: &str) -> Self {
81        FoamBc {
82            patch_name: patch.to_string(),
83            bc_type: "empty".to_string(),
84            value: None,
85        }
86    }
87}
88/// A polyhedral mesh for OpenFOAM output.
89pub struct FoamMesh {
90    /// Vertex coordinates.
91    pub points: Vec<[f64; 3]>,
92    /// Face vertex lists (each face is an arbitrary polygon).
93    pub faces: Vec<Vec<usize>>,
94    /// Owner cell index for each face.
95    pub owner: Vec<usize>,
96    /// Neighbour cell index for each face (-1 for boundary faces).
97    pub neighbour: Vec<i64>,
98    /// Total number of cells.
99    pub n_cells: usize,
100    /// Boundary patch definitions.
101    pub boundary_patches: Vec<FoamPatch>,
102}
103impl FoamMesh {
104    /// Build a simple structured hex mesh: `nx × ny × nz` cells filling
105    /// a box `[0, lx] × [0, ly] × [0, lz]`.
106    ///
107    /// Face ordering follows the OpenFOAM convention (lower-index owner,
108    /// internal faces first, then boundary faces grouped by patch).
109    pub fn box_mesh(lx: f64, ly: f64, lz: f64, nx: usize, ny: usize, nz: usize) -> Self {
110        let mut points = Vec::with_capacity((nx + 1) * (ny + 1) * (nz + 1));
111        for k in 0..=nz {
112            for j in 0..=ny {
113                for i in 0..=nx {
114                    points.push([
115                        lx * i as f64 / nx as f64,
116                        ly * j as f64 / ny as f64,
117                        lz * k as f64 / nz as f64,
118                    ]);
119                }
120            }
121        }
122        let pid =
123            |i: usize, j: usize, k: usize| -> usize { k * (ny + 1) * (nx + 1) + j * (nx + 1) + i };
124        let cid = |i: usize, j: usize, k: usize| -> usize { k * ny * nx + j * nx + i };
125        let n_cells = nx * ny * nz;
126        let mut faces: Vec<Vec<usize>> = Vec::new();
127        let mut owner: Vec<usize> = Vec::new();
128        let mut neighbour: Vec<i64> = Vec::new();
129        for k in 0..nz {
130            for j in 0..ny {
131                for i in 1..nx {
132                    faces.push(vec![
133                        pid(i, j, k),
134                        pid(i, j + 1, k),
135                        pid(i, j + 1, k + 1),
136                        pid(i, j, k + 1),
137                    ]);
138                    owner.push(cid(i - 1, j, k));
139                    neighbour.push(cid(i, j, k) as i64);
140                }
141            }
142        }
143        for k in 0..nz {
144            for j in 1..ny {
145                for i in 0..nx {
146                    faces.push(vec![
147                        pid(i, j, k),
148                        pid(i + 1, j, k),
149                        pid(i + 1, j, k + 1),
150                        pid(i, j, k + 1),
151                    ]);
152                    owner.push(cid(i, j - 1, k));
153                    neighbour.push(cid(i, j, k) as i64);
154                }
155            }
156        }
157        for k in 1..nz {
158            for j in 0..ny {
159                for i in 0..nx {
160                    faces.push(vec![
161                        pid(i, j, k),
162                        pid(i + 1, j, k),
163                        pid(i + 1, j + 1, k),
164                        pid(i, j + 1, k),
165                    ]);
166                    owner.push(cid(i, j, k - 1));
167                    neighbour.push(cid(i, j, k) as i64);
168                }
169            }
170        }
171        let xmin_start = faces.len();
172        for k in 0..nz {
173            for j in 0..ny {
174                faces.push(vec![
175                    pid(0, j, k),
176                    pid(0, j, k + 1),
177                    pid(0, j + 1, k + 1),
178                    pid(0, j + 1, k),
179                ]);
180                owner.push(cid(0, j, k));
181                neighbour.push(-1);
182            }
183        }
184        let xmin_n = faces.len() - xmin_start;
185        let xmax_start = faces.len();
186        for k in 0..nz {
187            for j in 0..ny {
188                faces.push(vec![
189                    pid(nx, j, k),
190                    pid(nx, j + 1, k),
191                    pid(nx, j + 1, k + 1),
192                    pid(nx, j, k + 1),
193                ]);
194                owner.push(cid(nx - 1, j, k));
195                neighbour.push(-1);
196            }
197        }
198        let xmax_n = faces.len() - xmax_start;
199        let ymin_start = faces.len();
200        for k in 0..nz {
201            for i in 0..nx {
202                faces.push(vec![
203                    pid(i, 0, k),
204                    pid(i + 1, 0, k),
205                    pid(i + 1, 0, k + 1),
206                    pid(i, 0, k + 1),
207                ]);
208                owner.push(cid(i, 0, k));
209                neighbour.push(-1);
210            }
211        }
212        let ymin_n = faces.len() - ymin_start;
213        let ymax_start = faces.len();
214        for k in 0..nz {
215            for i in 0..nx {
216                faces.push(vec![
217                    pid(i, ny, k),
218                    pid(i, ny, k + 1),
219                    pid(i + 1, ny, k + 1),
220                    pid(i + 1, ny, k),
221                ]);
222                owner.push(cid(i, ny - 1, k));
223                neighbour.push(-1);
224            }
225        }
226        let ymax_n = faces.len() - ymax_start;
227        let zmin_start = faces.len();
228        for j in 0..ny {
229            for i in 0..nx {
230                faces.push(vec![
231                    pid(i, j, 0),
232                    pid(i, j + 1, 0),
233                    pid(i + 1, j + 1, 0),
234                    pid(i + 1, j, 0),
235                ]);
236                owner.push(cid(i, j, 0));
237                neighbour.push(-1);
238            }
239        }
240        let zmin_n = faces.len() - zmin_start;
241        let zmax_start = faces.len();
242        for j in 0..ny {
243            for i in 0..nx {
244                faces.push(vec![
245                    pid(i, j, nz),
246                    pid(i + 1, j, nz),
247                    pid(i + 1, j + 1, nz),
248                    pid(i, j + 1, nz),
249                ]);
250                owner.push(cid(i, j, nz - 1));
251                neighbour.push(-1);
252            }
253        }
254        let zmax_n = faces.len() - zmax_start;
255        let boundary_patches = vec![
256            FoamPatch {
257                name: "xmin".into(),
258                patch_type: "patch".into(),
259                start_face: xmin_start,
260                n_faces: xmin_n,
261            },
262            FoamPatch {
263                name: "xmax".into(),
264                patch_type: "patch".into(),
265                start_face: xmax_start,
266                n_faces: xmax_n,
267            },
268            FoamPatch {
269                name: "ymin".into(),
270                patch_type: "patch".into(),
271                start_face: ymin_start,
272                n_faces: ymin_n,
273            },
274            FoamPatch {
275                name: "ymax".into(),
276                patch_type: "patch".into(),
277                start_face: ymax_start,
278                n_faces: ymax_n,
279            },
280            FoamPatch {
281                name: "zmin".into(),
282                patch_type: "patch".into(),
283                start_face: zmin_start,
284                n_faces: zmin_n,
285            },
286            FoamPatch {
287                name: "zmax".into(),
288                patch_type: "patch".into(),
289                start_face: zmax_start,
290                n_faces: zmax_n,
291            },
292        ];
293        FoamMesh {
294            points,
295            faces,
296            owner,
297            neighbour,
298            n_cells,
299            boundary_patches,
300        }
301    }
302    /// Write the `points` file content as a `String`.
303    pub fn write_points(&self) -> String {
304        let mut s = foam_header("vectorField", "points");
305        s.push('\n');
306        s.push_str(&format!("{}\n(\n", self.points.len()));
307        for p in &self.points {
308            s.push_str(&format!("({} {} {})\n", p[0], p[1], p[2]));
309        }
310        s.push_str(")\n");
311        s
312    }
313    /// Write the `faces` file content as a `String`.
314    pub fn write_faces(&self) -> String {
315        let mut s = foam_header("faceList", "faces");
316        s.push('\n');
317        s.push_str(&format!("{}\n(\n", self.faces.len()));
318        for face in &self.faces {
319            let verts: Vec<String> = face.iter().map(|v| v.to_string()).collect();
320            s.push_str(&format!("{}({})\n", face.len(), verts.join(" ")));
321        }
322        s.push_str(")\n");
323        s
324    }
325    /// Write the `owner` file content as a `String`.
326    pub fn write_owner(&self) -> String {
327        let mut s = foam_header("labelList", "owner");
328        s.push('\n');
329        s.push_str(&format!("{}\n(\n", self.owner.len()));
330        for &o in &self.owner {
331            s.push_str(&format!("{}\n", o));
332        }
333        s.push_str(")\n");
334        s
335    }
336    /// Write the `neighbour` file content as a `String`.
337    pub fn write_neighbour(&self) -> String {
338        let mut s = foam_header("labelList", "neighbour");
339        s.push('\n');
340        s.push_str(&format!("{}\n(\n", self.neighbour.len()));
341        for &n in &self.neighbour {
342            s.push_str(&format!("{}\n", n));
343        }
344        s.push_str(")\n");
345        s
346    }
347    /// Write the `boundary` file content as a `String`.
348    pub fn write_boundary(&self) -> String {
349        let mut s = foam_header("polyBoundaryMesh", "boundary");
350        s.push('\n');
351        s.push_str(&format!("{}\n(\n", self.boundary_patches.len()));
352        for patch in &self.boundary_patches {
353            s.push_str(
354                &format!(
355                    "    {}\n    {{\n        type        {};\n        nFaces      {};\n        startFace   {};\n    }}\n",
356                    patch.name, patch.patch_type, patch.n_faces, patch.start_face
357                ),
358            );
359        }
360        s.push_str(")\n");
361        s
362    }
363}
364#[allow(dead_code)]
365impl FoamMesh {
366    /// Count internal faces (those with a non-negative neighbour).
367    pub fn n_internal_faces(&self) -> usize {
368        self.neighbour.iter().filter(|&&n| n >= 0).count()
369    }
370    /// Count boundary faces (those with neighbour == -1).
371    pub fn n_boundary_faces(&self) -> usize {
372        self.neighbour.iter().filter(|&&n| n < 0).count()
373    }
374    /// Total number of faces.
375    pub fn n_faces(&self) -> usize {
376        self.faces.len()
377    }
378    /// Total number of points.
379    pub fn n_points(&self) -> usize {
380        self.points.len()
381    }
382    /// Compute the bounding box of the mesh: `([xmin, ymin, zmin], [xmax, ymax, zmax])`.
383    pub fn bounding_box(&self) -> ([f64; 3], [f64; 3]) {
384        if self.points.is_empty() {
385            return ([0.0; 3], [0.0; 3]);
386        }
387        let mut min = self.points[0];
388        let mut max = self.points[0];
389        for p in &self.points {
390            for i in 0..3 {
391                if p[i] < min[i] {
392                    min[i] = p[i];
393                }
394                if p[i] > max[i] {
395                    max[i] = p[i];
396                }
397            }
398        }
399        (min, max)
400    }
401    /// Compute the centre of the mesh bounding box.
402    pub fn centre(&self) -> [f64; 3] {
403        let (min, max) = self.bounding_box();
404        [
405            (min[0] + max[0]) * 0.5,
406            (min[1] + max[1]) * 0.5,
407            (min[2] + max[2]) * 0.5,
408        ]
409    }
410    /// Check basic mesh consistency: owner/neighbour length matches faces.
411    pub fn check_topology(&self) -> bool {
412        self.owner.len() == self.faces.len() && self.neighbour.len() == self.faces.len()
413    }
414    /// Find the patch with a given name.
415    pub fn find_patch(&self, name: &str) -> Option<&FoamPatch> {
416        self.boundary_patches.iter().find(|p| p.name == name)
417    }
418    /// Return all patch names.
419    pub fn patch_names(&self) -> Vec<&str> {
420        self.boundary_patches
421            .iter()
422            .map(|p| p.name.as_str())
423            .collect()
424    }
425}
426/// Represents a single time directory's metadata.
427#[allow(dead_code)]
428#[derive(Debug, Clone)]
429pub struct FoamTimeDir {
430    /// Time value (e.g. 0.0, 0.5, 1.0).
431    pub time: f64,
432    /// Directory name as a string (e.g. "0", "0.5", "1").
433    pub dir_name: String,
434    /// List of field names found at this time (e.g. \["U", "p"\]).
435    pub fields: Vec<String>,
436}
437/// Writer for the OpenFOAM `system/controlDict` file.
438pub struct ControlDict {
439    /// Solver application name (e.g. "icoFoam", "simpleFoam").
440    pub application: String,
441    /// Simulation start time.
442    pub start_time: f64,
443    /// Simulation end time.
444    pub end_time: f64,
445    /// Time step size.
446    pub delta_t: f64,
447    /// How often to write results (in time units).
448    pub write_interval: f64,
449    /// Output format: "ascii" or "binary".
450    pub write_format: String,
451    /// Number of significant digits for ASCII output.
452    pub write_precision: usize,
453}
454impl ControlDict {
455    /// Create a `ControlDict` with sensible defaults.
456    pub fn new(application: &str, end_time: f64, dt: f64) -> Self {
457        ControlDict {
458            application: application.to_string(),
459            start_time: 0.0,
460            end_time,
461            delta_t: dt,
462            write_interval: end_time / 10.0,
463            write_format: "ascii".to_string(),
464            write_precision: 6,
465        }
466    }
467    /// Render the controlDict as an OpenFOAM-formatted `String`.
468    #[allow(clippy::inherent_to_string)]
469    pub fn to_string(&self) -> String {
470        let mut s = foam_header("dictionary", "controlDict");
471        s.push('\n');
472        s.push_str(&format!("application     {};\n\n", self.application));
473        s.push_str("startFrom       startTime;\n\n");
474        s.push_str(&format!("startTime       {};\n\n", self.start_time));
475        s.push_str("stopAt          endTime;\n\n");
476        s.push_str(&format!("endTime         {};\n\n", self.end_time));
477        s.push_str(&format!("deltaT          {};\n\n", self.delta_t));
478        s.push_str(&format!("writeFormat     {};\n\n", self.write_format));
479        s.push_str(&format!("writePrecision  {};\n\n", self.write_precision));
480        s.push_str("writeCompression off;\n\n");
481        s.push_str("timeFormat      general;\n\n");
482        s.push_str("timePrecision   6;\n\n");
483        s.push_str("runTimeModifiable true;\n\n");
484        s.push_str(&format!("writeInterval   {};\n", self.write_interval));
485        s
486    }
487}
488/// Boundary patch definition for OpenFOAM output.
489#[derive(Debug, Clone)]
490pub struct FoamPatch {
491    /// Name of the patch (e.g. "inlet", "outlet", "walls").
492    pub name: String,
493    /// Patch type: "wall", "patch", "symmetryPlane", etc.
494    pub patch_type: String,
495    /// Index of the first face belonging to this patch.
496    pub start_face: usize,
497    /// Number of faces in this patch.
498    pub n_faces: usize,
499}
500/// A parsed OpenFOAM dictionary (key-value map preserving insertion order).
501#[allow(dead_code)]
502#[derive(Debug, Clone, PartialEq)]
503pub struct FoamDict {
504    /// Ordered key-value entries.
505    pub entries: Vec<(String, FoamValue)>,
506}
507#[allow(dead_code)]
508impl FoamDict {
509    /// Create an empty dictionary.
510    pub fn new() -> Self {
511        FoamDict {
512            entries: Vec::new(),
513        }
514    }
515    /// Insert a key-value pair (appends; does not deduplicate).
516    pub fn insert(&mut self, key: impl Into<String>, value: FoamValue) {
517        self.entries.push((key.into(), value));
518    }
519    /// Look up a key and return the first matching value.
520    pub fn get(&self, key: &str) -> Option<&FoamValue> {
521        self.entries.iter().find(|(k, _)| k == key).map(|(_, v)| v)
522    }
523    /// Look up a key and return it as a scalar, if possible.
524    pub fn get_scalar(&self, key: &str) -> Option<f64> {
525        match self.get(key) {
526            Some(FoamValue::Scalar(v)) => Some(*v),
527            _ => None,
528        }
529    }
530    /// Look up a key and return it as a word/string, if possible.
531    pub fn get_word(&self, key: &str) -> Option<&str> {
532        match self.get(key) {
533            Some(FoamValue::Word(w)) => Some(w.as_str()),
534            _ => None,
535        }
536    }
537    /// Look up a key and return it as a sub-dictionary, if possible.
538    pub fn get_dict(&self, key: &str) -> Option<&FoamDict> {
539        match self.get(key) {
540            Some(FoamValue::Dict(d)) => Some(d),
541            _ => None,
542        }
543    }
544    /// Look up a key and return it as a vector, if possible.
545    pub fn get_vector(&self, key: &str) -> Option<[f64; 3]> {
546        match self.get(key) {
547            Some(FoamValue::Vector(v)) => Some(*v),
548            _ => None,
549        }
550    }
551    /// Number of entries.
552    pub fn len(&self) -> usize {
553        self.entries.len()
554    }
555    /// Whether the dictionary is empty.
556    pub fn is_empty(&self) -> bool {
557        self.entries.is_empty()
558    }
559    /// All keys in insertion order.
560    pub fn keys(&self) -> Vec<&str> {
561        self.entries.iter().map(|(k, _)| k.as_str()).collect()
562    }
563    /// Serialise to OpenFOAM dictionary format string.
564    pub fn to_foam_string(&self, indent: usize) -> String {
565        let pad = "    ".repeat(indent);
566        let mut s = String::new();
567        for (key, value) in &self.entries {
568            match value {
569                FoamValue::Scalar(v) => {
570                    s.push_str(&format!("{}{:<16}{};\n", pad, key, v));
571                }
572                FoamValue::Word(w) => {
573                    s.push_str(&format!("{}{:<16}{};\n", pad, key, w));
574                }
575                FoamValue::Vector(v) => {
576                    s.push_str(&format!(
577                        "{}{:<16}({} {} {});\n",
578                        pad, key, v[0], v[1], v[2]
579                    ));
580                }
581                FoamValue::Dict(d) => {
582                    s.push_str(&format!("{}{}\n{}{{\n", pad, key, pad));
583                    s.push_str(&d.to_foam_string(indent + 1));
584                    s.push_str(&format!("{}}}\n", pad));
585                }
586                FoamValue::List(items) => {
587                    s.push_str(&format!("{}{}\n{}(\n", pad, key, pad));
588                    for item in items {
589                        match item {
590                            FoamValue::Scalar(v) => {
591                                s.push_str(&format!("{}    {}\n", pad, v));
592                            }
593                            FoamValue::Word(w) => {
594                                s.push_str(&format!("{}    {}\n", pad, w));
595                            }
596                            FoamValue::Vector(v) => {
597                                s.push_str(&format!("{}    ({} {} {})\n", pad, v[0], v[1], v[2]));
598                            }
599                            _ => {}
600                        }
601                    }
602                    s.push_str(&format!("{});\n", pad));
603                }
604            }
605        }
606        s
607    }
608    /// Parse a simplified OpenFOAM dictionary from a string.
609    ///
610    /// Supports: `key value;`, `key { ... }`, `key ( ... );`, and
611    /// `key (x y z);` vector syntax.  Comments (`//` and `/* */`) are stripped.
612    pub fn parse(input: &str) -> Self {
613        let cleaned = strip_foam_comments(input);
614        let tokens = tokenise_foam(&cleaned);
615        let (dict, _) = parse_dict_tokens(&tokens, 0);
616        dict
617    }
618}
619impl Default for FoamDict {
620    fn default() -> Self {
621        Self::new()
622    }
623}
624/// Writer for the OpenFOAM `system/fvSchemes` file.
625#[allow(dead_code)]
626pub struct FvSchemes {
627    /// Time derivative scheme.
628    pub ddt_scheme: String,
629    /// Gradient scheme.
630    pub grad_scheme: String,
631    /// Divergence schemes (key → scheme).
632    pub div_schemes: Vec<(String, String)>,
633    /// Laplacian schemes (key → scheme).
634    pub laplacian_schemes: Vec<(String, String)>,
635    /// Interpolation scheme.
636    pub interpolation_scheme: String,
637    /// Surface-normal gradient scheme.
638    pub sn_grad_scheme: String,
639}
640#[allow(dead_code)]
641impl FvSchemes {
642    /// Create default second-order schemes.
643    pub fn default_second_order() -> Self {
644        FvSchemes {
645            ddt_scheme: "Euler".to_string(),
646            grad_scheme: "Gauss linear".to_string(),
647            div_schemes: vec![
648                ("default".to_string(), "none".to_string()),
649                (
650                    "div(phi,U)".to_string(),
651                    "Gauss linearUpwind grad(U)".to_string(),
652                ),
653            ],
654            laplacian_schemes: vec![("default".to_string(), "Gauss linear corrected".to_string())],
655            interpolation_scheme: "linear".to_string(),
656            sn_grad_scheme: "corrected".to_string(),
657        }
658    }
659    /// Render to OpenFOAM format.
660    #[allow(clippy::inherent_to_string)]
661    pub fn to_string(&self) -> String {
662        let mut s = foam_header("dictionary", "fvSchemes");
663        s.push('\n');
664        s.push_str("ddtSchemes\n{\n");
665        s.push_str(&format!("    default         {};\n", self.ddt_scheme));
666        s.push_str("}\n\n");
667        s.push_str("gradSchemes\n{\n");
668        s.push_str(&format!("    default         {};\n", self.grad_scheme));
669        s.push_str("}\n\n");
670        s.push_str("divSchemes\n{\n");
671        for (key, val) in &self.div_schemes {
672            s.push_str(&format!("    {}    {};\n", key, val));
673        }
674        s.push_str("}\n\n");
675        s.push_str("laplacianSchemes\n{\n");
676        for (key, val) in &self.laplacian_schemes {
677            s.push_str(&format!("    {}    {};\n", key, val));
678        }
679        s.push_str("}\n\n");
680        s.push_str("interpolationSchemes\n{\n");
681        s.push_str(&format!(
682            "    default         {};\n",
683            self.interpolation_scheme
684        ));
685        s.push_str("}\n\n");
686        s.push_str("snGradSchemes\n{\n");
687        s.push_str(&format!("    default         {};\n", self.sn_grad_scheme));
688        s.push_str("}\n");
689        s
690    }
691}
692/// A parsed OpenFOAM solver residual entry (from log file).
693#[allow(dead_code)]
694#[derive(Debug, Clone)]
695pub struct FoamResidual {
696    /// Time step (or iteration) number.
697    pub time: f64,
698    /// Field name (e.g., "p", "U").
699    pub field: String,
700    /// Initial residual.
701    pub initial_residual: f64,
702    /// Final residual.
703    pub final_residual: f64,
704    /// Number of iterations.
705    pub n_iterations: usize,
706}
707/// A parsed OpenFOAM dictionary entry.
708#[allow(dead_code)]
709#[derive(Debug, Clone, PartialEq)]
710pub enum FoamValue {
711    /// A scalar number.
712    Scalar(f64),
713    /// A string or keyword.
714    Word(String),
715    /// A vector `(x y z)`.
716    Vector([f64; 3]),
717    /// A nested dictionary.
718    Dict(FoamDict),
719    /// A list of values.
720    List(Vec<FoamValue>),
721}
722/// Writer for `constant/transportProperties`.
723#[allow(dead_code)]
724pub struct TransportProperties {
725    /// Transport model: "Newtonian", "CrossPowerLaw", etc.
726    pub transport_model: String,
727    /// Kinematic viscosity \[m^2/s\].
728    pub nu: f64,
729}
730#[allow(dead_code)]
731impl TransportProperties {
732    /// Create Newtonian transport with given kinematic viscosity.
733    pub fn newtonian(nu: f64) -> Self {
734        TransportProperties {
735            transport_model: "Newtonian".to_string(),
736            nu,
737        }
738    }
739    /// Render to OpenFOAM format.
740    #[allow(clippy::inherent_to_string)]
741    pub fn to_string(&self) -> String {
742        let mut s = foam_header("dictionary", "transportProperties");
743        s.push('\n');
744        s.push_str(&format!("transportModel  {};\n\n", self.transport_model));
745        s.push_str(&format!("nu              [0 2 -1 0 0 0 0] {};\n", self.nu));
746        s
747    }
748}
749/// A scalar or vector field for OpenFOAM output (volScalarField / volVectorField).
750pub struct FoamField {
751    /// Number of internal cells.
752    pub n_cells: usize,
753    /// Field name used in the file header (e.g. "p", "U").
754    pub field_name: String,
755    /// OpenFOAM class: "volScalarField" or "volVectorField".
756    pub field_class: String,
757    /// Dimension set string, e.g. `"[0 2 -2 0 0 0 0]"` for pressure.
758    pub dimensions: String,
759    /// Internal field values.
760    pub internal_values: FieldValues,
761    /// Boundary condition list (one entry per patch).
762    pub boundary_conditions: Vec<FoamBc>,
763}
764impl FoamField {
765    /// Render this field as an OpenFOAM-formatted `String`.
766    #[allow(clippy::inherent_to_string)]
767    pub fn to_string(&self) -> String {
768        let mut s = foam_header(&self.field_class, &self.field_name);
769        s.push('\n');
770        s.push_str(&format!("dimensions      {};\n\n", self.dimensions));
771        match &self.internal_values {
772            FieldValues::Uniform(v) => {
773                s.push_str(&format!("internalField   uniform {};\n\n", v));
774            }
775            FieldValues::UniformVec(v) => {
776                s.push_str(&format!(
777                    "internalField   uniform ({} {} {});\n\n",
778                    v[0], v[1], v[2]
779                ));
780            }
781            FieldValues::NonUniform(vals) => {
782                s.push_str("internalField   nonuniform List<scalar>\n");
783                s.push_str(&format!("{}\n(\n", vals.len()));
784                for v in vals {
785                    s.push_str(&format!("{}\n", v));
786                }
787                s.push_str(");\n\n");
788            }
789            FieldValues::NonUniformVec(vals) => {
790                s.push_str("internalField   nonuniform List<vector>\n");
791                s.push_str(&format!("{}\n(\n", vals.len()));
792                for v in vals {
793                    s.push_str(&format!("({} {} {})\n", v[0], v[1], v[2]));
794                }
795                s.push_str(");\n\n");
796            }
797        }
798        s.push_str("boundaryField\n{\n");
799        for bc in &self.boundary_conditions {
800            s.push_str(&format!("    {}\n    {{\n", bc.patch_name));
801            s.push_str(&format!("        type        {};\n", bc.bc_type));
802            if let Some(val) = &bc.value {
803                s.push_str(&format!("        value       {};\n", val));
804            }
805            s.push_str("    }\n");
806        }
807        s.push_str("}\n");
808        s
809    }
810}
811/// Internal/boundary field values for a `FoamField`.
812pub enum FieldValues {
813    /// Spatially uniform scalar value.
814    Uniform(f64),
815    /// Spatially uniform vector value.
816    UniformVec([f64; 3]),
817    /// Non-uniform scalar field (one value per cell).
818    NonUniform(Vec<f64>),
819    /// Non-uniform vector field (one 3-component vector per cell).
820    NonUniformVec(Vec<[f64; 3]>),
821}
822/// Writer for the OpenFOAM `system/fvSolution` file.
823#[allow(dead_code)]
824pub struct FvSolution {
825    /// Solver settings per field (field_name, solver, preconditioner, tolerance, relTol).
826    pub solvers: Vec<FvSolverEntry>,
827    /// SIMPLE/PISO/PIMPLE algorithm settings.
828    pub algorithm: String,
829    /// Number of correctors for PISO/PIMPLE.
830    pub n_correctors: usize,
831    /// Number of non-orthogonal correctors.
832    pub n_non_orthogonal_correctors: usize,
833    /// Pressure reference cell.
834    pub p_ref_cell: usize,
835    /// Pressure reference value.
836    pub p_ref_value: f64,
837}
838#[allow(dead_code)]
839impl FvSolution {
840    /// Create default PISO solution settings.
841    pub fn default_piso() -> Self {
842        FvSolution {
843            solvers: vec![
844                FvSolverEntry {
845                    field_name: "p".to_string(),
846                    solver: "PCG".to_string(),
847                    preconditioner: "DIC".to_string(),
848                    tolerance: 1e-6,
849                    rel_tol: 0.05,
850                },
851                FvSolverEntry {
852                    field_name: "U".to_string(),
853                    solver: "smoothSolver".to_string(),
854                    preconditioner: "symGaussSeidel".to_string(),
855                    tolerance: 1e-5,
856                    rel_tol: 0.0,
857                },
858            ],
859            algorithm: "PISO".to_string(),
860            n_correctors: 2,
861            n_non_orthogonal_correctors: 0,
862            p_ref_cell: 0,
863            p_ref_value: 0.0,
864        }
865    }
866    /// Render to OpenFOAM format.
867    #[allow(clippy::inherent_to_string)]
868    pub fn to_string(&self) -> String {
869        let mut s = foam_header("dictionary", "fvSolution");
870        s.push('\n');
871        s.push_str("solvers\n{\n");
872        for entry in &self.solvers {
873            s.push_str(&format!("    {}\n    {{\n", entry.field_name));
874            s.push_str(&format!("        solver          {};\n", entry.solver));
875            s.push_str(&format!(
876                "        preconditioner  {};\n",
877                entry.preconditioner
878            ));
879            s.push_str(&format!("        tolerance       {};\n", entry.tolerance));
880            s.push_str(&format!("        relTol          {};\n", entry.rel_tol));
881            s.push_str("    }\n");
882        }
883        s.push_str("}\n\n");
884        s.push_str(&format!("{}\n{{\n", self.algorithm));
885        s.push_str(&format!("    nCorrectors     {};\n", self.n_correctors));
886        s.push_str(&format!(
887            "    nNonOrthogonalCorrectors {};\n",
888            self.n_non_orthogonal_correctors
889        ));
890        s.push_str(&format!("    pRefCell        {};\n", self.p_ref_cell));
891        s.push_str(&format!("    pRefValue       {};\n", self.p_ref_value));
892        s.push_str("}\n");
893        s
894    }
895}
896/// A single solver entry in fvSolution.
897#[allow(dead_code)]
898#[derive(Debug, Clone)]
899pub struct FvSolverEntry {
900    /// Field name (e.g. "p", "U").
901    pub field_name: String,
902    /// Linear solver name (e.g. "PCG", "smoothSolver").
903    pub solver: String,
904    /// Preconditioner name (e.g. "DIC", "symGaussSeidel").
905    pub preconditioner: String,
906    /// Absolute convergence tolerance.
907    pub tolerance: f64,
908    /// Relative tolerance.
909    pub rel_tol: f64,
910}
911/// Parsed contents of an OpenFOAM `FoamFile` header block.
912#[allow(dead_code)]
913#[derive(Debug, Clone, PartialEq)]
914pub struct FoamFileHeader {
915    /// Format version (e.g., `2.0`).
916    pub version: f64,
917    /// Format string: `"ascii"` or `"binary"`.
918    pub format: String,
919    /// OpenFOAM class (e.g., `"volScalarField"`, `"polyMesh"`).
920    pub class: String,
921    /// Object name (e.g., `"p"`, `"points"`).
922    pub object: String,
923    /// Optional note/description field.
924    pub note: Option<String>,
925    /// Optional location path.
926    pub location: Option<String>,
927}
928#[allow(dead_code)]
929impl FoamFileHeader {
930    /// Parse a `FoamFile { ... }` header from OpenFOAM file content.
931    ///
932    /// Returns `None` if no `FoamFile` block is found.
933    pub fn parse(input: &str) -> Option<Self> {
934        let cleaned = strip_foam_comments(input);
935        let start = cleaned.find("FoamFile")?;
936        let after = &cleaned[start + "FoamFile".len()..];
937        let brace_start = after.find('{')?;
938        let brace_content = &after[brace_start + 1..];
939        let brace_end = brace_content.find('}')?;
940        let block = &brace_content[..brace_end];
941        let mut version = 2.0_f64;
942        let mut format = "ascii".to_string();
943        let mut class = String::new();
944        let mut object = String::new();
945        let mut note = None;
946        let mut location = None;
947        for line in block.lines() {
948            let trimmed = line.trim();
949            if trimmed.is_empty() || trimmed.starts_with("//") {
950                continue;
951            }
952            let line_clean = trimmed.trim_end_matches(';').trim();
953            let parts: Vec<&str> = line_clean.splitn(2, char::is_whitespace).collect();
954            if parts.len() < 2 {
955                continue;
956            }
957            let key = parts[0].trim();
958            let val = parts[1].trim().trim_matches('"');
959            match key {
960                "version" => {
961                    version = val.parse().unwrap_or(2.0);
962                }
963                "format" => {
964                    format = val.to_string();
965                }
966                "class" => {
967                    class = val.to_string();
968                }
969                "object" => {
970                    object = val.to_string();
971                }
972                "note" => {
973                    note = Some(val.to_string());
974                }
975                "location" => {
976                    location = Some(val.to_string());
977                }
978                _ => {}
979            }
980        }
981        if class.is_empty() && object.is_empty() {
982            return None;
983        }
984        Some(FoamFileHeader {
985            version,
986            format,
987            class,
988            object,
989            note,
990            location,
991        })
992    }
993    /// Write a `FoamFile` header block as a string.
994    #[allow(clippy::inherent_to_string)]
995    pub fn to_string(&self) -> String {
996        let mut s = String::new();
997        s.push_str("FoamFile\n{\n");
998        s.push_str(&format!("    version     {};\n", self.version));
999        s.push_str(&format!("    format      {};\n", self.format));
1000        s.push_str(&format!("    class       {};\n", self.class));
1001        if let Some(ref loc) = self.location {
1002            s.push_str(&format!("    location    \"{}\";\n", loc));
1003        }
1004        s.push_str(&format!("    object      {};\n", self.object));
1005        if let Some(ref note) = self.note {
1006            s.push_str(&format!("    note        \"{}\";\n", note));
1007        }
1008        s.push_str("}\n");
1009        s
1010    }
1011}