Repository navigation
Releases: physwkim/tomoxide
Release list
v0.10.2
Fixed
- lprec on CUDA indexes past 2^31 elements on Windows. Its kernels
computed offsets inlong, which is 32 bits under MSVC, so a chunk of
more than 2^31 floats (8 GiB) wrapped and read and wrote the wrong
memory. They uselong longnow, as every other kernel already did.
v0.10.1
Fixed
- The
cudafeature builds with MSVC on Windows.build.rspassed
gcc's-fPICto cl.exe, linked astdc++MSVC does not have, and looked
for the CUDA libraries only inlib64. It now builds the kernels against
the CRT Rust links (/MD, or/MTwithcrt-static), leaves the C++
runtime to cl.exe's objects, and also searches%CUDA_PATH%\lib\x64.
stripe.curead the Titarenko sinogram through a GNU statement
expression, which nvcc rejects under cl.exe; it is a device function now.
CI builds and links it on Windows with CUDA 13.0.
v0.10.0
Changed
- Breaking:
TiltViewshands over the band-middle layer (layer, at
layer_z=recon::center::middle_slice) instead of thez_peakslice.
Across a thick band the argmax wanders (z 843 at 16° against 387–467 at
18–24° on one scan), so the montage compared different planes; the band
middle is one layer at every tilt.PreppedStack::default_slicereads the
same rule, and the GUI tilt montage shows that layer. - Breaking:
cuda::reconstruct_lamino_streamingtakes a
cancel: Option<&CancelToken>beforeon_tile, checked between the
chunks of every stage and every Fbp/Linerec round; the device path drains
its in-flight work before it returnsError::Cancelled.lamino_tilt_scan
hands its token through, so a cancelled Fourierrec tilt stops before its
first tile where it ran on for the whole volume.
Fixed
tomoxide --versionnames lightweight release tags. The release tags
from v0.7.0 on are lightweight, whichgit describeskips without
--tags, so a v0.9.0 build readv0.6.0-126-gf7ba8ec7fcb1. Tagging the
built commit now reruns the build script, which kept the old name.cuda::center_probeandlamino_tilt_probestop at the next tile. They
transposed the stack to sinogram order before their first cancel check, and
lamino_filter_to_hostthen filtered every tile unchecked, so a step1000
bin-2 sweep could not stop for up to 8 s. Each tile now reads its rows in
place and checks the token.
v0.9.0
Added
PreppedStack::reconstructandselect.reconstructruns the phase,
stripe and CUDA laminography tail ofreconstruct_lamino_streamingon the
normalized stack the driver already holds, so a preview does not reload the
file;selectkeeps a subset of the projections (an even/odd half).Config::algorithm,recon_paramsandprep— the one reading of a
recipe's algorithm, reconstruction parameters and stripe/phase prep.
tomoxide reconlays its flags over the config and calls them, and the GUI
builds its preview from them, so a preview runs what the saved recipe would.SampleBand::at: the band carried into another tilt's volume through the
detector rows, whichlamino_tilt_scanalready did privately. A caller that
read the band at one tilt can place it at another without a scan.tomoxide --versionnames the commit it was built from
(git describe --always --dirty --abbrev=12), so a job log identifies the
build. A copy built outside tomoxide's own checkout prints the version alone.
Changed
- Breaking:
lamino_tilt_scanandPreppedStack::tilt_scanhandon_tilta
TiltViews(thez_peakslice and the xz plane throughy = ny / 2over
the whole depth) instead of the slice alone. The xz plane is how the layers
stack, which the eye compares across tilts and which the streamed volume was
dropped before a caller could read. - Breaking: the align sweeps take a
cancel: Option<&CancelToken>:
PreppedStack::center_sweepandtilt_scan,cuda::center_probe,
center_probe_sweepandlamino_tilt_probestop withError::Cancelledat
the next probe, angle-chunk, tilt or tile boundary.lamino_tilt_scantakes
its tilts and band as oneTiltCandidates. The GUI cancels both the centre
sweep and the tilt scan with a per-job token. - CUDA Fourierrec laminography streams its output in z-tiles of ≤256 MiB,
each transposed fromp00as it is emitted, where it built the whole
volume besidep00and emitted it as one tile. Host peak drops by one
volume; tiles still arrive only once the reconstruction is done.
v0.8.0
Changed
tomoxide-guinow depends onrsplot/rsdm=0.5.5(was=0.5.2).
rsdm 0.5.5 builds against epics-rs 0.24 — the same version as the GUI's
live-testepics-pva-rsdev-dependency — so the dependency tree unifies on
one epics-pva-rs copy again (0.7.0 carried a 0.21 + 0.24 pair). rsplot moves
in lockstep because rsdm 0.5.5 requiresrsplot ^0.5.5; the jump also picks
up rsplot 0.5.3'sVolumeRaycasterrendering fixes (thickness cue,
faint-voxel hue) for the XANES 3-D view.- Every tilt↔phi conversion goes through
geometry::lamino_phi/
lamino_tilt_deg. The CPU lamino path spelled itd/180·π, which differs
by 1 ULP fromd·π/180at 298 of 1801 tenth-degree tilts; it now takes the
latter, as the CLI, GUI and CUDA sites already did.
Added
tomoxide::align::PreppedStack— the one alignment driver. Loads and
preps the stack once, then runs the rings estimate, the tilt-flow estimate,
the centre probe sweep and the tilt scan on it (align::gridbuilds the
sweep candidates).tomoxide alignand the GUI each carried their own copy;
both now drive this one.recon::tilt_flow::estimate_tilt_flow— the laminography tilt with no
reconstruction. Fits the vertical flow between projections a few steps apart
to a plane whose slope is the tilt and whose zero is the axis column. On the
261003 step scan (bin 2, 20° nominal) it reads 19.40 ± 1.46°, every pair
matching the Python original it ports.recon::center::register_shift,find_center_rings_from_mean,
focus_by_z.register_shiftis the phase-correlation registration
find_center_pcruns, exposed for the 0°-vs-360° drift check.
find_center_rings_from_meanruns the rings estimate on a mean projection
already in hand or on a band of its rows;find_center_ringsdelegates to it.
focus_by_zis the one bulkslice_focusscorer, which the tilt scan and
the centre sweep now share.Geometry::valid_fov— per slice, the radius every projection saw. Off
the middle of a laminography volume a voxel leaves the detector for part of
the orbit, and the slice there is no longer the sample's.prep_bandandPrepOptions::row_separable;Config::stripe_method/
phase_method.prep_bandpreps a detector-row band as the whole stack is
prepped, widening the read by the phase kernel's margin.row_separablesays
whether a prep may be run chunk by chunk. The twoConfigmethods are the one
method-name parser for the CLI and the GUI, so the GUI now accepts every case
and thevo_all/vo_sortspellings the CLI did.
Fixed
find_center_ringsandfind_center_siftread half a pixel right. Both
register an image against its left–right flip, which mirrors about column
(n − 1)/2, but converted the shift aboutn/2. They now use(n − 1)/2,
the axisfind_center_pcandfind_center_voalready use. A synthetic
bullseye now reads its own column (was +0.50); the aligned pouch scan reads
397.00 against a known 396 (was 397.50). tomocupy's SIFT formulan//2 − shift/2carries the same half pixel on even widths and is not copied.- Fourier laminography (
recon::lamino, CUDAFourierrec) came out flipped
in y against Linerec, FBP and the volume it was projected from. The USFFT
divker kept tomocupy'sn1−ty−1row, which matches tomocupy's own y-flipped
linerec; tomoxide's linerec is not flipped. The row is nowty, so
lamino_projectat zero tilt equals the parallel projection and Fourierrec
volumes are the y-flip of earlier ones and of tomocupy's. - Fourier laminography filtered its projections with its own ramp, a plain
linear|f|on a2·detwedge pad, while tomocupy'sLamFourierRecuses its
FBP filter: the_wintquadrature ramp on4·detw. Every backend now builds
the lamino filter frommake_fbp_filter(RampShape::Wint), and Pearson
against tomocupy on the 261003 scan (parzen) rose from 0.9975 to 0.99996
8×-binned and from 0.99933 to 0.999998 2×-binned.--filter noneis now no
filter, as for every other method, rather than the bare ramp. tomoxide reconwith phase retrieval took the row-chunked paths, the
CUDA/wgpu streaming pipeline and the multi-GPU shard, which refuse phase
retrieval because a 2D phase filter couples rows. A plan that is not
row_separablenow takes the whole-volume path, andrecon_stepsrefuses it
as it doeschainand--lamino_angle.--start_rowbands truncated the Paganin kernel at their edges. The band
was read as bare rowsz0..z1before phase retrieval (relative error 0.25 at
the edge rows of a 400..600 band); it now preps throughprep_band.
v0.7.0
Changed
tomoxide-gui's live-test dev-dependencyepics-pva-rsis now0.24
(was0.21). The in-process test server and rsdm's internal pvAccess client
(^0.21at the pinned rsdm) are coupled only by a TCP address, so the two
copies coexist and talk wire protocol — verified by the live source tests.tomoxide-guinow depends onrsplot/rsdm=0.5.2(was=0.5.0).
0.5.1 adds theVolumeRaycasterwidget the XANES 3-D chemical-map view uses
and 0.5.2 hardens it; the GUI builds against crates.io again, with the
local-checkout[patch.crates-io]block commented out.
Added
recon::center::judge_sweep— a focus sweep's verdict, not just its
argmax. ReturnsSweepVerdict::{Resolved, Railed, Ambiguous, Flat}, and only
Resolvedcarries a value (SweepVerdict::resolved());best()exists to
show the winner on a plot or montage, never to adopt it. Every caller —align,
the GUI's centre and tilt steps — now refuses rather than reports a number the
curve did not establish, which matters because a bad axis silently poisons the
tilt search that consumes it.
The verdict is one uniform test — does this lobe own at least 20 % of the
curve's height? — asked of the winner and of every other lobe. It replaces an
edge check, which measurement showed is blind to both ways a real sweep fails.
On the aligned pouch scan (known axis 396, tilt 44°), swept ±40 px: on one
probed slice the curve carries three lobes and the highest is 417 — 21 px
wrong, interior, beating the truth by 0.34 %, so no edge check can fire
(Ambiguous, naming 396 as the strongest rival at 55 % prominence). On
another the curve instead rises across the whole window and turns over one
sample short of the edge at 435 — an interior maximum with no rival to
contradict it, and still nothing but the range running out (Railed). Both are
the same fact: the winner owns no lobe of its own. Narrowed to ±8 px around a
prior, the same curve resolves 395.75 at 83 %. (These sweeps predate the
z-centring fix below, which found the probe's slice index offset by
(rh − nz)/2from the volume z it claimed — the curve shapes and their
verdicts, not the planes' names, are what these measurements establish.)tomoxide align— the alignment workflow as a subcommand, plus the two
library pieces it stands on.recon::center::find_center_ringsis the ring
estimator fromdocs/LAMINOGRAPHY_ALIGNMENT.md§1: the 360° mean projection is
a bullseye centred on the rotation axis, registered against its own mirror. It
reports aprominence—(peak − median)/MADof the registration profile —
and flags the scan when the rings never closed, which is the acquisition-time
misalignment no reconstruction geometry repairs (measured: aligned scan 397.50
at prominence 21.0 → trustworthy; misaligned scan 281.72 at 2.22 → flagged, and
alignstops there unless--force).cuda::center_probe/center_probe_sweep
then refine it:cfunc_linerec'sbackprojection_tryreconstructs one slice at
N candidate centres in one launch off a single filtering. A probe slice
equals the reconstruction exactly only when the shift is a whole number of
columns (measured 2e-7…9e-7 of peak) and differs by ~1.6 % at half a column, so
a naive sub-pixel sweep ranks integer offsets artificially sharp;
center_probe_sweepremoves that by construction, issuing one probe per
fractional lattice, and never producing the biased slices at all. End to end on
the aligned pouch scan, swept ±8 px around the axis read off the rings:
--center 395.00against a known 396. Around a prior is the whole contract —
the same sweep widened to ±40 px grows three lobes and picks 417, and that is
whatjudge_sweepabove now refuses rather than returns.
The two axes are searched by different machinery, and the asymmetry is physics,
not an optimisation left undone. A fixed slice is sound for the centre — an
in-plane shift does not move the in-focus layer, so one slice can rank the
candidates — and unsound for the tilt, whose response is broad
(~2 % per degree) while the in-focus layer moves in z with it (z_peak800 →
1120 as tilt went 40° → 58°), so a fixed slice scores the wrong plane by more
than the tilt signal is worth. Measured: a fixed-slice tilt sweep returns 48° at
one slice and rails to the top of any range at another. Hence
recon::center::lamino_tilt_scan(--tilt_width/--tilt_step): one full
reconstruction per candidate — that needs every (tilt, z) pair, which is
exactly the worklamino_tilt_probeskips, so the probe buys nothing there and
remains for the case its saving is real, a plane that is already known — scored
by the maxslice_focusover its slices, optionally inside the sample's z
band (recon::center::SampleBand,--focus_z LO:HI) — the reference
workflow's practice, and the guard wherever structured noise competes with
the sample. A band fixed in z would miss the layer (it moves: the sample's
spike sits at z 837 / 890 / 956 at tilts 40° / 44° / 48°), so the band is
stated once, at the tilt it was read from, and carried into each candidate's
volume through the detector rows — which belong to the data and do not move
(v − nz/2 = cos(tilt)·(z − rh/2); the mapping reproduces those measured
spikes to ~1 px, and that measurement is pinned as a unit test). End to end
on the aligned pouch scan at 44°: the unbanded tilt curve over 36…52° is
unimodal with its interior peak on the known 44° and a 3.0× span, every
candidate'sz_peakon the sample layer, the three measured spikes among
them. The scan streams the volume, so its memory cost is one
rh-tile regardless of depth, and it keeps what the reconstruction already
computed: the winning in-band slice (§3 confirms by eye) andfocus_by_z, the
focus of every slice — the profile the next band is read off, so it is never
truncated to the current one.recon::center::pick_interior_max— the argmax of a sweep, plus whether the
sweep earned the right to call it an optimum. A maximum on the first or last
candidate is the range running out, and there is no way to tell from inside the
range whether the true peak is at the edge or beyond it. Bothalignand the
GUI refuse to adopt a railed pick; measured worth: it caught a real tilt scan
railing at 47° on the pouch scan, and a centre sweep flat to 1.8 % and railing
when pointed at an empty slice.recon::center::slice_focusandmean_projection— the two measurements
the alignment method is written in, as library functions rather than copies in
each consumer.slice_focusis §2's score (mean |∇|² inside a 0.92-FOV disk;
the disk matters — reconstruction geometry decides where the sampling leaves the
detector, so scoring the full frame scores a boundary that moves with the
parameter being scored).mean_projectionis the bullseyefind_center_rings
measures, exposed because §1 makes looking at it step one and treats
eye-vs-correlation disagreement as the misalignment flag — which needs the
image, not only the number.tomoxide-gui: laminography alignment on the Center screen (Beam:
Parallel | Laminography). The toggle is not a display option: under a
laminographic tilt the axis leans along the beam, so 0°/180° is not a mirror of
the object andfind_center_vo/_pc/_siftall lose the symmetry they
assume (mirror registration scattered 395…607 against a known 396). It picks the
estimator family valid for the beam. The three steps are the doc's, and each is
a picture the CLI can only summarise as a number: 1 Rings shows the mean
projection itself, so closed rings versus arcs that never close is a thing you
see rather than a prominence you trust; 2 Centre is the probe sweep as a
montage plus a focus curve with click-to-pick; 3 Tilt streams one result per
full reconstruction — the in-focus slice into a montage, focus-vs-tilt, and
focus-by-slice for the selected candidate — with a cancel, since it is minutes
per candidate. Railed picks are shown as such and never adopted.docs/LAMINOGRAPHY_ALIGNMENT.md— how to find the laminography rotation
center and tilt. A field-tested recipe, validated against a scan whose center
was already known independently. Leads with reading the center off the raw data
before reconstructing anything: the mean of all projections over 360° is a
bullseye of rings centred on the rotation axis, so the center column is
readable by eye (automated flip-registration measured 397.5 vs a known 396),
and rings that open into arcs instead of closing diagnose a scan that was
mis-aligned at acquisition. Also documents the reconstruction sweep that
follows (full projections; focus scored as the max over z, optionally confined
to a sample band carried per tilt) and records the three methods that failed
validation — 0°/180° mirror registration (the tilted axis breaks the mirror
symmetry), 1-D column-profile symmetry (truncation destroys it), and
sub-sampled projections with a fixed z band.- Fourier laminography (
LamFourierRec) now honours--filter. The
Fourier/USFFT lamino path applied a plain|f|ramp and ignored the selected
apodisation, unlike tomocupy'sLamFourierRec(which filters with
args.fbp_filter, default parzen) and unlike tomoxide's own fbp/linerec lamino
paths. The apodisation window is now folded into the projection ramp on all
three paths (CPU golden, CUDA host, CUDA device) from a single window
definition shared withmake_fbp_filter. The default is parzen (tomocupy
parity); pass--filter rampfor the previous plain-ramp behaviour. - Multi-GPU angle-split SIRT for laminography. Laminographic SIRT can now run
across multiple GPUs by partitioning the projection angles into contiguous
per-device chun...
v0.6.0
Changed
tomoxide-guinow depends onrsplot0.5.0 from crates.io (was the
siplotgit dependency pinned to a rev). The plotting crate and its GitHub
repo were renamedsiplot→rsplot(and the sibling EPICS engine
sidm→rsdm) and published to crates.io; the GUI switches to
rsplot = "=0.5.0"with a commented-out[patch.crates-io]for local dev.
Mostly ause siplot→use rsplotidentifier rename. The one behavioural
gap: the localsiplotcarried an unpushed commit making aPlot2D
crosshair read out the pixel value under the cursor ("x, y, value");rsplot
0.5.0 instead exposes the value via the higher-levelImageView
(value_changed, silxPositionInfo"Data"). The Data sinogram inspector and
the Tune single-slice preview were migratedPlot2D→ImageViewto keep the
value readout (the preview'sColormapDialogis replaced byImageView's
interactive colorbar). GUI build/clippy/35 tests green on CPU and CUDA. The
Data sinogram inspector and Tune previewImageViewlayout was fixed and
verified on-screen: theImageViewposition-info bar is emptied and the value
readout is pinned to the bottom so the image fills exactly the available
height, keeping the sinogram's resizable bottom panel stable (an earlier
version let it grow into / collapse away from the projection browser).
Added
- CLI
--progress_json(recon,recon_steps) — one flushed JSON line
per completed output chunk on stdout
({"start":s,"end":e,"total":nz,"secs":t}; global slice range, full output
slice count, wall-clock seconds since run start), implemented as a thin
VolumeWritertee. The multi-GPU shard orchestrator forwards the flag and
lets children inherit stdout, so shard lines stream through with global
ranges against one total. Machine progress for wrappers — the GUI Run
screen tails these from its subprocess runs. Runtime-only (not a config
key); progress lines are exactly the stdout lines starting with{. - Cooperative cancellation for the chunked drivers — new
pipeline::CancelToken(Clone-able atomic flag) attachable via
ReconSteps::with_cancel; all three drivers (run,run_streaming,
run_streaming_pipelined_range) check it at chunk boundaries and stop with
the newError::Cancelled. Cancellation truncates: chunks already written
stay on disk and the writer finalizes the partial output. io::InMemoryWriter— aVolumeWriterthat collects the reconstruction
into a shared in-memoryInMemoryVolume(theArc<Mutex<…>>handle
survives the pipelined driver consuming the writer) with an optional
on_chunkprogress callback. Backing store for GUI previews.tomoxide::config(featureconfig) — the CLI's TOMLConfigmoved
into the library behind a default-off feature (optionalserde/toml
deps) so GUI recipes and CLI configs are one format. Gains three fields:
lamino_angle,dtype, andoutput(base path; each writer adds its own
suffix). The CLI gained the matching--outputflag,--confignow feeds
all three, and the multi-GPU z-shard fan-out forwards the resolved output
path to its children.io::read_h5_frame— read one[ny, nx]frame of a 3-D HDF5 stack as
f32through the reader's full dtype dispatch (u8/i8/u16/i16/u32/i32/f32/
f64). Real beamline stacks are usuallyuint16; the GUI projection browser
reads through this (rsplot's own HDF5 loader handles 4/8-byte floats only).io::read_h5_sizes— the(n, ny, nx)shape probe paired with
read_h5_frame, so a volume browser can size its frame list without
reading any data.- Vo 2018 single-method stripe variants through config/CLI — the
long-implementedStripeMethod::VoSort/VoFilter/VoLarge/VoDead/
VoFitare now selectable as--remove_stripe vo-sort | vo-filter | vo-large | vo-dead | vo-fit, with per-method config fields/flags
(vo_sort_*,vo_filter_*,vo_large_*,vo_dead_*,vo_fit_*;
median sizes use thefw_levelconvention0= tomopy auto) and
multi-GPU shard forwarding. tomoxide-guiM1 (offline preview loop) — new repo-internal but
workspace-excluded crate (rsplot is edition-2024/rust-1.92; workspace
membership would raise the repo's effective MSRV above 1.82) implementing
docs/GUI.md M1: a single worker thread owns theEngineand all HDF5
handles (!Send); Data (DXchange open + metadata, projection browser —
frames load throughio::read_h5_framesouint16beamline stacks render,
with the colormap scaled to the stack's raw-count range — theta plot, raw
sinogram inspector), Tune (single-slice preview through
run_streaming_pipelined_rangeintoio::InMemoryWriter, parameter panel
with auto-recon, A/B pin compare), Center (Vo / entropy /
phase-correlation / SIFT auto methods, ±0.5/±0.25 px tweak, hand-off to
Tune), and recipe save/load (recipe file = CLI config TOML plus a[gui]
table the CLI ignores).tomoxide-gui [FILE] [--mode <mode>]opens a
dataset and/or picks the starting mode from the command line, and Tune
fires the first preview of a fresh dataset by itself (the auto toggle
still gates re-runs on parameter changes).- GUI Tune — λ sweep (L-curve regularization tuner). For algorithms whose
reg_par[0]is a regularization strength λ (tv,grad,tikh,
pml_*,ospml_*), a new workerJob::LambdaSweepreconstructs the preview
slice once per λ across a log-spaced grid and scores each on the L-curve —
data residual‖A x − b‖₂(the fidelity term, viasim::project) vs the
reconstruction's isotropic TV seminorm (roughness). A floating window shows
the per-λ montage over the L-curve; the max-distance-to-chord corner is the
suggested λ, a click picks any point, and "Use selected λ" writes it to
reg_par[0]. The guide is the L-curve corner, not a sharpness auto-pick:
sharpness falls monotonically with λ and real data has no ground truth
(docs/BENCHMARKS.md§10), so the choice stays the user's. Verified finite
and λ-varying on both CPU and the CUDA device-resident 1-slice path. - GUI design document (
docs/GUI.md) — design for atomoxide-gui
desktop application built on rsplot (egui + wgpu) and rsdm (EPICS PVA):
offline workflow (dataset browsing, single-slice tune loop with A/B
compare, center finding with awrite_centersweep montage, subprocess
full-volume runs, output browsing) plus a tomostream-style live streaming
mode, with the prioritized list of library additions it requires.
Added
ext_pad— truncated-projection support extension for iterative
methods. Real samples routinely overhang the field of view, so the
projections don't end at zero; an iterative forward model whose support is
the detector-width grid then dumps that inconsistency into a huge FOV-edge
ring and background offset that swamp the (intact) interior — on real
800-wide data a 10-iteration CGLS correlated only 0.56 with fbp full-frame
while agreeing 0.99 in the interior. WithReconParams::ext_pad(CLI
--ext_pad, configext_pad, GUI Tune "extend FOV" — on by default in the
GUI) the sinogram is edge-replicate extended byncols/4per side, the
solve runs on the wider grid, and the central crop is returned; the wrapper
sits above the backend dispatch so CPU/CUDA/wgpu see the identical extended
problem. Real-data result: CGLS-10 full-frame correlation with fbp 0.56 →
0.996, interior display contrast restored to fbp's level. Off by default in
the library (tomopy-parity semantics unchanged; ~2.25× cost per iteration).
Changed
- Analytic reconstructions now emit the physical μ — the shared
make_fbp_filterbase ramp is the physical|ω|inversion filter (peak
0.5at Nyquist) instead of tomopy/tomocupy's doubled ramp (peak1), and
recon::gridrec'sramp_scaledrops its matching empirical×2. Every
analytic method (FBP, linerec, fourierrec, lprec, gridrec, on all backends)
therefore reconstructs the attenuation μ per pixel-unit rather than2×μ—
the same scale the iterative solvers converge to, so fbp→iterative
warm-starts are now scale-consistent. All analytic output amplitudes are
halved; downstream code that hard-coded the old scale (rescale windows,
8/16-bit export ranges) must adjust. Cross-method and cross-backend ratios
are unchanged (both sides halve). Pinned bytests/analytic_amplitude.rs(a
unit disk reconstructs to core mean ≈ 1.0). This is a deliberate departure
from both upstreams, whose absolute analytic amplitude is itself
convention-dependent (tomopy gridrec ≈ 1.16×μ, tomocupy ≈ 4/π×μ). - Iterative reconstructions now converge to the physical μ — the
forward/back-projector pair used by the iterative solvers is the plain
line-integral Radon transformWand its pure adjointWᵀon every
backend (CPU, CUDA, wgpu); theπ/nprojFBP angular-quadrature weight
previously baked into both operators is now passed by the analytic FBP
call sites only, where it belongs. A converged SIRT/CGLS/MLEM/… solve of
W x = ptherefore lands on the attenuation per pixel-unit (pinned by the
newtests/iterative_amplitude.rsagainst an analytic disk sinogram)
instead of(nproj/π)·μ— e.g. ≈ 143× smaller at 450 projections —
matching ART/BART, which always solved the ungained ray equations. Iterate
trajectories are unchanged up to that overall scale for the self-scaling
methods (SIRT/CGLS/MLEM/OSEM/PML/OSPML); forgrad/tikh/tvthe fixed
step and regularization now act on the physical scale, so hand-tuned
reg_parvalues from before may need retuning. The grad/tv host and CUDA
gain-compensation machinery (adj_scale/fwd_gain_inv) is deleted.
Analytic (FBP/gridrec/…) outputs are unchanged. - GUI preview autoscale is percentile-robust — image colormaps scale to
the 0.5–99.5 % range instead of the absolu...
v0.5.1
Changed
- HDF5 reads are now rayon-parallel (rust-hdf5 0.3.0 with the
parallel
feature: parallel chunk decode — deflate inflate + chunk gather). Read sits on
the critical path of the streaming reconstruction (each row-chunk re-decodes
the whole projection-chunked dataset), so this is a large end-to-end win. On a
1200×512×512fbpstreaming reconstruction (1 GPU): gzip-chunked input
73.5 s → 4.3 s (17×), uncompressed-chunked 9.5 s → 3.1 s (3.1×);
reconstruction output is byte-for-byte identical (the decode is deterministic).
v0.5.0
Headline: the wgpu (portable GPU) backend becomes device-resident end to end,
closing most of the gap to CUDA — analytic recon, streaming, SIRT and the
gridding recons now keep their data on the GPU across the pipeline, and the
scatter kernels use native f32 atomics (wgpu 30). The workspace is also now
self-contained (all dependencies resolve from crates.io) and gains
CI on every branch push across Linux, Windows and macOS.
Added
- CGLS reconstruction (
--algorithm cgls). Conjugate-gradient least squares
(the standard algorithm; recurrence parity-checked against ASTRA's
CglsAlgorithmbut implemented independently — ASTRA is GPL-3.0, no ASTRA code
is used): a Krylov solver of the same‖Ax − b‖²normal equations as SIRT/GRAD
but with the optimal step and conjugate directions, so it reaches a given
residual in far fewer iterations (≈4–30× vs SIRT). Runs on every backend via
the generic solver, plus a CUDA device-resident fast path (per-slice dot/axpy
kernels; one upload / one download across all iterations). Parameter-free;
supports warm-start chaining. No built-in regularization, so it needs early
stopping on ill-posed data. - Per-stage iteration budgets in algorithm chains. An iterative stage in a
--algorithmchain can carry a:iterssuffix
(e.g.--algorithm fbp,sirt:30,tv:10); stages without one fall back to
--num_iter. Analytic stages reject the suffix. Lets a chain spend, say, 30
SIRT iterations then 10 TV iterations in one run — previously every stage shared
a single--num_iter. - Continuous integration (GitHub Actions). Runs on every branch push and
every pull request:rustfmt, then a per-platform matrix — Linux/x86_64,
Windows/x86_64, macOS/arm64 — each runningclippy -D warnings, the test
suite and doctests on the default (CPU) build plus agpu-wgpucompile-check,
and a Linux job that type-checks the CUDA FFI bindings without an nvcc toolkit.
Changed
- wgpu backend upgraded to wgpu 30 (from 23). Enables
SHADER_FLOAT32_ATOMIC
(VulkanVK_EXT_shader_atomic_float); devices without it fall back to the
portable compare-exchange emulation automatically. - wgpu reconstruction is now device-resident. A fused analytic path keeps the
filtered sinogram on-GPU (fbp/linerec/fourierrec); device-resident streaming
reconstructs chunk-by-chunk (fbp/linerec/fourierrec and lprec, the latter
caching its log-polar grids across chunks); SIRT keeps volume/sinogram resident
across iterations; the FBP filter and the fourierrec/lprec gridding run on the
GPU. The scatter kernels (forward projection, fourierrec gather/wrap, lprec
gather) use nativef32atomicAdd, replacing the CAS-emulation penalty —
forward projection ~6.6× and SIRT ~6.4× faster (same-build A/B). Net effect:
fbp/linerec reach CUDA parity, SIRT and lprec beat CUDA on this hardware. - Shared host precompute sped up. lprec precomputes its FFTs on the CPU
(rustfft) instead of a GPU round-trip (wgpu and CUDA),build_gridscoordinate
loops are parallelised with rayon, and the CPUminus_logprep is parallelised
across the projection volume. xraylib(the optionalbeam-hardeningdependency) is now sourced from
crates.io instead of an out-of-repo path dependency, so a fresh clone / CI
resolves the workspace without a sibling checkout. Local development against a
sibling checkout can override via[patch.crates-io].
Fixed
- wgpu forward projection now applies the
π/nprojadjoint gain, so the
{A, Aᵀ}pair is matched — fixesforward_projectoutput scale and the
iterative solvers built on it (SIRT). - stripe TI block-size division uses
checked_div(satisfies
clippy::manual_checked_ops, new on stable Rust 1.96). - HDF5 test fixtures were swallowed by the repo-wide
*.h5gitignore and
never committed; they are now tracked so the test suite runs on a fresh clone.
tomoxide v0.4.0
tomoxide v0.4.0
tomoxide is a Rust tomographic reconstruction toolkit combining the algorithmic
breadth of tomopy (CPU libtomo) with the GPU-accelerated streaming
reconstruction of tomocupy (CUDA), behind a single tri-backend abstraction:
CPU / CUDA / wgpu.
This release has two headline themes: the full iterative reconstruction suite
now runs on the GPU (device-resident), and a cross-backend convention
unification that makes CUDA analytic output match the CPU/wgpu (tomopy)
convention. The CLI also gains the full preprocessing / iterative / filter
composition surface and a live TOML config.
Behaviour changes — read before upgrading
- CUDA analytic output changed orientation and amplitude. CUDA analytic
reconstruction (fbp/linerec/fourierrec/lprec) now matches the
CPU/wgpu (tomopy) convention: the tomocupy vertical (y-axis) flip is removed and
the amplitude is tomopy scale (cuda/cpu ≈ 1), not tomocupy's. In 0.3.0 CUDA
deliberately matched tomocupy; if you depended on that, this is a breaking
change. Backends now agree up to a deterministic ~1.6%_wint-vs-linear ramp
shape residual (Pearson ≈ 1.0). sim::projectoutput changed byπ/nproj. The CPU forward projector is now
a true adjoint of the back-projector at a single scale, matching the CUDA pair,
so the iterative solvers stay well-posed across backends. The fixed-step
grad/tvsolvers gain-normalize the residual, so their behaviour is unchanged.- Laminography is excluded from the unification. The CUDA (tilted linerec) and
CPU (USFFT) lamino paths are different algorithms and are not scale-comparable;
each is validated against its own reference and both stay y-flipped. Do not
warm-start one lamino backend from the other.
Highlights
- The iterative suite runs on the GPU, device-resident. A CUDA forward
projector (exact adjoint of the linerec back-projector) unlocks tomopy's
iterative family on the device:sirt,mlem,osem,ospml,pml,grad,
tikh,tvkeep the volume and sinogram on the GPU across all iterations
(one upload, one download) — 1.3–11.4× over a per-iteration CUDA loop and
51–95× over CPU at 512².art/bartalso run on CUDA (bit-identical to CPU). - Warm-start / algorithm chaining. Seed a solver from a prior volume via
ReconParams.init, or from the CLI with--algorithm fbp,sirt— the analytic
result warm-starts the iterative refinement so it converges in fewer iterations. - A composable CLI.
recon/recon_stepsnow expose stripe removal, phase
retrieval (with physics), filter choice, iterations, regularization, and every
per-method parameter, and--config(atomoxide initTOML) drives a run with
flag > config > defaultprecedence. - Cross-backend convention unification. CUDA analytic reconstruction matches
CPU/wgpu in orientation and amplitude; pinned by
tests/cuda_cpu_convention_parity.rsand documented indocs/ARCHITECTURE.md.
Install
[dependencies]
tomoxide = "0.4.0"CLI:
cargo install tomoxide-cliGPU features are opt-in (cuda, gpu-wgpu); see the README for the feature
matrix, the command-line reference, and the benchmark tables.
See CHANGELOG.md for the complete list of changes.