Diffraction integration · performance

Dioptrin 0.6.1 benchmarks.

← Dioptrin software and licensing

fps means frames per second: the number of complete detector images processed each second. Higher fps means higher processing throughput.

This is the final dataset, measured on 25 October 2026, using the published Dioptrin 0.6.1 wheel, pyFAI 2026.9.0, and a CPU-tuned azint build. The methods are described in the MatFRAIA paper, the pyFAI library paper and the pyFAI calibration/new-integration paper. Full references are below.

File loading and integration without errors

Every task opens and decodes its TIFF and integrates the image. Error estimation is off in the main chart. This matches the usual intensity-only calculation in Dioptas. Each library is shown with one thread and eight Python frame workers, with one integration thread per worker. Hatched bars show one thread; solid bars show eight workers.

The operating-system file cache is warm; calibration setup is measured separately. These are library file-processing rates. Dioptas currently uses its own image readers and passes arrays to the integration backend, so its full application throughput can differ. Acquisition, screen drawing and output-file writing are excluded.

File loading and integration without error estimation: pyFAI, tuned azint and Dioptrin 0.6.1 at one thread and eight workers on both detectors.

At eight workers, Dioptrin achieved 3.4–3.7× the pyFAI throughput and 1.6–1.9× the tuned azint throughput for file loading and integration without Poisson error estimation on these two datasets. These factors compare equal worker counts.

The whiskers show the range of two fresh-process timing trials. Each detector has its own scale, starting at zero; that scale is kept the same in both file-processing charts.

File throughput without errors at all worker counts
DetectorLibrary1 thread fps2 workers fps4 workers fps8 workers fps
Pilatus1MDioptrin 0.6.1290.5523.21,018.11,808.5
Pilatus1Mazint · CPU-tuned187.4346.6591.3940.0
Pilatus1MpyFAI 2026.9.080.0154.5284.1490.2
Eiger9MDioptrin 0.6.140.677.9148.1258.1
Eiger9Mazint · CPU-tuned30.859.1106.9166.2
Eiger9MpyFAI 2026.9.011.522.543.274.9

With Poisson error estimation

The second chart repeats the same file-loading and integration workload, with Poisson estimation enabled for every library. It uses the same images, builds, corrections, masks and worker settings as the main chart.

File loading and integration with Poisson error estimation: pyFAI, tuned azint and Dioptrin 0.6.1 at one thread and eight workers on both detectors.

At eight workers, Dioptrin achieved 3.2–3.6× the pyFAI throughput and 1.9–2.5× the tuned azint throughput for file loading and integration with Poisson error estimation on these two datasets. These factors compare equal worker counts.

Poisson estimation adds a counting uncertainty alongside each integrated intensity, useful for weighted fitting and error-bearing exports. It is optional for routine viewing and off by default in Dioptas. Read the Dioptas integration guide for what the estimate means, when to enable it and how to export it.

The whiskers on these speed charts show timing variation between runs, rather than counting uncertainty in a diffraction pattern.

File throughput with Poisson errors at all worker counts
DetectorLibrary1 thread fps2 workers fps4 workers fps8 workers fps
Pilatus1MDioptrin 0.6.1279.3518.91,006.31,779.8
Pilatus1Mazint · CPU-tuned138.1255.6444.3709.3
Pilatus1MpyFAI 2026.9.080.7155.4287.4498.0
Eiger9MDioptrin 0.6.138.173.1139.3239.9
Eiger9Mazint · CPU-tuned21.440.876.9126.0
Eiger9MpyFAI 2026.9.011.623.144.975.8

Multicore scaling and shared memory

With error estimation off, increasing from one to eight Python frame workers raised Dioptrin’s file-loading-plus-integration throughput by 6.2–6.4×, or 78–80% of ideal linear scaling on these two datasets. Four workers achieved 3.5–3.6× the one-thread rate. Workers process different images concurrently: these are batch-throughput gains, rather than the latency of one image.

Detector1 thread2 workers4 workers8 workers
Pilatus1M1.00×1.80×3.50×6.23×
Eiger9M1.00×1.92×3.65×6.36×

Dioptrin’s workers share the calibration and integration tables. Total process memory still includes per-worker decoding buffers and temporary allocations. For these int32 TIFFs, each buffer is about 3.9 MiB per Pilatus worker or 38.7 MiB per Eiger worker.

For Eiger images, the live heap after file processing with errors off rose from 765 MiB at one worker to 1,036 MiB at eight workers. After releasing the cached file workers it was approximately 727 MiB and 727 MiB, respectively. RSS also includes allocator-retained pages and setup allocations, so it is different from the live heap.

Integration-only controls

These controls use images already in memory. They exclude file opening and decoding; the main chart above includes both. The controls use float64 arrays, whereas Dioptrin’s file path can integrate directly from compact int32 decoding buffers. The file workflow can therefore exceed an array-control rate without caching decoded images. Their difference is not an isolated measurement of disk-reading cost.

Integration only, without Poisson uncertainties

Images are already loaded and Poisson errors are off in this chart, matching the usual intensity-only Dioptas pyFAI path. Each library is shown with one thread (hatched bars) and eight Python frame workers (solid bars), with one integration thread per worker. Whiskers show the range of two fresh-process trials. Each detector panel has its own linear scale, starting at zero.

Bar chart comparing integration fps for Dioptrin 0.6.1, tuned azint and pyFAI at one thread and eight workers on Pilatus1M and Eiger9M images.

At eight workers, Dioptrin achieved 2.6–3.1× the pyFAI throughput and about 1.2× the tuned azint throughput on these two datasets. These factors compare equal worker counts. The chart also shows all three libraries with one thread; pyFAI uses Dioptas’s usual integration method.

Open the chart as SVG · Download PDF

Software and methods behind the charts

The charts compare Dioptrin 0.6.1 in its standard geometric pixel-splitting mode, the pyFAI 2026.9.0 method selected by Dioptas (bounding-box splitting, CSR/Cython), and the tuned azint build. The tables also retain the additional Dioptrin supersampling and pyFAI full-splitting measurements. These method names are implementation details; all charts use the same selected method for each library.

All worker counts, including single-thread results

DetectorMethod1 worker fps2 workers fps4 workers fps8 workers fps
Pilatus1MDioptrin 0.6.1 pixel-split232.5441.9886.71,666.0
Pilatus1MDioptrin 0.6.1 supersampled239.6449.5912.31,757.9
Pilatus1Mazint · CPU-tuned225.8397.0710.41,336.6
Pilatus1MpyFAI 2026.9.0 full CSR88.9172.3331.3631.2
Pilatus1MpyFAI 2026.9.0 bbox CSR90.9168.5332.8630.5
Eiger9MDioptrin 0.6.1 pixel-split37.569.9145.2269.1
Eiger9MDioptrin 0.6.1 supersampled39.673.8155.3288.8
Eiger9Mazint · CPU-tuned31.864.1122.2217.7
Eiger9MpyFAI 2026.9.0 full CSR12.625.449.486.1
Eiger9MpyFAI 2026.9.0 bbox CSR12.725.550.186.3

Dioptas’s ordinary pyFAI path does use pixel splitting, specifically bounding-box splitting with CSR storage. Its integrate1d call defaults to method="csr" and does not normally request Poisson errors. In pyFAI 2026.9.0, integrate1d and integrate1d_ng are the same implementation, and csr resolves to bbox/CSR/Cython. Bbox is a pixel-splitting scheme; the separately tested full CSR method uses the pixel edges instead. See pyFAI’s pixel-splitting explanation.

The one-thread control uses Dioptas’s integration method and fixes OMP_NUM_THREADS=1. Dioptas processes one frame at a time in this path, but does not itself impose that native thread limit: a pyFAI build with OpenMP can use internal threads. The control is therefore an explicit single-thread reference, not a measurement of every Dioptas installation’s default CPU use. These are engine timings, not a measurement of the full Dioptas interface.

