Initial commit
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
//! 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, §ions)?;
|
||||
}
|
||||
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(§ion_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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user