Getting Started
Installation
Install Julia
Sentinel needs Julia 1.10+. The easiest way to install and manage Julia is juliaup:
# macOS / Linux
curl -fsSL https://install.julialang.org | shRestart your shell, then verify (and keep Julia current):
juliaup status # installed channels
julia --version # should be ≥ 1.10
juliaup update # update to the latest stable releaseOn macOS you can alternatively brew install juliaup.
Install Sentinel
Sentinel.jl is not yet registered in the Julia General registry. Two ways to install:
As a package — use it from your own environment:
using Pkg
Pkg.add(url="https://github.com/mechneurolab/sentinel")As a clone — recommended for development (editing source, running the test suite, using the bin/sentinel CLI):
git clone https://github.com/mechneurolab/sentinel.git
cd sentinelusing Pkg
Pkg.activate(".") # use Sentinel's own project environment
Pkg.instantiate() # install the pinned dependenciesOr develop the clone into another environment so your edits are picked up live:
using Pkg
Pkg.develop(path="/path/to/sentinel")Requirements: Julia 1.10+, Ferrite.jl 1.3.0 (installed automatically).
Optional Dependencies
# MUMPS solver (recommended for large problems)
Pkg.add("MUMPS")
# GPU acceleration (Apple Silicon)
Pkg.add("Metal")
# GPU acceleration (NVIDIA)
Pkg.add("CUDA")
Pkg.add("CUDSS")
# AppleAccelerate (macOS — loaded automatically, ~13% faster sparse solves)
Pkg.add("AppleAccelerate")AppleAccelerate
On macOS, Sentinel automatically loads AppleAccelerate if available, switching the BLAS/LAPACK backend to Apple's Accelerate framework. This gives ~13% faster sparse LU solves on Apple Silicon.
Development setup (VS Code)
The Julia extension for VS Code gives an integrated REPL, debugger, and plot pane — a convenient way to run Sentinel.
Install the extension. In VS Code → Extensions, install "Julia" (
julialang.language-julia). It auto-detects thejuliaup-managed Julia.Open the cloned repo (
File → Open Folder→ yoursentinel/clone).Use Sentinel's environment. Start a Julia REPL (Command Palette → "Julia: Start REPL"). The status bar should show the
sentinelproject; if not, click it and choose the repo folder, or run] activate .in the REPL.Set the thread count. The inversion parallelizes across Julia threads, but VS Code starts the REPL with one thread by default. In Settings, set
julia.NumThreadsto"auto"(all cores) or a number, then restart the REPL. Verify withThreads.nthreads().
Why threads matter
The inversion auto-selects a multicore strategy from the material model and the thread count (see Parallel Execution and Command-Line Interface). With a single thread it runs serially — so set julia.NumThreads before a real run.
Run a basic inversion from a .mat file
With the REPL on the sentinel environment and threads set, reconstruct directly from an MRE .mat data file (the mesh is generated automatically):
using Sentinel
prob = InversionProblem(data="scan.mat", frequency=60.0,
model=:isotropic, output="inv/scan")
result = solve(prob) # → InversionResult
result.state.converged # did it converge?
result.material # reconstructed propertiesSentinel.invert("scan.mat"; opts=(...)) is the one-shot variant returning the shear modulus on the image voxel grid. The three .mat entry points are covered in Tutorial 5 below; to get results out to MATLAB/ParaView see Visualization & Data Export.
Tutorial 1: Forward Problem
This tutorial solves a forward problem on a simple hex27 mesh with isotropic incompressible material (Model 1).
using Sentinel, Ferrite
# 1. Create a hex27 mesh (4x4x4 elements)
grid = generate_grid(Hexahedron, (4, 4, 4))
dh = setup_hex27_dofhandler(grid)
# 2. Set up cell values for integration
ip_scalar = Lagrange{RefHexahedron, 2}()
ip_press = Lagrange{RefHexahedron, 1}()
qr = QuadratureRule{RefHexahedron}(3)
cv_disp = CellValues(qr, ip_scalar)
cv_press = CellValues(qr, ip_press)
# 3. Define material properties
model = IsotropicIncompressible()
omega = 2pi * 60.0 # 60 Hz excitation frequency
rho = 1000.0 # density [kg/m^3]
# Create Gauss-point material with uniform properties
ne = getncells(grid)
ngp = 27 # 3x3x3 Gauss points
mu_val = complex(3000.0, -300.0) # shear modulus [Pa]
kappa_val = complex(2.0e9, 0.0) # bulk modulus [Pa]
props = GaussPointMaterial(
mu = fill(mu_val, ne, ngp),
kappa = fill(kappa_val, ne, ngp),
rho = fill(rho, ne, ngp),
ρ = fill(rho, ne, ngp)
)
# 4. Allocate and assemble
K = allocate_stiffness(dh, model)
assemble_stiffness!(K, dh, cv_disp, cv_press, model, props, omega)
# 5. Apply boundary conditions and solve
f = zeros(ComplexF64, size(K, 1))
# ... set up BCs and RHS, then:
# forward_solve!(disp, K, f, model, dispset; solver=DirectSolver())Tutorial 2: Runfile Workflow
The most common workflow uses Fortran-compatible runfiles (.dat format):
using Sentinel
# Parse a .dat runfile (same format as Fortran MRE-Zone)
config = parse_runfile("brain_mre.dat")
# Load all files and set up the complete problem
setup = setup_forward_problem(config, "/path/to/data/")
# Access the loaded components
grid = setup.grid
dh = setup.dh
model = setup.model
material = setup.material
bcs = setup.bcs
K = setup.KThe setup_forward_problem function reads all referenced files (.nod, .elm, .dsp, .mtr, .bnd, .bcs) and constructs the complete problem including the DOF handler, material mesh interpolation, and pre-assembled stiffness matrix.
Tutorial 3: Inverse Problem
Set up and run an inverse reconstruction using conjugate gradient with Tikhonov regularization:
using Sentinel
# ... (load data via runfile or manual setup) ...
# Set up regularization
tikhonov = TikhonovReg(1, 1e-4, 1e-4, material) # prop 1, weights, reference from material
# Build the forward problem context
ctx = ForwardProblemContext(
grid=grid, dh=dh, cv_disp=cv_disp, cv_press=cv_press,
model=model, bcs=bcs, meas=meas, omega=omega, rho=rho,
regularizations=[tikhonov], solver=DirectSolver(),
numdispsets=1
)
# Initialize gradient structure
grad = MaterialGradient()
init_gradient!(grad, material)
# Run CG optimizer
result_material, history = conjugate_gradient!(material, ctx;
max_iter=20, tol=1e-6)
# Check convergence
print_convergence_table(history)Tutorial 4: MRI Data to Mesh
Generate a finite element mesh from MRI displacement data:
using Sentinel
# Load MRI data (Siemens DICOM format)
mre_data = read_mre_siemens("dicom_dir/", 60.0, 1.0)
# Configure mesh generation
config = HexMeshConfig(
mesh_strategy=2, # wavelength-based resolution
mu_estimate=3000.0, # estimated shear modulus [Pa]
rho_estimate=1000.0, # density [kg/m^3]
nodes_per_wavelength=8.0, # FEM nodes per shear wavelength
buffer_size=2 # boundary buffer elements
)
# Generate hex27 mesh
result = generate_hex_mesh(mre_data, config)
# Write mesh files in NLI format
write_hex_mesh_files(result, "output/brain")
# Export to VTK for visualization
export_vtk("output/brain_mesh", result)Tutorial 5: Inversion from a .mat File
You can start an inversion directly from an MRE .mat data file — the mesh and displacement files are generated from it automatically, so no pre-built mesh is needed. There are three equivalent ways.
1. Sentinel.invert — one shot, shear modulus on the image voxel grid:
using Sentinel
res = invert("scan.mat";
opts = (material_model="isotropic", regularization="total_variation",
reg_weight=1e-3, frequency_hz=60.0, density_kg_m3=1000.0,
max_global_iters=20, mesh_resolution=1,
compute_backend="auto")) # gradient device: "auto" | "gpu" | "cpu"
res.real_shear # storage modulus (Pa) on the original voxel grid (NaN outside the mask)
res.imag_shear # loss modulus (Pa)
res.voxel_size # (mm); res.dims; res.stats (convergence/QA summary)2. InversionProblem(data=…) + solve — declarative, writes files + provenance:
prob = InversionProblem(data="scan.mat", frequency=60.0, mesh_strategy=2,
model=:isotropic, output="inv/scan")
result = solve(prob) # generates inv/generated_mesh/, writes inv/scan.config.toml3. TOML + the sentinel CLI:
# inversion.toml
[data]
mat = "scan.mat"
frequency_hz = 60.0
output = "recon"
[model]
type = "isotropic"bin/sentinel invert inversion.toml --threads 8 # Model 1 → multicore (auto)Export the result to MATLAB (.mat). Sample the reconstructed shear modulus onto a grid and write it with MAT.jl. The grid is already in physical Pa (do not re-scale), and properties can live on different material meshes, so scope to property 1 (shear):
using MAT
mesh = result.setup.meshes[result.setup.config.meshind[1, 1]] # the shear mesh
grid = interpolate_to_grid(result.material, mesh,
collect(mesh.origin), collect(mesh.res), mesh.dims;
prop_indices = [1])
matwrite("inv/scan_shear.mat", Dict(
"mu_storage" => grid.data["prop1_real"], # μ′ (Pa)
"mu_loss" => grid.data["prop1_imag"], # μ″ (Pa)
))(With the Sentinel.invert one-shot, res.real_shear / res.imag_shear are already on the image voxel grid — matwrite them directly.) For coordinate axes, a MATLAB load/visualize snippet, ParaView, and the standard ReconProps.mat, see Visualization & Data Export.
A runnable, no-download version is in examples/run_mat_demo.sh, which uses the bundled test/fixtures/forge_mre_uiuc.mat. See the Command-Line Interface guide for the full CLI reference.
Tutorial 6: VTK Export
Export results to VTK format for visualization in ParaView:
using Sentinel
# Export mesh with displacement data
export_vtk("result", hex_mesh_result;
write_disp=true,
write_anatomical=true,
write_region=true
)
# Export convergence history to CSV
export_convergence_csv("convergence.csv", history)For full export coverage — MATLAB (.mat), ParaView, and the standard ReconProps.mat — see Visualization & Data Export.