1#![allow(clippy::manual_strip, clippy::should_implement_trait)]
2#![allow(dead_code)]
11
12use std::collections::HashMap;
13
14pub struct FoamHeader {
20 pub foam_file_version: String,
22 pub format: String,
24 pub class_name: String,
26 pub object_name: String,
28}
29
30impl FoamHeader {
31 #[allow(clippy::inherent_to_string)]
33 pub fn to_string(&self) -> String {
34 format!(
35 "FoamFile\n{{\n version {};\n format {};\n class {};\n object {};\n}}\n",
36 self.foam_file_version, self.format, self.class_name, self.object_name,
37 )
38 }
39
40 pub fn from_str(s: &str) -> Result<Self, String> {
42 let start = s
43 .find("FoamFile")
44 .ok_or_else(|| "No FoamFile block found".to_string())?;
45 let brace_open = s[start..]
46 .find('{')
47 .ok_or_else(|| "No opening brace for FoamFile".to_string())?
48 + start;
49 let brace_close = s[brace_open..]
50 .find('}')
51 .ok_or_else(|| "No closing brace for FoamFile".to_string())?
52 + brace_open;
53
54 let block = &s[brace_open + 1..brace_close];
55
56 let mut kv: HashMap<&str, &str> = HashMap::new();
57 for line in block.lines() {
58 let line = line.trim().trim_end_matches(';');
59 let mut parts = line.splitn(2, char::is_whitespace);
60 if let (Some(k), Some(v)) = (parts.next(), parts.next()) {
61 kv.insert(k.trim(), v.trim());
62 }
63 }
64
65 Ok(FoamHeader {
66 foam_file_version: kv.get("version").unwrap_or(&"2.0").to_string(),
67 format: kv.get("format").unwrap_or(&"ascii").to_string(),
68 class_name: kv.get("class").unwrap_or(&"").to_string(),
69 object_name: kv.get("object").unwrap_or(&"").to_string(),
70 })
71 }
72}
73
74pub type FoamPoint = [f64; 3];
80
81pub struct FoamFace {
83 pub node_indices: Vec<usize>,
85}
86
87pub struct FoamBoundaryPatch {
89 pub name: String,
91 pub patch_type: String,
93 pub n_faces: usize,
95 pub start_face: usize,
97}
98
99pub struct FoamPolyMesh {
105 pub points: Vec<FoamPoint>,
107 pub faces: Vec<FoamFace>,
109 pub owner: Vec<usize>,
111 pub neighbour: Vec<usize>,
113 pub boundary: Vec<FoamBoundaryPatch>,
115}
116
117impl FoamPolyMesh {
118 pub fn n_cells(&self) -> usize {
120 self.owner.iter().copied().max().map(|m| m + 1).unwrap_or(0)
121 }
122
123 pub fn n_internal_faces(&self) -> usize {
125 self.neighbour.len()
126 }
127
128 pub fn n_boundary_faces(&self) -> usize {
130 self.faces.len().saturating_sub(self.n_internal_faces())
131 }
132
133 pub fn face_centers(&self) -> Vec<[f64; 3]> {
135 self.faces
136 .iter()
137 .map(|f| {
138 let n = f.node_indices.len() as f64;
139 let mut c = [0.0_f64; 3];
140 for &idx in &f.node_indices {
141 let p = self.points[idx];
142 c[0] += p[0];
143 c[1] += p[1];
144 c[2] += p[2];
145 }
146 [c[0] / n, c[1] / n, c[2] / n]
147 })
148 .collect()
149 }
150
151 pub fn face_areas(&self) -> Vec<f64> {
153 self.faces
154 .iter()
155 .map(|f| face_area(&f.node_indices, &self.points))
156 .collect()
157 }
158
159 pub fn cell_centers(&self) -> Vec<[f64; 3]> {
161 let fc = self.face_centers();
162 let nc = self.n_cells();
163 let mut sums = vec![[0.0_f64; 3]; nc];
164 let mut counts = vec![0usize; nc];
165
166 for (fi, &ow) in self.owner.iter().enumerate() {
167 if ow < nc {
168 let c = fc[fi];
169 sums[ow][0] += c[0];
170 sums[ow][1] += c[1];
171 sums[ow][2] += c[2];
172 counts[ow] += 1;
173 }
174 }
175 for (fi, &nb) in self.neighbour.iter().enumerate() {
176 if nb < nc {
177 let c = fc[fi];
178 sums[nb][0] += c[0];
179 sums[nb][1] += c[1];
180 sums[nb][2] += c[2];
181 counts[nb] += 1;
182 }
183 }
184
185 sums.iter()
186 .zip(counts.iter())
187 .map(|(s, &cnt)| {
188 if cnt == 0 {
189 [0.0; 3]
190 } else {
191 let n = cnt as f64;
192 [s[0] / n, s[1] / n, s[2] / n]
193 }
194 })
195 .collect()
196 }
197
198 pub fn cell_volumes(&self) -> Vec<f64> {
200 let fc = self.face_centers();
201 let fa = self.face_areas();
202 let cc = self.cell_centers();
203 let nc = self.n_cells();
204 let mut vols = vec![0.0_f64; nc];
205
206 let contribute = |vols: &mut Vec<f64>, cell: usize, fi: usize| {
207 let c = cc[cell];
208 let f = fc[fi];
209 let d = ((f[0] - c[0]).powi(2) + (f[1] - c[1]).powi(2) + (f[2] - c[2]).powi(2)).sqrt();
210 vols[cell] += fa[fi] * d / 3.0;
211 };
212
213 for (fi, &ow) in self.owner.iter().enumerate() {
214 if ow < nc {
215 contribute(&mut vols, ow, fi);
216 }
217 }
218 for (fi, &nb) in self.neighbour.iter().enumerate() {
219 if nb < nc {
220 contribute(&mut vols, nb, fi);
221 }
222 }
223 vols
224 }
225}
226
227fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
232 [
233 a[1] * b[2] - a[2] * b[1],
234 a[2] * b[0] - a[0] * b[2],
235 a[0] * b[1] - a[1] * b[0],
236 ]
237}
238
239fn norm(v: [f64; 3]) -> f64 {
240 (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
241}
242
243fn face_area(indices: &[usize], points: &[FoamPoint]) -> f64 {
244 if indices.len() < 3 {
245 return 0.0;
246 }
247 let p0 = points[indices[0]];
249 let mut area = 0.0_f64;
250 for i in 1..indices.len() - 1 {
251 let p1 = points[indices[i]];
252 let p2 = points[indices[i + 1]];
253 let ab = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
254 let ac = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
255 area += norm(cross(ab, ac)) * 0.5;
256 }
257 area
258}
259
260fn extract_data_block(content: &str) -> Option<&str> {
267 let search_from = if let Some(pos) = content.find("FoamFile") {
269 let brace_open = content[pos..].find('{')? + pos;
271 content[brace_open..].find('}')? + brace_open + 1
272 } else {
273 0
274 };
275 let rest = &content[search_from..];
276 let start = rest.find('(')?;
277 let end = rest.rfind(')')?;
278 if end > start {
279 Some(&rest[start..=end])
280 } else {
281 None
282 }
283}
284
285pub fn parse_foam_list_usize(s: &str) -> Result<Vec<usize>, String> {
297 let block =
298 extract_data_block(s).ok_or_else(|| "Could not find list block '(' … ')'".to_string())?;
299 let inner = &block[1..block.len() - 1]; let mut result = Vec::new();
301 for token in inner.split_whitespace() {
302 let v: usize = token
303 .parse()
304 .map_err(|_| format!("Cannot parse usize: {:?}", token))?;
305 result.push(v);
306 }
307 Ok(result)
308}
309
310pub fn parse_foam_list_f64(s: &str) -> Result<Vec<f64>, String> {
312 let block =
313 extract_data_block(s).ok_or_else(|| "Could not find list block '(' … ')'".to_string())?;
314 let inner = &block[1..block.len() - 1];
315 let mut result = Vec::new();
316 for token in inner.split_whitespace() {
317 let v: f64 = token
318 .parse()
319 .map_err(|_| format!("Cannot parse f64: {:?}", token))?;
320 result.push(v);
321 }
322 Ok(result)
323}
324
325pub fn parse_foam_points(content: &str) -> Result<Vec<FoamPoint>, String> {
334 let block =
335 extract_data_block(content).ok_or_else(|| "Could not find points block".to_string())?;
336
337 let mut points = Vec::new();
338 let inner = &block[1..block.len() - 1];
340 let chars = inner.char_indices().peekable();
341 for (i, ch) in chars {
342 if ch == '(' {
343 let sub = &inner[i..];
345 let end = sub
346 .find(')')
347 .ok_or_else(|| "Unmatched '(' in points".to_string())?;
348 let triplet = &sub[1..end];
349 let vals: Vec<f64> = triplet
350 .split_whitespace()
351 .map(|t| t.parse::<f64>().map_err(|_| format!("Bad f64: {:?}", t)))
352 .collect::<Result<_, _>>()?;
353 if vals.len() < 3 {
354 return Err(format!("Point needs 3 coords, got {:?}", vals));
355 }
356 points.push([vals[0], vals[1], vals[2]]);
357 }
358 }
359 Ok(points)
360}
361
362pub fn parse_foam_faces(content: &str) -> Result<Vec<FoamFace>, String> {
366 let block =
367 extract_data_block(content).ok_or_else(|| "Could not find faces block".to_string())?;
368 let inner = &block[1..block.len() - 1];
369
370 let mut faces = Vec::new();
371 let mut i = 0;
372 let bytes = inner.as_bytes();
373 while i < bytes.len() {
374 if bytes[i].is_ascii_whitespace() {
376 i += 1;
377 continue;
378 }
379 if bytes[i].is_ascii_digit() {
381 let start = i;
382 while i < bytes.len() && bytes[i].is_ascii_digit() {
383 i += 1;
384 }
385 let _count: usize = inner[start..i]
386 .parse()
387 .map_err(|_| "Bad face count".to_string())?;
388 while i < bytes.len() && bytes[i] != b'(' {
390 i += 1;
391 }
392 if i >= bytes.len() {
393 return Err("Missing '(' after face count".to_string());
394 }
395 i += 1; let j = i;
398 while i < bytes.len() && bytes[i] != b')' {
399 i += 1;
400 }
401 let indices: Vec<usize> = inner[j..i]
402 .split_whitespace()
403 .map(|t| {
404 t.parse::<usize>()
405 .map_err(|_| format!("Bad index: {:?}", t))
406 })
407 .collect::<Result<_, _>>()?;
408 faces.push(FoamFace {
409 node_indices: indices,
410 });
411 i += 1; } else {
413 i += 1;
414 }
415 }
416 Ok(faces)
417}
418
419pub fn parse_foam_boundary(content: &str) -> Result<Vec<FoamBoundaryPatch>, String> {
421 let search_from = if let Some(pos) = content.find("FoamFile") {
423 let brace_open = content[pos..]
424 .find('{')
425 .ok_or_else(|| "No '{' in FoamFile".to_string())?
426 + pos;
427 content[brace_open..]
428 .find('}')
429 .ok_or_else(|| "No '}' in FoamFile".to_string())?
430 + brace_open
431 + 1
432 } else {
433 0
434 };
435 let rest = &content[search_from..];
436
437 let outer_open = rest
440 .find('(')
441 .unwrap_or_else(|| rest.find('{').unwrap_or(0));
442 let body = &rest[outer_open..];
443
444 let mut patches = Vec::new();
445 let mut chars = body.char_indices().peekable();
447 while let Some((i, ch)) = chars.next() {
448 if ch == '{' || ch == '(' || ch == ')' || ch == '}' {
449 continue;
450 }
451 if ch.is_alphabetic() || ch == '_' {
452 let sub = &body[i..];
454 let name_end = sub.find(|c: char| c.is_whitespace()).unwrap_or(sub.len());
455 let name = sub[..name_end].to_string();
456 if name == "FoamFile" {
457 continue;
458 }
459 let brace_start = match sub.find('{') {
461 Some(p) => p,
462 None => continue,
463 };
464 let brace_body_start = brace_start + 1;
465 let brace_end = match sub[brace_body_start..].find('}') {
466 Some(p) => p + brace_body_start,
467 None => continue,
468 };
469 let patch_body = &sub[brace_body_start..brace_end];
470
471 let mut patch_type = String::new();
472 let mut n_faces = 0usize;
473 let mut start_face = 0usize;
474
475 for line in patch_body.lines() {
476 let line = line.trim().trim_end_matches(';');
477 let mut parts = line.splitn(2, char::is_whitespace);
478 match (parts.next(), parts.next()) {
479 (Some("type"), Some(v)) => patch_type = v.trim().to_string(),
480 (Some("nFaces"), Some(v)) => n_faces = v.trim().parse().unwrap_or(0),
481 (Some("startFace"), Some(v)) => start_face = v.trim().parse().unwrap_or(0),
482 _ => {}
483 }
484 }
485
486 if !name.is_empty() && !patch_type.is_empty() {
487 patches.push(FoamBoundaryPatch {
488 name,
489 patch_type,
490 n_faces,
491 start_face,
492 });
493 }
494
495 let consumed = i + brace_end + 1;
497 while chars.peek().map(|&(j, _)| j < consumed).unwrap_or(false) {
499 chars.next();
500 }
501 }
502 }
503 Ok(patches)
504}
505
506pub fn write_foam_header(class: &str, object: &str) -> String {
512 FoamHeader {
513 foam_file_version: "2.0".to_string(),
514 format: "ascii".to_string(),
515 class_name: class.to_string(),
516 object_name: object.to_string(),
517 }
518 .to_string()
519}
520
521pub fn write_foam_points(points: &[FoamPoint]) -> String {
523 let mut s = write_foam_header("vectorField", "points");
524 s.push('\n');
525 s.push_str(&format!("{}\n(\n", points.len()));
526 for p in points {
527 s.push_str(&format!(" ({} {} {})\n", p[0], p[1], p[2]));
528 }
529 s.push_str(")\n");
530 s
531}
532
533pub fn write_foam_faces(faces: &[FoamFace]) -> String {
535 let mut s = write_foam_header("faceList", "faces");
536 s.push('\n');
537 s.push_str(&format!("{}\n(\n", faces.len()));
538 for f in faces {
539 let indices: Vec<String> = f.node_indices.iter().map(|i| i.to_string()).collect();
540 s.push_str(&format!(
541 " {}({})\n",
542 f.node_indices.len(),
543 indices.join(" ")
544 ));
545 }
546 s.push_str(")\n");
547 s
548}
549
550pub fn write_foam_owner_neighbour(owner: &[usize], neighbour: &[usize]) -> (String, String) {
553 let mut o = write_foam_header("labelList", "owner");
554 o.push('\n');
555 o.push_str(&format!("{}\n(\n", owner.len()));
556 for &v in owner {
557 o.push_str(&format!(" {}\n", v));
558 }
559 o.push_str(")\n");
560
561 let mut n = write_foam_header("labelList", "neighbour");
562 n.push('\n');
563 n.push_str(&format!("{}\n(\n", neighbour.len()));
564 for &v in neighbour {
565 n.push_str(&format!(" {}\n", v));
566 }
567 n.push_str(")\n");
568
569 (o, n)
570}
571
572pub fn write_scalar_field(name: &str, values: &[f64], boundary: &[FoamBoundaryPatch]) -> String {
574 let mut s = write_foam_header("volScalarField", name);
575 s.push('\n');
576 s.push_str("dimensions [0 0 0 0 0 0 0];\n\n");
577 s.push_str(&format!(
578 "internalField nonuniform List<scalar>\n{}\n(\n",
579 values.len()
580 ));
581 for &v in values {
582 s.push_str(&format!(" {}\n", v));
583 }
584 s.push_str(");\n\nboundaryField\n{\n");
585 for patch in boundary {
586 s.push_str(&format!(
587 " {}\n {{\n type zeroGradient;\n }}\n",
588 patch.name
589 ));
590 }
591 s.push_str("}\n");
592 s
593}
594
595pub struct FoamScalarField {
601 pub name: String,
603 pub internal_field: Vec<f64>,
605 pub boundary_values: Vec<Vec<f64>>,
607}
608
609impl FoamScalarField {
610 pub fn uniform(name: &str, n_cells: usize, value: f64) -> Self {
612 FoamScalarField {
613 name: name.to_string(),
614 internal_field: vec![value; n_cells],
615 boundary_values: Vec::new(),
616 }
617 }
618
619 pub fn to_foam_string(&self) -> String {
621 let mut s = write_foam_header("volScalarField", &self.name);
622 s.push('\n');
623 s.push_str("dimensions [0 0 0 0 0 0 0];\n\n");
624 if self.internal_field.is_empty() {
625 s.push_str("internalField uniform 0;\n");
626 } else {
627 s.push_str(&format!(
628 "internalField nonuniform List<scalar>\n{}\n(\n",
629 self.internal_field.len()
630 ));
631 for &v in &self.internal_field {
632 s.push_str(&format!(" {}\n", v));
633 }
634 s.push_str(");\n");
635 }
636 s.push_str("\nboundaryField\n{\n");
637 for (i, bv) in self.boundary_values.iter().enumerate() {
638 let patch_name = format!("patch{}", i);
639 if bv.is_empty() {
640 s.push_str(&format!(
641 " {}\n {{\n type zeroGradient;\n }}\n",
642 patch_name
643 ));
644 } else {
645 s.push_str(&format!(
646 " {}\n {{\n type fixedValue;\n value nonuniform List<scalar>\n{}\n(\n",
647 patch_name, bv.len()
648 ));
649 for &v in bv {
650 s.push_str(&format!(" {}\n", v));
651 }
652 s.push_str(" );\n }\n");
653 }
654 }
655 s.push_str("}\n");
656 s
657 }
658}
659
660pub struct SimpleCaseWriter {
666 pub case_dir: String,
668 pub mesh: FoamPolyMesh,
670}
671
672impl SimpleCaseWriter {
673 pub fn new(case_dir: &str, mesh: FoamPolyMesh) -> Self {
675 SimpleCaseWriter {
676 case_dir: case_dir.to_string(),
677 mesh,
678 }
679 }
680
681 pub fn write_mesh_to_string(&self) -> String {
683 let mut out = String::new();
684 out.push_str("=== constant/polyMesh/points ===\n");
685 out.push_str(&write_foam_points(&self.mesh.points));
686 out.push_str("\n=== constant/polyMesh/faces ===\n");
687 out.push_str(&write_foam_faces(&self.mesh.faces));
688 let (owner_str, neighbour_str) =
689 write_foam_owner_neighbour(&self.mesh.owner, &self.mesh.neighbour);
690 out.push_str("\n=== constant/polyMesh/owner ===\n");
691 out.push_str(&owner_str);
692 out.push_str("\n=== constant/polyMesh/neighbour ===\n");
693 out.push_str(&neighbour_str);
694 out
695 }
696
697 pub fn file_list(&self) -> Vec<String> {
699 let base = &self.case_dir;
700 vec![
701 format!("{}/constant/polyMesh/points", base),
702 format!("{}/constant/polyMesh/faces", base),
703 format!("{}/constant/polyMesh/owner", base),
704 format!("{}/constant/polyMesh/neighbour", base),
705 format!("{}/constant/polyMesh/boundary", base),
706 format!("{}/system/controlDict", base),
707 format!("{}/system/fvSchemes", base),
708 format!("{}/system/fvSolution", base),
709 ]
710 }
711}
712
713pub fn parse_foam_vector_field(content: &str) -> Result<Vec<[f64; 3]>, String> {
729 parse_foam_points(content)
730}
731
732pub fn parse_foam_scalar_field_internal(content: &str) -> Result<Vec<f64>, String> {
736 for line in content.lines() {
738 let line = line.trim();
739 if line.starts_with("internalField") {
740 let rest = &line["internalField".len()..].trim_start();
741 if let Some(rest2) = rest.strip_prefix("uniform") {
742 let v: f64 = rest2
743 .trim()
744 .trim_end_matches(';')
745 .parse()
746 .map_err(|_| format!("Cannot parse uniform scalar: {:?}", rest2))?;
747 return Ok(vec![v]);
748 }
749 }
750 }
751 parse_foam_list_f64(content)
753}
754
755pub fn write_foam_boundary(patches: &[FoamBoundaryPatch]) -> String {
761 let mut s = write_foam_header("polyBoundaryMesh", "boundary");
762 s.push('\n');
763 s.push_str(&format!("{}\n(\n", patches.len()));
764 for p in patches {
765 s.push_str(&format!(
766 " {}\n {{\n type {};\n nFaces {};\n startFace {};\n }}\n",
767 p.name, p.patch_type, p.n_faces, p.start_face
768 ));
769 }
770 s.push_str(")\n");
771 s
772}
773
774pub fn write_foam_control_dict(
776 start_time: f64,
777 end_time: f64,
778 delta_t: f64,
779 write_interval: f64,
780) -> String {
781 let mut s = write_foam_header("dictionary", "controlDict");
782 s.push('\n');
783 s.push_str("application icoFoam;\n\n");
784 s.push_str(&format!(
785 "startFrom latestTime;\nstartTime {};\n",
786 start_time
787 ));
788 s.push_str(&format!(
789 "stopAt endTime;\nendTime {};\n",
790 end_time
791 ));
792 s.push_str(&format!("deltaT {};\n", delta_t));
793 s.push_str(&format!(
794 "writeControl timeStep;\nwriteInterval {};\n",
795 write_interval
796 ));
797 s.push_str("purgeWrite 0;\nwriteFormat ascii;\nwritePrecision 6;\n");
798 s
799}
800
801pub fn write_foam_fv_schemes() -> String {
803 let mut s = write_foam_header("dictionary", "fvSchemes");
804 s.push('\n');
805 s.push_str("ddtSchemes\n{\n default Euler;\n}\n\n");
806 s.push_str("gradSchemes\n{\n default Gauss linear;\n}\n\n");
807 s.push_str("divSchemes\n{\n default none;\n div(phi,U) Gauss linearUpwind grad(U);\n}\n\n");
808 s.push_str("laplacianSchemes\n{\n default Gauss linear corrected;\n}\n\n");
809 s.push_str("interpolationSchemes\n{\n default linear;\n}\n\n");
810 s.push_str("snGradSchemes\n{\n default corrected;\n}\n");
811 s
812}
813
814pub fn write_foam_fv_solution() -> String {
816 let mut s = write_foam_header("dictionary", "fvSolution");
817 s.push('\n');
818 s.push_str("solvers\n{\n");
819 s.push_str(" p\n {\n solver PCG;\n preconditioner DIC;\n tolerance 1e-06;\n relTol 0.05;\n }\n\n");
820 s.push_str(" U\n {\n solver smoothSolver;\n smoother symGaussSeidel;\n tolerance 1e-05;\n relTol 0;\n }\n}\n\n");
821 s.push_str("PISO\n{\n nCorrectors 2;\n nNonOrthogonalCorrectors 0;\n}\n");
822 s
823}
824
825#[allow(dead_code)]
831#[derive(Debug, Clone, PartialEq)]
832pub enum FoamBcType {
833 ZeroGradient,
835 FixedValue(f64),
837 FixedVectorValue([f64; 3]),
839 InletOutlet(f64),
841 NoSlip,
843 Slip,
845 Empty,
847}
848
849impl FoamBcType {
850 pub fn to_foam_block(&self) -> String {
852 match self {
853 FoamBcType::ZeroGradient => " type zeroGradient;\n".to_string(),
854 FoamBcType::FixedValue(v) => format!(
855 " type fixedValue;\n value uniform {};\n",
856 v
857 ),
858 FoamBcType::FixedVectorValue(v) => format!(
859 " type fixedValue;\n value uniform ({} {} {});\n",
860 v[0], v[1], v[2]
861 ),
862 FoamBcType::InletOutlet(v) => format!(
863 " type inletOutlet;\n inletValue uniform {};\n value uniform {};\n",
864 v, v
865 ),
866 FoamBcType::NoSlip => " type noSlip;\n".to_string(),
867 FoamBcType::Slip => " type slip;\n".to_string(),
868 FoamBcType::Empty => " type empty;\n".to_string(),
869 }
870 }
871}
872
873pub fn write_scalar_field_with_bc(
875 name: &str,
876 dimensions: &str,
877 internal_value: &str,
878 patches: &[(&str, FoamBcType)],
879) -> String {
880 let mut s = write_foam_header("volScalarField", name);
881 s.push('\n');
882 s.push_str(&format!("dimensions {};\n\n", dimensions));
883 s.push_str(&format!(
884 "internalField {};\n\nboundaryField\n{{\n",
885 internal_value
886 ));
887 for (patch_name, bc) in patches {
888 s.push_str(&format!(
889 " {}\n {{\n{}",
890 patch_name,
891 bc.to_foam_block()
892 ));
893 s.push_str(" }\n");
894 }
895 s.push_str("}\n");
896 s
897}
898
899pub fn write_vector_field(
901 name: &str,
902 dimensions: &str,
903 values: &[[f64; 3]],
904 patches: &[(&str, FoamBcType)],
905) -> String {
906 let mut s = write_foam_header("volVectorField", name);
907 s.push('\n');
908 s.push_str(&format!("dimensions {};\n\n", dimensions));
909 if values.is_empty() {
910 s.push_str("internalField uniform (0 0 0);\n");
911 } else {
912 s.push_str(&format!(
913 "internalField nonuniform List<vector>\n{}\n(\n",
914 values.len()
915 ));
916 for v in values {
917 s.push_str(&format!(" ({} {} {})\n", v[0], v[1], v[2]));
918 }
919 s.push_str(");\n");
920 }
921 s.push_str("\nboundaryField\n{\n");
922 for (patch_name, bc) in patches {
923 s.push_str(&format!(
924 " {}\n {{\n{}",
925 patch_name,
926 bc.to_foam_block()
927 ));
928 s.push_str(" }\n");
929 }
930 s.push_str("}\n");
931 s
932}
933
934pub fn decompose_scalar_field(values: &[f64], n_procs: usize) -> Vec<Vec<f64>> {
943 if n_procs == 0 || values.is_empty() {
944 return vec![values.to_vec()];
945 }
946 let n = values.len();
947 let chunk = n.div_ceil(n_procs);
948 (0..n_procs)
949 .map(|p| {
950 let start = (p * chunk).min(n);
951 let end = ((p + 1) * chunk).min(n);
952 values[start..end].to_vec()
953 })
954 .collect()
955}
956
957pub fn reconstruct_scalar_field(parts: &[Vec<f64>]) -> Vec<f64> {
959 parts.iter().flat_map(|v| v.iter().copied()).collect()
960}
961
962pub fn decompose_cell_indices(n_cells: usize, n_procs: usize) -> Vec<Vec<usize>> {
967 if n_procs == 0 {
968 return vec![(0..n_cells).collect()];
969 }
970 let chunk = n_cells.div_ceil(n_procs);
971 (0..n_procs)
972 .map(|p| {
973 let start = (p * chunk).min(n_cells);
974 let end = ((p + 1) * chunk).min(n_cells);
975 (start..end).collect()
976 })
977 .collect()
978}
979
980pub struct FoamCase {
986 pub case_dir: String,
988 pub mesh: FoamPolyMesh,
990 pub scalar_fields: Vec<FoamScalarField>,
992 pub start_time: f64,
994 pub end_time: f64,
996 pub delta_t: f64,
998 pub write_interval: f64,
1000}
1001
1002impl FoamCase {
1003 pub fn new(case_dir: &str, mesh: FoamPolyMesh) -> Self {
1005 Self {
1006 case_dir: case_dir.to_string(),
1007 mesh,
1008 scalar_fields: Vec::new(),
1009 start_time: 0.0,
1010 end_time: 1.0,
1011 delta_t: 0.001,
1012 write_interval: 0.1,
1013 }
1014 }
1015
1016 pub fn add_scalar_field(&mut self, field: FoamScalarField) {
1018 self.scalar_fields.push(field);
1019 }
1020
1021 pub fn file_list(&self) -> Vec<String> {
1023 let base = &self.case_dir;
1024 let mut files = vec![
1025 format!("{}/constant/polyMesh/points", base),
1026 format!("{}/constant/polyMesh/faces", base),
1027 format!("{}/constant/polyMesh/owner", base),
1028 format!("{}/constant/polyMesh/neighbour", base),
1029 format!("{}/constant/polyMesh/boundary", base),
1030 format!("{}/system/controlDict", base),
1031 format!("{}/system/fvSchemes", base),
1032 format!("{}/system/fvSolution", base),
1033 ];
1034 for sf in &self.scalar_fields {
1035 files.push(format!("{}/0/{}", base, sf.name));
1036 }
1037 files
1038 }
1039
1040 pub fn control_dict_content(&self) -> String {
1042 write_foam_control_dict(
1043 self.start_time,
1044 self.end_time,
1045 self.delta_t,
1046 self.write_interval,
1047 )
1048 }
1049
1050 pub fn boundary_content(&self) -> String {
1052 write_foam_boundary(&self.mesh.boundary)
1053 }
1054}
1055
1056#[cfg(test)]
1061mod tests {
1062 use super::*;
1063
1064 fn simple_mesh() -> FoamPolyMesh {
1065 let points = vec![
1068 [0.0, 0.0, 0.0],
1069 [1.0, 0.0, 0.0],
1070 [0.5, 1.0, 0.0],
1071 [0.5, 0.5, 1.0],
1072 [1.5, 0.5, 1.0],
1073 ];
1074 let faces = vec![
1075 FoamFace {
1076 node_indices: vec![0, 1, 3],
1077 }, FoamFace {
1079 node_indices: vec![0, 2, 3],
1080 },
1081 FoamFace {
1082 node_indices: vec![1, 2, 3],
1083 },
1084 FoamFace {
1085 node_indices: vec![0, 1, 2],
1086 },
1087 FoamFace {
1088 node_indices: vec![1, 4, 3],
1089 },
1090 FoamFace {
1091 node_indices: vec![1, 2, 4],
1092 },
1093 FoamFace {
1094 node_indices: vec![3, 4, 2],
1095 },
1096 ];
1097 let owner = vec![0, 0, 0, 0, 1, 1, 1];
1098 let neighbour = vec![1]; let boundary = vec![FoamBoundaryPatch {
1100 name: "wall".to_string(),
1101 patch_type: "wall".to_string(),
1102 n_faces: 6,
1103 start_face: 1,
1104 }];
1105 FoamPolyMesh {
1106 points,
1107 faces,
1108 owner,
1109 neighbour,
1110 boundary,
1111 }
1112 }
1113
1114 #[test]
1115 fn test_foam_header_to_string_contains_foamfile() {
1116 let h = FoamHeader {
1117 foam_file_version: "2.0".to_string(),
1118 format: "ascii".to_string(),
1119 class_name: "polyBoundaryMesh".to_string(),
1120 object_name: "boundary".to_string(),
1121 };
1122 let s = h.to_string();
1123 assert!(s.contains("FoamFile"));
1124 assert!(s.contains("polyBoundaryMesh"));
1125 }
1126
1127 #[test]
1128 fn test_parse_foam_list_usize() {
1129 let input = "3\n(\n1\n2\n3\n)";
1130 let result = parse_foam_list_usize(input).expect("parse failed");
1131 assert_eq!(result, vec![1, 2, 3]);
1132 }
1133
1134 #[test]
1135 fn test_parse_foam_points() {
1136 let input = "1\n(\n (3.0 2.0 1.0)\n)";
1137 let pts = parse_foam_points(input).expect("parse failed");
1138 assert_eq!(pts.len(), 1);
1139 assert!((pts[0][0] - 3.0).abs() < 1e-12);
1140 assert!((pts[0][1] - 2.0).abs() < 1e-12);
1141 assert!((pts[0][2] - 1.0).abs() < 1e-12);
1142 }
1143
1144 #[test]
1145 fn test_n_cells() {
1146 let mesh = simple_mesh();
1147 assert_eq!(mesh.n_cells(), 2);
1148 }
1149
1150 #[test]
1151 fn test_write_foam_header_contains_class() {
1152 let s = write_foam_header("volScalarField", "p");
1153 assert!(s.contains("FoamFile"));
1154 assert!(s.contains("volScalarField"));
1155 assert!(s.contains("p"));
1156 }
1157
1158 #[test]
1159 fn test_foam_scalar_field_uniform() {
1160 let f = FoamScalarField::uniform("T", 4, 300.0);
1161 assert_eq!(f.internal_field.len(), 4);
1162 assert!(f.internal_field.iter().all(|&v| (v - 300.0).abs() < 1e-12));
1163 }
1164
1165 #[test]
1166 fn test_face_centers_centroid() {
1167 let mesh = simple_mesh();
1168 let fc = mesh.face_centers();
1169 assert!((fc[0][0] - (0.0 + 1.0 + 0.5) / 3.0).abs() < 1e-9);
1172 assert!((fc[0][1] - (0.0 + 0.0 + 0.5) / 3.0).abs() < 1e-9);
1173 assert!((fc[0][2] - (0.0 + 0.0 + 1.0) / 3.0).abs() < 1e-9);
1174 }
1175
1176 #[test]
1177 fn test_simple_case_writer_file_list_nonempty() {
1178 let writer = SimpleCaseWriter::new("/tmp/test_case", simple_mesh());
1179 let files = writer.file_list();
1180 assert!(!files.is_empty());
1181 assert!(files.iter().any(|f| f.contains("points")));
1182 }
1183
1184 #[test]
1189 fn test_write_foam_boundary_contains_patch_name() {
1190 let mesh = simple_mesh();
1191 let s = write_foam_boundary(&mesh.boundary);
1192 assert!(
1193 s.contains("wall"),
1194 "Boundary output should contain patch name 'wall'"
1195 );
1196 assert!(
1197 s.contains("FoamFile"),
1198 "Boundary output should contain FoamFile header"
1199 );
1200 }
1201
1202 #[test]
1203 fn test_write_foam_control_dict() {
1204 let s = write_foam_control_dict(0.0, 1.0, 0.001, 0.1);
1205 assert!(s.contains("endTime"), "controlDict should contain endTime");
1206 assert!(s.contains("deltaT"), "controlDict should contain deltaT");
1207 }
1208
1209 #[test]
1210 fn test_write_foam_fv_schemes() {
1211 let s = write_foam_fv_schemes();
1212 assert!(
1213 s.contains("ddtSchemes"),
1214 "fvSchemes should contain ddtSchemes"
1215 );
1216 assert!(
1217 s.contains("gradSchemes"),
1218 "fvSchemes should contain gradSchemes"
1219 );
1220 }
1221
1222 #[test]
1223 fn test_write_foam_fv_solution() {
1224 let s = write_foam_fv_solution();
1225 assert!(s.contains("solvers"), "fvSolution should contain solvers");
1226 assert!(s.contains("PISO"), "fvSolution should contain PISO block");
1227 }
1228
1229 #[test]
1234 fn test_foam_bc_zero_gradient() {
1235 let bc = FoamBcType::ZeroGradient;
1236 let s = bc.to_foam_block();
1237 assert!(s.contains("zeroGradient"));
1238 }
1239
1240 #[test]
1241 fn test_foam_bc_fixed_value() {
1242 let bc = FoamBcType::FixedValue(42.0);
1243 let s = bc.to_foam_block();
1244 assert!(s.contains("fixedValue"));
1245 assert!(s.contains("42"));
1246 }
1247
1248 #[test]
1249 fn test_foam_bc_no_slip() {
1250 let bc = FoamBcType::NoSlip;
1251 let s = bc.to_foam_block();
1252 assert!(s.contains("noSlip"));
1253 }
1254
1255 #[test]
1256 fn test_write_scalar_field_with_bc() {
1257 let patches = vec![
1258 ("inlet", FoamBcType::FixedValue(1.0)),
1259 ("outlet", FoamBcType::ZeroGradient),
1260 ];
1261 let s = write_scalar_field_with_bc("p", "[0 2 -2 0 0 0 0]", "uniform 0", &patches);
1262 assert!(s.contains("inlet"));
1263 assert!(s.contains("outlet"));
1264 assert!(s.contains("fixedValue"));
1265 assert!(s.contains("zeroGradient"));
1266 }
1267
1268 #[test]
1269 fn test_write_vector_field() {
1270 let vals = vec![[1.0, 0.0, 0.0], [0.5, 0.5, 0.0]];
1271 let patches: Vec<(&str, FoamBcType)> = vec![("walls", FoamBcType::NoSlip)];
1272 let s = write_vector_field("U", "[0 1 -1 0 0 0 0]", &vals, &patches);
1273 assert!(s.contains("volVectorField"));
1274 assert!(s.contains("noSlip"));
1275 }
1276
1277 #[test]
1282 fn test_decompose_scalar_field_total_count() {
1283 let values: Vec<f64> = (0..100).map(|i| i as f64).collect();
1284 let parts = decompose_scalar_field(&values, 4);
1285 let total: usize = parts.iter().map(|p| p.len()).sum();
1286 assert_eq!(total, 100, "Decomposed parts should cover all cells");
1287 }
1288
1289 #[test]
1290 fn test_reconstruct_scalar_field() {
1291 let original: Vec<f64> = (0..50).map(|i| i as f64).collect();
1292 let parts = decompose_scalar_field(&original, 5);
1293 let reconstructed = reconstruct_scalar_field(&parts);
1294 assert_eq!(reconstructed, original);
1295 }
1296
1297 #[test]
1298 fn test_decompose_cell_indices() {
1299 let n_cells = 100;
1300 let n_procs = 4;
1301 let parts = decompose_cell_indices(n_cells, n_procs);
1302 assert_eq!(parts.len(), n_procs);
1303 let total: usize = parts.iter().map(|p| p.len()).sum();
1304 assert_eq!(total, n_cells);
1305 let mut all: Vec<usize> = parts.into_iter().flatten().collect();
1307 all.sort_unstable();
1308 let expected: Vec<usize> = (0..n_cells).collect();
1309 assert_eq!(all, expected);
1310 }
1311
1312 #[test]
1313 fn test_decompose_single_proc() {
1314 let values = vec![1.0, 2.0, 3.0];
1315 let parts = decompose_scalar_field(&values, 1);
1316 assert_eq!(parts.len(), 1);
1317 assert_eq!(parts[0], values);
1318 }
1319
1320 #[test]
1325 fn test_foam_case_file_list() {
1326 let mesh = simple_mesh();
1327 let case = FoamCase::new("/tmp/mycase", mesh);
1328 let files = case.file_list();
1329 assert!(files.iter().any(|f| f.contains("controlDict")));
1330 assert!(files.iter().any(|f| f.contains("fvSchemes")));
1331 }
1332
1333 #[test]
1334 fn test_foam_case_with_scalar_field() {
1335 let mesh = simple_mesh();
1336 let mut case = FoamCase::new("/tmp/mycase2", mesh);
1337 case.add_scalar_field(FoamScalarField::uniform("p", 10, 0.0));
1338 let files = case.file_list();
1339 assert!(files.iter().any(|f| f.ends_with("/0/p")));
1340 }
1341
1342 #[test]
1343 fn test_foam_case_control_dict_content() {
1344 let mesh = simple_mesh();
1345 let mut case = FoamCase::new("/tmp/mycase3", mesh);
1346 case.end_time = 2.5;
1347 let s = case.control_dict_content();
1348 assert!(s.contains("2.5"), "controlDict should contain the end time");
1349 }
1350
1351 #[test]
1352 fn test_foam_case_boundary_content() {
1353 let mesh = simple_mesh();
1354 let case = FoamCase::new("/tmp/mycase4", mesh);
1355 let s = case.boundary_content();
1356 assert!(
1357 s.contains("wall"),
1358 "boundary content should include patch name"
1359 );
1360 }
1361
1362 #[test]
1367 fn test_parse_foam_vector_field() {
1368 let input = "1\n(\n (1.0 2.0 3.0)\n)";
1369 let vecs = parse_foam_vector_field(input).expect("parse failed");
1370 assert_eq!(vecs.len(), 1);
1371 assert!((vecs[0][0] - 1.0).abs() < 1e-12);
1372 }
1373
1374 #[test]
1375 fn test_parse_foam_scalar_field_internal_uniform() {
1376 let content = "internalField uniform 101325;\n";
1377 let vals = parse_foam_scalar_field_internal(content).expect("parse failed");
1378 assert_eq!(vals.len(), 1);
1379 assert!((vals[0] - 101325.0).abs() < 1e-8);
1380 }
1381
1382 #[test]
1383 fn test_write_foam_faces_round_trip() {
1384 let faces = vec![
1385 FoamFace {
1386 node_indices: vec![0, 1, 2],
1387 },
1388 FoamFace {
1389 node_indices: vec![1, 3, 2],
1390 },
1391 ];
1392 let s = write_foam_faces(&faces);
1393 let parsed = parse_foam_faces(&s).expect("round-trip parse failed");
1395 assert_eq!(parsed.len(), 2);
1396 assert_eq!(parsed[0].node_indices, vec![0, 1, 2]);
1397 assert_eq!(parsed[1].node_indices, vec![1, 3, 2]);
1398 }
1399}