Files
rustytorch/crates/specialized/rtx-fem-export/src/lsdyna/reader.rs
T
2026-03-04 00:08:42 +00:00

400 lines
12 KiB
Rust

//! LS-DYNA keyword file reader.
use crate::error::{FemExportError, Result};
use crate::lsdyna::keyword::{Keyword, KeywordData, parse_field};
use crate::model::{Element, ElementType, FEModel, Material, Node, Part};
use rtx_materials::{KelvinMaxwell, LinearElastic};
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
/// Read an FE model from an LS-DYNA keyword file.
pub fn read_k_file(path: impl AsRef<Path>) -> Result<FEModel> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let keyword_blocks = parse_keyword_blocks(reader)?;
let model = build_model_from_blocks(keyword_blocks)?;
Ok(model)
}
/// Parse keyword blocks from the file.
fn parse_keyword_blocks<R: BufRead>(reader: R) -> Result<Vec<KeywordData>> {
let mut blocks = Vec::new();
let mut current_block: Option<KeywordData> = None;
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
// Skip empty lines and comments
if trimmed.is_empty() {
continue;
}
// Check for keyword line
if trimmed.starts_with('*') {
// Save previous block
if let Some(block) = current_block.take() {
blocks.push(block);
}
// Start new block
let keyword = Keyword::from_str(trimmed);
if keyword != Keyword::End {
current_block = Some(KeywordData::new(keyword));
}
} else if trimmed.starts_with('$') {
// Comment line - skip or extract parameters
continue;
} else if let Some(ref mut block) = current_block {
// Data line
block.add_line(&line);
}
}
// Save last block
if let Some(block) = current_block {
blocks.push(block);
}
Ok(blocks)
}
/// Build an FE model from parsed keyword blocks.
fn build_model_from_blocks(blocks: Vec<KeywordData>) -> Result<FEModel> {
let mut model = FEModel::new("", "");
let mut materials: HashMap<u64, Material> = HashMap::new();
let mut sections: HashMap<u64, ElementType> = HashMap::new();
for block in blocks {
match block.keyword {
Keyword::Title => {
if let Some(title) = block.lines.first() {
model.title = title.trim().to_string();
}
}
Keyword::Node => {
parse_nodes(&block, &mut model)?;
}
Keyword::ElementSolid => {
parse_solid_elements(&block, &mut model)?;
}
Keyword::Part => {
parse_parts(&block, &mut model, &sections)?;
}
Keyword::SectionSolid => {
parse_section_solid(&block, &mut sections)?;
}
Keyword::MatElastic => {
parse_mat_elastic(&block, &mut materials)?;
}
Keyword::MatKelvinMaxwellViscoelastic => {
parse_mat_kelvin_maxwell(&block, &mut materials)?;
}
_ => {
// Ignore unsupported keywords
}
}
}
model.materials = materials;
Ok(model)
}
/// Parse *NODE section.
fn parse_nodes(block: &KeywordData, model: &mut FEModel) -> Result<()> {
for line in &block.lines {
if line.trim().is_empty() || line.trim().starts_with('$') {
continue;
}
// LS-DYNA NODE format: nid, x, y, z, tc, rc
// Fields are typically: 8, 16, 16, 16, 8, 8
let id: u64 = parse_field(line, 0, 8).ok_or_else(|| {
FemExportError::ParseError(format!("Failed to parse node ID from: {}", line))
})?;
let x: f64 = parse_field(line, 8, 16).unwrap_or(0.0);
let y: f64 = parse_field(line, 24, 16).unwrap_or(0.0);
let z: f64 = parse_field(line, 40, 16).unwrap_or(0.0);
model.add_node(Node::new(id, x, y, z));
}
Ok(())
}
/// Parse *ELEMENT_SOLID section.
fn parse_solid_elements(block: &KeywordData, model: &mut FEModel) -> Result<()> {
for line in &block.lines {
if line.trim().is_empty() || line.trim().starts_with('$') {
continue;
}
// LS-DYNA ELEMENT_SOLID format: eid, pid, n1-n8 (or more for higher order)
// Fields are typically 8 characters each
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 6 {
continue;
}
let id: u64 = parts[0].parse().map_err(|_| {
FemExportError::ParseError(format!("Failed to parse element ID: {}", parts[0]))
})?;
let part_id: u64 = parts[1].parse().map_err(|_| {
FemExportError::ParseError(format!("Failed to parse part ID: {}", parts[1]))
})?;
let node_ids: Vec<u64> = parts[2..].iter().filter_map(|s| s.parse().ok()).collect();
// Determine element type from connectivity
let element_type = match node_ids.len() {
4 => ElementType::Tet4,
8 => {
// Check if it's a collapsed tet (all last 4 nodes same)
if node_ids[4] == node_ids[5]
&& node_ids[5] == node_ids[6]
&& node_ids[6] == node_ids[7]
{
ElementType::Tet4
} else {
ElementType::Hex8
}
}
10 => ElementType::Tet10,
20 => ElementType::Hex20,
_ => {
// Default to treating it as whatever nodes we have
if node_ids.len() <= 4 {
ElementType::Tet4
} else {
ElementType::Hex8
}
}
};
// For collapsed tets, only keep unique nodes
let final_nodes = if element_type == ElementType::Tet4 && node_ids.len() > 4 {
node_ids[0..4].to_vec()
} else {
node_ids
};
model.add_element(Element::new(id, part_id, element_type, final_nodes));
}
Ok(())
}
/// Parse *PART section.
fn parse_parts(
block: &KeywordData,
model: &mut FEModel,
sections: &HashMap<u64, ElementType>,
) -> Result<()> {
let mut name = String::new();
let mut line_idx = 0;
for line in &block.lines {
if line.trim().is_empty() || line.trim().starts_with('$') {
continue;
}
if line_idx == 0 {
// First line is the part title
name = line.trim().to_string();
} else {
// Second line has: pid, secid, mid, eosid, hgid, grav, adpopt, tmid
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 3 {
let id: u64 = parts[0].parse().unwrap_or(0);
let section_id: u64 = parts[1].parse().unwrap_or(0);
let material_id: u64 = parts[2].parse().unwrap_or(0);
let element_type = sections
.get(&section_id)
.copied()
.unwrap_or(ElementType::Tet4);
model.add_part(Part::new(id, &name, section_id, material_id, element_type));
}
}
line_idx += 1;
}
Ok(())
}
/// Parse *SECTION_SOLID.
fn parse_section_solid(
block: &KeywordData,
sections: &mut HashMap<u64, ElementType>,
) -> Result<()> {
for line in &block.lines {
if line.trim().is_empty() || line.trim().starts_with('$') {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let id: u64 = parts[0].parse().unwrap_or(0);
let elform: i32 = parts[1].parse().unwrap_or(0);
let element_type = match elform {
10 | 13 => ElementType::Tet4,
16 | 17 => ElementType::Tet10,
1..=3 => ElementType::Hex8,
_ => ElementType::Tet4,
};
sections.insert(id, element_type);
}
break; // Only process first data line
}
Ok(())
}
/// Parse *MAT_ELASTIC.
fn parse_mat_elastic(block: &KeywordData, materials: &mut HashMap<u64, Material>) -> Result<()> {
for line in &block.lines {
if line.trim().is_empty() || line.trim().starts_with('$') {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 {
let id: u64 = parts[0].parse().unwrap_or(0);
let density: f64 = parts[1].parse().unwrap_or(0.0);
let youngs_modulus: f64 = parts[2].parse().unwrap_or(0.0);
let poissons_ratio: f64 = parts[3].parse().unwrap_or(0.3);
// LinearElastic::new takes (youngs_modulus, poissons_ratio, density)
let material = LinearElastic::new(youngs_modulus, poissons_ratio, density);
materials.insert(id, Material::Elastic(material));
}
break; // Only process first data line
}
Ok(())
}
/// Parse *MAT_KELVIN-MAXWELL_VISCOELASTIC.
fn parse_mat_kelvin_maxwell(
block: &KeywordData,
materials: &mut HashMap<u64, Material>,
) -> Result<()> {
let mut id: u64 = 0;
let mut density: f64 = 0.0;
let mut bulk: f64 = 0.0;
let mut g0: f64 = 0.0;
let mut gi: f64 = 0.0;
let mut beta_i: f64 = 0.0;
let mut data_line = 0;
for line in &block.lines {
if line.trim().is_empty() || line.trim().starts_with('$') {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
match data_line {
0 => {
// First data line: mid, ro, bulk, g0
if parts.len() >= 4 {
id = parts[0].parse().unwrap_or(0);
density = parts[1].parse().unwrap_or(0.0);
bulk = parts[2].parse().unwrap_or(0.0);
g0 = parts[3].parse().unwrap_or(0.0);
}
}
1 => {
// Second data line: gi, betai
if parts.len() >= 2 {
gi = parts[0].parse().unwrap_or(0.0);
beta_i = parts[1].parse().unwrap_or(0.0);
}
let material = KelvinMaxwell::new(density, bulk, g0, gi, beta_i);
materials.insert(id, Material::KelvinMaxwell(material));
break;
}
_ => break,
}
data_line += 1;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_parse_simple_k_file() {
let k_content = r#"*KEYWORD
*TITLE
Test Model
*NODE
1 0.000000 0.000000 0.000000
2 1.000000 0.000000 0.000000
3 0.500000 1.000000 0.000000
4 0.500000 0.500000 1.000000
*MAT_ELASTIC
1 1.0400e+03 3.0000e+03 4.9000e-01
*SECTION_SOLID
1 10
*PART
Brain
1 1 1
*ELEMENT_SOLID
1 1 1 2 3 4 4 4 4 4
*END
"#;
let reader = BufReader::new(Cursor::new(k_content));
let blocks = parse_keyword_blocks(reader).unwrap();
// Check we found the expected blocks
let keywords: Vec<_> = blocks.iter().map(|b| &b.keyword).collect();
assert!(keywords.contains(&&Keyword::Title));
assert!(keywords.contains(&&Keyword::Node));
assert!(keywords.contains(&&Keyword::MatElastic));
assert!(keywords.contains(&&Keyword::ElementSolid));
}
#[test]
fn test_build_model() {
let k_content = r#"*KEYWORD
*TITLE
Test Model
*NODE
1 0.000000 0.000000 0.000000
2 1.000000 0.000000 0.000000
3 0.500000 1.000000 0.000000
4 0.500000 0.500000 1.000000
*MAT_ELASTIC
1 1040.0 3000.0 0.49
*SECTION_SOLID
1 10
*PART
Brain
1 1 1
*ELEMENT_SOLID
1 1 1 2 3 4 4 4 4 4
*END
"#;
let reader = BufReader::new(Cursor::new(k_content));
let blocks = parse_keyword_blocks(reader).unwrap();
let model = build_model_from_blocks(blocks).unwrap();
assert_eq!(model.title, "Test Model");
assert_eq!(model.num_nodes(), 4);
assert_eq!(model.num_elements(), 1);
assert_eq!(model.materials.len(), 1);
}
}