CTR Acceleration Backend Benchmark

Benchmark the CTR structure-factor backends for a representative test crystal. The notebook compares the original NumPy path with the optional C++ and Numba acceleration backends when they are available.

The first accelerated call may include setup or compilation overhead, so the benchmark does an explicit warmup before recording timings. The reported timings are runtime-only measurements after warmup.

The accelerated CTR kernels are intentionally single-threaded at this level. This benchmark measures the benefit from fused loops, low temporary-array allocation, and compiler optimization without adding thread parallelism inside the structure-factor kernels.

Setup

Start Jupyter from the benchmarks/ directory and run this notebook there. The benchmark imports the installed orgui package from the active Python environment. The benchmark uses the copied CTR regression fixture in benchmarks/fixtures/0V12_calculated.xpr.

Backend Selection

CTR structure-factor calculations default to C++ when the extension is available and run the explicit NumPy structure-factor path otherwise. Numba is not imported automatically unless selected. To select the active process-global backend before importing CTRuc, set the shared backend variable:

ORGUI_ACCEL_BACKEND=cpp python my_ctr_script.py
ORGUI_ACCEL_BACKEND=numba python my_ctr_script.py
ORGUI_ACCEL_BACKEND=numpy python my_ctr_script.py

The same selection can be changed inside a Python process:

from orgui.datautils.xrayutils import CTRuc

CTRuc.set_accel_backend("cpp")
CTRuc.set_accel_backend("numba")
CTRuc.set_accel_backend("numpy")  # disable acceleration

CTRuc.set_accel_backend("numba") raises ValueError if the optional Numba backend cannot be imported. See Acceleration Backends in the main documentation for the ROI and CTR selector summary.

[1]:
from pathlib import Path
import statistics
import time

import numpy as np


BENCHMARK_DIR = Path.cwd().resolve()
FIXTURE_DIR = BENCHMARK_DIR / "fixtures"
XPR_PATH = FIXTURE_DIR / "0V12_calculated.xpr"

if (BENCHMARK_DIR / "orgui").is_dir():
    raise RuntimeError(
        "Notebook is running from the repository root. Start Jupyter from "
        "benchmarks/ to benchmark the installed package."
    )
if not XPR_PATH.exists():
    raise FileNotFoundError(
        f"Could not find {XPR_PATH}. Start Jupyter from the benchmarks/ "
        "directory so benchmark fixtures are available."
    )

print(f"Benchmark directory: {BENCHMARK_DIR}")
print(f"Fixture:             {XPR_PATH}")

Benchmark directory: C:\Users\timof\Documents\repos\orGUI\benchmarks
Fixture:             C:\Users\timof\Documents\repos\orGUI\benchmarks\fixtures\0V12_calculated.xpr
[2]:
!hostname

CTRsurfacePC
[3]:
from orgui import __version__, get_build_config
print("orGUI version", __version__)
print(get_build_config())

orGUI version 1.5.1.dev39+g72e3872c2.d20260704
{'native_optimization': True, 'target_cpu': 'x86_64', 'allowed_instruction_set': ['AVX512'], 'native_arguments': '/arch:AVX512', 'cpp_compiler_id': 'msvc', 'cpp_compiler_version': '19.43.34810', 'host_system': 'windows', 'host_cpu_family': 'x86_64', 'host_cpu': 'x86_64', 'available': True}
[4]:
from orgui.datautils import util
from orgui.datautils.xrayutils import CTRcalc, CTRuc


AVAILABLE_BACKENDS = ["numpy"]
if CTRuc.HAS_CPP_ACCEL:
    AVAILABLE_BACKENDS.append("cpp")
if CTRuc.ctr_numba_accel_available():
    AVAILABLE_BACKENDS.append("numba")

print(f"Default CTR acceleration backend: {CTRuc.CTR_ACCEL_BACKEND}")
print(f"Available CTR acceleration backends: {AVAILABLE_BACKENDS}")

Default CTR acceleration backend: cpp
Available CTR acceleration backends: ['numpy', 'cpp', 'numba']

Benchmark Helpers

CTRuc.CTR_ACCEL_BACKEND selects the active implementation. Use CTRuc.set_accel_backend("cpp" | "numba" | "numpy") to compare the direct C++ extension, Numba kernels, and original NumPy implementation.

[5]:
def set_backend(backend):
    CTRuc.set_accel_backend(backend)


def load_crystal():
    xtal = CTRcalc.SXRDCrystal.fromFile(XPR_PATH)
    pt100 = CTRcalc.UnitCell(
        [3.9242, 3.9242, 3.9242],
        [90.0, 90.0, 90.0],
    )
    xtal.setGlobalReferenceUnitCell(
        pt100,
        util.z_rotation(np.deg2rad(45.0)),
    )
    return xtal


def make_rod(n_points):
    l_values = np.ascontiguousarray(
        np.linspace(0.05, 7.0, n_points),
        dtype=np.float64,
    )
    zeros = np.zeros_like(l_values)
    return zeros, zeros, l_values


def time_call(func, repeats=7, warmups=1):
    for _ in range(warmups):
        func()

    timings = []
    for _ in range(repeats):
        start = time.perf_counter()
        func()
        timings.append(time.perf_counter() - start)

    return {
        "min_s": min(timings),
        "median_s": statistics.median(timings),
        "mean_s": statistics.mean(timings),
        "repeats": repeats,
    }


