Skip to main content

mesh_sieve/io/
fluent.rs

1//! Fluent ASCII mesh reader.
2//!
3//! This reader imports a practical ASCII subset of ANSYS Fluent `.msh` files:
4//! vertex coordinate sections `(10 ...)` and cell connectivity sections
5//! `(12 ...)` with explicit node lists. It also accepts a compact fixture form
6//! with `vertices`/`cells` records for tests and examples.
7
8use crate::data::storage::VecStorage;
9use crate::io::{MeshData, SieveSectionReader};
10use crate::mesh_error::MeshSieveError;
11use crate::topology::cell_type::CellType;
12use crate::topology::labels::LabelSet;
13use crate::topology::point::PointId;
14use crate::topology::sieve::MeshSieve;
15use std::io::Read;
16
17/// Reader for ASCII Fluent meshes.
18#[derive(Debug, Default, Clone)]
19pub struct FluentReader;
20
21impl SieveSectionReader for FluentReader {
22    type Sieve = MeshSieve;
23    type Value = f64;
24    type Storage = VecStorage<f64>;
25    type CellStorage = VecStorage<CellType>;
26
27    fn read<R: Read>(
28        &self,
29        mut reader: R,
30    ) -> Result<MeshData<Self::Sieve, Self::Value, Self::Storage, Self::CellStorage>, MeshSieveError>
31    {
32        let mut text = String::new();
33        reader.read_to_string(&mut text)?;
34        parse_fluent_ascii(&text)
35    }
36}
37
38fn parse_fluent_ascii(
39    text: &str,
40) -> Result<MeshData<MeshSieve, f64, VecStorage<f64>, VecStorage<CellType>>, MeshSieveError> {
41    if text.lines().any(|l| l.trim_start().starts_with("vertices")) {
42        return parse_compact(text);
43    }
44    parse_sexpr_subset(text)
45}
46
47fn parse_compact(
48    text: &str,
49) -> Result<MeshData<MeshSieve, f64, VecStorage<f64>, VecStorage<CellType>>, MeshSieveError> {
50    let mut vertices = Vec::new();
51    let mut cells = Vec::new();
52    let mut labels = LabelSet::new();
53    for line in text.lines() {
54        let line = line.trim();
55        if line.is_empty() || line.starts_with('#') {
56            continue;
57        }
58        let parts: Vec<_> = line.split_whitespace().collect();
59        match parts.as_slice() {
60            ["v", x, y, z] => vertices.push([parse_f64(x)?, parse_f64(y)?, parse_f64(z)?]),
61            ["label", name, value, ids @ ..] if !ids.is_empty() => {
62                let value = value.parse::<i32>().map_err(|_| {
63                    MeshSieveError::MeshIoParse(format!("invalid Fluent label value: {value}"))
64                })?;
65                for id in ids {
66                    labels.set_label(PointId::new(parse_u64(id)?)?, name, value);
67                }
68            }
69            ["cell", rest @ ..] if !rest.is_empty() => {
70                let conn = rest
71                    .iter()
72                    .map(|v| PointId::new(parse_u64(v)?))
73                    .collect::<Result<Vec<_>, _>>()?;
74                cells.push(conn);
75            }
76            ["vertices", _] | ["cells", _] => {}
77            _ => {
78                return Err(MeshSieveError::MeshIoParse(format!(
79                    "unsupported Fluent compact line: {line}"
80                )));
81            }
82        }
83    }
84    let mut mesh = crate::io::ply::build_mesh(vertices, cells)?;
85    mesh.labels = (!labels.is_empty()).then_some(labels);
86    Ok(mesh)
87}
88
89fn parse_sexpr_subset(
90    text: &str,
91) -> Result<MeshData<MeshSieve, f64, VecStorage<f64>, VecStorage<CellType>>, MeshSieveError> {
92    let mut vertices: Vec<[f64; 3]> = Vec::new();
93    let mut cells: Vec<Vec<PointId>> = Vec::new();
94    let mut labels = LabelSet::new();
95    let lines: Vec<_> = text.lines().collect();
96    let mut i = 0;
97    while i < lines.len() {
98        let line = lines[i].trim();
99        if line.starts_with("(10 (") && !line.contains(" 0 ") {
100            i += 1;
101            while i < lines.len() {
102                let l = lines[i].trim().trim_matches(|c| c == '(' || c == ')');
103                if l.is_empty() {
104                    i += 1;
105                    break;
106                }
107                let vals: Vec<_> = l.split_whitespace().collect();
108                if vals.len() < 2 {
109                    break;
110                }
111                let x = parse_hex_or_float(vals[0])?;
112                let y = parse_hex_or_float(vals[1])?;
113                let z = vals.get(2).map_or(Ok(0.0), |v| parse_hex_or_float(v))?;
114                vertices.push([x, y, z]);
115                if lines[i].contains("))") {
116                    i += 1;
117                    break;
118                }
119                i += 1;
120            }
121            continue;
122        }
123        if line.starts_with("(12 (") && !line.contains(" 0 ") {
124            i += 1;
125            while i < lines.len() {
126                let l = lines[i].trim().trim_matches(|c| c == '(' || c == ')');
127                if l.is_empty() {
128                    i += 1;
129                    break;
130                }
131                let vals: Vec<_> = l.split_whitespace().collect();
132                if vals.len() < 2 {
133                    break;
134                }
135                let conn = vals
136                    .iter()
137                    .map(|v| PointId::new(parse_hex_u64(v)?))
138                    .collect::<Result<Vec<_>, _>>()?;
139                cells.push(conn);
140                if lines[i].contains("))") {
141                    i += 1;
142                    break;
143                }
144                i += 1;
145            }
146            continue;
147        }
148        if line.starts_with("(13 (") && !line.contains(" 0 ") {
149            let zone_id = line
150                .split_whitespace()
151                .nth(1)
152                .and_then(|raw| raw.trim_matches('(').parse::<i32>().ok())
153                .unwrap_or(1);
154            i += 1;
155            while i < lines.len() {
156                let l = lines[i].trim().trim_matches(|c| c == '(' || c == ')');
157                if l.is_empty() {
158                    i += 1;
159                    break;
160                }
161                let vals: Vec<_> = l.split_whitespace().collect();
162                if vals.len() < 4 {
163                    break;
164                }
165                let vertex_tokens = &vals[..vals.len().saturating_sub(2)];
166                for token in vertex_tokens {
167                    let point = PointId::new(parse_hex_u64(token)?)?;
168                    labels.set_label(point, "fluent:bc", zone_id);
169                    labels.set_label(point, &format!("fluent:zone:{zone_id}"), 1);
170                }
171                if lines[i].contains("))") {
172                    i += 1;
173                    break;
174                }
175                i += 1;
176            }
177            continue;
178        }
179        i += 1;
180    }
181    if vertices.is_empty() {
182        return Err(MeshSieveError::MeshIoParse(
183            "no Fluent vertex coordinates found".into(),
184        ));
185    }
186    let mut mesh = crate::io::ply::build_mesh(vertices, cells)?;
187    mesh.labels = (!labels.is_empty()).then_some(labels);
188    Ok(mesh)
189}
190
191fn parse_f64(token: &str) -> Result<f64, MeshSieveError> {
192    token
193        .parse::<f64>()
194        .map_err(|_| MeshSieveError::MeshIoParse(format!("invalid float: {token}")))
195}
196fn parse_u64(token: &str) -> Result<u64, MeshSieveError> {
197    token
198        .parse::<u64>()
199        .map_err(|_| MeshSieveError::MeshIoParse(format!("invalid integer: {token}")))
200}
201fn parse_hex_u64(token: &str) -> Result<u64, MeshSieveError> {
202    u64::from_str_radix(token, 16)
203        .or_else(|_| token.parse::<u64>())
204        .map_err(|_| MeshSieveError::MeshIoParse(format!("invalid Fluent integer: {token}")))
205}
206fn parse_hex_or_float(token: &str) -> Result<f64, MeshSieveError> {
207    token
208        .parse::<f64>()
209        .or_else(|_| u64::from_str_radix(token, 16).map(|v| v as f64))
210        .map_err(|_| MeshSieveError::MeshIoParse(format!("invalid Fluent number: {token}")))
211}