1#![allow(clippy::manual_div_ceil)]
6#![allow(clippy::items_after_test_module)]
7use super::types::{
8 ElementQuality, ExodusMesh, ExodusResult, ExodusSideSet, ExodusTimeStep, ExodusVariable,
9 FieldStats, MeshPartition, MeshValidationReport,
10};
11
12pub fn write_text(mesh: &ExodusMesh) -> String {
34 let mut s = String::new();
35 s.push_str("EXODUS_MESH\n");
36 s.push_str(&format!("TITLE {}\n", mesh.title));
37 s.push_str(&format!("NODES {}\n", mesh.coordinates.len()));
38 for coord in &mesh.coordinates {
39 s.push_str(&format!("{} {} {}\n", coord[0], coord[1], coord[2]));
40 }
41 s.push_str(&format!("BLOCKS {}\n", mesh.blocks.len()));
42 for block in &mesh.blocks {
43 let n_elems = block
44 .connectivity
45 .len()
46 .checked_div(block.n_nodes_per_elem)
47 .unwrap_or(0);
48 s.push_str(&format!(
49 "BLOCK {} {} {} {} {}\n",
50 block.id, block.name, block.element_type, block.n_nodes_per_elem, n_elems
51 ));
52 if !block.connectivity.is_empty() {
53 let strs: Vec<String> = block.connectivity.iter().map(|v| v.to_string()).collect();
54 s.push_str(&strs.join(" "));
55 s.push('\n');
56 }
57 }
58 s.push_str(&format!("NODE_SETS {}\n", mesh.node_sets.len()));
59 for ns in &mesh.node_sets {
60 s.push_str(&format!(
61 "NODE_SET {} {} {}\n",
62 ns.id,
63 ns.name,
64 ns.node_ids.len()
65 ));
66 if !ns.node_ids.is_empty() {
67 let strs: Vec<String> = ns.node_ids.iter().map(|v| v.to_string()).collect();
68 s.push_str(&strs.join(" "));
69 s.push('\n');
70 }
71 }
72 s.push_str(&format!("SIDE_SETS {}\n", mesh.side_sets.len()));
73 for ss in &mesh.side_sets {
74 s.push_str(&format!(
75 "SIDE_SET {} {} {}\n",
76 ss.id,
77 ss.name,
78 ss.element_ids.len()
79 ));
80 if !ss.element_ids.is_empty() {
81 let mut pairs = Vec::new();
82 for (eid, sid) in ss.element_ids.iter().zip(ss.side_ids.iter()) {
83 pairs.push(format!("{} {}", eid, sid));
84 }
85 s.push_str(&pairs.join(" "));
86 s.push('\n');
87 }
88 }
89 s.push_str("END\n");
90 s
91}
92pub fn parse_text(s: &str) -> Result<ExodusMesh, String> {
94 let mut lines = s.lines().peekable();
95 match lines.next() {
96 Some("EXODUS_MESH") => {}
97 other => return Err(format!("Expected EXODUS_MESH header, got: {:?}", other)),
98 }
99 let title = match lines.next() {
100 Some(line) if line.starts_with("TITLE ") => line[6..].trim().to_string(),
101 other => return Err(format!("Expected TITLE, got: {:?}", other)),
102 };
103 let mut mesh = ExodusMesh::new(&title);
104 let n_nodes: usize = match lines.next() {
105 Some(line) if line.starts_with("NODES ") => line[6..]
106 .trim()
107 .parse()
108 .map_err(|e| format!("Bad node count: {}", e))?,
109 other => return Err(format!("Expected NODES, got: {:?}", other)),
110 };
111 for _ in 0..n_nodes {
112 let line = lines.next().ok_or("Unexpected EOF reading nodes")?;
113 let parts: Vec<&str> = line.split_whitespace().collect();
114 if parts.len() < 3 {
115 return Err(format!("Expected 3 coordinates, got: {}", line));
116 }
117 let x: f64 = parts[0].parse().map_err(|e| format!("Bad x: {}", e))?;
118 let y: f64 = parts[1].parse().map_err(|e| format!("Bad y: {}", e))?;
119 let z: f64 = parts[2].parse().map_err(|e| format!("Bad z: {}", e))?;
120 mesh.add_node(x, y, z);
121 }
122 let n_blocks: usize = match lines.next() {
123 Some(line) if line.starts_with("BLOCKS ") => line[7..]
124 .trim()
125 .parse()
126 .map_err(|e| format!("Bad block count: {}", e))?,
127 other => return Err(format!("Expected BLOCKS, got: {:?}", other)),
128 };
129 for _ in 0..n_blocks {
130 let header = lines.next().ok_or("Unexpected EOF reading block header")?;
131 let parts: Vec<&str> = header.split_whitespace().collect();
132 if parts.len() < 6 || parts[0] != "BLOCK" {
133 return Err(format!("Expected BLOCK header, got: {}", header));
134 }
135 let id: u32 = parts[1]
136 .parse()
137 .map_err(|e| format!("Bad block id: {}", e))?;
138 let name = parts[2].to_string();
139 let etype = parts[3].to_string();
140 let n_nodes_per_elem: usize = parts[4]
141 .parse()
142 .map_err(|e| format!("Bad n_nodes_per_elem: {}", e))?;
143 let n_elems: usize = parts[5]
144 .parse()
145 .map_err(|e| format!("Bad n_elems: {}", e))?;
146 let block_idx = mesh.add_block(id, &name, &etype, n_nodes_per_elem);
147 if n_elems > 0 {
148 let conn_line = lines.next().ok_or("Unexpected EOF reading connectivity")?;
149 let conn: Result<Vec<usize>, _> = conn_line
150 .split_whitespace()
151 .map(|v| v.parse::<usize>())
152 .collect();
153 let conn = conn.map_err(|e| format!("Bad connectivity: {}", e))?;
154 mesh.add_element_to_block(block_idx, &conn);
155 }
156 }
157 let n_node_sets: usize = match lines.next() {
158 Some(line) if line.starts_with("NODE_SETS ") => line[10..]
159 .trim()
160 .parse()
161 .map_err(|e| format!("Bad node set count: {}", e))?,
162 other => return Err(format!("Expected NODE_SETS, got: {:?}", other)),
163 };
164 for _ in 0..n_node_sets {
165 let header = lines
166 .next()
167 .ok_or("Unexpected EOF reading node set header")?;
168 let parts: Vec<&str> = header.split_whitespace().collect();
169 if parts.len() < 4 || parts[0] != "NODE_SET" {
170 return Err(format!("Expected NODE_SET header, got: {}", header));
171 }
172 let id: u32 = parts[1]
173 .parse()
174 .map_err(|e| format!("Bad node set id: {}", e))?;
175 let name = parts[2].to_string();
176 let count: usize = parts[3]
177 .parse()
178 .map_err(|e| format!("Bad node set count: {}", e))?;
179 let nodes = if count > 0 {
180 let data_line = lines.next().ok_or("Unexpected EOF reading node set data")?;
181 let parsed: Result<Vec<usize>, _> = data_line
182 .split_whitespace()
183 .map(|v| v.parse::<usize>())
184 .collect();
185 parsed.map_err(|e| format!("Bad node set data: {}", e))?
186 } else {
187 Vec::new()
188 };
189 mesh.add_node_set(id, &name, nodes);
190 }
191 let n_side_sets: usize = match lines.next() {
192 Some(line) if line.starts_with("SIDE_SETS ") => line[10..]
193 .trim()
194 .parse()
195 .map_err(|e| format!("Bad side set count: {}", e))?,
196 other => return Err(format!("Expected SIDE_SETS, got: {:?}", other)),
197 };
198 for _ in 0..n_side_sets {
199 let header = lines
200 .next()
201 .ok_or("Unexpected EOF reading side set header")?;
202 let parts: Vec<&str> = header.split_whitespace().collect();
203 if parts.len() < 4 || parts[0] != "SIDE_SET" {
204 return Err(format!("Expected SIDE_SET header, got: {}", header));
205 }
206 let id: u32 = parts[1]
207 .parse()
208 .map_err(|e| format!("Bad side set id: {}", e))?;
209 let name = parts[2].to_string();
210 let count: usize = parts[3]
211 .parse()
212 .map_err(|e| format!("Bad side set count: {}", e))?;
213 let (element_ids, side_ids) = if count > 0 {
214 let data_line = lines.next().ok_or("Unexpected EOF reading side set data")?;
215 let nums: Result<Vec<usize>, _> = data_line
216 .split_whitespace()
217 .map(|v| v.parse::<usize>())
218 .collect();
219 let nums = nums.map_err(|e| format!("Bad side set data: {}", e))?;
220 let mut eids = Vec::with_capacity(count);
221 let mut sids = Vec::with_capacity(count);
222 for chunk in nums.chunks(2) {
223 if chunk.len() == 2 {
224 eids.push(chunk[0]);
225 sids.push(chunk[1]);
226 }
227 }
228 (eids, sids)
229 } else {
230 (Vec::new(), Vec::new())
231 };
232 mesh.side_sets.push(ExodusSideSet {
233 id,
234 name,
235 element_ids,
236 side_ids,
237 });
238 }
239 match lines.next() {
240 Some("END") => {}
241 other => return Err(format!("Expected END, got: {:?}", other)),
242 }
243 Ok(mesh)
244}
245pub fn bounding_box(mesh: &ExodusMesh) -> ([f64; 3], [f64; 3]) {
249 if mesh.coordinates.is_empty() {
250 return ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
251 }
252 let first = mesh.coordinates[0];
253 let mut min = first;
254 let mut max = first;
255 for &coord in &mesh.coordinates {
256 for i in 0..3 {
257 if coord[i] < min[i] {
258 min[i] = coord[i];
259 }
260 if coord[i] > max[i] {
261 max[i] = coord[i];
262 }
263 }
264 }
265 (min, max)
266}
267pub fn translate_mesh(mesh: &mut ExodusMesh, offset: [f64; 3]) {
269 for coord in &mut mesh.coordinates {
270 coord[0] += offset[0];
271 coord[1] += offset[1];
272 coord[2] += offset[2];
273 }
274}
275#[allow(dead_code)]
277pub fn linear_time_steps(t_start: f64, t_end: f64, n_steps: usize) -> Vec<f64> {
278 if n_steps == 0 {
279 return vec![];
280 }
281 if n_steps == 1 {
282 return vec![t_start];
283 }
284 let dt = (t_end - t_start) / (n_steps - 1) as f64;
285 (0..n_steps).map(|i| t_start + i as f64 * dt).collect()
286}
287#[allow(dead_code)]
291pub fn log_time_steps(t_start: f64, t_end: f64, n_steps: usize) -> Vec<f64> {
292 if n_steps == 0 || t_start <= 0.0 || t_end <= 0.0 {
293 return vec![];
294 }
295 if n_steps == 1 {
296 return vec![t_start];
297 }
298 let log_start = t_start.ln();
299 let log_end = t_end.ln();
300 let d = (log_end - log_start) / (n_steps - 1) as f64;
301 (0..n_steps)
302 .map(|i| (log_start + i as f64 * d).exp())
303 .collect()
304}
305#[allow(dead_code)]
307pub fn add_node_variable_step(
308 result: &mut ExodusResult,
309 time: f64,
310 var_name: &str,
311 values: Vec<f64>,
312) {
313 result.time_steps.push(ExodusTimeStep {
314 time,
315 variables: vec![ExodusVariable {
316 name: var_name.to_string(),
317 entity_type: "node".to_string(),
318 values,
319 }],
320 });
321}
322#[allow(dead_code)]
324pub fn add_element_variable_step(
325 result: &mut ExodusResult,
326 time: f64,
327 var_name: &str,
328 values: Vec<f64>,
329) {
330 result.time_steps.push(ExodusTimeStep {
331 time,
332 variables: vec![ExodusVariable {
333 name: var_name.to_string(),
334 entity_type: "element".to_string(),
335 values,
336 }],
337 });
338}
339#[allow(dead_code)]
349pub fn validate_mesh(mesh: &ExodusMesh) -> MeshValidationReport {
350 let mut report = MeshValidationReport {
351 is_valid: true,
352 errors: Vec::new(),
353 warnings: Vec::new(),
354 };
355 let n_nodes = mesh.node_count();
356 let total_elements = mesh.element_count();
357 if n_nodes == 0 {
358 report.add_warning("Mesh has no nodes".to_string());
359 }
360 if total_elements == 0 {
361 report.add_warning("Mesh has no elements".to_string());
362 }
363 let mut seen_block_ids: Vec<u32> = Vec::new();
364 for (bi, block) in mesh.blocks.iter().enumerate() {
365 if seen_block_ids.contains(&block.id) {
366 report.add_error(format!(
367 "Duplicate block id {} at block index {}",
368 block.id, bi
369 ));
370 } else {
371 seen_block_ids.push(block.id);
372 }
373 if block.n_nodes_per_elem == 0 {
374 report.add_error(format!("Block {} has zero nodes_per_elem", block.id));
375 continue;
376 }
377 if block.connectivity.len() % block.n_nodes_per_elem != 0 {
378 report.add_error(format!(
379 "Block {}: connectivity len {} not divisible by n_nodes_per_elem {}",
380 block.id,
381 block.connectivity.len(),
382 block.n_nodes_per_elem
383 ));
384 }
385 for &nid in &block.connectivity {
386 if nid == 0 || nid > n_nodes {
387 report.add_error(format!(
388 "Block {}: node id {} out of range [1, {}]",
389 block.id, nid, n_nodes
390 ));
391 }
392 }
393 }
394 let mut seen_ns_ids: Vec<u32> = Vec::new();
395 for ns in &mesh.node_sets {
396 if seen_ns_ids.contains(&ns.id) {
397 report.add_error(format!("Duplicate node set id {}", ns.id));
398 } else {
399 seen_ns_ids.push(ns.id);
400 }
401 for &nid in &ns.node_ids {
402 if nid == 0 || nid > n_nodes {
403 report.add_error(format!(
404 "Node set {}: node id {} out of range [1, {}]",
405 ns.id, nid, n_nodes
406 ));
407 }
408 }
409 }
410 for ss in &mesh.side_sets {
411 if ss.element_ids.len() != ss.side_ids.len() {
412 report.add_error(format!(
413 "Side set {}: element_ids and side_ids have different lengths ({} vs {})",
414 ss.id,
415 ss.element_ids.len(),
416 ss.side_ids.len()
417 ));
418 }
419 for &eid in &ss.element_ids {
420 if eid == 0 || eid > total_elements {
421 report.add_error(format!(
422 "Side set {}: element id {} out of range [1, {}]",
423 ss.id, eid, total_elements
424 ));
425 }
426 }
427 }
428 report
429}
430#[allow(dead_code)]
432pub fn scale_mesh(mesh: &mut ExodusMesh, factor: f64) {
433 for coord in &mut mesh.coordinates {
434 coord[0] *= factor;
435 coord[1] *= factor;
436 coord[2] *= factor;
437 }
438}
439#[allow(dead_code)]
441pub fn mesh_centroid(mesh: &ExodusMesh) -> [f64; 3] {
442 let n = mesh.coordinates.len();
443 if n == 0 {
444 return [0.0, 0.0, 0.0];
445 }
446 let mut sum = [0.0_f64; 3];
447 for &c in &mesh.coordinates {
448 sum[0] += c[0];
449 sum[1] += c[1];
450 sum[2] += c[2];
451 }
452 [sum[0] / n as f64, sum[1] / n as f64, sum[2] / n as f64]
453}
454#[allow(dead_code)]
456pub fn build_unit_hex_mesh(title: &str) -> ExodusMesh {
457 let mut mesh = ExodusMesh::new(title);
458 let coords = [
459 (0.0, 0.0, 0.0),
460 (1.0, 0.0, 0.0),
461 (1.0, 1.0, 0.0),
462 (0.0, 1.0, 0.0),
463 (0.0, 0.0, 1.0),
464 (1.0, 0.0, 1.0),
465 (1.0, 1.0, 1.0),
466 (0.0, 1.0, 1.0),
467 ];
468 for (x, y, z) in coords {
469 mesh.add_node(x, y, z);
470 }
471 let bi = mesh.add_block(1, "hex_block", "HEX8", 8);
472 mesh.add_element_to_block(bi, &[1, 2, 3, 4, 5, 6, 7, 8]);
473 mesh
474}
475#[allow(dead_code)]
477pub fn build_unit_tet_mesh(title: &str) -> ExodusMesh {
478 let mut mesh = ExodusMesh::new(title);
479 mesh.add_node(0.0, 0.0, 0.0);
480 mesh.add_node(1.0, 0.0, 0.0);
481 mesh.add_node(0.0, 1.0, 0.0);
482 mesh.add_node(0.0, 0.0, 1.0);
483 let bi = mesh.add_block(1, "tet_block", "TET4", 4);
484 mesh.add_element_to_block(bi, &[1, 2, 3, 4]);
485 mesh
486}
487#[allow(dead_code)]
492pub fn merge_meshes(a: &ExodusMesh, b: &ExodusMesh) -> ExodusMesh {
493 let mut out = ExodusMesh::new(&format!("merged_{}_{}", a.title, b.title));
494 for &c in &a.coordinates {
495 out.coordinates.push(c);
496 }
497 let offset = a.node_count();
498 for &c in &b.coordinates {
499 out.coordinates.push(c);
500 }
501 for block in &a.blocks {
502 let bi = out.add_block(
503 block.id,
504 &block.name,
505 &block.element_type,
506 block.n_nodes_per_elem,
507 );
508 out.add_element_to_block(bi, &block.connectivity);
509 }
510 let max_bid = a.blocks.iter().map(|b| b.id).max().unwrap_or(0);
511 for block in &b.blocks {
512 let new_id = block.id + max_bid;
513 let bi = out.add_block(
514 new_id,
515 &block.name,
516 &block.element_type,
517 block.n_nodes_per_elem,
518 );
519 let shifted: Vec<usize> = block.connectivity.iter().map(|&n| n + offset).collect();
520 out.add_element_to_block(bi, &shifted);
521 }
522 for ns in &a.node_sets {
523 out.add_node_set(ns.id, &ns.name, ns.node_ids.clone());
524 }
525 for ns in &b.node_sets {
526 let shifted: Vec<usize> = ns.node_ids.iter().map(|&n| n + offset).collect();
527 out.add_node_set(ns.id + a.node_sets.len() as u32, &ns.name, shifted);
528 }
529 out
530}
531#[cfg(test)]
532mod tests {
533 use super::*;
534 fn make_simple_mesh() -> ExodusMesh {
535 let mut mesh = ExodusMesh::new("test_mesh");
536 mesh.add_node(0.0, 0.0, 0.0);
537 mesh.add_node(1.0, 0.0, 0.0);
538 mesh.add_node(1.0, 1.0, 0.0);
539 mesh.add_node(0.0, 1.0, 0.0);
540 let bi = mesh.add_block(1, "solid", "QUAD4", 4);
541 mesh.add_element_to_block(bi, &[1, 2, 3, 4]);
542 mesh
543 }
544 #[test]
545 fn test_new_mesh_title() {
546 let mesh = ExodusMesh::new("MyMesh");
547 assert_eq!(mesh.title, "MyMesh");
548 }
549 #[test]
550 fn test_new_mesh_empty() {
551 let mesh = ExodusMesh::new("empty");
552 assert_eq!(mesh.node_count(), 0);
553 assert_eq!(mesh.element_count(), 0);
554 assert!(mesh.blocks.is_empty());
555 assert!(mesh.node_sets.is_empty());
556 assert!(mesh.side_sets.is_empty());
557 }
558 #[test]
559 fn test_add_node_returns_one_based_id() {
560 let mut mesh = ExodusMesh::new("t");
561 let id1 = mesh.add_node(1.0, 2.0, 3.0);
562 let id2 = mesh.add_node(4.0, 5.0, 6.0);
563 assert_eq!(id1, 1);
564 assert_eq!(id2, 2);
565 }
566 #[test]
567 fn test_node_count() {
568 let mesh = make_simple_mesh();
569 assert_eq!(mesh.node_count(), 4);
570 }
571 #[test]
572 fn test_element_count_quad() {
573 let mesh = make_simple_mesh();
574 assert_eq!(mesh.element_count(), 1);
575 }
576 #[test]
577 fn test_add_block_returns_index() {
578 let mut mesh = ExodusMesh::new("t");
579 let idx0 = mesh.add_block(1, "a", "TET4", 4);
580 let idx1 = mesh.add_block(2, "b", "HEX8", 8);
581 assert_eq!(idx0, 0);
582 assert_eq!(idx1, 1);
583 }
584 #[test]
585 fn test_block_properties() {
586 let mut mesh = ExodusMesh::new("t");
587 mesh.add_block(42, "myblock", "HEX8", 8);
588 assert_eq!(mesh.blocks[0].id, 42);
589 assert_eq!(mesh.blocks[0].name, "myblock");
590 assert_eq!(mesh.blocks[0].element_type, "HEX8");
591 assert_eq!(mesh.blocks[0].n_nodes_per_elem, 8);
592 }
593 #[test]
594 fn test_add_node_set() {
595 let mut mesh = ExodusMesh::new("t");
596 mesh.add_node(0.0, 0.0, 0.0);
597 mesh.add_node(1.0, 0.0, 0.0);
598 mesh.add_node_set(10, "bottom", vec![1, 2]);
599 assert_eq!(mesh.node_sets.len(), 1);
600 assert_eq!(mesh.node_sets[0].id, 10);
601 assert_eq!(mesh.node_sets[0].name, "bottom");
602 assert_eq!(mesh.node_sets[0].node_ids, vec![1, 2]);
603 }
604 #[test]
605 fn test_element_count_multiple_blocks() {
606 let mut mesh = ExodusMesh::new("t");
607 for i in 0..8 {
608 mesh.add_node(i as f64, 0.0, 0.0);
609 }
610 let b0 = mesh.add_block(1, "b0", "QUAD4", 4);
611 mesh.add_element_to_block(b0, &[1, 2, 3, 4]);
612 mesh.add_element_to_block(b0, &[5, 6, 7, 8]);
613 let b1 = mesh.add_block(2, "b1", "TET4", 4);
614 mesh.add_element_to_block(b1, &[1, 2, 3, 4]);
615 assert_eq!(mesh.element_count(), 3);
616 }
617 #[test]
618 fn test_bounding_box_single_node() {
619 let mut mesh = ExodusMesh::new("t");
620 mesh.add_node(3.0, 4.0, 5.0);
621 let (min, max) = bounding_box(&mesh);
622 assert_eq!(min, [3.0, 4.0, 5.0]);
623 assert_eq!(max, [3.0, 4.0, 5.0]);
624 }
625 #[test]
626 fn test_bounding_box_multiple_nodes() {
627 let mesh = make_simple_mesh();
628 let (min, max) = bounding_box(&mesh);
629 assert_eq!(min, [0.0, 0.0, 0.0]);
630 assert_eq!(max, [1.0, 1.0, 0.0]);
631 }
632 #[test]
633 fn test_bounding_box_empty_mesh() {
634 let mesh = ExodusMesh::new("empty");
635 let (min, max) = bounding_box(&mesh);
636 assert_eq!(min, [0.0, 0.0, 0.0]);
637 assert_eq!(max, [0.0, 0.0, 0.0]);
638 }
639 #[test]
640 fn test_bounding_box_negative_coords() {
641 let mut mesh = ExodusMesh::new("t");
642 mesh.add_node(-5.0, -3.0, -1.0);
643 mesh.add_node(2.0, 4.0, 6.0);
644 let (min, max) = bounding_box(&mesh);
645 assert_eq!(min, [-5.0, -3.0, -1.0]);
646 assert_eq!(max, [2.0, 4.0, 6.0]);
647 }
648 #[test]
649 fn test_translate_mesh() {
650 let mut mesh = make_simple_mesh();
651 translate_mesh(&mut mesh, [1.0, 2.0, 3.0]);
652 assert_eq!(mesh.coordinates[0], [1.0, 2.0, 3.0]);
653 assert_eq!(mesh.coordinates[1], [2.0, 2.0, 3.0]);
654 assert_eq!(mesh.coordinates[2], [2.0, 3.0, 3.0]);
655 assert_eq!(mesh.coordinates[3], [1.0, 3.0, 3.0]);
656 }
657 #[test]
658 fn test_translate_mesh_zero() {
659 let mut mesh = make_simple_mesh();
660 let orig: Vec<[f64; 3]> = mesh.coordinates.clone();
661 translate_mesh(&mut mesh, [0.0, 0.0, 0.0]);
662 assert_eq!(mesh.coordinates, orig);
663 }
664 #[test]
665 fn test_write_text_roundtrip() {
666 let mesh = make_simple_mesh();
667 let text = write_text(&mesh);
668 let parsed = parse_text(&text).expect("parse failed");
669 assert_eq!(parsed.title, mesh.title);
670 assert_eq!(parsed.node_count(), mesh.node_count());
671 assert_eq!(parsed.element_count(), mesh.element_count());
672 }
673 #[test]
674 fn test_write_text_contains_header() {
675 let mesh = make_simple_mesh();
676 let text = write_text(&mesh);
677 assert!(text.starts_with("EXODUS_MESH\n"));
678 assert!(text.contains("TITLE test_mesh"));
679 assert!(text.contains("END"));
680 }
681 #[test]
682 fn test_parse_text_coordinates() {
683 let mesh = make_simple_mesh();
684 let text = write_text(&mesh);
685 let parsed = parse_text(&text).expect("parse failed");
686 assert!((parsed.coordinates[0][0] - 0.0).abs() < 1e-12);
687 assert!((parsed.coordinates[1][0] - 1.0).abs() < 1e-12);
688 }
689 #[test]
690 fn test_parse_text_block_type() {
691 let mesh = make_simple_mesh();
692 let text = write_text(&mesh);
693 let parsed = parse_text(&text).expect("parse failed");
694 assert_eq!(parsed.blocks[0].element_type, "QUAD4");
695 assert_eq!(parsed.blocks[0].n_nodes_per_elem, 4);
696 }
697 #[test]
698 fn test_parse_text_bad_header() {
699 let result = parse_text("BAD_HEADER\n");
700 assert!(result.is_err());
701 }
702 #[test]
703 fn test_parse_text_node_sets_roundtrip() {
704 let mut mesh = ExodusMesh::new("ns_test");
705 mesh.add_node(0.0, 0.0, 0.0);
706 mesh.add_node(1.0, 0.0, 0.0);
707 mesh.add_node_set(5, "left", vec![1]);
708 mesh.add_node_set(6, "right", vec![2]);
709 let text = write_text(&mesh);
710 let parsed = parse_text(&text).expect("parse failed");
711 assert_eq!(parsed.node_sets.len(), 2);
712 assert_eq!(parsed.node_sets[0].id, 5);
713 assert_eq!(parsed.node_sets[0].name, "left");
714 assert_eq!(parsed.node_sets[0].node_ids, vec![1]);
715 assert_eq!(parsed.node_sets[1].id, 6);
716 }
717 #[test]
718 fn test_side_sets_roundtrip() {
719 let mut mesh = ExodusMesh::new("ss_test");
720 for i in 0..4 {
721 mesh.add_node(i as f64, 0.0, 0.0);
722 }
723 let bi = mesh.add_block(1, "b", "QUAD4", 4);
724 mesh.add_element_to_block(bi, &[1, 2, 3, 4]);
725 mesh.side_sets.push(ExodusSideSet {
726 id: 10,
727 name: "top".to_string(),
728 element_ids: vec![1],
729 side_ids: vec![3],
730 });
731 let text = write_text(&mesh);
732 let parsed = parse_text(&text).expect("parse failed");
733 assert_eq!(parsed.side_sets.len(), 1);
734 assert_eq!(parsed.side_sets[0].id, 10);
735 assert_eq!(parsed.side_sets[0].element_ids, vec![1]);
736 assert_eq!(parsed.side_sets[0].side_ids, vec![3]);
737 }
738 #[test]
739 fn test_exodus_result_write_summary() {
740 let mut mesh = ExodusMesh::new("result_test");
741 mesh.add_node(0.0, 0.0, 0.0);
742 let bi = mesh.add_block(1, "b", "TET4", 4);
743 mesh.add_element_to_block(bi, &[1, 1, 1, 1]);
744 let result = ExodusResult {
745 mesh,
746 time_steps: vec![
747 ExodusTimeStep {
748 time: 0.0,
749 variables: vec![ExodusVariable {
750 name: "displacement".to_string(),
751 entity_type: "node".to_string(),
752 values: vec![0.0],
753 }],
754 },
755 ExodusTimeStep {
756 time: 1.0,
757 variables: vec![],
758 },
759 ],
760 };
761 let summary = result.write_summary();
762 assert!(summary.contains("Title: result_test"));
763 assert!(summary.contains("Nodes: 1"));
764 assert!(summary.contains("Time Steps: 2"));
765 assert!(summary.contains("t=0.000000"));
766 assert!(summary.contains("t=1.000000"));
767 }
768 #[test]
769 fn test_hex8_mesh() {
770 let mut mesh = ExodusMesh::new("hex_mesh");
771 let coords = [
772 (0.0, 0.0, 0.0),
773 (1.0, 0.0, 0.0),
774 (1.0, 1.0, 0.0),
775 (0.0, 1.0, 0.0),
776 (0.0, 0.0, 1.0),
777 (1.0, 0.0, 1.0),
778 (1.0, 1.0, 1.0),
779 (0.0, 1.0, 1.0),
780 ];
781 for (x, y, z) in coords {
782 mesh.add_node(x, y, z);
783 }
784 let bi = mesh.add_block(1, "vol", "HEX8", 8);
785 mesh.add_element_to_block(bi, &[1, 2, 3, 4, 5, 6, 7, 8]);
786 assert_eq!(mesh.node_count(), 8);
787 assert_eq!(mesh.element_count(), 1);
788 let (min, max) = bounding_box(&mesh);
789 assert_eq!(min, [0.0, 0.0, 0.0]);
790 assert_eq!(max, [1.0, 1.0, 1.0]);
791 }
792 #[test]
793 fn test_write_text_empty_mesh() {
794 let mesh = ExodusMesh::new("empty");
795 let text = write_text(&mesh);
796 let parsed = parse_text(&text).expect("parse of empty mesh failed");
797 assert_eq!(parsed.node_count(), 0);
798 assert_eq!(parsed.element_count(), 0);
799 }
800 #[test]
801 fn test_linear_time_steps_count() {
802 let ts = linear_time_steps(0.0, 1.0, 5);
803 assert_eq!(ts.len(), 5);
804 assert!((ts[0] - 0.0).abs() < 1e-12);
805 assert!((ts[4] - 1.0).abs() < 1e-12);
806 }
807 #[test]
808 fn test_linear_time_steps_spacing() {
809 let ts = linear_time_steps(0.0, 4.0, 5);
810 for w in ts.windows(2) {
811 assert!((w[1] - w[0] - 1.0).abs() < 1e-12, "Spacing should be 1.0");
812 }
813 }
814 #[test]
815 fn test_linear_time_steps_empty() {
816 let ts = linear_time_steps(0.0, 1.0, 0);
817 assert!(ts.is_empty());
818 }
819 #[test]
820 fn test_linear_time_steps_single() {
821 let ts = linear_time_steps(3.0, 7.0, 1);
822 assert_eq!(ts.len(), 1);
823 assert!((ts[0] - 3.0).abs() < 1e-12);
824 }
825 #[test]
826 fn test_log_time_steps_count() {
827 let ts = log_time_steps(1.0, 100.0, 3);
828 assert_eq!(ts.len(), 3);
829 assert!((ts[0] - 1.0).abs() < 1e-9);
830 assert!((ts[2] - 100.0).abs() < 1e-6);
831 }
832 #[test]
833 fn test_log_time_steps_monotone() {
834 let ts = log_time_steps(0.1, 10.0, 10);
835 for w in ts.windows(2) {
836 assert!(w[1] > w[0], "Log steps should be monotonically increasing");
837 }
838 }
839 #[test]
840 fn test_add_node_variable_step() {
841 let mesh = ExodusMesh::new("t");
842 let mut result = ExodusResult {
843 mesh,
844 time_steps: vec![],
845 };
846 add_node_variable_step(&mut result, 1.5, "temperature", vec![300.0, 310.0]);
847 assert_eq!(result.time_steps.len(), 1);
848 assert!((result.time_steps[0].time - 1.5).abs() < 1e-12);
849 assert_eq!(result.time_steps[0].variables[0].name, "temperature");
850 assert_eq!(result.time_steps[0].variables[0].values.len(), 2);
851 }
852 #[test]
853 fn test_add_element_variable_step() {
854 let mesh = ExodusMesh::new("t");
855 let mut result = ExodusResult {
856 mesh,
857 time_steps: vec![],
858 };
859 add_element_variable_step(&mut result, 2.0, "stress", vec![1e6]);
860 assert_eq!(result.time_steps[0].variables[0].entity_type, "element");
861 }
862 #[test]
863 fn test_validate_valid_mesh() {
864 let mesh = make_simple_mesh();
865 let report = validate_mesh(&mesh);
866 assert!(
867 report.is_valid,
868 "Simple mesh should be valid: {:?}",
869 report.errors
870 );
871 }
872 #[test]
873 fn test_validate_empty_mesh_has_warnings() {
874 let mesh = ExodusMesh::new("empty");
875 let report = validate_mesh(&mesh);
876 assert!(report.is_valid);
877 assert!(
878 !report.warnings.is_empty(),
879 "Empty mesh should have warnings"
880 );
881 }
882 #[test]
883 fn test_validate_bad_node_id() {
884 let mut mesh = ExodusMesh::new("bad");
885 mesh.add_node(0.0, 0.0, 0.0);
886 let bi = mesh.add_block(1, "b", "QUAD4", 4);
887 mesh.add_element_to_block(bi, &[1, 2, 3, 4]);
888 let report = validate_mesh(&mesh);
889 assert!(!report.is_valid, "Invalid node IDs should fail validation");
890 assert!(!report.errors.is_empty());
891 }
892 #[test]
893 fn test_validate_duplicate_block_id() {
894 let mut mesh = ExodusMesh::new("dup");
895 mesh.add_node(0.0, 0.0, 0.0);
896 mesh.add_block(1, "a", "TET4", 4);
897 mesh.add_block(1, "b", "TET4", 4);
898 let report = validate_mesh(&mesh);
899 assert!(!report.is_valid);
900 assert!(
901 report
902 .errors
903 .iter()
904 .any(|e| e.contains("Duplicate block id"))
905 );
906 }
907 #[test]
908 fn test_validate_mismatched_connectivity_length() {
909 let mut mesh = ExodusMesh::new("mis");
910 for _ in 0..4 {
911 mesh.add_node(0.0, 0.0, 0.0);
912 }
913 let bi = mesh.add_block(1, "b", "QUAD4", 4);
914 mesh.blocks[bi].connectivity = vec![1, 2, 3];
915 let report = validate_mesh(&mesh);
916 assert!(!report.is_valid);
917 assert!(report.errors.iter().any(|e| e.contains("not divisible")));
918 }
919 #[test]
920 fn test_scale_mesh() {
921 let mut mesh = make_simple_mesh();
922 scale_mesh(&mut mesh, 2.0);
923 assert!((mesh.coordinates[1][0] - 2.0).abs() < 1e-12);
924 assert!((mesh.coordinates[2][0] - 2.0).abs() < 1e-12);
925 assert!((mesh.coordinates[2][1] - 2.0).abs() < 1e-12);
926 }
927 #[test]
928 fn test_mesh_centroid() {
929 let mesh = make_simple_mesh();
930 let c = mesh_centroid(&mesh);
931 assert!((c[0] - 0.5).abs() < 1e-12);
932 assert!((c[1] - 0.5).abs() < 1e-12);
933 assert!(c[2].abs() < 1e-12);
934 }
935 #[test]
936 fn test_mesh_centroid_empty() {
937 let mesh = ExodusMesh::new("empty");
938 let c = mesh_centroid(&mesh);
939 assert_eq!(c, [0.0, 0.0, 0.0]);
940 }
941 #[test]
942 fn test_build_unit_hex_mesh() {
943 let mesh = build_unit_hex_mesh("test_hex");
944 assert_eq!(mesh.node_count(), 8);
945 assert_eq!(mesh.element_count(), 1);
946 assert_eq!(mesh.blocks[0].element_type, "HEX8");
947 let report = validate_mesh(&mesh);
948 assert!(
949 report.is_valid,
950 "Unit HEX8 mesh should be valid: {:?}",
951 report.errors
952 );
953 }
954 #[test]
955 fn test_build_unit_tet_mesh() {
956 let mesh = build_unit_tet_mesh("test_tet");
957 assert_eq!(mesh.node_count(), 4);
958 assert_eq!(mesh.element_count(), 1);
959 assert_eq!(mesh.blocks[0].element_type, "TET4");
960 let report = validate_mesh(&mesh);
961 assert!(report.is_valid);
962 }
963 #[test]
964 fn test_merge_meshes_node_count() {
965 let a = make_simple_mesh();
966 let b = build_unit_tet_mesh("b");
967 let merged = merge_meshes(&a, &b);
968 assert_eq!(merged.node_count(), 8);
969 }
970 #[test]
971 fn test_merge_meshes_element_count() {
972 let a = make_simple_mesh();
973 let b = build_unit_tet_mesh("b");
974 let merged = merge_meshes(&a, &b);
975 assert_eq!(merged.element_count(), 2);
976 }
977 #[test]
978 fn test_unit_hex_mesh_validation() {
979 let mesh = build_unit_hex_mesh("validation_test");
980 let report = validate_mesh(&mesh);
981 assert!(report.is_valid);
982 assert!(report.errors.is_empty());
983 }
984}
985#[allow(dead_code)]
987pub fn node_distance(a: [f64; 3], b: [f64; 3]) -> f64 {
988 let dx = b[0] - a[0];
989 let dy = b[1] - a[1];
990 let dz = b[2] - a[2];
991 (dx * dx + dy * dy + dz * dz).sqrt()
992}
993#[allow(dead_code)]
997pub fn tet4_quality(mesh: &ExodusMesh) -> Vec<ElementQuality> {
998 let mut result = Vec::new();
999 for block in &mesh.blocks {
1000 if block.element_type != "TET4" || block.n_nodes_per_elem != 4 {
1001 continue;
1002 }
1003 let n_elem = block.connectivity.len() / 4;
1004 for ei in 0..n_elem {
1005 let base = ei * 4;
1006 let n0 = mesh.coordinates[block.connectivity[base] - 1];
1007 let n1 = mesh.coordinates[block.connectivity[base + 1] - 1];
1008 let n2 = mesh.coordinates[block.connectivity[base + 2] - 1];
1009 let n3 = mesh.coordinates[block.connectivity[base + 3] - 1];
1010 let edges = [
1011 node_distance(n0, n1),
1012 node_distance(n0, n2),
1013 node_distance(n0, n3),
1014 node_distance(n1, n2),
1015 node_distance(n1, n3),
1016 node_distance(n2, n3),
1017 ];
1018 let min_e = edges.iter().cloned().fold(f64::INFINITY, f64::min);
1019 let max_e = edges.iter().cloned().fold(0.0_f64, f64::max);
1020 let aspect = if max_e > 1e-30 { min_e / max_e } else { 0.0 };
1021 let e1 = [n1[0] - n0[0], n1[1] - n0[1], n1[2] - n0[2]];
1022 let e2 = [n2[0] - n0[0], n2[1] - n0[1], n2[2] - n0[2]];
1023 let e3 = [n3[0] - n0[0], n3[1] - n0[1], n3[2] - n0[2]];
1024 let jdet = e1[0] * (e2[1] * e3[2] - e2[2] * e3[1])
1025 - e1[1] * (e2[0] * e3[2] - e2[2] * e3[0])
1026 + e1[2] * (e2[0] * e3[1] - e2[1] * e3[0]);
1027 let len1 = (e1[0] * e1[0] + e1[1] * e1[1] + e1[2] * e1[2]).sqrt();
1028 let len2 = (e2[0] * e2[0] + e2[1] * e2[1] + e2[2] * e2[2]).sqrt();
1029 let len3 = (e3[0] * e3[0] + e3[1] * e3[1] + e3[2] * e3[2]).sqrt();
1030 let denom = len1 * len2 * len3;
1031 let scaled_jac = if denom > 1e-30 { jdet / denom } else { 0.0 };
1032 result.push(ElementQuality {
1033 block_id: block.id,
1034 element_index: ei,
1035 aspect_ratio: aspect,
1036 scaled_jacobian: scaled_jac,
1037 min_edge: min_e,
1038 max_edge: max_e,
1039 });
1040 }
1041 }
1042 result
1043}
1044#[allow(dead_code)]
1046pub fn hex8_quality(mesh: &ExodusMesh) -> Vec<ElementQuality> {
1047 let mut result = Vec::new();
1048 for block in &mesh.blocks {
1049 if block.element_type != "HEX8" || block.n_nodes_per_elem != 8 {
1050 continue;
1051 }
1052 let n_elem = block.connectivity.len() / 8;
1053 for ei in 0..n_elem {
1054 let base = ei * 8;
1055 let nodes: Vec<[f64; 3]> = (0..8)
1056 .map(|k| mesh.coordinates[block.connectivity[base + k] - 1])
1057 .collect();
1058 let edge_pairs = [
1059 (0, 1),
1060 (1, 2),
1061 (2, 3),
1062 (3, 0),
1063 (4, 5),
1064 (5, 6),
1065 (6, 7),
1066 (7, 4),
1067 (0, 4),
1068 (1, 5),
1069 (2, 6),
1070 (3, 7),
1071 ];
1072 let edges: Vec<f64> = edge_pairs
1073 .iter()
1074 .map(|&(a, b)| node_distance(nodes[a], nodes[b]))
1075 .collect();
1076 let min_e = edges.iter().cloned().fold(f64::INFINITY, f64::min);
1077 let max_e = edges.iter().cloned().fold(0.0_f64, f64::max);
1078 let aspect = if max_e > 1e-30 { min_e / max_e } else { 0.0 };
1079 let e1 = [
1080 nodes[1][0] - nodes[0][0],
1081 nodes[1][1] - nodes[0][1],
1082 nodes[1][2] - nodes[0][2],
1083 ];
1084 let e2 = [
1085 nodes[3][0] - nodes[0][0],
1086 nodes[3][1] - nodes[0][1],
1087 nodes[3][2] - nodes[0][2],
1088 ];
1089 let e3 = [
1090 nodes[4][0] - nodes[0][0],
1091 nodes[4][1] - nodes[0][1],
1092 nodes[4][2] - nodes[0][2],
1093 ];
1094 let jdet = e1[0] * (e2[1] * e3[2] - e2[2] * e3[1])
1095 - e1[1] * (e2[0] * e3[2] - e2[2] * e3[0])
1096 + e1[2] * (e2[0] * e3[1] - e2[1] * e3[0]);
1097 let len1 = (e1[0] * e1[0] + e1[1] * e1[1] + e1[2] * e1[2]).sqrt();
1098 let len2 = (e2[0] * e2[0] + e2[1] * e2[1] + e2[2] * e2[2]).sqrt();
1099 let len3 = (e3[0] * e3[0] + e3[1] * e3[1] + e3[2] * e3[2]).sqrt();
1100 let denom = len1 * len2 * len3;
1101 let scaled_jac = if denom > 1e-30 { jdet / denom } else { 0.0 };
1102 result.push(ElementQuality {
1103 block_id: block.id,
1104 element_index: ei,
1105 aspect_ratio: aspect,
1106 scaled_jacobian: scaled_jac,
1107 min_edge: min_e,
1108 max_edge: max_e,
1109 });
1110 }
1111 }
1112 result
1113}
1114#[allow(dead_code)]
1119pub fn idw_interpolate(mesh: &ExodusMesh, field: &[f64], query: [f64; 3], power: f64) -> f64 {
1120 if field.is_empty() || mesh.coordinates.is_empty() {
1121 return 0.0;
1122 }
1123 let mut num = 0.0_f64;
1124 let mut den = 0.0_f64;
1125 for (i, &c) in mesh.coordinates.iter().enumerate() {
1126 if i >= field.len() {
1127 break;
1128 }
1129 let d = node_distance(c, query);
1130 if d < 1e-15 {
1131 return field[i];
1132 }
1133 let w = 1.0 / d.powf(power);
1134 num += w * field[i];
1135 den += w;
1136 }
1137 if den.abs() < 1e-30 { 0.0 } else { num / den }
1138}
1139#[allow(dead_code)]
1141pub fn find_nearest_node(mesh: &ExodusMesh, query: [f64; 3]) -> Option<usize> {
1142 if mesh.coordinates.is_empty() {
1143 return None;
1144 }
1145 let mut best_idx = 0;
1146 let mut best_dist = f64::INFINITY;
1147 for (i, &c) in mesh.coordinates.iter().enumerate() {
1148 let d = node_distance(c, query);
1149 if d < best_dist {
1150 best_dist = d;
1151 best_idx = i;
1152 }
1153 }
1154 Some(best_idx)
1155}
1156#[allow(dead_code)]
1160pub fn nodes_in_sphere(mesh: &ExodusMesh, center: [f64; 3], radius: f64) -> Vec<usize> {
1161 mesh.coordinates
1162 .iter()
1163 .enumerate()
1164 .filter(|&(_, c)| node_distance(*c, center) <= radius)
1165 .map(|(i, _)| i)
1166 .collect()
1167}
1168#[allow(dead_code)]
1170pub fn extract_block(mesh: &ExodusMesh, block_id: u32) -> Option<ExodusMesh> {
1171 let block = mesh.blocks.iter().find(|b| b.id == block_id)?;
1172 let mut out = ExodusMesh::new(&format!("{}_block_{}", mesh.title, block_id));
1173 let mut used_nodes: Vec<usize> = block.connectivity.clone();
1174 used_nodes.sort_unstable();
1175 used_nodes.dedup();
1176 let mut remap = std::collections::HashMap::new();
1177 for (new_idx, &old_id) in used_nodes.iter().enumerate() {
1178 remap.insert(old_id, new_idx + 1);
1179 }
1180 for &old_id in &used_nodes {
1181 if old_id <= mesh.coordinates.len() {
1182 let c = mesh.coordinates[old_id - 1];
1183 out.coordinates.push(c);
1184 }
1185 }
1186 let new_conn: Vec<usize> = block
1187 .connectivity
1188 .iter()
1189 .map(|&n| *remap.get(&n).unwrap_or(&n))
1190 .collect();
1191 let bi = out.add_block(
1192 block.id,
1193 &block.name,
1194 &block.element_type,
1195 block.n_nodes_per_elem,
1196 );
1197 out.add_element_to_block(bi, &new_conn);
1198 Some(out)
1199}
1200#[allow(dead_code)]
1202pub fn extract_node_set_coords(mesh: &ExodusMesh, ns_id: u32) -> Option<Vec<[f64; 3]>> {
1203 let ns = mesh.node_sets.iter().find(|ns| ns.id == ns_id)?;
1204 let coords: Vec<[f64; 3]> = ns
1205 .node_ids
1206 .iter()
1207 .filter_map(|&nid| {
1208 if nid >= 1 && nid <= mesh.coordinates.len() {
1209 Some(mesh.coordinates[nid - 1])
1210 } else {
1211 None
1212 }
1213 })
1214 .collect();
1215 Some(coords)
1216}
1217#[allow(dead_code)]
1222pub fn refine_tet4_mesh(mesh: &ExodusMesh) -> ExodusMesh {
1223 let mut out = ExodusMesh::new(&format!("{}_refined", mesh.title));
1224 for &c in &mesh.coordinates {
1225 out.coordinates.push(c);
1226 }
1227 let mut edge_mid: std::collections::HashMap<(usize, usize), usize> =
1228 std::collections::HashMap::new();
1229 let add_midpoint = |coords: &mut Vec<[f64; 3]>, a: usize, b: usize| -> usize {
1230 let ca = coords[a - 1];
1231 let cb = coords[b - 1];
1232 let mid = [
1233 (ca[0] + cb[0]) * 0.5,
1234 (ca[1] + cb[1]) * 0.5,
1235 (ca[2] + cb[2]) * 0.5,
1236 ];
1237 coords.push(mid);
1238 coords.len()
1239 };
1240 for block in &mesh.blocks {
1241 if block.element_type != "TET4" || block.n_nodes_per_elem != 4 {
1242 let bi = out.add_block(
1243 block.id,
1244 &block.name,
1245 &block.element_type,
1246 block.n_nodes_per_elem,
1247 );
1248 out.add_element_to_block(bi, &block.connectivity);
1249 continue;
1250 }
1251 let bi = out.add_block(block.id, &block.name, "TET4", 4);
1252 let n_elem = block.connectivity.len() / 4;
1253 for ei in 0..n_elem {
1254 let base = ei * 4;
1255 let (v0, v1, v2, v3) = (
1256 block.connectivity[base],
1257 block.connectivity[base + 1],
1258 block.connectivity[base + 2],
1259 block.connectivity[base + 3],
1260 );
1261 let get_mid = |a: usize,
1262 b: usize,
1263 coords: &mut Vec<[f64; 3]>,
1264 cache: &mut std::collections::HashMap<(usize, usize), usize>|
1265 -> usize {
1266 let key = if a < b { (a, b) } else { (b, a) };
1267 if let Some(&mid) = cache.get(&key) {
1268 mid
1269 } else {
1270 let mid = add_midpoint(coords, a, b);
1271 cache.insert(key, mid);
1272 mid
1273 }
1274 };
1275 let m01 = get_mid(v0, v1, &mut out.coordinates, &mut edge_mid);
1276 let m02 = get_mid(v0, v2, &mut out.coordinates, &mut edge_mid);
1277 let m03 = get_mid(v0, v3, &mut out.coordinates, &mut edge_mid);
1278 let m12 = get_mid(v1, v2, &mut out.coordinates, &mut edge_mid);
1279 let m13 = get_mid(v1, v3, &mut out.coordinates, &mut edge_mid);
1280 let m23 = get_mid(v2, v3, &mut out.coordinates, &mut edge_mid);
1281 let sub_tets = [
1282 [v0, m01, m02, m03],
1283 [v1, m01, m12, m13],
1284 [v2, m02, m12, m23],
1285 [v3, m03, m13, m23],
1286 [m01, m02, m03, m13],
1287 [m01, m02, m12, m13],
1288 [m02, m03, m13, m23],
1289 [m02, m12, m13, m23],
1290 ];
1291 for tet in &sub_tets {
1292 out.add_element_to_block(bi, tet);
1293 }
1294 }
1295 }
1296 out
1297}
1298#[allow(dead_code)]
1300pub fn all_boundary_nodes(mesh: &ExodusMesh) -> Vec<usize> {
1301 let mut ids: Vec<usize> = mesh
1302 .node_sets
1303 .iter()
1304 .flat_map(|ns| ns.node_ids.iter().cloned())
1305 .collect();
1306 ids.sort_unstable();
1307 ids.dedup();
1308 ids
1309}
1310#[allow(dead_code)]
1314pub fn node_valence(mesh: &ExodusMesh) -> Vec<usize> {
1315 let n = mesh.node_count();
1316 let mut valence = vec![0usize; n];
1317 for block in &mesh.blocks {
1318 for &nid in &block.connectivity {
1319 if nid >= 1 && nid <= n {
1320 valence[nid - 1] += 1;
1321 }
1322 }
1323 }
1324 valence
1325}
1326#[allow(dead_code)]
1330pub fn write_result_text(result: &ExodusResult) -> String {
1331 let mut s = write_text(&result.mesh);
1332 if s.ends_with("END\n") {
1333 s.truncate(s.len() - 4);
1334 }
1335 s.push_str(&format!("TIME_STEPS {}\n", result.time_steps.len()));
1336 for ts in &result.time_steps {
1337 s.push_str(&format!("TIME_STEP {:.15e}\n", ts.time));
1338 s.push_str(&format!("VARIABLES {}\n", ts.variables.len()));
1339 for var in &ts.variables {
1340 s.push_str(&format!(
1341 "VAR {} {} {}\n",
1342 var.entity_type,
1343 var.name,
1344 var.values.len()
1345 ));
1346 let vals: Vec<String> = var.values.iter().map(|v| format!("{:.15e}", v)).collect();
1347 s.push_str(&vals.join(" "));
1348 s.push('\n');
1349 }
1350 }
1351 s.push_str("END\n");
1352 s
1353}
1354#[allow(dead_code)]
1356pub fn parse_result_text(data: &str) -> std::result::Result<ExodusResult, String> {
1357 let ts_marker = "TIME_STEPS ";
1358 if let Some(pos) = data.find(ts_marker) {
1359 let mesh_part = format!("{}END\n", &data[..pos]);
1360 let mesh = parse_text(&mesh_part)?;
1361 let mut result = ExodusResult {
1362 mesh,
1363 time_steps: Vec::new(),
1364 };
1365 let rest = &data[pos..];
1366 let mut lines = rest.lines().peekable();
1367 let n_steps: usize = match lines.next() {
1368 Some(line) if line.starts_with("TIME_STEPS ") => line[11..]
1369 .trim()
1370 .parse()
1371 .map_err(|e| format!("Bad TIME_STEPS: {}", e))?,
1372 other => return Err(format!("Expected TIME_STEPS, got: {:?}", other)),
1373 };
1374 for _ in 0..n_steps {
1375 let time: f64 = match lines.next() {
1376 Some(line) if line.starts_with("TIME_STEP ") => line[10..]
1377 .trim()
1378 .parse()
1379 .map_err(|e| format!("Bad TIME_STEP: {}", e))?,
1380 other => return Err(format!("Expected TIME_STEP, got: {:?}", other)),
1381 };
1382 let n_vars: usize = match lines.next() {
1383 Some(line) if line.starts_with("VARIABLES ") => line[10..]
1384 .trim()
1385 .parse()
1386 .map_err(|e| format!("Bad VARIABLES: {}", e))?,
1387 other => return Err(format!("Expected VARIABLES, got: {:?}", other)),
1388 };
1389 let mut variables = Vec::new();
1390 for _ in 0..n_vars {
1391 let hdr = lines.next().ok_or("Unexpected EOF reading VAR")?;
1392 let parts: Vec<&str> = hdr.split_whitespace().collect();
1393 if parts.len() < 4 || parts[0] != "VAR" {
1394 return Err(format!("Expected VAR header, got: {}", hdr));
1395 }
1396 let entity_type = parts[1].to_string();
1397 let name = parts[2].to_string();
1398 let n_vals: usize = parts[3]
1399 .parse()
1400 .map_err(|e| format!("Bad VAR count: {}", e))?;
1401 let vals_line = lines.next().ok_or("Unexpected EOF reading VAR data")?;
1402 let values: std::result::Result<Vec<f64>, _> = vals_line
1403 .split_whitespace()
1404 .take(n_vals)
1405 .map(|v| v.parse::<f64>())
1406 .collect();
1407 let values = values.map_err(|e| format!("Bad VAR values: {}", e))?;
1408 variables.push(ExodusVariable {
1409 name,
1410 entity_type,
1411 values,
1412 });
1413 }
1414 result.time_steps.push(ExodusTimeStep { time, variables });
1415 }
1416 Ok(result)
1417 } else {
1418 let mesh = parse_text(data)?;
1419 Ok(ExodusResult {
1420 mesh,
1421 time_steps: Vec::new(),
1422 })
1423 }
1424}
1425#[allow(dead_code)]
1429pub fn partition_mesh_round_robin(mesh: &ExodusMesh, n_parts: usize) -> Vec<MeshPartition> {
1430 if n_parts == 0 || mesh.blocks.is_empty() {
1431 return Vec::new();
1432 }
1433 let mut parts: Vec<ExodusMesh> = (0..n_parts)
1434 .map(|i| ExodusMesh::new(&format!("{}_part_{}", mesh.title, i)))
1435 .collect();
1436 for p in &mut parts {
1437 for &c in &mesh.coordinates {
1438 p.coordinates.push(c);
1439 }
1440 }
1441 let block = &mesh.blocks[0];
1442 let n_elem = block
1443 .connectivity
1444 .len()
1445 .checked_div(block.n_nodes_per_elem)
1446 .unwrap_or(0);
1447 let block_idxs: Vec<usize> = (0..n_parts)
1448 .map(|pi| {
1449 parts[pi].add_block(
1450 block.id,
1451 &block.name,
1452 &block.element_type,
1453 block.n_nodes_per_elem,
1454 )
1455 })
1456 .collect();
1457 for ei in 0..n_elem {
1458 let part = ei % n_parts;
1459 let base = ei * block.n_nodes_per_elem;
1460 let conn = &block.connectivity[base..base + block.n_nodes_per_elem];
1461 parts[part].add_element_to_block(block_idxs[part], conn);
1462 }
1463 for ns in &mesh.node_sets {
1464 for p in &mut parts {
1465 p.add_node_set(ns.id, &ns.name, ns.node_ids.clone());
1466 }
1467 }
1468 parts
1469 .into_iter()
1470 .enumerate()
1471 .map(|(i, m)| MeshPartition {
1472 partition_id: i,
1473 mesh: m,
1474 })
1475 .collect()
1476}
1477#[allow(dead_code)]
1479pub fn field_stats(values: &[f64]) -> FieldStats {
1480 if values.is_empty() {
1481 return FieldStats {
1482 min: 0.0,
1483 max: 0.0,
1484 mean: 0.0,
1485 std_dev: 0.0,
1486 count: 0,
1487 };
1488 }
1489 let count = values.len();
1490 let mut min_v = values[0];
1491 let mut max_v = values[0];
1492 let mut sum = 0.0_f64;
1493 for &v in values {
1494 if v < min_v {
1495 min_v = v;
1496 }
1497 if v > max_v {
1498 max_v = v;
1499 }
1500 sum += v;
1501 }
1502 let mean = sum / count as f64;
1503 let var: f64 = values
1504 .iter()
1505 .map(|&v| {
1506 let d = v - mean;
1507 d * d
1508 })
1509 .sum::<f64>()
1510 / count as f64;
1511 FieldStats {
1512 min: min_v,
1513 max: max_v,
1514 mean,
1515 std_dev: var.sqrt(),
1516 count,
1517 }
1518}
1519#[allow(dead_code)]
1521pub fn normalize_field(values: &[f64]) -> Vec<f64> {
1522 if values.is_empty() {
1523 return Vec::new();
1524 }
1525 let stats = field_stats(values);
1526 let range = stats.max - stats.min;
1527 if range < 1e-30 {
1528 return vec![0.5; values.len()];
1529 }
1530 values.iter().map(|&v| (v - stats.min) / range).collect()
1531}
1532#[allow(dead_code)]
1534pub fn field_l2_norm(values: &[f64]) -> f64 {
1535 let sum_sq: f64 = values.iter().map(|&v| v * v).sum();
1536 sum_sq.sqrt()
1537}
1538#[allow(dead_code)]
1540pub fn export_nodes_csv(mesh: &ExodusMesh) -> String {
1541 let mut s = String::from("node_id,x,y,z\n");
1542 for (i, &c) in mesh.coordinates.iter().enumerate() {
1543 s.push_str(&format!("{},{},{},{}\n", i + 1, c[0], c[1], c[2]));
1544 }
1545 s
1546}
1547#[allow(dead_code)]
1549pub fn export_field_csv(mesh: &ExodusMesh, field_name: &str, values: &[f64]) -> String {
1550 let mut s = format!("node_id,x,y,z,{}\n", field_name);
1551 for (i, &c) in mesh.coordinates.iter().enumerate() {
1552 let v = values.get(i).copied().unwrap_or(0.0);
1553 s.push_str(&format!("{},{},{},{},{}\n", i + 1, c[0], c[1], c[2], v));
1554 }
1555 s
1556}
1557#[allow(dead_code)]
1559pub fn export_connectivity_csv(mesh: &ExodusMesh) -> String {
1560 let mut s = String::from("block_id,element_id");
1561 let max_npe = mesh
1562 .blocks
1563 .iter()
1564 .map(|b| b.n_nodes_per_elem)
1565 .max()
1566 .unwrap_or(0);
1567 for k in 0..max_npe {
1568 s.push_str(&format!(",n{}", k));
1569 }
1570 s.push('\n');
1571 let mut global_elem = 1;
1572 for block in &mesh.blocks {
1573 if block.n_nodes_per_elem == 0 {
1574 continue;
1575 }
1576 let n_elem = block.connectivity.len() / block.n_nodes_per_elem;
1577 for ei in 0..n_elem {
1578 let base = ei * block.n_nodes_per_elem;
1579 s.push_str(&format!("{},{}", block.id, global_elem));
1580 for k in 0..block.n_nodes_per_elem {
1581 s.push_str(&format!(",{}", block.connectivity[base + k]));
1582 }
1583 for _ in block.n_nodes_per_elem..max_npe {
1584 s.push_str(",-1");
1585 }
1586 s.push('\n');
1587 global_elem += 1;
1588 }
1589 }
1590 s
1591}