feat(io): add Abaqus mesh parsing implementation

New 305-line Abaqus mesh parser:
- element_has_nodes(), element_has_type(): Abaqus element type mappings
- Supports C3D4, C3D8, C3D10, C3D20, S3, CPS4, T2D2, T3D2, B33, etc.
- parse_abaqus(): main parsing function for .inp files
- parse_numbers(), parse_definition(), matchset(): parsing utilities
- empty_or_comment_line(): comment/empty line detection
- Required by AbaqusReader module for mesh reading

Provides core Abaqus .inp file mesh parsing functionality.
This commit is contained in:
Jukka Aho
2025-12-15 06:12:43 +02:00
parent d9fcc6f80c
commit ee536f7a53
+305
View File
@@ -0,0 +1,305 @@
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/AbaqusReader.jl/blob/master/LICENSE
import Base.parse
# using Logging # Already imported in JuliaFEM.jl
# Define element type and number of nodes in element
element_has_nodes(::Type{Val{:C3D4}}) = 4
element_has_type(::Type{Val{:C3D4}}) = :Tet4
element_has_nodes(::Type{Val{:C3D6}}) = 6
element_has_type(::Type{Val{:C3D6}}) = :Wedge6
element_has_nodes(::Type{Val{:C3D4H}}) = 4
element_has_type(::Type{Val{:C3D4H}}) = :Tet4
element_has_nodes(::Type{Val{:C3D8}}) = 8
element_has_type(::Type{Val{:C3D8}}) = :Hex8
element_has_nodes(::Type{Val{:C3D8R}}) = 8
element_has_type(::Type{Val{:C3D8R}}) = :Hex8
element_has_nodes(::Type{Val{:COH3D8}}) = 8
element_has_type(::Type{Val{:COH3D8}}) = :Hex8
element_has_nodes(::Type{Val{:C3D10}}) = 10
element_has_type(::Type{Val{:C3D10}}) = :Tet10
element_has_nodes(::Type{Val{:C3D10H}}) = 10
element_has_type(::Type{Val{:C3D10H}}) = :Tet10
element_has_nodes(::Type{Val{:C3D20}}) = 20
element_has_type(::Type{Val{:C3D20}}) = :Hex20
element_has_nodes(::Type{Val{:C3D20E}}) = 20
element_has_nodes(::Type{Val{:S3}}) = 3
element_has_type(::Type{Val{:S3}}) = :Tri3
element_has_nodes(::Type{Val{:CPS3}}) = 3
element_has_type(::Type{Val{:CPS3}}) = :CPS3
element_has_nodes(::Type{Val{:STRI65}}) = 6
element_has_type(::Type{Val{:STRI65}}) = :Tri6
element_has_nodes(::Type{Val{:CPS4}}) = 4
element_has_type(::Type{Val{:CPS4}}) = :Quad4
element_has_nodes(::Type{Val{:CPS4R}}) = 4
element_has_type(::Type{Val{:CPS4R}}) = :Quad4
element_has_nodes(::Type{Val{:T2D2}}) = 2
element_has_type(::Type{Val{:T2D2}}) = :Seg2
element_has_nodes(::Type{Val{:T3D2}}) = 2
element_has_type(::Type{Val{:T3D2}}) = :Seg2
element_has_nodes(::Type{Val{:B33}}) = 2
element_has_type(::Type{Val{:B33}}) = :Seg2
"""Checks for a comment or empty line
Function return true, if line starts with comment character "**"
or has length of 0
"""
function empty_or_comment_line(line::T) where {T<:AbstractString}
startswith(line, "**") || (length(line) == 0)
end
"""Match words from both sides of '=' character
"""
function matchset(definition)
regexp = r"([\w\_\-]+[ ]*=[ ]*[\w\_\-]+)"
collect(m.match for m = eachmatch(regexp, definition))
end
"""Parse string to get set type and name
"""
function parse_definition(definition)
set_defs = Dict()
set_definition = matchset(definition)
set_definition == nothing && return nothing
for x in set_definition
name, vals = map(strip, split(x, "="))
set_defs[lowercase(name)] = vals
end
set_defs
end
"""Parse all the numbers from string
"""
function parse_numbers(line, type_::Type{T})::Vector{T} where {T}
regexp = r"[0-9]+"
matches = collect((m.match for m = eachmatch(regexp, line)))
map(x -> Base.parse(type_, x), matches)
end
"""Add set to model, if set exists
"""
function add_set!(model, definition, model_key, abaqus_key, ids)
has_set_def = parse_definition(definition)
if haskey(has_set_def, "elset")
set_name = has_set_def[abaqus_key]
@debug("Adding $abaqus_key: $set_name")
model[model_key][set_name] = ids
end
end
"""Parse nodes from the lines
"""
function parse_section(model, lines, ::Symbol, idx_start, idx_end, ::Type{Val{:NODE}})
nnodes = 0
ids = Int[]
definition = lines[idx_start]
for line in lines[idx_start+1:idx_end]
if !(empty_or_comment_line(line))
m = collect((m.match for m = eachmatch(r"[-0-9.eE+]+", line)))
node_id = parse(Int, m[1])
coords = parse.(Float64, m[2:end])
model["nodes"][node_id] = coords
push!(ids, node_id)
nnodes += 1
end
end
@debug("$nnodes nodes found")
add_set!(model, definition, "node_sets", "nset", ids)
end
"""Custon regex to find match from string. Index used if there are multiple matches
"""
function regex_match(regex_str, line, idx)
return match(regex_str, line).captures[idx]
end
"""Custom list iterator
Simple iterator for comsuming element list. Depending
on the used element, connectivity nodes might be listed
in multiple lines, which is why iterator is used to handle
this problem.
"""
function consumeList(arr, start, stop)
idx = start - 1
function _it()
idx += 1
if idx > stop
return nothing
end
arr[idx]
end
_it
end
"""Parse elements from input lines
Reads element ids and their connectivity nodes from input lines.
If elset definition exists, also adds the set to model.
"""
function parse_section(model, lines, ::Symbol, idx_start, idx_end, ::Type{Val{:ELEMENT}})
ids = Int[]
definition = lines[idx_start]
regexp = r"TYPE=([\w\-\_]+)"i
m = match(regexp, definition)
m == nothing && error("Could not match regexp $regexp to line $definition")
element_type = uppercase(m[1])
eltype_sym = Symbol(element_type)
eltype_nodes = element_has_nodes(Val{eltype_sym})
element_type = element_has_type(Val{eltype_sym})
@debug("Parsing elements. Type: $(m[1]). Topology: $(element_type)")
list_iterator = consumeList(lines, idx_start + 1, idx_end)
line = list_iterator()
while line != nothing
numbers = parse_numbers(line, Int)
if !(empty_or_comment_line(line))
id = numbers[1]
push!(ids, id)
connectivity = numbers[2:end]
while length(connectivity) != eltype_nodes
@assert length(connectivity) < eltype_nodes
line = list_iterator()
numbers = parse_numbers(line, Int)
push!(connectivity, numbers...)
end
model["elements"][id] = connectivity
model["element_types"][id] = element_type
end
line = list_iterator()
end
add_set!(model, definition, "element_sets", "elset", ids)
end
"""Parse node and elementset from input lines
"""
function parse_section(model, lines, key, idx_start, idx_end, ::Union{Type{Val{:NSET}},
Type{Val{:ELSET}}})
data = Int[]
set_regex_string = Dict(:NSET => r"((?<=NSET=)([\w\-\_]+)|(?<=NSET=\")([\w\-\_\ ]+)(?=\"))"i,
:ELSET => r"((?<=ELSET=)([\w\-\_]+)|(?<=ELSET=\")([\w\-\_\ ]+)(?=\"))"i)
selected_set = key == :NSET ? "node_sets" : "element_sets"
definition = lines[idx_start]
regex_string = set_regex_string[key]
set_name = regex_match(regex_string, definition, 1)
@debug("Creating $(lowercase(string(key))) $set_name")
if endswith(strip(uppercase(definition)), "GENERATE")
line = lines[idx_start+1]
first_id, last_id, step_ = parse_numbers(line, Int)
set_ids = collect(first_id:step_:last_id)
push!(data, set_ids...)
else
for line in lines[idx_start+1:idx_end]
if !(empty_or_comment_line(line))
set_ids = parse_numbers(line, Int)::Vector{Int}
push!(data, set_ids...)
end
end
end
model[selected_set][set_name] = data
end
"""Parse SURFACE keyword
"""
function parse_section(model, lines, ::Symbol, idx_start, idx_end, ::Type{Val{:SURFACE}})
data = Vector{Tuple{Int,Symbol}}()
definition = lines[idx_start]
has_set_def = parse_definition(definition)
has_set_def != nothing || return
set_type = get(has_set_def, "type", "UNKNOWN")
set_name = has_set_def["name"]
for line in lines[idx_start+1:idx_end]
empty_or_comment_line(line) && continue
m = match(r"(?P<element_id>\d+),.*(?P<element_side>S\d+).*", line)
element_id = parse(Int, m[:element_id])
element_side = Symbol(m[:element_side])
push!(data, (element_id, element_side))
end
model["surface_types"][set_name] = Symbol(set_type)
model["surface_sets"][set_name] = data
return
end
"""Find lines, which contain keywords, for example "*NODE"
"""
function find_keywords(lines)
indexes = Int[]
for (idx, line) in enumerate(lines)
if startswith(line, "*") && !startswith(line, "**")
push!(indexes, idx)
end
end
return indexes
end
"""Main function for parsing Abaqus input file.
Function parses Abaqus input file and generates a dictionary of
all the available keywords.
"""
function parse_abaqus(fid::IOStream, verbose::Bool)
model = Dict{String,Dict}()
model["nodes"] = Dict{Int,Vector{Float64}}()
model["node_sets"] = Dict{String,Vector{Int}}()
model["elements"] = Dict{Int,Vector{Int}}()
model["element_types"] = Dict{Int,Symbol}()
model["element_sets"] = Dict{String,Vector{Int}}()
model["surface_sets"] = Dict{String,Vector{Tuple{Int,Symbol}}}()
model["surface_types"] = Dict{String,Symbol}()
keyword_sym::Symbol = :none
lines = readlines(fid)
keyword_indexes = find_keywords(lines)
nkeyword_indexes = length(keyword_indexes)
push!(keyword_indexes, length(lines) + 1)
idx_start = keyword_indexes[1]
for idx_end in keyword_indexes[2:end]
keyword_line = strip(uppercase(lines[idx_start]))
keyword = strip(regex_match(r"\s*([\w ]+)", keyword_line, 1))
k_sym = Symbol(keyword)
args = Tuple{Dict,Vector{Int},Symbol,Int,Int,Type{Val{k_sym}}}
if hasmethod(parse_section, args)
parse_section(model, lines, k_sym, idx_start, idx_end - 1, Val{k_sym})
else
if verbose
@warn("Unknown section: '$(keyword)'")
end
end
idx_start = idx_end
end
return model
end
"""
abaqus_read_mesh(fn::String)
Read ABAQUS mesh from file `fn`. Returns a dict with elements, nodes,
element sets, node sets and other topologically imporant things, but
not the actual model with boundary conditions, load steps and so on.
"""
function abaqus_read_mesh(fn::String; kwargs...)
verbose = get(kwargs, :verbose, true)
return parse_abaqus(open(fn), verbose)
end