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()
[ ]: