Your First Inversion
In this tutorial we will reconstruct a map of tissue stiffness from magnetic resonance elastography (MRE) displacement data, and print it out in pascals.
We will use a small dataset that ships inside Sentinel, so there is nothing to download. The whole reconstruction takes a few seconds.
Before you start
You need Julia 1.11 or newer. Check with:
julia --versionIf that command is not found, install Julia with juliaup first — see Getting Started for the installation steps.
Step 1: Get Sentinel
The dataset we will use lives inside the repository, so clone it:
git clone https://github.com/mechneurolab/sentinel.git
cd sentinelStep 2: Start Julia and load Sentinel
Start Julia on Sentinel's own environment — the --project=. is what makes Julia use the dependency versions Sentinel was tested with:
julia --project=.At the julia> prompt, load the package:
using SentinelThe first time you do this, Julia compiles Sentinel and its dependencies. This takes a minute or two and prints a running list as it goes:
Precompiling packages...
27768.8 ms ✓ SentinelHow many packages appear depends on what your Julia installation has already compiled, so your list will be longer than this one. Wait for the julia> prompt to come back. Subsequent loads take a couple of seconds.
If it doesn't work
ERROR: ArgumentError: Package Sentinel not found means Julia is not using Sentinel's environment — quit and restart with julia --project=. from inside the sentinel directory.
A precompilation error saying some package is required but does not seem to be installed means the dependencies have not been downloaded yet. Run using Pkg; Pkg.instantiate() and then using Sentinel again.
Step 3: Find the bundled data
Sentinel ships with a small MRE dataset for testing. Locate it:
data = joinpath(pkgdir(Sentinel), "test", "fixtures", "forge_mre_uiuc.mat")
isfile(data)You should see:
trueIf you see false, you are not running from a clone of the repository — go back to Step 1.
This .mat file holds a displacement field and the geometry it belongs to — the two things an MRE inversion needs. Sentinel will build the finite element mesh from it for us.
Step 4: Describe the inversion
Make a directory for the results, then describe the reconstruction we want:
outdir = mkpath("first-inversion")
prob = InversionProblem(data = data,
frequency = 60.0,
model = :isotropic,
output = joinpath(outdir, "recon"),
max_global_iter = 3)Those four settings are: the drive frequency this data was acquired at (60 Hz), the simplest of Sentinel's material models, where to write results, and a cap of three iterations to keep this tutorial quick.
Notice that nothing has been computed yet. InversionProblem only describes the reconstruction; it does not run it.
Step 5: Run the reconstruction
result = solve(prob)Sentinel now generates a mesh from the data and iterates. The output should look something like this:
Inverse solve: 3 global iterations max
Zone edge: [0.0196, 0.0196, 0.0196], overlap: 15.0%
3 iteration structures
Grid: 147 nodes, 122 boundary nodes → numsig target
────────────────────────────────────────────────────────────────────────
Global iter 1/3 — 1 zones (seed elem 2)...
Zones: 1/1 (100%) — 3.2s elapsed, ETA 0s
── Iter 1/3 done in 3.6s ──
epsilon=2.1449e-02 (tol=1.0e-03), obj=1.2323e-08, disp_err=1.2323e-08
mean_shear=3340.9 Pa, zones=1/1 (seed=2) in 3.2s, CG_wt=0.040
Global iter 2/3 — 1 zones (seed elem 7)...
Zones: 1/1 (100%) — 0.1s elapsed, ETA 0s
── Iter 2/3 done in 0.1s ──
epsilon=8.2444e-03 (tol=1.0e-03), obj=1.2296e-08, disp_err=1.2296e-08
mean_shear=3366.4 Pa, zones=1/1 (seed=7) in 0.1s, CG_wt=0.025
Global iter 3/3 — 1 zones (seed elem 5)...
Zones: 1/1 (100%) — 0.1s elapsed, ETA 0s
── Iter 3/3 done in 0.1s ──
epsilon=5.5457e-03 (tol=1.0e-03), obj=1.2421e-08, disp_err=1.2421e-08
mean_shear=3378.0 Pa, zones=1/1 (seed=5) in 0.1s, CG_wt=0.010
────────────────────────────────────────────────────────────────────────
Inverse solve completed 3 iterations (not converged, eps=5.5457e-03)Look at the mean_shear value on each iteration: 3340.9 Pa, then 3366.4 Pa, then 3378.0 Pa. That is the average stiffness of the reconstruction being refined as the solver works. epsilon is how much the estimate changed on that iteration, and it is falling.
Notice too that the first iteration took about three seconds and the rest took a tenth of a second each — the first one pays for compiling the solver.
Your numbers may differ in the last digit or two depending on your machine.
Stopping at the iteration cap is expected
The last line says not converged. The run stopped because it hit our three-iteration cap, not because anything went wrong — we set that cap in Step 4 to keep the tutorial fast. A production reconstruction runs for many more iterations.
Step 6: Check what came back
result.state.convergedfalseThat is the same "not converged" from above, now as a value you can test in code. The reconstruction itself is perfectly usable.
Step 7: Look at the stiffness map
The reconstructed properties live on a mesh. Sample them onto a regular grid so we can look at them as an array:
mesh = result.setup.meshes[result.setup.config.meshind[1, 1]]
grid = interpolate_to_grid(result.material, mesh,
collect(mesh.origin), collect(mesh.res), mesh.dims;
prop_indices = [1])
mu = grid.data["prop1_real"]
size(mu)(5, 5, 2)This dataset is deliberately tiny, so the map is a 5 × 5 × 2 block — small enough to print. Let's look at the first slice, rounded to whole pascals:
round.(Int, mu[:, :, 1])5×5 Matrix{Int64}:
3217 3147 3070 3153 3263
3249 3218 3180 3322 3368
3432 3558 3618 3724 3587
3490 3628 3636 3665 3487
3440 3534 3454 3431 3339This is the shear modulus map — the stiffness Sentinel recovered at each point, in pascals, roughly 3.1 to 3.7 kPa.
Check the range across the whole volume:
extrema(mu)(3070.213590426092, 3727.671873216156)These values are already in pascals; the export helpers apply Sentinel's internal scaling for you. See How to Export Results to MATLAB and ParaView for the details and for the other export paths.
Don't read anything physical into these numbers
The bundled dataset is synthetic — its displacement field is a smooth numerical ramp, not a real scan — and the reconstruction starts from a uniform 3300 Pa guess. So the map you just produced stays within a few percent of that starting value, and its shape is not a measurement of anything. It tells you the pipeline ran correctly, which is what we came here for.
Step 8: Save it for ParaView
Write the same reconstruction to a VTK file you can open in ParaView:
export_vtk(joinpath(outdir, "recon"), result.material, mesh; prop_indices = [1])Check that it landed:
readdir(outdir)3-element Vector{String}:
"generated_mesh"
"recon.config.toml"
"recon.vtu"recon.vtu is the stiffness map, generated_mesh/ is the mesh Sentinel built from the .mat file in Step 5, and recon.config.toml is a full record of the settings that produced this run.
What you have done
You have run a complete MRE inversion: you took measured displacement data, reconstructed the shear modulus field that explains it, looked at the resulting stiffness map, and exported it for visualization.
That is the same pipeline used for real brain reconstructions. The difference is scale — this dataset had 147 nodes and one zone, where a brain dataset has hundreds of thousands of nodes spread over hundreds of zones.
Where to go next:
To run this from the shell instead of the REPL, see the Command-Line Interface.
To reconstruct your own
.matdata, see Getting Started.To pick a material model for your tissue, see How to Choose a Material Model.
To understand how a large volume is split into zones, see Zone-Based Domain Decomposition.
For the equations behind the reconstruction, see the Mathematical Reference.