◆ FORGE Suite
GitHubMechanical Neuroimaging Lab · Univ. of Delaware
Skip to content

I/O ​

Mesh I/O ​

Sentinel.read_mesh_legacy Function
julia
read_mesh_legacy(nod_file, elm_file)

Read a legacy NLI mesh from .nod and .elm files and construct a Ferrite Grid.

Handles hex27 (27 nodes/element) and tet4 (4 nodes/element) meshes. Applies the Fortran-to-Ferrite node ordering permutation for hex27 elements.

For hex27 meshes, Ferrite's Hexahedron cell stores only 8 corner nodes. The full 27-node connectivity (in Ferrite order) is returned separately as full_connectivity for use in write_mesh_legacy and compute_gp_physical_coords.

Returns

Named tuple (grid, node_mtrltype, elem_mtrltype, full_connectivity):

  • grid::Grid — Ferrite grid object (8-node Hexahedron cells for hex27 meshes)

  • node_mtrltype::Vector{Int} — material type tag per node

  • elem_mtrltype::Vector{Int} — material type tag per element

  • full_connectivity::Union{Nothing, Matrix{Int}} — full 27-node Ferrite-ordered connectivity (ne × 27), or nothing for non-hex27 meshes

source
Sentinel.write_mesh_legacy Function
julia
write_mesh_legacy(grid, stem; node_mtrltype, elem_mtrltype, full_connectivity)

Write a Ferrite Grid to legacy NLI .nod and .elm files.

Writes stem.nod and stem.elm.

Arguments

  • grid — Ferrite Grid object

  • stem — output file stem (without extension)

  • node_mtrltype — material type per node (default: all ones)

  • elem_mtrltype — material type per element (default: all ones)

  • full_connectivity — for hex27 meshes: the full 27-node Ferrite-ordered connectivity (ne × 27) from read_mesh_legacy. Required for hex27 output since Ferrite Hexahedron cells only store 8 corner nodes.

source

Legacy File Formats ​

Sentinel.read_nod_file Function
julia
read_nod_file(filename) -> (coords, mtrltype)

Read a legacy .nod file containing node coordinates and material types.

Returns

  • coords::Matrix{Float64}: (nn, 3) matrix of [x, y, z] coordinates

  • mtrltype::Vector{Int}: (nn,) material type tag per node

Format

Each row: node_index x y z [mtrltype] (1-based, ASCII, free-format). The mtrltype column is optional; defaults to 1 if absent (e.g., 4-column material mesh files).

source
Sentinel.write_nod_file Function
julia
write_nod_file(filename, coords, mtrltype)

Write a legacy .nod file.

Arguments

  • filename: output file path

  • coords::Matrix{Float64}: (nn, 3) node coordinates

  • mtrltype::Vector{Int}: (nn,) material type per node

source
Sentinel.read_elm_file Function
julia
read_elm_file(filename) -> (connectivity, mtrltype, npe)

Read a legacy .elm file containing element connectivity.

Returns

  • connectivity::Matrix{Int}: (ne, npe) node indices (1-based)

  • mtrltype::Vector{Int}: (ne,) element material type

  • npe::Int: nodes per element (auto-detected from column count)

Format

Each row: elm_index n₁ n₂ … nₙₚₑ elmmtrltype (1-based, ASCII, free-format)

source
Sentinel.write_elm_file Function
julia
write_elm_file(filename, connectivity, mtrltype)

Write a legacy .elm file.

Arguments

  • filename: output file path

  • connectivity::Matrix{Int}: (ne, npe) node indices (1-based)

  • mtrltype::Vector{Int}: (ne,) element material type

source
Sentinel.read_dsp_file Function
julia
read_dsp_file(filename) -> Matrix{ComplexF64}

Read a legacy .dsp file containing complex displacement vectors.

Returns

  • disp::Matrix{ComplexF64}: (nn, 3) complex displacements [u, v, w]

Format

Each row: node_index Re(u) Im(u) Re(v) Im(v) Re(w) Im(w) (ASCII)

source
Sentinel.write_dsp_file Function
julia
write_dsp_file(filename, disp)

Write a legacy .dsp file.

Arguments

  • filename: output file path

  • disp::Matrix{ComplexF64}: (nn, 3) complex displacements [u, v, w]

source
Sentinel.read_mtr_file Function
julia
read_mtr_file(filename) -> Matrix{Float64}

Read a legacy .mtr file containing material property values.

Note: In the Fortran convention, real and imaginary parts are stored in separate files (suffixed .RE. and .IM.). This function reads a single file (either real or imaginary part).

Returns

  • values::Matrix{Float64}: (np, nvpp) property values

Format

Each row: index val₁ val₂ … valₙᵥₚₚ (ASCII)

source
Sentinel.write_mtr_file Function
julia
write_mtr_file(filename, values)

Write a legacy .mtr file.

Arguments

  • filename: output file path

  • values::Matrix{Float64}: (np, nvpp) property values

source
Sentinel.read_pre_file Function
julia
read_pre_file(filename) -> Matrix{ComplexF64}

Read a legacy .pre file containing complex pressure values.

Returns

  • press::Matrix{ComplexF64}: (np, nppp) complex pressure values where np is number of elements (viscoelastic) or nodes (poroelastic) and nppp is pressure DOFs per point.

Format

Each row: index Re(p₁) Im(p₁) Re(p₂) Im(p₂) … (ASCII)

source
Sentinel.write_pre_file Function
julia
write_pre_file(filename, press)

Write a legacy .pre file.

Arguments

  • filename: output file path

  • press::Matrix{ComplexF64}: (np, nppp) complex pressure values

source
Sentinel.read_bnd_file Function
julia
read_bnd_file(filename) -> Vector{Int}

Read a legacy .bnd file containing a list of boundary node IDs.

Returns

  • node_ids::Vector{Int}: sorted boundary node indices (1-based)

Format

Each row: seq_index node_id (1-based, ASCII)

source
Sentinel.write_bnd_file Function
julia
write_bnd_file(filename, node_ids)

Write a legacy .bnd file.

Arguments

  • filename: output file path

  • node_ids: boundary node indices (will be sorted in output)

source
Sentinel.read_bcs_file Function
julia
read_bcs_file(filename) -> Matrix{ComplexF64}

Read a legacy .bcs file containing complex displacement boundary conditions.

Returns

  • bcs::Matrix{ComplexF64}: (n, 3) complex displacements [ux, uy, uz] where n is the number of lines (typically total nodes in the mesh)

Format

Each row: node_id Re(ux) Im(ux) Re(uy) Im(uy) Re(uz) Im(uz) (ASCII)

source
Sentinel.write_bcs_file Function
julia
write_bcs_file(filename, bcs)

Write a legacy .bcs file.

Arguments

  • filename: output file path

  • bcs::Matrix{ComplexF64}: (n, 3) complex displacements [ux, uy, uz]

source
Sentinel.read_basis_file Function
julia
read_basis_file(filename) -> Vector{Int}

Read a .basis file containing per-property basis indices (1=nodal, 2=elemental).

Returns

  • basis::Vector{Int}: basis index per property

Format

Single line: basis₁ basis₂ … basisₙ (space-separated integers)

source
Sentinel.write_basis_file Function
julia
write_basis_file(filename, basis)

Write a .basis file.

Arguments

  • filename: output file path

  • basis::Vector{Int}: basis index per property

source
Sentinel.read_meshind_file Function
julia
read_meshind_file(filename) -> Matrix{Int}

Read a .meshind file containing material mesh assignment indices.

Returns

  • meshind::Matrix{Int}: (2, numprop) — row 1 = real part mesh index, row 2 = imaginary part mesh index, per property

Format

Two rows of numprop space-separated integers each.

source
Sentinel.write_meshind_file Function
julia
write_meshind_file(filename, meshind)

Write a .meshind file.

Arguments

  • filename: output file path

  • meshind::Matrix{Int}: (2, numprop) mesh indices

source

Material Mesh ​

Sentinel.MaterialMesh Type
julia
MaterialMesh

Structured hex8 material property mesh used for material parameterization. Not a Ferrite Grid — this is a simple structured grid used only for property interpolation via the GP2MTR mapping.

Fields

  • nn, ne: number of nodes and elements

  • coords::Matrix{Float64}: (nn, 3) node coordinates

  • connectivity::Matrix{Int}: (ne, 8) element connectivity (Fortran hex8 ordering)

  • origin::SVector{3,Float64}: mesh minimum corner (material mesh, may extend beyond disp mesh)

  • res::SVector{3,Float64}: element size [dx, dy, dz]

  • dims::NTuple{3,Int}: (nex, ney, nez) element counts per direction

  • elnum::Array{Int,3}: (nex, ney, nez) → element number lookup

source
Sentinel.build_material_mesh Function
julia
build_material_mesh(disp_grid, res8) -> MaterialMesh

Construct a structured hex8 material property mesh that overlaps the displacement mesh with the specified resolution.

Ports buildhex8mtrlmesh from MRE-Zone.f90 (lines 6819-6937).

Arguments

  • disp_grid: Ferrite Grid (displacement mesh)

  • res8: element size [dx, dy, dz] as a 3-element vector

source
Sentinel.read_material_mesh Function
julia
read_material_mesh(nod_file, elm_file) -> MaterialMesh

Read a structured hex8 material mesh from legacy .nod/.elm files. Computes resolution and grid structure from the mesh geometry.

Assumes the mesh is a regular structured hex8 grid.

source

GP2MTR Mapping ​

Sentinel.GP2MtrPoint Type
julia
GP2MtrPoint

Mapping from a single displacement Gauss point to its location on the material property mesh.

Ports the Fortran mtrlconvert type from FEmesh.f90.

Fields

  • mtrelm::Int: material mesh element containing this Gauss point (1-based)

  • basis::SVector{8,Float64}: hex8 basis function values at the GP location

  • localcoords::SVector{3,Float64}: local (ξ,η,ζ) coords within mtrelm

source
Sentinel.build_gp2mtr Function
julia
build_gp2mtr(disp_grid, mtrl_mesh::MaterialMesh; ngp::Int=3) -> GP2Mtr

Build the Gauss-point-to-material-mesh mapping for all elements in the displacement mesh.

For each hex27 displacement element and each 3D Gauss point:

  1. Computes physical GP coordinates using the axis-aligned hex27 formula

  2. Finds containing hex8 material element via floor-division

  3. Computes local (ξ,η,ζ) coordinates within the material element

  4. Evaluates hex8 basis functions at those coordinates

Ports the gp2mtr builder from MRE-Zone.f90 (lines 6997-7069).

Arguments

  • disp_grid: Ferrite Grid (hex27 displacement mesh)

  • mtrl_mesh: MaterialMesh (structured hex8 material mesh)

  • ngp: number of Gauss points per direction (default 3)

Returns

GP2Mtr with mapping data indexed as gp2mtr.data[el, ig, jg, kg].

source
Sentinel.compute_gp_physical_coords Function
julia
compute_gp_physical_coords(grid, el, xi_g, eta_g, zeta_g) -> (x, y, z)

Compute physical coordinates of a Gauss point for an axis-aligned hexahedral element using its 8 corner nodes.

Equivalent to the Fortran formula (MRE-Zone.f90 line 7031) but works with Ferrite's 8-node Hexahedron cells instead of requiring all 27 nodes.

Ferrite Hexahedron corner ordering (VTK): 1(-,-,-), 2(+,-,-), 3(+,+,-), 4(-,+,-), 5(-,-,+), 6(+,-,+), 7(+,+,+), 8(-,+,+)

Formula: x_gp = center + ξ * half_width for each direction.

source
Sentinel.hex8_basis Function
julia
hex8_basis(xi, eta, zeta) -> SVector{8, Float64}

Evaluate the 8 trilinear hex8 basis functions at local coordinates (xi, eta, zeta).

Uses the Fortran hex8 node ordering (hex8.f90): 1:(-1,-1,-1), 2:(-1,+1,-1), 3:(+1,+1,-1), 4:(+1,-1,-1) 5:(-1,-1,+1), 6:(-1,+1,+1), 7:(+1,+1,+1), 8:(+1,-1,+1)

Each: P_a = (1/8)(1 + ξ_a·ξ)(1 + η_a·η)(1 + ζ_a·ζ)

source
Sentinel.hex8_basis_gradient Function
julia
hex8_basis_gradient(xi, eta, zeta) -> SMatrix{8, 3, Float64}

Evaluate the gradients of the 8 trilinear hex8 basis functions with respect to local coordinates (ξ, η, ζ). Returns an 8×3 matrix where row i gives [∂Nᵢ/∂ξ, ∂Nᵢ/∂η, ∂Nᵢ/∂ζ].

Uses the Fortran hex8 node ordering (same as hex8_basis).

source
Sentinel.interpolate_property_at_gp Function
julia
interpolate_property_at_gp(gpt::GP2MtrPoint, values::AbstractMatrix,
                            connectivity::AbstractMatrix{Int}, val_idx::Int=1;
                            basis_type::Int=1) -> Float64

Interpolate a single (real or imaginary) material property value at a Gauss point using the precomputed GP2MTR mapping.

Arguments

  • gpt: precomputed GP2MtrPoint for this Gauss point

  • values: property values matrix (npr × nvpp) or (npi × nvpp)

  • connectivity: material mesh element connectivity (ne × 8)

  • val_idx: which column of values to interpolate

  • basis_type: 1 = nodal (interpolate using hex8 basis), 2 = elemental (constant per element)

Returns

Interpolated property value at the Gauss point.

source
Sentinel.interpolate_complex_property_at_gp Function
julia
interpolate_complex_property_at_gp(gpt_r::GP2MtrPoint, gpt_i::GP2MtrPoint,
                                    prop::SingleProperty,
                                    r_connectivity::AbstractMatrix{Int},
                                    i_connectivity::AbstractMatrix{Int},
                                    val_idx::Int=1) -> ComplexF64

Interpolate a complex material property at a Gauss point. Real and imaginary parts may be on different material meshes with different GP2MTR mappings.

Returns

Scaled complex property value: scalar[1] * r_interp + i * scalar[2] * i_interp

source
Sentinel.gauss_points_1d Function
julia
gauss_points_1d(n) -> SVector{n, Float64}

Return the n-point Gauss-Legendre quadrature points on [-1, 1]. Matches the Fortran gaussinit routine.

source
Sentinel.gauss_weights_1d Function
julia
gauss_weights_1d(n) -> SVector{n, Float64}

Return the n-point Gauss-Legendre quadrature weights on [-1, 1]. Matches the Fortran gaussinit routine.

source

Material Evaluation ​

Sentinel.powerlaw_complex Function
julia
powerlaw_complex(θ₀_r, α_r, θ₀_i, α_i, ω, ω₀) -> ComplexF64

Compute the complex material property using a power-law frequency model: θ*(ω) = (θ₀_r · (ω/ω₀)^α_r) + i·(θ₀_i · (ω/ω₀)^α_i)

This is used when multifreqind[prop] == true in the Fortran code.

Arguments

  • θ₀_r, α_r: real part amplitude and power-law exponent

  • θ₀_i, α_i: imaginary part amplitude and power-law exponent

  • ω: current angular frequency rad/s

  • ω₀: reference angular frequency rad/s (from rvalue[j, 3] / ivalue[j, 3])

source
Sentinel.standard_complex Function
julia
standard_complex(θ_r::Float64, θ_i::Float64,
                 scalar_r::Float64, scalar_i::Float64) -> ComplexF64

Compute the complex material property using simple real/imaginary scaling: θ* = scalar_r · θ_r + i · scalar_i · θ_i

Used when multifreqind[prop] == false.

source
Sentinel.evaluate_property_at_gp Function
julia
evaluate_property_at_gp(prop::SingleProperty, gp_elem::Int,
                         gp_basis, r_connectivity::AbstractMatrix{Int},
                         i_connectivity::AbstractMatrix{Int},
                         ω::Float64, powerlaw::Bool=false,
                         val_idx::Int=1) -> ComplexF64

Evaluate a single material property at a Gauss point by interpolating from the material mesh using the precomputed basis function values and material mesh connectivity.

This function is @inlined and uses positional arguments for performance since it is called millions of times per forward/adjoint solve.

Arguments

  • prop: the SingleProperty to evaluate

  • gp_elem: index of the material mesh element containing this Gauss point

  • gp_basis: basis function values at the Gauss point within gp_elem (SVector{8,Float64} for hex8 material mesh)

  • r_connectivity: material mesh element connectivity for real part (ne × 8)

  • i_connectivity: material mesh element connectivity for imaginary part (ne × 8)

  • ω: angular frequency rad/s

  • powerlaw: if true, use power-law frequency scaling (positional, default false)

  • val_idx: which value column to interpolate (positional, default 1)

Returns

Complex property value at the Gauss point.

source

Runfile Parser ​

Sentinel.RunfileConfig Type
julia
RunfileConfig

Stores all parsed data from an NLI .dat runfile. File paths are stored as strings (not loaded) — use setup_forward_problem to load everything.

Supports problemtype=0 (forward) with forwardtype=0 (simulation) and forwardtype=1 (parameter test).

source
Sentinel.parse_runfile Function
julia
parse_runfile(filename) -> RunfileConfig

Parse an NLI .dat runfile and return a RunfileConfig with all settings.

Skips ! comment lines and blank lines. Processes data lines sequentially matching the Fortran readinitial / readforward read order.

Currently supports problemtype=0 (forward) with forwardtype=0 and 1.

source
Sentinel.write_runfile Function
julia
write_runfile(filename, config::RunfileConfig)

Write a .dat runfile from a RunfileConfig. The output matches the Fortran read order so that parse_runfile(write_runfile(f, c)) round-trips correctly.

source
Sentinel.setup_forward_problem Function
julia
setup_forward_problem(config::RunfileConfig, basedir::AbstractString=".")

Load all files referenced by a RunfileConfig and return a NamedTuple with the complete forward problem setup ready for assembly and solve.

Resolves relative paths against basedir.

Returns

Named tuple with fields:

  • grid: Ferrite Grid (hex27 elements stored as 8-node Hexahedron)

  • dh: DofHandler with vector displacement field

  • cv_disp: CellValues for scalar hex27 basis (27 shape functions)

  • model: AbstractMaterialModel dispatch singleton

  • material: Material struct with loaded properties

  • bcs: BoundaryConditions (populated for forwardtype=1)

  • meshes: Vector{MaterialMesh} — material property meshes

  • gp2mtrs: Vector{GP2Mtr} — Gauss-point-to-material-mesh mappings

  • omega: angular frequency ω = 2π·f

  • K: pre-allocated global stiffness matrix

  • full_connectivity: ne×27 Ferrite-ordered connectivity (or nothing)

  • config: the original RunfileConfig

  • solver: DirectSolver()

source

Inverse Runfile Parser & Setup ​

The inverse runfile parser, inverse problem setup, and zone-based solver are documented in Zone Decomposition.

Material Mesh Generation ​

Generate structured hex8 material meshes for inverse problems, matching the voxel grid used by Fortran MRE-Zone.

Sentinel.generate_regular_material_mesh Function
julia
generate_regular_material_mesh(bbox_min, bbox_max, resolution) -> MaterialMesh

Generate a regular hex8 material property mesh covering the specified bounding box.

Arguments

  • bbox_min::SVector{3,Float64}: minimum corner of the bounding box

  • bbox_max::SVector{3,Float64}: maximum corner of the bounding box

  • resolution::SVector{3,Float64}: element size [dx, dy, dz]

Returns

A MaterialMesh with structured hex8 elements covering the bounding box, compatible with build_gp2mtr for GP-to-material-mesh mapping.

source