1#![allow(clippy::type_complexity)]
2use std::collections::HashMap;
14use std::fmt::Write as FmtWrite;
15use std::fs;
16use std::io::{self, BufRead};
17
18use crate::Error as IoError;
19use crate::finite_element_io::{
20 AnalysisStep, DirichletBc, FeElement, FeElementType, FeMesh, FeNode, LinearElasticMaterial,
21 NodalForce,
22};
23
24fn fe_type_to_calculix(et: &FeElementType) -> &'static str {
30 match et {
31 FeElementType::Tet4 => "C3D4",
32 FeElementType::Tet10 => "C3D10",
33 FeElementType::Hex8 => "C3D8",
34 FeElementType::Hex20 => "C3D20",
35 FeElementType::Tri3 => "S3",
36 FeElementType::Tri6 => "S6",
37 FeElementType::Quad4 => "S4",
38 FeElementType::Quad8 => "S8",
39 FeElementType::Line2 => "T3D2",
40 FeElementType::Unknown(_) => "UNKNOWN",
41 }
42}
43
44fn calculix_type_to_fe(s: &str) -> FeElementType {
46 match s.trim().to_uppercase().as_str() {
47 "C3D4" => FeElementType::Tet4,
48 "C3D10" => FeElementType::Tet10,
49 "C3D8" => FeElementType::Hex8,
50 "C3D20" => FeElementType::Hex20,
51 "S3" => FeElementType::Tri3,
52 "S6" => FeElementType::Tri6,
53 "S4" => FeElementType::Quad4,
54 "S8" => FeElementType::Quad8,
55 "T3D2" => FeElementType::Line2,
56 other => FeElementType::Unknown(other.to_string()),
57 }
58}
59
60#[derive(Debug, Clone, Default)]
66pub struct CalculixWriter;
67
68impl CalculixWriter {
69 pub fn new() -> Self {
71 Self
72 }
73
74 pub fn write_string(&self, mesh: &FeMesh) -> Result<String, IoError> {
76 let mut buf = String::new();
77
78 writeln!(buf, "*HEADING").map_err(|e| IoError::General(format!("fmt error: {e}")))?;
80 writeln!(buf, "Generated by OxiPhysics CalculixWriter")
81 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
82
83 writeln!(buf, "*NODE").map_err(|e| IoError::General(format!("fmt error: {e}")))?;
85 for node in &mesh.nodes {
86 writeln!(
87 buf,
88 "{}, {:.15e}, {:.15e}, {:.15e}",
89 node.id, node.coords[0], node.coords[1], node.coords[2]
90 )
91 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
92 }
93
94 let mut types_seen: Vec<FeElementType> = Vec::new();
96 for el in &mesh.elements {
97 if !types_seen.contains(&el.element_type) {
98 types_seen.push(el.element_type.clone());
99 }
100 }
101
102 for etype in &types_seen {
103 let ccx_type = fe_type_to_calculix(etype);
104 writeln!(buf, "*ELEMENT, TYPE={ccx_type}")
105 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
106 for el in mesh.elements.iter().filter(|e| &e.element_type == etype) {
107 let ids: Vec<String> = el.connectivity.iter().map(|n| n.to_string()).collect();
108 writeln!(buf, "{}, {}", el.id, ids.join(", "))
109 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
110 }
111 }
112
113 if !mesh.element_sets.is_empty() {
115 for (set_name, ids) in &mesh.element_sets {
116 writeln!(buf, "*ELSET, ELSET={set_name}")
117 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
118 let id_strs: Vec<String> = ids.iter().map(|i| i.to_string()).collect();
119 for chunk in id_strs.chunks(16) {
121 writeln!(buf, "{}", chunk.join(", "))
122 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
123 }
124 }
125 }
126
127 for mat in &mesh.materials {
129 writeln!(buf, "*MATERIAL, NAME={}", mat.name)
130 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
131 writeln!(buf, "*DENSITY").map_err(|e| IoError::General(format!("fmt error: {e}")))?;
132 writeln!(buf, "{:.15e}", mat.density)
133 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
134 writeln!(buf, "*ELASTIC").map_err(|e| IoError::General(format!("fmt error: {e}")))?;
135 writeln!(
136 buf,
137 "{:.15e}, {:.15e}",
138 mat.young_modulus, mat.poisson_ratio
139 )
140 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
141 }
142
143 for mat in &mesh.materials {
147 if mesh.element_sets.contains_key(&mat.name) {
148 writeln!(
149 buf,
150 "*SOLID SECTION, ELSET={}, MATERIAL={}",
151 mat.name, mat.name
152 )
153 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
154 }
155 }
156
157 for step in &mesh.steps {
159 writeln!(buf, "*STEP").map_err(|e| IoError::General(format!("fmt error: {e}")))?;
160 writeln!(buf, "*STATIC").map_err(|e| IoError::General(format!("fmt error: {e}")))?;
161 writeln!(
162 buf,
163 "{:.6e}, {:.6e}",
164 step.initial_increment, step.time_period
165 )
166 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
167
168 if !step.bcs.is_empty() {
170 writeln!(buf, "*BOUNDARY")
171 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
172 for bc in &step.bcs {
173 writeln!(
174 buf,
175 "{}, {}, {}, {:.15e}",
176 bc.node_id, bc.dof, bc.dof, bc.value
177 )
178 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
179 }
180 }
181
182 if !step.forces.is_empty() {
184 writeln!(buf, "*CLOAD").map_err(|e| IoError::General(format!("fmt error: {e}")))?;
185 for f in &step.forces {
186 writeln!(buf, "{}, {}, {:.15e}", f.node_id, f.dof, f.magnitude)
187 .map_err(|e| IoError::General(format!("fmt error: {e}")))?;
188 }
189 }
190
191 writeln!(buf, "*END STEP").map_err(|e| IoError::General(format!("fmt error: {e}")))?;
192 }
193
194 Ok(buf)
195 }
196
197 pub fn write(&self, mesh: &FeMesh, path: &str) -> Result<(), IoError> {
199 let content = self.write_string(mesh)?;
200 fs::write(path, content).map_err(IoError::Io)
201 }
202}
203
204#[derive(Debug, Clone, PartialEq)]
210enum CcxBlock {
211 None,
213 Heading,
215 Node,
217 Element(FeElementType),
219 Elastic(String),
221 Density(String),
223 Boundary,
225 Cload,
227 Static,
229 Elset(String),
231 Nset(String),
233}
234
235#[derive(Debug, Clone, Default)]
237pub struct CalculixReader;
238
239impl CalculixReader {
240 pub fn new() -> Self {
242 Self
243 }
244
245 pub fn parse(&self, path: &str) -> Result<FeMesh, IoError> {
247 let file = fs::File::open(path).map_err(IoError::Io)?;
248 let reader = io::BufReader::new(file);
249 let mut lines = Vec::new();
250 for line_res in reader.lines() {
251 lines.push(line_res.map_err(IoError::Io)?);
252 }
253 self.parse_lines(&lines)
254 }
255
256 pub fn parse_string(&self, source: &str) -> Result<FeMesh, IoError> {
258 let lines: Vec<String> = source.lines().map(|l| l.to_string()).collect();
259 self.parse_lines(&lines)
260 }
261
262 fn parse_lines(&self, lines: &[String]) -> Result<FeMesh, IoError> {
264 let mut mesh = FeMesh::new();
265 let mut block = CcxBlock::None;
266 let mut current_material: Option<String> = None;
268 let mut mat_data: HashMap<String, (Option<f64>, Option<f64>, Option<f64>)> = HashMap::new();
270 let mut in_step = false;
272
273 for raw_line in lines {
274 let line = raw_line.trim();
275
276 if line.is_empty() || line.starts_with("**") {
278 continue;
279 }
280
281 if line.starts_with('*') {
283 let upper = line.to_uppercase();
284
285 if upper.starts_with("*END STEP") {
286 in_step = false;
287 block = CcxBlock::None;
288 continue;
289 }
290
291 if upper.starts_with("*HEADING") {
292 block = CcxBlock::Heading;
293 continue;
294 }
295
296 if upper.starts_with("*NODE") && !upper.starts_with("*NSET") {
297 block = CcxBlock::Node;
298 continue;
299 }
300
301 if upper.starts_with("*ELEMENT") {
302 let etype_str =
303 extract_param(line, "TYPE").unwrap_or_else(|| "UNKNOWN".to_string());
304 block = CcxBlock::Element(calculix_type_to_fe(&etype_str));
305 continue;
306 }
307
308 if upper.starts_with("*NSET") {
309 let name = extract_param(line, "NSET").unwrap_or_else(|| "DEFAULT".to_string());
310 block = CcxBlock::Nset(name);
311 continue;
312 }
313
314 if upper.starts_with("*ELSET") {
315 let name =
316 extract_param(line, "ELSET").unwrap_or_else(|| "DEFAULT".to_string());
317 block = CcxBlock::Elset(name);
318 continue;
319 }
320
321 if upper.starts_with("*MATERIAL") {
322 let name = extract_param(line, "NAME").unwrap_or_else(|| "UNNAMED".to_string());
323 current_material = Some(name.clone());
324 mat_data.entry(name).or_insert((None, None, None));
325 block = CcxBlock::None;
326 continue;
327 }
328
329 if upper.starts_with("*ELASTIC") {
330 if let Some(ref mat_name) = current_material {
331 block = CcxBlock::Elastic(mat_name.clone());
332 } else {
333 block = CcxBlock::None;
334 }
335 continue;
336 }
337
338 if upper.starts_with("*DENSITY") {
339 if let Some(ref mat_name) = current_material {
340 block = CcxBlock::Density(mat_name.clone());
341 } else {
342 block = CcxBlock::None;
343 }
344 continue;
345 }
346
347 if upper.starts_with("*BOUNDARY") {
348 block = CcxBlock::Boundary;
349 continue;
350 }
351
352 if upper.starts_with("*CLOAD") {
353 block = CcxBlock::Cload;
354 continue;
355 }
356
357 if upper.starts_with("*STEP") {
358 in_step = true;
359 let name = extract_param(line, "NAME").unwrap_or_else(|| "Step-1".to_string());
360 mesh.steps.push(AnalysisStep::new(name));
361 block = CcxBlock::None;
362 continue;
363 }
364
365 if upper.starts_with("*STATIC") {
366 block = CcxBlock::Static;
367 continue;
368 }
369
370 if upper.starts_with("*SOLID SECTION") {
371 block = CcxBlock::None;
373 continue;
374 }
375
376 block = CcxBlock::None;
378 continue;
379 }
380
381 match &block {
383 CcxBlock::None | CcxBlock::Heading => {
384 }
386 CcxBlock::Node => {
387 if let Some(node) = parse_node_line(line) {
388 mesh.nodes.push(node);
389 }
390 }
391 CcxBlock::Element(etype) => {
392 if let Some(elem) = parse_element_line(line, etype.clone()) {
393 mesh.elements.push(elem);
394 }
395 }
396 CcxBlock::Elastic(mat_name) => {
397 let parts: Vec<&str> = line.split(',').collect();
398 if parts.len() >= 2
399 && let (Some(e), Some(nu)) = (
400 parts[0].trim().parse::<f64>().ok(),
401 parts[1].trim().parse::<f64>().ok(),
402 )
403 {
404 let entry = mat_data
405 .entry(mat_name.clone())
406 .or_insert((None, None, None));
407 entry.0 = Some(e);
408 entry.1 = Some(nu);
409 }
410 block = CcxBlock::None;
411 }
412 CcxBlock::Density(mat_name) => {
413 if let Some(d) = line
414 .split(',')
415 .next()
416 .and_then(|s| s.trim().parse::<f64>().ok())
417 {
418 let entry = mat_data
419 .entry(mat_name.clone())
420 .or_insert((None, None, None));
421 entry.2 = Some(d);
422 }
423 block = CcxBlock::None;
424 }
425 CcxBlock::Boundary => {
426 if let Some(bc) = parse_boundary_line(line)
427 && in_step
428 && let Some(step) = mesh.steps.last_mut()
429 {
430 step.bcs.push(bc);
431 }
432 }
434 CcxBlock::Cload => {
435 if let Some(force) = parse_cload_line(line)
436 && in_step
437 && let Some(step) = mesh.steps.last_mut()
438 {
439 step.forces.push(force);
440 }
441 }
442 CcxBlock::Static => {
443 let parts: Vec<&str> = line.split(',').collect();
445 if parts.len() >= 2
446 && let (Some(inc), Some(period)) = (
447 parts[0].trim().parse::<f64>().ok(),
448 parts[1].trim().parse::<f64>().ok(),
449 )
450 && let Some(step) = mesh.steps.last_mut()
451 {
452 step.initial_increment = inc;
453 step.time_period = period;
454 }
455 block = CcxBlock::None;
456 }
457 CcxBlock::Elset(name) => {
458 let ids: Vec<usize> = line
459 .split(',')
460 .filter_map(|s| s.trim().parse::<usize>().ok())
461 .collect();
462 mesh.element_sets
463 .entry(name.clone())
464 .or_default()
465 .extend(ids);
466 }
467 CcxBlock::Nset(name) => {
468 let ids: Vec<usize> = line
469 .split(',')
470 .filter_map(|s| s.trim().parse::<usize>().ok())
471 .collect();
472 mesh.node_sets.entry(name.clone()).or_default().extend(ids);
473 }
474 }
475 }
476
477 for (mat_id, (name, (young, poisson, density))) in (1usize..).zip(mat_data.iter()) {
479 let e = young.unwrap_or(0.0);
480 let nu = poisson.unwrap_or(0.0);
481 let rho = density.unwrap_or(0.0);
482 mesh.materials
483 .push(LinearElasticMaterial::new(mat_id, name.clone(), e, nu, rho));
484 }
485
486 Ok(mesh)
487 }
488}
489
490fn extract_param(line: &str, key: &str) -> Option<String> {
496 let upper = line.to_uppercase();
497 let key_eq = format!("{}=", key.to_uppercase());
498 let pos = upper.find(&key_eq)?;
499 let rest = &line[pos + key_eq.len()..];
500 let end = rest.find(',').unwrap_or(rest.len());
501 let val = rest[..end].trim();
502 if val.is_empty() {
503 None
504 } else {
505 Some(val.to_string())
506 }
507}
508
509fn parse_node_line(line: &str) -> Option<FeNode> {
511 let parts: Vec<&str> = line.split(',').collect();
512 if parts.len() < 4 {
513 return None;
514 }
515 let id: usize = parts[0].trim().parse().ok()?;
516 let x: f64 = parts[1].trim().parse().ok()?;
517 let y: f64 = parts[2].trim().parse().ok()?;
518 let z: f64 = parts[3].trim().parse().ok()?;
519 Some(FeNode::new(id, [x, y, z]))
520}
521
522fn parse_element_line(line: &str, etype: FeElementType) -> Option<FeElement> {
524 let parts: Vec<&str> = line.split(',').collect();
525 if parts.len() < 2 {
526 return None;
527 }
528 let id: usize = parts[0].trim().parse().ok()?;
529 let connectivity: Vec<usize> = parts[1..]
530 .iter()
531 .filter_map(|s| s.trim().parse::<usize>().ok())
532 .collect();
533 if connectivity.is_empty() {
534 return None;
535 }
536 Some(FeElement::new(id, etype, connectivity))
537}
538
539fn parse_boundary_line(line: &str) -> Option<DirichletBc> {
545 let parts: Vec<&str> = line.split(',').collect();
546 if parts.len() < 3 {
547 return None;
548 }
549 let node_id: usize = parts[0].trim().parse().ok()?;
550 let dof_start: u8 = parts[1].trim().parse().ok()?;
551 let dof_end: u8 = parts[2].trim().parse().ok()?;
552 let value: f64 = if parts.len() >= 4 {
553 parts[3].trim().parse().unwrap_or(0.0)
554 } else {
555 0.0
556 };
557
558 if dof_start == dof_end {
565 Some(DirichletBc {
566 node_id,
567 dof: dof_start,
568 value,
569 })
570 } else {
571 Some(DirichletBc {
573 node_id,
574 dof: dof_start,
575 value,
576 })
577 }
578}
579
580pub fn parse_boundary_line_expanded(line: &str) -> Vec<DirichletBc> {
583 let parts: Vec<&str> = line.split(',').collect();
584 if parts.len() < 3 {
585 return Vec::new();
586 }
587 let node_id = match parts[0].trim().parse::<usize>() {
588 Ok(v) => v,
589 Err(_) => return Vec::new(),
590 };
591 let dof_start = match parts[1].trim().parse::<u8>() {
592 Ok(v) => v,
593 Err(_) => return Vec::new(),
594 };
595 let dof_end = match parts[2].trim().parse::<u8>() {
596 Ok(v) => v,
597 Err(_) => return Vec::new(),
598 };
599 let value: f64 = if parts.len() >= 4 {
600 parts[3].trim().parse().unwrap_or(0.0)
601 } else {
602 0.0
603 };
604
605 let mut bcs = Vec::new();
606 let lo = dof_start.min(dof_end);
607 let hi = dof_start.max(dof_end);
608 for dof in lo..=hi {
609 bcs.push(DirichletBc {
610 node_id,
611 dof,
612 value,
613 });
614 }
615 bcs
616}
617
618fn parse_cload_line(line: &str) -> Option<NodalForce> {
620 let parts: Vec<&str> = line.split(',').collect();
621 if parts.len() < 3 {
622 return None;
623 }
624 let node_id: usize = parts[0].trim().parse().ok()?;
625 let dof: u8 = parts[1].trim().parse().ok()?;
626 let magnitude: f64 = parts[2].trim().parse().ok()?;
627 Some(NodalForce::new(node_id, dof, magnitude))
628}
629
630#[cfg(test)]
635mod tests {
636 use super::*;
637 use std::path::Path;
638
639 #[test]
642 fn test_fe_type_to_calculix_tet4() {
643 assert_eq!(fe_type_to_calculix(&FeElementType::Tet4), "C3D4");
644 }
645
646 #[test]
647 fn test_fe_type_to_calculix_tet10() {
648 assert_eq!(fe_type_to_calculix(&FeElementType::Tet10), "C3D10");
649 }
650
651 #[test]
652 fn test_fe_type_to_calculix_hex8() {
653 assert_eq!(fe_type_to_calculix(&FeElementType::Hex8), "C3D8");
654 }
655
656 #[test]
657 fn test_fe_type_to_calculix_hex20() {
658 assert_eq!(fe_type_to_calculix(&FeElementType::Hex20), "C3D20");
659 }
660
661 #[test]
662 fn test_fe_type_to_calculix_tri3() {
663 assert_eq!(fe_type_to_calculix(&FeElementType::Tri3), "S3");
664 }
665
666 #[test]
667 fn test_fe_type_to_calculix_tri6() {
668 assert_eq!(fe_type_to_calculix(&FeElementType::Tri6), "S6");
669 }
670
671 #[test]
672 fn test_fe_type_to_calculix_quad4() {
673 assert_eq!(fe_type_to_calculix(&FeElementType::Quad4), "S4");
674 }
675
676 #[test]
677 fn test_fe_type_to_calculix_quad8() {
678 assert_eq!(fe_type_to_calculix(&FeElementType::Quad8), "S8");
679 }
680
681 #[test]
682 fn test_fe_type_to_calculix_line2() {
683 assert_eq!(fe_type_to_calculix(&FeElementType::Line2), "T3D2");
684 }
685
686 #[test]
687 fn test_calculix_type_to_fe_roundtrip() {
688 let types = vec![
689 FeElementType::Tet4,
690 FeElementType::Tet10,
691 FeElementType::Hex8,
692 FeElementType::Hex20,
693 FeElementType::Tri3,
694 FeElementType::Tri6,
695 FeElementType::Quad4,
696 FeElementType::Quad8,
697 FeElementType::Line2,
698 ];
699 for t in types {
700 let ccx_str = fe_type_to_calculix(&t);
701 let back = calculix_type_to_fe(ccx_str);
702 assert_eq!(back, t, "roundtrip failed for {ccx_str}");
703 }
704 }
705
706 #[test]
707 fn test_calculix_type_to_fe_case_insensitive() {
708 assert_eq!(calculix_type_to_fe("c3d4"), FeElementType::Tet4);
709 assert_eq!(calculix_type_to_fe("C3D10"), FeElementType::Tet10);
710 assert_eq!(calculix_type_to_fe("s8"), FeElementType::Quad8);
711 }
712
713 #[test]
714 fn test_calculix_type_to_fe_unknown() {
715 match calculix_type_to_fe("FOOBAR") {
716 FeElementType::Unknown(s) => assert_eq!(s, "FOOBAR"),
717 other => panic!("expected Unknown, got {:?}", other),
718 }
719 }
720
721 #[test]
724 fn test_extract_param_type() {
725 let line = "*ELEMENT, TYPE=C3D8";
726 assert_eq!(extract_param(line, "TYPE"), Some("C3D8".to_string()));
727 }
728
729 #[test]
730 fn test_extract_param_name() {
731 let line = "*MATERIAL, NAME=Steel";
732 assert_eq!(extract_param(line, "NAME"), Some("Steel".to_string()));
733 }
734
735 #[test]
736 fn test_extract_param_missing() {
737 let line = "*NODE";
738 assert_eq!(extract_param(line, "TYPE"), None);
739 }
740
741 #[test]
742 fn test_parse_node_line_valid() {
743 let n = parse_node_line("1, 0.5, 1.0, 2.0");
744 assert!(n.is_some());
745 let n = n.expect("should parse");
746 assert_eq!(n.id, 1);
747 assert!((n.coords[0] - 0.5).abs() < 1e-12);
748 assert!((n.coords[1] - 1.0).abs() < 1e-12);
749 assert!((n.coords[2] - 2.0).abs() < 1e-12);
750 }
751
752 #[test]
753 fn test_parse_node_line_invalid() {
754 assert!(parse_node_line("1, 0.5").is_none());
755 }
756
757 #[test]
758 fn test_parse_element_line_valid() {
759 let el = parse_element_line("1, 10, 20, 30, 40", FeElementType::Tet4);
760 assert!(el.is_some());
761 let el = el.expect("should parse");
762 assert_eq!(el.id, 1);
763 assert_eq!(el.connectivity, vec![10, 20, 30, 40]);
764 }
765
766 #[test]
767 fn test_parse_element_line_too_short() {
768 assert!(parse_element_line("1", FeElementType::Hex8).is_none());
769 }
770
771 #[test]
774 fn test_parse_boundary_line_with_value() {
775 let bc = parse_boundary_line("5, 1, 1, 0.01");
776 assert!(bc.is_some());
777 let bc = bc.expect("should parse");
778 assert_eq!(bc.node_id, 5);
779 assert_eq!(bc.dof, 1);
780 assert!((bc.value - 0.01).abs() < 1e-15);
781 }
782
783 #[test]
784 fn test_parse_boundary_line_zero_value() {
785 let bc = parse_boundary_line("10, 2, 2");
786 assert!(bc.is_some());
787 let bc = bc.expect("should parse");
788 assert_eq!(bc.node_id, 10);
789 assert_eq!(bc.dof, 2);
790 assert!((bc.value).abs() < 1e-15);
791 }
792
793 #[test]
794 fn test_parse_boundary_line_invalid() {
795 assert!(parse_boundary_line("bad, data").is_none());
796 }
797
798 #[test]
799 fn test_parse_boundary_line_expanded_range() {
800 let bcs = parse_boundary_line_expanded("1, 1, 3, 0.0");
801 assert_eq!(bcs.len(), 3);
802 assert_eq!(bcs[0].dof, 1);
803 assert_eq!(bcs[1].dof, 2);
804 assert_eq!(bcs[2].dof, 3);
805 }
806
807 #[test]
810 fn test_parse_cload_line_valid() {
811 let f = parse_cload_line("7, 2, -1000.0");
812 assert!(f.is_some());
813 let f = f.expect("should parse");
814 assert_eq!(f.node_id, 7);
815 assert_eq!(f.dof, 2);
816 assert!((f.magnitude - (-1000.0)).abs() < 1e-10);
817 }
818
819 #[test]
820 fn test_parse_cload_line_invalid() {
821 assert!(parse_cload_line("7, 2").is_none());
822 }
823
824 fn sample_mesh() -> FeMesh {
827 let mut mesh = FeMesh::new();
828 mesh.nodes = vec![
829 FeNode::new(1, [0.0, 0.0, 0.0]),
830 FeNode::new(2, [1.0, 0.0, 0.0]),
831 FeNode::new(3, [0.0, 1.0, 0.0]),
832 FeNode::new(4, [0.0, 0.0, 1.0]),
833 ];
834 mesh.elements = vec![FeElement::new(1, FeElementType::Tet4, vec![1, 2, 3, 4])];
835 mesh.materials = vec![LinearElasticMaterial::new(1, "Steel", 210e9, 0.3, 7800.0)];
836
837 let mut step = AnalysisStep::new("LoadStep");
838 step.bcs.push(DirichletBc::fixed(1, 1));
839 step.bcs.push(DirichletBc::fixed(1, 2));
840 step.bcs.push(DirichletBc::fixed(1, 3));
841 step.forces.push(NodalForce::new(4, 2, -1000.0));
842 step.initial_increment = 0.1;
843 step.time_period = 1.0;
844 mesh.steps.push(step);
845
846 mesh
847 }
848
849 fn tmp_path(name: &str) -> String {
850 let dir = std::env::temp_dir();
851 dir.join(name).to_string_lossy().to_string()
852 }
853
854 #[test]
855 fn test_write_creates_file() {
856 let path = tmp_path("oxiphysics_ccx_write.inp");
857 let mesh = sample_mesh();
858 CalculixWriter::new()
859 .write(&mesh, &path)
860 .expect("write failed");
861 assert!(Path::new(&path).exists());
862 }
863
864 #[test]
865 fn test_roundtrip_node_count() {
866 let path = tmp_path("oxiphysics_ccx_rt_nodes.inp");
867 let mesh = sample_mesh();
868 CalculixWriter::new()
869 .write(&mesh, &path)
870 .expect("write failed");
871 let parsed = CalculixReader::new().parse(&path).expect("parse failed");
872 assert_eq!(parsed.nodes.len(), mesh.nodes.len());
873 }
874
875 #[test]
876 fn test_roundtrip_node_ids() {
877 let path = tmp_path("oxiphysics_ccx_rt_nids.inp");
878 let mesh = sample_mesh();
879 CalculixWriter::new().write(&mesh, &path).expect("write");
880 let parsed = CalculixReader::new().parse(&path).expect("parse");
881 let ids: Vec<usize> = parsed.nodes.iter().map(|n| n.id).collect();
882 assert_eq!(ids, vec![1, 2, 3, 4]);
883 }
884
885 #[test]
886 fn test_roundtrip_node_coords() {
887 let path = tmp_path("oxiphysics_ccx_rt_coords.inp");
888 let mesh = sample_mesh();
889 CalculixWriter::new().write(&mesh, &path).expect("write");
890 let parsed = CalculixReader::new().parse(&path).expect("parse");
891 assert!((parsed.nodes[0].coords[0]).abs() < 1e-10);
892 assert!((parsed.nodes[1].coords[0] - 1.0).abs() < 1e-10);
893 assert!((parsed.nodes[2].coords[1] - 1.0).abs() < 1e-10);
894 assert!((parsed.nodes[3].coords[2] - 1.0).abs() < 1e-10);
895 }
896
897 #[test]
898 fn test_roundtrip_element_count() {
899 let path = tmp_path("oxiphysics_ccx_rt_elems.inp");
900 let mesh = sample_mesh();
901 CalculixWriter::new().write(&mesh, &path).expect("write");
902 let parsed = CalculixReader::new().parse(&path).expect("parse");
903 assert_eq!(parsed.elements.len(), 1);
904 }
905
906 #[test]
907 fn test_roundtrip_element_type() {
908 let path = tmp_path("oxiphysics_ccx_rt_etype.inp");
909 let mesh = sample_mesh();
910 CalculixWriter::new().write(&mesh, &path).expect("write");
911 let parsed = CalculixReader::new().parse(&path).expect("parse");
912 assert_eq!(parsed.elements[0].element_type, FeElementType::Tet4);
913 }
914
915 #[test]
916 fn test_roundtrip_element_connectivity() {
917 let path = tmp_path("oxiphysics_ccx_rt_econn.inp");
918 let mesh = sample_mesh();
919 CalculixWriter::new().write(&mesh, &path).expect("write");
920 let parsed = CalculixReader::new().parse(&path).expect("parse");
921 assert_eq!(parsed.elements[0].connectivity, vec![1, 2, 3, 4]);
922 }
923
924 #[test]
925 fn test_roundtrip_material() {
926 let path = tmp_path("oxiphysics_ccx_rt_mat.inp");
927 let mesh = sample_mesh();
928 CalculixWriter::new().write(&mesh, &path).expect("write");
929 let parsed = CalculixReader::new().parse(&path).expect("parse");
930 assert_eq!(parsed.materials.len(), 1);
931 let mat = &parsed.materials[0];
932 assert_eq!(mat.name, "Steel");
933 assert!((mat.young_modulus - 210e9).abs() < 1.0);
934 assert!((mat.poisson_ratio - 0.3).abs() < 1e-10);
935 assert!((mat.density - 7800.0).abs() < 1e-6);
936 }
937
938 #[test]
939 fn test_roundtrip_boundary_conditions() {
940 let path = tmp_path("oxiphysics_ccx_rt_bc.inp");
941 let mesh = sample_mesh();
942 CalculixWriter::new().write(&mesh, &path).expect("write");
943 let parsed = CalculixReader::new().parse(&path).expect("parse");
944 assert_eq!(parsed.steps.len(), 1);
945 assert_eq!(parsed.steps[0].bcs.len(), 3);
946 assert_eq!(parsed.steps[0].bcs[0].node_id, 1);
947 assert_eq!(parsed.steps[0].bcs[0].dof, 1);
948 assert!((parsed.steps[0].bcs[0].value).abs() < 1e-15);
949 }
950
951 #[test]
952 fn test_roundtrip_cload() {
953 let path = tmp_path("oxiphysics_ccx_rt_cload.inp");
954 let mesh = sample_mesh();
955 CalculixWriter::new().write(&mesh, &path).expect("write");
956 let parsed = CalculixReader::new().parse(&path).expect("parse");
957 assert_eq!(parsed.steps[0].forces.len(), 1);
958 let f = &parsed.steps[0].forces[0];
959 assert_eq!(f.node_id, 4);
960 assert_eq!(f.dof, 2);
961 assert!((f.magnitude - (-1000.0)).abs() < 1e-6);
962 }
963
964 #[test]
965 fn test_roundtrip_step_timing() {
966 let path = tmp_path("oxiphysics_ccx_rt_step.inp");
967 let mesh = sample_mesh();
968 CalculixWriter::new().write(&mesh, &path).expect("write");
969 let parsed = CalculixReader::new().parse(&path).expect("parse");
970 let step = &parsed.steps[0];
971 assert!((step.initial_increment - 0.1).abs() < 1e-6);
972 assert!((step.time_period - 1.0).abs() < 1e-6);
973 }
974
975 #[test]
976 fn test_roundtrip_multiple_element_types() {
977 let path = tmp_path("oxiphysics_ccx_rt_multi.inp");
978 let mut mesh = FeMesh::new();
979 mesh.nodes = vec![
980 FeNode::new(1, [0.0, 0.0, 0.0]),
981 FeNode::new(2, [1.0, 0.0, 0.0]),
982 FeNode::new(3, [0.0, 1.0, 0.0]),
983 FeNode::new(4, [0.0, 0.0, 1.0]),
984 FeNode::new(5, [1.0, 1.0, 0.0]),
985 ];
986 mesh.elements = vec![
987 FeElement::new(1, FeElementType::Tet4, vec![1, 2, 3, 4]),
988 FeElement::new(2, FeElementType::Line2, vec![4, 5]),
989 ];
990 CalculixWriter::new().write(&mesh, &path).expect("write");
991 let parsed = CalculixReader::new().parse(&path).expect("parse");
992 assert_eq!(parsed.elements.len(), 2);
993 assert_eq!(parsed.elements[0].element_type, FeElementType::Tet4);
994 assert_eq!(parsed.elements[1].element_type, FeElementType::Line2);
995 }
996
997 #[test]
998 fn test_write_string_contains_heading() {
999 let mesh = sample_mesh();
1000 let content = CalculixWriter::new()
1001 .write_string(&mesh)
1002 .expect("write_string");
1003 assert!(content.contains("*HEADING"));
1004 }
1005
1006 #[test]
1007 fn test_write_string_contains_node() {
1008 let mesh = sample_mesh();
1009 let content = CalculixWriter::new()
1010 .write_string(&mesh)
1011 .expect("write_string");
1012 assert!(content.contains("*NODE"));
1013 }
1014
1015 #[test]
1016 fn test_write_string_contains_element() {
1017 let mesh = sample_mesh();
1018 let content = CalculixWriter::new()
1019 .write_string(&mesh)
1020 .expect("write_string");
1021 assert!(content.contains("*ELEMENT"));
1022 }
1023
1024 #[test]
1025 fn test_write_string_contains_material() {
1026 let mesh = sample_mesh();
1027 let content = CalculixWriter::new()
1028 .write_string(&mesh)
1029 .expect("write_string");
1030 assert!(content.contains("*MATERIAL"));
1031 assert!(content.contains("*DENSITY"));
1032 assert!(content.contains("*ELASTIC"));
1033 }
1034
1035 #[test]
1036 fn test_write_string_contains_step() {
1037 let mesh = sample_mesh();
1038 let content = CalculixWriter::new()
1039 .write_string(&mesh)
1040 .expect("write_string");
1041 assert!(content.contains("*STEP"));
1042 assert!(content.contains("*STATIC"));
1043 assert!(content.contains("*END STEP"));
1044 }
1045
1046 #[test]
1047 fn test_write_string_contains_boundary() {
1048 let mesh = sample_mesh();
1049 let content = CalculixWriter::new()
1050 .write_string(&mesh)
1051 .expect("write_string");
1052 assert!(content.contains("*BOUNDARY"));
1053 }
1054
1055 #[test]
1056 fn test_write_string_contains_cload() {
1057 let mesh = sample_mesh();
1058 let content = CalculixWriter::new()
1059 .write_string(&mesh)
1060 .expect("write_string");
1061 assert!(content.contains("*CLOAD"));
1062 }
1063
1064 #[test]
1065 fn test_parse_empty_file() {
1066 let reader = CalculixReader::new();
1067 let mesh = reader.parse_string("** empty file\n").expect("parse");
1068 assert!(mesh.nodes.is_empty());
1069 assert!(mesh.elements.is_empty());
1070 }
1071
1072 #[test]
1073 fn test_parse_missing_file() {
1074 let result = CalculixReader::new().parse("/tmp/does_not_exist_oxiphysics_ccx.inp");
1075 assert!(result.is_err());
1076 }
1077
1078 #[test]
1079 fn test_roundtrip_precision() {
1080 let path = tmp_path("oxiphysics_ccx_rt_prec.inp");
1081 let mut mesh = FeMesh::new();
1082 mesh.nodes.push(FeNode::new(
1083 1,
1084 [1.23456789012345, -9.87654321098765, 2.89793238462643],
1085 ));
1086 CalculixWriter::new().write(&mesh, &path).expect("write");
1087 let parsed = CalculixReader::new().parse(&path).expect("parse");
1088 let c = parsed.nodes[0].coords;
1089 assert!((c[0] - 1.23456789012345).abs() < 1e-10);
1090 assert!((c[1] - (-9.87654321098765)).abs() < 1e-10);
1091 assert!((c[2] - 2.89793238462643).abs() < 1e-10);
1092 }
1093
1094 #[test]
1095 fn test_large_mesh_roundtrip() {
1096 let path = tmp_path("oxiphysics_ccx_rt_large.inp");
1097 let mut mesh = FeMesh::new();
1098 for i in 1..=100 {
1099 mesh.nodes.push(FeNode::new(i, [i as f64, 0.0, 0.0]));
1100 }
1101 for i in 0..24usize {
1102 mesh.elements.push(FeElement::new(
1103 i + 1,
1104 FeElementType::Tet4,
1105 vec![
1106 i * 4 % 97 + 1,
1107 i * 4 % 97 + 2,
1108 i * 4 % 97 + 3,
1109 i * 4 % 97 + 4,
1110 ],
1111 ));
1112 }
1113 CalculixWriter::new().write(&mesh, &path).expect("write");
1114 let parsed = CalculixReader::new().parse(&path).expect("parse");
1115 assert_eq!(parsed.nodes.len(), 100);
1116 assert_eq!(parsed.elements.len(), 24);
1117 }
1118
1119 #[test]
1120 fn test_hex8_roundtrip() {
1121 let path = tmp_path("oxiphysics_ccx_rt_hex8.inp");
1122 let mut mesh = FeMesh::new();
1123 for i in 1..=8 {
1124 mesh.nodes.push(FeNode::new(i, [i as f64, 0.0, 0.0]));
1125 }
1126 mesh.elements.push(FeElement::new(
1127 1,
1128 FeElementType::Hex8,
1129 vec![1, 2, 3, 4, 5, 6, 7, 8],
1130 ));
1131 CalculixWriter::new().write(&mesh, &path).expect("write");
1132 let parsed = CalculixReader::new().parse(&path).expect("parse");
1133 assert_eq!(parsed.elements[0].element_type, FeElementType::Hex8);
1134 assert_eq!(
1135 parsed.elements[0].connectivity,
1136 vec![1, 2, 3, 4, 5, 6, 7, 8]
1137 );
1138 }
1139
1140 #[test]
1141 fn test_parse_string_with_nset_and_elset() {
1142 let inp = "\
1143*NODE
11441, 0.0, 0.0, 0.0
11452, 1.0, 0.0, 0.0
1146*ELEMENT, TYPE=T3D2
11471, 1, 2
1148*NSET, NSET=FIXED
11491
1150*ELSET, ELSET=ALL
11511
1152";
1153 let reader = CalculixReader::new();
1154 let mesh = reader.parse_string(inp).expect("parse");
1155 assert_eq!(mesh.nodes.len(), 2);
1156 assert_eq!(mesh.elements.len(), 1);
1157 assert_eq!(mesh.node_sets.get("FIXED"), Some(&vec![1]));
1158 assert_eq!(mesh.element_sets.get("ALL"), Some(&vec![1]));
1159 }
1160
1161 #[test]
1162 fn test_parse_boundary_prescribed() {
1163 let inp = "\
1164*NODE
11651, 0.0, 0.0, 0.0
1166*STEP
1167*STATIC
11680.01, 1.0
1169*BOUNDARY
11701, 1, 1, 0.05
1171*END STEP
1172";
1173 let reader = CalculixReader::new();
1174 let mesh = reader.parse_string(inp).expect("parse");
1175 assert_eq!(mesh.steps.len(), 1);
1176 assert_eq!(mesh.steps[0].bcs.len(), 1);
1177 assert_eq!(mesh.steps[0].bcs[0].node_id, 1);
1178 assert_eq!(mesh.steps[0].bcs[0].dof, 1);
1179 assert!((mesh.steps[0].bcs[0].value - 0.05).abs() < 1e-15);
1180 }
1181}