diffct#

diffct provides differentiable projectors and matched adjoints for computed tomography (CT) in PyTorch. It supports 2D parallel and fan beams and 3D cone and parallel beams. Each view can follow any trajectory, and autograd gives gradients for the volume and for trajectory tensors. Detector pixels can follow parameterized surfaces, with first-order gradients for their parameters. Views can be split across several GPUs.

Native kernels run on NVIDIA CUDA GPUs. The portable PyTorch backend runs on CPU, PyTorch ROCm and Apple Silicon (MPS) devices.

Code written for diffct 1.x must be updated; see Migrating from diffct 1.x to 2.0.

Install#

diffct needs Python 3.10 or newer, and a Python version for which PyTorch publishes wheels. Install PyTorch for your device first. Then print the CUDA version of your PyTorch build:

python -c "import torch; print(torch.version.cuda)"

Install diffct with the extra that matches this version. The extra installs the CUDA compiler libraries that Numba CUDA needs.

pip install "diffct[cu13]"
pip install "diffct[cu12]"
pip install diffct

Use backend="torch". Importing diffct and using this backend require no CUDA runtime.

git clone https://github.com/sypsyp97/diffct.git
cd diffct
pip install -e ".[cu13]"    # or ".[cu12]", or "." without CUDA
python examples/quickstart.py

Most scripts in examples/ need an NVIDIA GPU; plot_trajectory.py and benchmark_trajectory_admission.py --backend torch run on CPU.

Check the installation:

python -c "import torch, diffct; print(diffct.__version__); print(torch.cuda.is_available())"

Backend, precision and solver contracts are in Precision, training and reconstruction.

Quickstart#

Create the acquisition once. Then call project() and backproject().

import torch
from diffct import Projector, spiral_trajectory_3d

trajectory = spiral_trajectory_3d(360, sid=320.0, sdd=512.0, z_range=40.0, n_turns=1.0,
                                  device="cpu")
A = Projector(trajectory, volume_shape=(128, 128, 128), detector_shape=(384, 256),
              detector_spacing=0.8)

volume = torch.rand(A.volume_shape, device="cuda")
sinogram = A.project(volume)        # (360, 384, 256) line integrals
adjoint = A.backproject(sinogram)   # matched adjoint A^T

x = torch.zeros_like(volume, requires_grad=True)
loss = 0.5 * (A.project(x) - sinogram).square().sum()
loss.backward()                     # x.grad = A^T (A x - y)
import torch
from diffct import Projector, spiral_trajectory_3d

trajectory = spiral_trajectory_3d(60, sid=120.0, sdd=192.0, z_range=15.0, n_turns=1.0,
                                  device="cpu")
A = Projector(trajectory, volume_shape=(48, 48, 48), detector_shape=(144, 96),
              detector_spacing=0.8, backend="torch")

volume = torch.rand(A.volume_shape)
sinogram = A.project(volume)        # (60, 144, 96) line integrals
adjoint = A.backproject(sinogram)   # matched adjoint A^T

x = torch.zeros_like(volume, requires_grad=True)
loss = 0.5 * (A.project(x) - sinogram).square().sum()
loss.backward()                     # x.grad = A^T (A x - y)
import torch
from diffct import Projector, spiral_trajectory_3d

trajectory = spiral_trajectory_3d(60, sid=120.0, sdd=192.0, z_range=15.0, n_turns=1.0,
                                  device="cpu")
A = Projector(trajectory, volume_shape=(48, 48, 48), detector_shape=(144, 96),
              detector_spacing=0.8, backend="torch")

volume = torch.rand(A.volume_shape, device="mps")
sinogram = A.project(volume)        # (60, 144, 96) line integrals
adjoint = A.backproject(sinogram)   # matched adjoint A^T

x = torch.zeros_like(volume, requires_grad=True)
loss = 0.5 * (A.project(x) - sinogram).square().sum()
loss.backward()                     # x.grad = A^T (A x - y)

See Quickstart for shapes, devices and troubleshooting, Examples for complete reconstructions, and Performance for multi-GPU timings.

Citation and license#

For software and technical-report citations, see the README. The project uses the Apache 2.0 license. This site documents the source checkout, including unreleased APIs.