Integration only, with Poisson uncertainties
DetectorMethod1 worker fps2 workers fps4 workers fps8 workers fps
Pilatus1MDioptrin 0.6.1 pixel-split226.3431.2862.91,630.4
Pilatus1MDioptrin 0.6.1 supersampled231.6440.9890.01,666.8
Pilatus1Mazint · CPU-tuned156.7269.7472.5889.4
Pilatus1MpyFAI 2026.9.0 full CSR88.9173.7333.9637.9
Pilatus1MpyFAI 2026.9.0 bbox CSR90.9170.7334.7631.3
Eiger9MDioptrin 0.6.1 pixel-split35.266.2137.6254.1
Eiger9MDioptrin 0.6.1 supersampled37.671.0148.1273.2
Eiger9Mazint · CPU-tuned20.942.281.3146.2
Eiger9MpyFAI 2026.9.0 full CSR13.125.750.587.9
Eiger9MpyFAI 2026.9.0 bbox CSR13.025.950.787.5

Setup time and memory

Setup is excluded from warm throughput. Both error modes share a fresh process, and their order reverses between trials. Initialization uses a Poisson-enabled first image for both modes. The first batch in a later mode may reuse existing workers and caches; it is not an independent cold-start comparison. RSS includes setup and allocator effects, while retained heap measures live allocations. No preloaded image stack is retained in these file tests.

One- and eight-worker setup and memory
DetectorLibraryWorkersPoissonEngines ready sFirst 32 files in mode sWarm peak RSS GiBRetained heap MiB
Pilatus1MDioptrin 0.6.11off0.2270.1240.476116.5
Pilatus1MDioptrin 0.6.11on0.2270.1140.477116.5
Pilatus1Mazint · CPU-tuned1off4.6290.1690.33390.2
Pilatus1Mazint · CPU-tuned1on4.6290.2430.33490.2
Pilatus1MpyFAI 2026.9.01off0.8690.3990.373177.7
Pilatus1MpyFAI 2026.9.01on0.8690.3920.375177.7
Pilatus1MDioptrin 0.6.18off0.1140.0300.586144.0
Pilatus1MDioptrin 0.6.18on0.1140.0180.586144.0
Pilatus1Mazint · CPU-tuned8off0.7670.0350.41990.3
Pilatus1Mazint · CPU-tuned8on0.7670.0470.41490.3
Pilatus1MpyFAI 2026.9.08off0.9780.0670.799525.4
Pilatus1MpyFAI 2026.9.08on0.9780.0640.800525.4
Eiger9MDioptrin 0.6.11off1.3140.7901.994765.4
Eiger9MDioptrin 0.6.11on1.3140.8451.992765.4
Eiger9Mazint · CPU-tuned1off6.4621.0282.132639.5
Eiger9Mazint · CPU-tuned1on6.4621.4982.132639.5
Eiger9MpyFAI 2026.9.01off1.5482.7761.4091,094.9
Eiger9MpyFAI 2026.9.01on1.5482.7561.4091,094.9
Eiger9MDioptrin 0.6.18off0.5910.1302.6731,036.4
Eiger9MDioptrin 0.6.18on0.5910.1362.6731,036.4
Eiger9Mazint · CPU-tuned8off1.8560.1942.381431.6
Eiger9Mazint · CPU-tuned8on1.8560.2552.380431.6
Eiger9MpyFAI 2026.9.08off3.8200.4514.5493,266.0
Eiger9MpyFAI 2026.9.08on3.8200.4664.5523,266.0

Dioptrin shares its calibration tables and keeps reusable file buffers. pyFAI uses a separate CSR engine per Python frame worker; azint shares its sparse matrix through shallow copies with separate integration state. Releasing Dioptrin file workers frees their live buffers, although RSS may remain elevated.

Numerical checks

The new paired file measurements check 6,144 frame results against their frozen same-backend references, including axes, intensities and Poisson sigma when enabled. All checks are byte-identical. The intensities also match byte-for-byte with error estimation off versus on in all 48 accepted processes.

The integration-only and additional-method controls retain 7,680 validation frame results from the original matched-build cohort. All results matched their own backend’s reference byte-for-byte for the axes, intensities and available Poisson sigmas. The Eiger batch cycles through 16 different real images, including transitions back to an earlier image, so it also checks that retained buffers do not return stale data. The Dioptrin native binary was checked against the previously verified published 0.6.1 wheel.

