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
                        +--> PostProcessor

The 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})
    ...
end

without 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:
      traction

which 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 positionnum_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=true

can 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

  1. identifies unique mesh edges,
  2. inserts edge nodes,
  3. inserts element interior nodes,
  4. 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.FileMeshType
struct FileMesh{MeshObj, MeshType} <: FiniteElementContainers.AbstractMesh
  • file_name::String

  • mesh_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.

source
FiniteElementContainers.block_namesMethod
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).

source

Structured Meshes

Simple structured meshes on rectangles or parallepipeds can be create through StructuredMesh mesh type.

FiniteElementContainers.StructuredMeshType
struct StructuredMesh{ND, RT<:Number, IT<:Integer} <: FiniteElementContainers.AbstractMesh
  • nodal_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<:Integer

  • element_id_map::Vector{IT} where IT<:Integer

  • element_id_maps::Dict{String, Vector{IT}} where IT<:Integer

  • node_id_map::Vector{IT} where IT<:Integer

  • nodeset_names::Dict{IT, String} where IT<:Integer

  • nodeset_nodes::Dict{String, Vector{IT}} where IT<:Integer

  • sideset_names::Dict{IT, String} where IT<:Integer

  • sideset_elems::Dict{String, Vector{IT}} where IT<:Integer

  • sideset_nodes::Dict{String, Vector{IT}} where IT<:Integer

  • sideset_sides::Dict{String, Vector{IT}} where IT<:Integer

  • sideset_side_nodes::Dict{String, Matrix{IT}} where IT<:Integer

source

Unstructured Meshes

Unstructured meshes (e.g. those read from a file created by a mesher) can be created with the following mesh type

FiniteElementContainers.UnstructuredMeshType
struct UnstructuredMesh{MeshObj, ND, RT<:Number, IT<:Integer, EdgeConns, FaceConns} <: FiniteElementContainers.AbstractMesh
  • mesh_obj::Any

  • nodal_coords::H1Field{RT, Vector{RT}, ND} where {ND, RT<:Number}

  • element_block_names::Vector{String}

  • element_block_names_map::Dict{IT, String} where IT<:Integer

  • element_types::Dict{String, String}

  • element_conns::Dict{String, Matrix{IT}} where IT<:Integer

  • element_id_map::Vector{IT} where IT<:Integer

  • element_id_maps::Dict{String, Vector{IT}} where IT<:Integer

  • node_id_map::Vector{IT} where IT<:Integer

  • nodeset_names::Dict{IT, String} where IT<:Integer

  • nodeset_nodes::Dict{String, Vector{IT}} where IT<:Integer

  • sideset_names::Dict{IT, String} where IT<:Integer

  • sideset_elems::Dict{String, Vector{IT}} where IT<:Integer

  • sideset_nodes::Dict{String, Vector{IT}} where IT<:Integer

  • sideset_sides::Dict{String, Vector{IT}} where IT<:Integer

  • sideset_side_nodes::Dict{String, Matrix{IT}} where IT<:Integer

  • edge_conns::Any

  • face_conns::Any

source
FiniteElementContainers.UnstructuredMeshMethod
UnstructuredMesh(
    file_type::FiniteElementContainers.AbstractMeshFileType,
    file_name::String;
    kwargs...
) -> UnstructuredMesh{MeshObj, ND, RT, Int64, EdgeConns, Nothing} where {MeshObj, ND, RT<:Number, EdgeConns}
source
FiniteElementContainers.UnstructuredMeshMethod
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...

source

Exodus interface API

Gmsh interface API

GmshExt.GmshFileType
struct FileMesh{Nothing, FiniteElementContainers.GmshMesh} <: FiniteElementContainers.AbstractMesh
  • file_name::String

  • mesh_obj::Nothing

source
FiniteElementContainers.sidesetsMethod

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.

source
GmshExt._build_boundary_to_parent_mapMethod
_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 / Quad4
  • dim == 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.

source