feat(io): add Code Aster .med mesh reading (requires HDF5)

New 222-line Code Aster mesh reader:
- MEDFile: Code Aster binary .med file structure
- aster_read_mesh_raw(): read mesh from .med file using HDF5
- aster_parse_nodes(): parse node coordinates from text sections
- get_mesh_names(), get_mesh(): mesh access utilities
- get_node_sets(), get_element_sets(): extract named sets
- to_ascii(): convert Int8 vectors to ASCII strings
- Requires HDF5.jl (optional dependency)
- Required by AsterReader module

Provides Code Aster .med file mesh reading functionality.
This commit is contained in:
Jukka Aho
2025-12-15 06:14:40 +02:00
parent d7607f09fd
commit ae9de8dd91
+222
View File
@@ -0,0 +1,222 @@
# This file is a part of JuliaFEM.
# License is MIT: see https://github.com/JuliaFEM/AsterReader.jl/blob/master/LICENSE
function aster_parse_nodes(section; strip_characters=true)
nodes = Dict{Any,Vector{Float64}}()
has_started = false
for line in split(section, '\n')
m = collect((string(m.match) for m in eachmatch(r"[\w.-]+", line)))
if length(m) == 1
if (m[1] == "COOR_2D") || (m[1] == "COOR_3D")
has_started = true
continue
end
if m[1] == "FINSF"
break
end
end
if !has_started # we have not found COOR_2D or COOR_3D yet.
continue
end
to(T, x) = map(x -> parse(T, x), x)
if length(m) == 4
if strip_characters
nodes[parse_node_id(m[1])] = to(Float64, m[2:end])
else
nodes[nid] = to(Float64, m[2:end])
end
end
end
return nodes
end
""" Code Aster binary file (.med). """
mutable struct MEDFile
data::Dict
end
function MEDFile(fn::String)
return MEDFile(h5read(fn, "/"))
end
function get_mesh_names(med::MEDFile)
return sort(collect(keys(med.data["FAS"])))
end
""" Convert vector of Int8 to ASCII string. """
function to_ascii(data::Vector{Int8})
return ascii(unsafe_string(pointer(convert(Vector{UInt8}, data))))
end
function get_mesh(med::MEDFile, mesh_name::String)
if !haskey(med.data["FAS"], mesh_name)
@warn("Mesh $mesh_name not found from med file.")
meshes = get_mesh_names(med)
all_meshes = join(meshes, ", ")
@warn("Available meshes: $all_meshes")
error("Mesh $mesh_name not found.")
end
return med.data["FAS"][mesh_name]
end
""" Return node sets from med file.
Notes
-----
One node set id can have multiple names.
"""
function get_node_sets(med::MEDFile, mesh_name::String)::Dict{Int64,Vector{String}}
mesh = get_mesh(med, mesh_name)
node_sets = Dict{Int64,Vector{String}}(0 => ["OTHER"])
if !haskey(mesh, "NOEUD")
return node_sets
end
for (k, v) in mesh["NOEUD"]
nset_id = parse(Int, split(k, "_")[2])
node_sets[nset_id] = collect(to_ascii(d) for d in v["GRO"]["NOM"])
end
return node_sets
end
"""
get_element_sets(med, mesh_name)
Return element sets from med file. Return type is a dictionary, where the key is
the element set id number (integer) and value is a vector of strings, containing
human-readable name for element set.
# Notes
One element set id can have multiple names.
"""
function get_element_sets(med::MEDFile, mesh_name::String)::Dict{Int64,Vector{String}}
mesh = get_mesh(med, mesh_name)
element_sets = Dict{Int64,Vector{String}}()
if !haskey(mesh, "ELEME")
return element_sets
end
elset_keys = sort(collect(keys(mesh["ELEME"])))
for (i, k) in enumerate(elset_keys)
v = mesh["ELEME"][k]
if startswith(k, "FAM") # exported from Salome
elset_id = parse(Int, split(k, '_')[2])
else # exported from Gmsh
elset_id = -i
end
if !isempty(v)
element_sets[elset_id] = map(strip, collect(to_ascii(d) for d in v["GRO"]["NOM"]))
else
element_sets[elset_id] = [""]
end
end
return element_sets
end
function get_nodes(med::MEDFile, nsets::Dict{Int,Vector{String}}, mesh_name::String)
increments = keys(med.data["ENS_MAA"][mesh_name])
@assert length(increments) == 1
increment = first(increments)
nodes = med.data["ENS_MAA"][mesh_name][increment]["NOE"]
nset_ids = nodes["FAM"]
nnodes = length(nset_ids)
node_ids = get(nodes, "NUM", collect(1:nnodes))
node_coords = nodes["COO"]
dim = round(Int, length(node_coords) / nnodes)
node_coords = reshape(node_coords, nnodes, dim)'
d = Dict{Int64}{Tuple{Vector{String},Vector{Float64}}}()
for i = 1:nnodes
nset = nsets[nset_ids[i]]
d[node_ids[i]] = (nset, node_coords[:, i])
end
return d
end
function get_connectivity(med::MEDFile, elsets::Dict{Int64,Vector{String}}, mesh_name::String)
if !haskey(elsets, 0)
elsets[0] = ["OTHER"]
end
increments = keys(med.data["ENS_MAA"][mesh_name])
@assert length(increments) == 1
increment = first(increments)
all_elements = med.data["ENS_MAA"][mesh_name][increment]["MAI"]
d = Dict{Int64,Tuple{Symbol,Vector{String},Vector{Int64}}}()
for eltype in keys(all_elements)
elements = all_elements[eltype]
elset_ids = elements["FAM"]
nelements = length(elset_ids)
element_ids = get(elements, "NUM", collect(1:nelements))
element_connectivity = elements["NOD"]
element_dim = round(Int, length(element_connectivity) / nelements)
element_connectivity = reshape(element_connectivity, nelements, element_dim)'
for i = 1:nelements
eltype = Symbol(eltype)
elco = element_connectivity[:, i]
elset = elsets[elset_ids[i]]
d[element_ids[i]] = (eltype, elset, elco)
end
end
return d
end
"""
aster_read_mesh(filename, mesh_name=nothing)
Parse code aster .med file and return mesh data in a dictionary.
Dictionary contains additional dictionaries `nodes`, `node_sets`, `elements`,
`element_sets`, `element_types`, `surface_sets` and `surface_types`.
If mesh file contains several meshes, one must provide the mesh name as
additional argument or expcetion will be thrown.
"""
function aster_read_mesh_raw(filename::String, mesh_name=nothing)
isfile(filename) || error("Cannot read mesh file $filename: file not found.")
aster_read_mesh_(MEDFile(filename), mesh_name)
end
function aster_read_mesh_(med::MEDFile, mesh_name=nothing)
mesh_names = get_mesh_names(med)
all_meshes = join(mesh_names, ", ")
if mesh_name == nothing
length(mesh_names) == 1 || error("several meshes found from med, pick one: $all_meshes")
mesh_name = mesh_names[1]
else
mesh_name in mesh_names || error("Mesh $mesh_name not found from mesh file $fn. Available meshes: $all_meshes")
end
mesh = Dict{String,Dict}()
mesh["nodes"] = Dict{Int,Vector{Float64}}()
mesh["node_sets"] = Dict{String,Vector{Int}}()
mesh["elements"] = Dict{Int,Vector{Int}}()
mesh["element_types"] = Dict{Int,Symbol}()
mesh["element_sets"] = Dict{String,Vector{Int}}()
mesh["surface_sets"] = Dict{String,Vector{Tuple{Int,Symbol}}}()
mesh["surface_types"] = Dict{String,Symbol}()
elsets = get_element_sets(med, mesh_name)
nsets = get_node_sets(med, mesh_name)
for (nid, (nset_, coords)) in get_nodes(med, nsets, mesh_name)
mesh["nodes"][nid] = coords
for nset in nset_
if !haskey(mesh["node_sets"], nset)
mesh["node_sets"][nset] = []
end
push!(mesh["node_sets"][nset], nid)
end
end
for (elid, (eltyp, elset_, elcon)) in get_connectivity(med, elsets, mesh_name)
mesh["elements"][elid] = elcon
mesh["element_types"][elid] = eltyp
for elset in elset_
if !haskey(mesh["element_sets"], elset)
mesh["element_sets"][elset] = []
end
push!(mesh["element_sets"][elset], elid)
end
end
return mesh
end