This self-consistency does not mean different algorithms give identical patterns. Dioptrin uses float64 weights; pyFAI uses its native float32 CSR data path. Bbox, geometric pixel splitting and subpixel sampling also differ, as do azint’s and Dioptrin’s correction-normalization conventions. The following table compares the first image with Dioptrin supersampling for azint, and with Dioptrin pixel-split for pyFAI. Differences are not an independent accuracy ranking.

Intensity, uncertainty and axis differences between implementations
DetectorCompared methodIntensity relative L2Poisson-sigma relative L2Max axis offset (degrees)
Pilatus1Mazint · CPU-tuned1.168%0.479%3.55e-15
Pilatus1MpyFAI 2026.9.0 full CSR0.243%1.789%3.68e-06
Pilatus1MpyFAI 2026.9.0 bbox CSR5.320%25.265%3.68e-06
Eiger9Mazint · CPU-tuned2.945%0.282%7.11e-15
Eiger9MpyFAI 2026.9.0 full CSR1.331%316.162%5.27e-06
Eiger9MpyFAI 2026.9.0 bbox CSR1.255%7.554%5.27e-06

Reproducible settings

Apple M4 Pro (10 performance and 4 efficiency cores), 48 GiB RAM, CPython 3.13.5 and NumPy 2.4.2. Pilatus1M: 1043 × 981 pixels, 32 separate file paths with copies of one image. Eiger9M: 3262 × 3108 pixels, 16 distinct real images read twice per 32-file batch. The same equivalent PONI geometry, masks, radial ranges, 2,000 uniform two-theta bins, solid-angle correction and polarization factor 0.965 are used throughout. The integration-only stack is contiguous float64; file inputs are uncompressed signed int32 TIFFs.

Dioptrin uses its published 0.6.1 wheel, with fast CSR arithmetic enabled by default. Pixel-split is geometric. Supersampling uses n=15 per dimension for Pilatus and n=5 for Eiger; the azint sampling count matches. azint is commit 4f78cc3b1, compiled with Clang 22.1.8, -O3 -mcpu=native -flto=thin -ffast-math. This deliberately includes the tuned competitor. pyFAI uses the official 2026.9.0 wheel with bbox and full CSR Cython methods, safe=True. No GPU backend is tested.

Two fresh processes per library, detector and worker count: 48 accepted processes for the paired file workloads. Each measures errors off and on using three warm blocks per mode, targeting 0.5 seconds; a single batch may take longer. Mode order, worker order and library order reverse in the second trial. Both modes must pass the screen of at most 3.6 other busy logical cores. 14 rejected attempts are retained. No unrelated calculations were suspended. The same detector-specific scale is used in both file charts. Whiskers are two-trial ranges, not confidence intervals. These results describe observed throughput on this machine and workload; they do not establish statistical significance or a universal ranking.

The integration-only and additional-method controls retain the earlier 80-process cohort, with the same published builds, images and scientific settings. Its complete data remain available separately. No unreleased Dioptas or Dioptrin changes are used in these website charts.

Data and references

Paired file measurements CSV · File trial results · File numerical checks · File protocol · Integration/additional-method measurements · Control trial results · Download all current-release data

  1. azint / MatFRAIA: Jensen, A. B., Christensen, T. E. K., Weninger, C. and Birkedal, H. (2022). Very large-scale diffraction investigations enabled by a matrix-multiplication facilitated radial and azimuthal integration algorithm: MatFRAIA. Journal of Synchrotron Radiation 29, 1420–1428. doi:10.1107/S1600577522008232. The measured Python implementation is azint, at the commit and compiler settings recorded in the protocol; these CPU measurements are not the hardware benchmarks from the paper.
  2. pyFAI library: Ashiotis, G., Deschildre, A., Nawaz, Z., Wright, J. P., Karkoulis, D., Picca, F. E. and Kieffer, J. (2015). The fast azimuthal integration Python library: pyFAI. Journal of Applied Crystallography 48, 510–519. doi:10.1107/S1600576715004306.
  3. pyFAI calibration and newer integration framework: Kieffer, J., Valls, V., Blanc, N. and Hennig, C. (2020). New tools for calibrating diffraction setups. Journal of Synchrotron Radiation 27, 558–566. doi:10.1107/S1600577520000776.

Dioptrin is available from PyPI and requires a valid license. Contact me to request a license.