def format_seconds(value):
    if value < 1e-3:
        return f"{value * 1e6:8.1f} us"
    if value < 1:
        return f"{value * 1e3:8.2f} ms"
    return f"{value:8.3f} s"

Warmup and Correctness Check

The accelerated and non-accelerated paths should produce the same complex amplitudes within normal floating-point tolerance before any timing results are trusted.

[6]:
xtal = load_crystal()
h, k, l = make_rod(2_000)

set_backend("numpy")
numpy_uc = xtal.uc_bulk.F_uc(h, k, l)
numpy_xtal = xtal.F(h, k, l)

for backend in AVAILABLE_BACKENDS:
    if backend == "numpy":
        continue
    set_backend(backend)
    backend_uc = xtal.uc_bulk.F_uc(h, k, l)
    backend_xtal = xtal.F(h, k, l)
    np.testing.assert_allclose(backend_uc, numpy_uc, rtol=1e-12, atol=1e-12)
    np.testing.assert_allclose(backend_xtal, numpy_xtal, rtol=1e-10, atol=1e-10)

print("Available accelerated backends match NumPy for UnitCell.F_uc and SXRDCrystal.F")

Available accelerated backends match NumPy for UnitCell.F_uc and SXRDCrystal.F

Benchmark Structure-Factor Evaluation

This measures direct repeated evaluations for increasing CTR rod lengths. Use the median time for stable comparisons; the minimum time is also shown as a lower bound on a quiet machine.

[7]:
def benchmark_structure_factor(n_points, repeats=7):
    xtal = load_crystal()
    h, k, l = make_rod(n_points)

    def run_unitcell(backend):
        set_backend(backend)
        return lambda: xtal.uc_bulk.F_uc(h, k, l)

    def run_crystal(backend):
        set_backend(backend)
        return lambda: xtal.F(h, k, l)

    rows = []
    for label, factory in (
        ("UnitCell.F_uc", run_unitcell),
        ("SXRDCrystal.F", run_crystal),
    ):
        stats = {
            backend: time_call(factory(backend), repeats=repeats)
            for backend in AVAILABLE_BACKENDS
        }
        row = {
            "benchmark": label,
            "points": n_points,
        }
        for backend, backend_stats in stats.items():
            row[f"{backend}_median_s"] = backend_stats["median_s"]
            row[f"{backend}_min_s"] = backend_stats["min_s"]
        for backend in AVAILABLE_BACKENDS:
            if backend == "numpy":
                continue
            row[f"{backend}_speedup"] = (
                row["numpy_median_s"] / row[f"{backend}_median_s"]
            )
        rows.append(row)
    return rows


results = []
for n_points in (200, 2_000, 20_000):
    results.extend(benchmark_structure_factor(n_points))

header = f"{'benchmark':<18} {'points':>8}"
for backend in AVAILABLE_BACKENDS:
    header += f" {backend + ' median':>15}"
for backend in AVAILABLE_BACKENDS:
    if backend != "numpy":
        header += f" {backend + ' speedup':>14}"
print(header)
for row in results:
    line = f"{row['benchmark']:<18} {row['points']:>8}"
    for backend in AVAILABLE_BACKENDS:
        line += f" {format_seconds(row[f'{backend}_median_s']):>15}"
    for backend in AVAILABLE_BACKENDS:
        if backend != "numpy":
            line += f" {row[f'{backend}_speedup']:>13.2f}x"
    print(line)

benchmark            points    numpy median      cpp median    numba median    cpp speedup  numba speedup
UnitCell.F_uc           200        130.9 us         29.3 us         27.0 us          4.47x          4.85x
SXRDCrystal.F           200        370.5 us         99.5 us         97.9 us          3.72x          3.78x
UnitCell.F_uc          2000        551.5 us        207.7 us        216.1 us          2.66x          2.55x
SXRDCrystal.F          2000         1.10 ms        767.0 us        981.5 us          1.44x          1.12x
UnitCell.F_uc         20000         3.02 ms         2.04 ms         2.20 ms          1.48x          1.37x
SXRDCrystal.F         20000        11.02 ms         7.37 ms         9.38 ms          1.50x          1.18x

Benchmark Results

Plot the median runtime comparison for the direct structure-factor benchmarks.

[8]:
from matplotlib import pyplot as plt

labels = [f"{row['benchmark']}\n{row['points']} pts" for row in results]
x = np.arange(len(labels))
width = min(0.8 / len(AVAILABLE_BACKENDS), 0.28)
offsets = (np.arange(len(AVAILABLE_BACKENDS)) - (len(AVAILABLE_BACKENDS) - 1) / 2) * width

fig, ax = plt.subplots(figsize=(11, 4.5))
for offset, backend in zip(offsets, AVAILABLE_BACKENDS):
    ax.bar(
        x + offset,
        [row[f"{backend}_median_s"] for row in results],
        width,
        label=backend,
    )

ax.set_ylabel("Median runtime [s]")
ax.set_yscale("log")
ax.set_xticks(x)
ax.set_xticklabels(labels, rotation=30, ha="right")
ax.legend(title="Backend")
ax.grid(axis="y", which="both", alpha=0.25)
fig.tight_layout()

../_images/benchmarks_ctr_accel_backends_14_0.png
[ ]: