◆ 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

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::AbstractVector{Float64},
                         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.

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 (8 values 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

  • val_idx: which value column to interpolate (1 for standard)

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