Examples
The runnable example scripts.
Run these from the checkout root after installation. They use public APIs and write no data unless an output is requested.
Project and reconstruct a volume
uv run --no-sync python examples/simulate_and_reconstruct.pysimulate_and_reconstruct.py creates a 32³
Shepp–Logan object, generates 60 parallel projections with the Joseph model,
and runs 24 CGLS iterations with that same model. It prints the reference and
reconstruction shapes and the full-volume relative L2 error.
The iteration budget may end before convergence. The JAX backend runs on CPU or
an available GPU; prefix the command with JAX_PLATFORMS=cpu to require CPU.
Simulation and reconstruction share a discretization, so this is a usage example, not independent accuracy evidence. See the measurements for independent data and accepted-result comparisons.
Reproduce the README figure
Install the optional plotting group in your chosen environment. For CPU:
uv sync --locked --extra cpu --group examples --no-dev
JAX_PLATFORMS=cpu uv run --no-sync python examples/plot_reconstruction.py \
--size 64 --views 90 --iterations 40 \
--out images/reconstruction-example.pngThe checked-in figure was generated on CUDA using:
uv sync --locked --extra cuda12 --group examples --no-dev
uv run --no-sync python examples/plot_reconstruction.py \
--size 64 --views 90 --iterations 40 \
--out images/reconstruction-example.pngCPU and CUDA use matched models but floating-point reductions can differ; byte identical images across backends or plotting-library versions are not expected. Use another output path to preserve the checked-in illustration.
plot_reconstruction.py writes the PNG and an adjacent JSON file containing solver settings, full-volume error, termination, display ranges, versions, and script/image hashes. Both planes share attenuation and absolute-error scales, with no per-panel contrast fitting. Error is measured before display and uses the entire volume. The figure includes negative reconstruction values rather than clipping them away.
Align a scan with per-view motion
uv sync --locked --extra cuda12 --group examples --no-dev
uv run --no-sync python examples/align_misaligned_scan.pyalign_misaligned_scan.py simulates a 96³, 240-view,
full-turn 30° laminography scan of continuous Gaussian blobs, with analytic
line integrals at randomly perturbed poses (±1° rotations, ±2 px shifts per
view) and 0.2% noise. The data match no voxel discretisation. It reconstructs
once with the nominal poses (CGLS) and once jointly with the poses using
tj.align, then writes images/alignment-example.png and its JSON metrics,
replacing the checked-in figure; it takes no options.
On an RTX 4070 Laptop GPU it recovers rotations to 0.0022° RMS in about 18 s
including compilation, lowering the volume error from 0.68 to 0.047.
tj.align reports the least-motion estimate, so the truth is compared in its
least-motion version too (see
where the aligned volume sits).
Prepare and align complex ptychography projections
uv sync --locked --extra cuda12 --extra fourier-cuda12 --no-dev
uv run --no-sync python examples/align_ptychography.py /path/to/porous_glass_dataset.matalign_ptychography.py reads either PSI MATLAB example:
porous_glass_dataset.mat (v7.3, measured and already aligned) or
example_data.mat (v5, simulated with motion). The measured-data route crops
unreliable borders; the smaller simulation uses its full detector. Both prepare
complex transmission in bounded device batches. For a CPU-sized preview of the
simulation, keep all angles but bin the unwrapped phase, not complex pixels:
JAX_PLATFORMS=cpu uv run --no-sync python examples/align_ptychography.py \
/path/to/example_data.mat --binning 4 --iterations 5 -o phase-slices.npzThe five-iteration run is a preview, not a convergence or accuracy guarantee. The same workflow in Python, from the checkout root, is:
from examples.align_ptychography import run_example
volume, poses, report = run_example("example_data.mat", binning=4, iterations=5)
print(volume.shape, report["value_unit"])Its core steps are public APIs:
import tomojax as tj
from tomojax.alignment import ProjectionMatchingConfig
from tomojax.corrections import remove_phase_ramp, unwrap_phase
phase = -remove_phase_ramp(unwrap_phase(objects), air_mask=air * abs(objects)**2)
scan = tj.Scan(phase, geometry) # (view, v, u), geometry in pixel units
aligned = tj.align(scan, mode="pose", config=ProjectionMatchingConfig(iterations=20))
reconstruction = tj.reconstruct(aligned.scan, "fourier")The air mask must cover actual air, not sample pixels. The example's measured-data crop and air strips describe these supplied scans; they are not per-view alignment corrections and must be reconsidered for other samples. The joint estimator returns per-view translations without resampling the input. It can fit noise or model mismatch as motion, including on an already aligned scan; inspect pose estimates and physical slices rather than treating improved consistency or convergence as proof of physical accuracy.
No supplied aligned phase or reference image is used in inference. For the
measured file, wavelength and pixel size convert reconstructed phase to
refractive-index decrement, wavelength / (2*pi*pixel_size). The simulation has
no physical calibration: its output is phase per original pixel (rad/pixel),
not refractive index. Binning keeps geometry and detector-frame translations in
original-pixel units; one output voxel spans binning original pixels.
The example reports read, preparation, alignment, inverse and whole-workflow
times. It writes nothing unless -o slices.npz is supplied; that archive holds
central delta_xy/xz/yz or phase_per_pixel_xy/xz/yz slices, poses and unit
metadata. pixel_size_m is NaN when the input lacks physical calibration.
Open all three planes and check value_unit before interpreting values.
To keep the simulated example as a TomoJAX dataset and use the standard CLI, prepare phase in Python (do not apply an absorption logarithm):
import numpy as np
import tomojax as tj
from tomojax.alignment import ProjectionMatchingConfig
from examples.align_ptychography import read_psi_objects, prepare_phase, make_scan
objects, angles, _, _ = read_psi_objects("example_data.mat")
air = np.zeros(objects.shape[-2:], dtype=np.float32)
air[:, :50] = air[:, -50:] = 1 # known air in this supplied example
scan = make_scan(prepare_phase(objects, air), angles, binning=4)
aligned = tj.align(scan, mode="pose", config=ProjectionMatchingConfig(iterations=5))
tj.save("aligned-phase.nxs", aligned.scan)uv run --no-sync tomojax recon aligned-phase.nxs -o phase-recon.nxs --method fourier --roi off
uv run --no-sync tomojax inspect phase-recon.nxs --preview phase-previewsThese datasets retain original-pixel geometry: reconstructed values remain
rad/pixel. The joint translation configuration is a Python API; tomojax align
uses the general rigid-pose configuration, not this specialized estimator.
Preview a real cone-beam orbit on a CPU
import_walnut.py prepares a small single-orbit FIPS walnut scan for FDK or an iterative reconstruction, without ASTRA or a GPU. It uses shared TIFF/geometry loading with the multi-orbit example below. Follow the complete Python and CLI commands.
Bring a real multi-orbit scan into register
This one needs data: download a walnut from the FIPS collection (Der Sarkissian et al., Scientific Data 6, 215, 2019; CC BY 4.0; Zenodo record 2686726) and unzip it, then
uv run --no-sync python examples/align_walnut_orbits.py ~/data/walnuts/Walnut1 -o walnut.nxsalign_walnut_orbits.py reads the three orbits'
projections and the scanner's uncorrected geometry with tj.Scan.from_astra,
bins the detector 2 x 2, aligns every 4th view (900 in all) with tj.align, prints each
orbit's height relative to orbit 1 and reconstructs with 20 iterations of
non-negative least squares. On Walnut 1 it finds -0.38 and -0.76 mm for orbits
2 and 3, where the authors' corrected geometry has -0.40 and -0.79 mm, in about
six minutes on an 8 GB laptop GPU. The data fix the orbits' heights relative to
one another but can't pin down the walnut's absolute height: moving the whole
object, and every view with it, predicts the same projections, so the volume
can sit a fraction of a millimetre from where the scanner's record would put
it. The measurements compare the
reconstruction with the authors' reference.
Reconstruct raw laminography from nominal geometry
align_laminography.py combines public setup/motion
initialization, data-selected region, sparse pose fitting, untouched-view checks
and positive reconstruction. It is a conservative example, not a new alignment
mode or default. See the raw K11 recipe
for the command and limitations. Its HDF5 dataset paths and nominal tilt are
explicit arguments; it writes nothing unless -o is supplied.
Gallery figures
The scripts in gallery make the figures on the
gallery page: laminography alignment of a gVXR-rendered
chip package, ring removal on a real Diamond I12 scan, few-view sandstone
micro-CT, an offset-detector battery scan and wide-cone FDK against CGLS on a
lattice part. Each takes an output path; the phantoms are built on first use
and cached under .artifacts/figures.
Train a denoiser through the PyTorch projector
Install a CUDA-enabled PyTorch separately, then run train_denoiser.py on a Diamond I12 NXtomo row cut (raw counts with flat and dark frames, Zenodo 3339629):
python examples/train_denoiser.py full_rows1048-1111.nxs --center -138The small residual CNN learns from two disjoint, angle-interleaved view sets:
one supplies an FBP input, the other an independent image and projection target.
The loss sums image and projection mean-square errors in dimensionless units.
The CUDA PyTorch projector backpropagates with its exact transpose. A third view set is used
only for final projection-error reporting, never for training or input FBP.
The default bins 8×8, uses all views and takes 500 training steps.
Do not undersample each split: missing angular detail can teach the CNN to
blur structures even while its held-out projection error improves. Binning preserves physical
units, so the calibrated detector centre stays −138, not −138/8. Omit
--center to estimate it from the scan before splitting.
No clean image is needed for training. This is a single-scan demonstration;
projection residual is not image accuracy, and shared calibration errors,
rings and truncation can bias the result. Validate against an independent
reconstruction before interpreting learned features. Nothing is written
unless --output result.npz is given.