Meshes
Meshes in FiniteElementContainers leverage a very abstract interface. Currently, only an Exodus interface is directly supported within the main package but others could be readily supported through package extensions which we are planning on.
Mesh Infrastructure
The mesh system provides a common interface for reading, storing, manipulating, and writing finite element meshes independently of the underlying file format.
Rather than exposing Exodus, Gmsh, or Abaqus APIs directly, the library converts all mesh data into a common AbstractMesh representation that can be consumed by function spaces, assemblers, physics models, and post-processing routines.
The overall design can be viewed as
Exodus (.g/.exo)
/
Mesh File -----> FileMesh ------> UnstructuredMesh ------> FEM Application
\
Gmsh (.msh/.geo)
|
+--> FunctionSpace
+--> Boundary Conditions
+--> Initial Conditions
+--> Physics
+--> Assembly
+--> PostProcessorThe goal is that application code never depends on the mesh file format being used.
Mesh Types
The mesh system consists of three layers.
File Meshes
A FileMesh represents an open mesh file.
meshfile = FileMesh(ExodusMesh(), "mesh.g")A FileMesh is intentionally lightweight and provides only an interface for reading data from disk.
The following methods define the minimal mesh reader interface:
element_blocks(mesh)
nodal_coordinates_and_ids(mesh)
num_dimensions(mesh)Optional helper methods include
nodesets(mesh)
sidesets(mesh)
node_id_map(mesh)New mesh file formats can be added simply by implementing these methods.
For example,
struct MyMeshFormat <: AbstractMeshFileType
end
function element_blocks(mesh::FileMesh{...,MyMeshFormat})
...
endwithout modifying the rest of the FEM library.
UnstructuredMesh
Most FEM algorithms operate on an UnstructuredMesh.
This object owns all mesh connectivity and geometric information required during analysis.
mesh = UnstructuredMesh("mesh.g")Internally this stores
- nodal coordinates
- element connectivity
- element blocks
- element types
- node ID maps
- element ID maps
- nodesets
- sidesets
- optional edge connectivity
- optional face connectivity
allowing the remainder of the code base to be completely independent of Exodus or Gmsh.
For example,
mesh.nodal_coords
mesh.element_conns["block_1"]
mesh.nodeset_nodes["left"]
mesh.sideset_elems["traction"]provide direct access to commonly used mesh data structures.
Reading Meshes
The simplest way to construct a mesh is
mesh = UnstructuredMesh("mesh.g")The constructor automatically dispatches based on file extension.
.g
.e
.exo -> Exodus reader
.msh
.geo -> Gmsh reader
.inp
.i -> Abaqus reader (planned)Application code therefore remains independent of mesh format.
mesh = UnstructuredMesh(input_file)works regardless of whether the mesh originated from Exodus or Gmsh.
Mesh Summary
Meshes provide a convenient summary through Julia's display system.
println(mesh)produces output similar to
UnstructuredMesh:
Number of dimensions = 2
Number of nodes = 1245
Element Blocks:
block_1
Element type = QUAD4
Number of elements = 1024
Node sets:
left
right
Side sets:
tractionwhich is often useful when debugging new applications.
Element Blocks
Elements are grouped into blocks.
Each block has
- a name
- an element type
- a connectivity matrix
- an element ID map
For example,
conn = mesh.element_conns["solid"]
etype = mesh.element_types["solid"]returns the connectivity and element type associated with the block named "solid".
This organization makes it straightforward to assign different material models or physics objects to different regions of a mesh.
Block order
Per-block mesh data is stored in Dicts keyed by block name, but a great deal of the library refers to blocks by position — num_elements(fspace, b), foreach_block, and the per-block entries of physics and properties all index block b. Dict iteration is hash order, which is neither the order the blocks appear in the mesh file nor stable under a change of block names, so it must never be used to establish that position.
The canonical order is given by
block_names(mesh)and block b is block_names(mesh)[b]. The matching per-block data in the same order is available as
block_conns(mesh)
block_id_maps(mesh)A FunctionSpace carries the same order, so block_names(fspace)[b] names the block whose connectivity, reference element, physics and properties all live at index b.
Nodesets and Sidesets
Boundary conditions are typically applied through named mesh entities.
Node sets contain collections of nodes,
mesh.nodeset_nodes["fixed"]while side sets contain element faces or edges,
mesh.sideset_elems["traction"]
mesh.sideset_sides["traction"]allowing boundary conditions to be specified symbolically rather than through explicit node lists.
For example,
DirichletBC(
"u",
x -> 0.0;
nodeset_name="fixed"
)can automatically locate the appropriate degrees of freedom.
Creating Edge and Face Connectivity
Many finite element methods require explicit edge or face connectivity.
These may be requested during mesh construction
mesh = UnstructuredMesh(
"mesh.g",
create_edges=true
)which constructs a globally unique edge numbering from the element connectivity.
Similarly,
create_faces=truecan be used for three-dimensional meshes.
These connectivity objects are useful for
- H(curl) elements
- H(div) elements
- discontinuous Galerkin methods
- mortar methods
- adaptive refinement
without requiring repeated reconstruction of edge topology.
Higher Order Mesh Generation
Linear meshes may be automatically upgraded to higher-order Lagrange meshes.
For example,
mesh2 = UnstructuredMesh(
"mesh.g",
p_order=2
)creates a quadratic mesh by inserting mid-edge nodes and interior nodes into each element.
Internally this process
- identifies unique mesh edges,
- inserts edge nodes,
- inserts element interior nodes,
- updates element connectivity.
This provides a convenient mechanism for generating higher-order discretizations from existing linear meshes.
Rigid Body Modes
The mesh infrastructure can automatically compute rigid body modes.
R = rigid_body_modes(mesh)For two-dimensional meshes this returns
- x translation
- y translation
- rotation
while three-dimensional meshes return
- Tx
- Ty
- Tz
- Rx
- Ry
- Rz
These modes are useful for
- nullspace construction,
- multigrid methods,
- constrained optimization,
- singular stiffness matrices,
- elasticity verification tests.
Writing Meshes
Meshes may be written back to disk using
write_to_file(mesh, "output.g")which currently outputs Exodus meshes.
This allows generated meshes or modified connectivity to be saved for visualization or reuse.
Copying Meshes
Existing Exodus meshes may be duplicated without reading all mesh data into memory.
copy_mesh(mesh, "new_mesh.g")This is particularly useful for creating output databases prior to writing simulation results.
Post Processing
The mesh infrastructure integrates directly with the post-processing system.
A postprocessor may be created from an existing mesh using
pp = PostProcessor(
ExodusMesh,
mesh,
"results.g"
)or from an existing Exodus file
pp = PostProcessor(
ExodusMesh,
"results.g",
displacement,
pressure
)which automatically registers nodal and element variables.
Fields can then be written directly
write_field(
pp,
step,
names(U),
U
)or
write_field(
pp,
step,
"solid",
"stress",
stress
)allowing visualization in ParaView, VisIt, or other Exodus-compatible tools.
Extending the Mesh System
Supporting a new mesh format requires only implementing the minimal FileMesh interface.
element_blocks(mesh)
nodal_coordinates_and_ids(mesh)
num_dimensions(mesh)along with optional helpers such as
nodesets(mesh)
sidesets(mesh)
node_id_map(mesh)Once these methods are provided, the entire FEM infrastructure—including function spaces, assembly, physics objects, boundary conditions, and post-processing—works automatically without modification.
This separation between mesh I/O and finite element algorithms allows new mesh formats to be integrated with minimal effort while keeping the core FEM implementation completely file-format independent.
FiniteElementContainers.AbstractMesh — Type
abstract type AbstractMeshFiniteElementContainers.FileMesh — Type
struct FileMesh{MeshObj, MeshType} <: FiniteElementContainers.AbstractMeshfile_name::Stringmesh_obj::Any
Mesh type that has a handle to an open mesh file object. This type's methods are "overridden" in extensions.
See FiniteElementContainersExodusExt for an example.
FiniteElementContainers.block_conns — Method
block_conns(
mesh::FiniteElementContainers.AbstractMesh
) -> Any
Per-block element connectivities in canonical block order. See block_names.
FiniteElementContainers.block_id_maps — Method
block_id_maps(
mesh::FiniteElementContainers.AbstractMesh
) -> Any
Per-block element ID maps in canonical block order. See block_names.
FiniteElementContainers.block_names — Method
block_names(
mesh::FiniteElementContainers.AbstractMesh
) -> Vector
Element block names in canonical block order, i.e. the order the blocks appear in the mesh file.
This is the single source of truth for what "block b" means. Meshes store per-block data (element_conns, element_id_maps, element_types) in Dicts keyed by block name, and Dict iteration order is hash order – neither the mesh file order nor stable under a change of block names. Anything that indexes blocks by position must derive that order from here rather than from values(some_dict).
FiniteElementContainers.file_name — Method
file_name(mesh::FiniteElementContainers.AbstractMesh) -> Any
Returns file name for an mesh type
Structured Meshes
Simple structured meshes on rectangles or parallepipeds can be create through StructuredMesh mesh type.
FiniteElementContainers.StructuredMesh — Type
struct StructuredMesh{ND, RT<:Number, IT<:Integer} <: FiniteElementContainers.AbstractMeshnodal_coords::H1Field{RT, Vector{RT}, ND} where {ND, RT<:Number}element_block_names::Vector{String}element_block_names_map::Dict{Int64, String}element_types::Dict{String, String}element_conns::Dict{String, Matrix{IT}} where IT<:Integerelement_id_map::Vector{IT} where IT<:Integerelement_id_maps::Dict{String, Vector{IT}} where IT<:Integernode_id_map::Vector{IT} where IT<:Integernodeset_names::Dict{IT, String} where IT<:Integernodeset_nodes::Dict{String, Vector{IT}} where IT<:Integersideset_names::Dict{IT, String} where IT<:Integersideset_elems::Dict{String, Vector{IT}} where IT<:Integersideset_nodes::Dict{String, Vector{IT}} where IT<:Integersideset_sides::Dict{String, Vector{IT}} where IT<:Integersideset_side_nodes::Dict{String, Matrix{IT}} where IT<:Integer
Unstructured Meshes
Unstructured meshes (e.g. those read from a file created by a mesher) can be created with the following mesh type
FiniteElementContainers.UnstructuredMesh — Type
struct UnstructuredMesh{MeshObj, ND, RT<:Number, IT<:Integer, EdgeConns, FaceConns} <: FiniteElementContainers.AbstractMeshmesh_obj::Anynodal_coords::H1Field{RT, Vector{RT}, ND} where {ND, RT<:Number}element_block_names::Vector{String}element_block_names_map::Dict{IT, String} where IT<:Integerelement_types::Dict{String, String}element_conns::Dict{String, Matrix{IT}} where IT<:Integerelement_id_map::Vector{IT} where IT<:Integerelement_id_maps::Dict{String, Vector{IT}} where IT<:Integernode_id_map::Vector{IT} where IT<:Integernodeset_names::Dict{IT, String} where IT<:Integernodeset_nodes::Dict{String, Vector{IT}} where IT<:Integersideset_names::Dict{IT, String} where IT<:Integersideset_elems::Dict{String, Vector{IT}} where IT<:Integersideset_nodes::Dict{String, Vector{IT}} where IT<:Integersideset_sides::Dict{String, Vector{IT}} where IT<:Integersideset_side_nodes::Dict{String, Matrix{IT}} where IT<:Integeredge_conns::Anyface_conns::Any
FiniteElementContainers.UnstructuredMesh — Method
UnstructuredMesh(
file_type::FiniteElementContainers.AbstractMeshFileType,
file_name::String;
kwargs...
) -> UnstructuredMesh{MeshObj, ND, RT, Int64, EdgeConns, Nothing} where {MeshObj, ND, RT<:Number, EdgeConns}
FiniteElementContainers.UnstructuredMesh — Method
UnstructuredMesh(
file_name::String;
kwargs...
) -> UnstructuredMesh{MeshObj, ND, RT, Int64, EdgeConns, Nothing} where {MeshObj, ND, RT<:Number, EdgeConns}
FiniteElementContainers.UnstructuredMesh — Method
UnstructuredMesh(
file::FileMesh{T, V};
create_edges,
create_faces,
interp_type,
p_order,
space_type
) -> UnstructuredMesh{MeshObj, ND, RT, Int64, EdgeConns, Nothing} where {MeshObj, ND, RT<:Number, EdgeConns}
TODO change the interface so we don't need createedges/createfaces. We should parse the interpolation/space type and determine this change in behavior from these inputs...
Exodus interface API
FiniteElementContainers.FileMesh — Method
struct FileMesh{MeshObj, MeshType} <: FiniteElementContainers.AbstractMeshfile_name::Stringmesh_obj::Any
FiniteElementContainers.nodal_coordinates — Method
nodal_coordinates(
mesh::FileMesh{<:Exodus.ExodusDatabase, FiniteElementContainers.ExodusMesh}
) -> H1Field
FiniteElementContainers.nodal_coordinates_and_ids — Method
nodal_coordinates_and_ids(
mesh::FileMesh{<:Exodus.ExodusDatabase, FiniteElementContainers.ExodusMesh}
) -> Tuple{H1Field, Any}
FiniteElementContainers.node_cmaps — Method
node_cmaps(
mesh::FileMesh{<:Exodus.ExodusDatabase, FiniteElementContainers.ExodusMesh},
rank
) -> Vector
Gmsh interface API
FiniteElementContainers.FileMesh — Method
struct FileMesh{MeshObj, MeshType} <: FiniteElementContainers.AbstractMeshfile_name::Stringmesh_obj::Any
GmshExt.GmshFile — Type
struct FileMesh{Nothing, FiniteElementContainers.GmshMesh} <: FiniteElementContainers.AbstractMeshfile_name::Stringmesh_obj::Nothing
FiniteElementContainers.sidesets — Method
Constructs side sets (for e.g. Neumann BCs) from gmsh physical groups of dimension num_dimensions(mesh) - 1. Each boundary element in the physical group is matched against its parent volume/area element via shared nodes, using _build_boundary_to_parent_map, to recover the (element, side) pairs Exodus-style side sets require.
Supports:
- 2D meshes (boundary = linear edges of Tri3 / Quad4)
- 3D meshes (boundary = linear faces of Tet4 / Hex8)
Higher-order parent elements (e.g. Quad9) are not yet supported and will produce a warning + skipped boundary elements rather than a hard failure.
GmshExt._build_boundary_to_parent_map — Method
_build_boundary_to_parent_map(
dim::Int64
) -> Dict{Vector{Int64}, Tuple{Int64, Int64}}
Builds a map from a canonical (sorted) set of boundary-entity nodes to the parent element tag and the local side number of that element, for all dim-dimensional elements in the mesh. This lets us match a lower dimensional boundary element (from a sideset physical group) back to the volume/area element it bounds, and the side number on that element, the way Exodus side sets are defined.
Works for both:
dim == 2: boundary = edges of Tri3 / Quad4dim == 3: boundary = faces of Tet4 / Hex8
The node ordering within a side only matters for orientation-sensitive uses elsewhere (e.g. _create_local_edges); here we key on the sorted node tuple, since a boundary entity and its parent's matching side are made up of exactly the same set of nodes regardless of traversal order.
Higher order or otherwise unsupported parent element types are skipped with a warning rather than silently mismatched.