Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ Read the most local `AGENTS.md` for the files you touch:
user-facing unit boundaries.
- `orgui/datautils/xrayutils/AGENTS.md`: scientific core, detector geometry,
reciprocal-space math, CTR calculations, and related tests.
- `orgui/reconstruction_*.py`, `orgui/backend/`: no nested `AGENTS.md` yet.
Follow this file plus `orgui/datautils/xrayutils/AGENTS.md` — the
reconstruction pipeline is scientific/geometry code built on
`orgui/datautils/xrayutils/reconstruction.py`, and `orgui/backend/` handles
the beamline-specific metadata normalization called out in
`orgui/app/AGENTS.md`.
- `doc/AGENTS.md`: Sphinx documentation source, the changelog/release-notes
workflow, and what counts as a user-facing change worth documenting.

Expand Down Expand Up @@ -79,6 +85,7 @@ it:
- `pytest orgui/datautils/xrayutils/test/test_HKLcalc.py`
- `pytest orgui/datautils/xrayutils/test/test_DetectorCalibration.py`
- `pytest orgui/datautils/xrayutils/test/test_CTRcalc.py`
- `pytest orgui/datautils/xrayutils/test/test_reconstruction*.py`

When changing config-loading or unit-conversion behavior, inspect
`examples/config_minimal` and the other `examples/config_*` files.
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,20 @@ ESRF ID31 beamline support and reciprocal-space display:
raised before showing the dialog. Subscans such as ``1.10`` are no longer
mistaken for the ``1.1`` fast-counter subscan.

Reciprocal-space reconstruction:

- Added a centralized, out-of-core reciprocal-space reconstruction pipeline.
A new "Reconstruct reciprocal space" dialog (Configuration menu) defines
HKL/Q output grids, previews coverage and storage cost, and prepares,
runs, and resumes jobs. Jobs can run locally, as SGE/Slurm cluster batch
arrays with parallel submap reduction, or from the command line via a new
``reconstruction_cli`` entry point.
- Added a shared HDF5 output-settings dialog (chunk shape, compression)
reachable from both the reconstruction dialog and the Configuration menu.
- Exposure-time normalization and monitor-counter corrections now live in
the reconstruction dialog rather than the shared scan-options panel, since
they affect reconstruction output only, not ROI/CTR image integration.


## [1.5.0] (2026-06-07)

Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
149 changes: 149 additions & 0 deletions benchmarks/benchmark_reconstruction_alignment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""Measure native reconstruction sensitivity to input-buffer alignment."""

from __future__ import annotations

import argparse
from dataclasses import replace
import gc
import json
from pathlib import Path
from statistics import median
from time import perf_counter

import numpy as np

from orgui.datautils.xrayutils.reconstruction import (
_detector_corner_rays,
_kernel_for_grid,
)
from orgui.reconstruction_job import (
_correction_pipeline,
_load_assets,
read_job,
)


def _aligned_copy(values, address_mod_64):
values = np.ascontiguousarray(values)
storage = np.empty(values.nbytes + 63, dtype=np.uint8)
offset = (address_mod_64 - storage.ctypes.data) % 64
result = storage[offset : offset + values.nbytes].view(values.dtype)
result = result.reshape(values.shape)
np.copyto(result, values)
if result.ctypes.data % 64 != address_mod_64:
raise RuntimeError("Failed to construct requested input alignment")
return result


def _arguments():
parser = argparse.ArgumentParser()
parser.add_argument("job", type=Path)
parser.add_argument("--frame", type=int, default=0)
parser.add_argument("--tile-size", type=int, default=1024)
parser.add_argument("--threads", type=int, default=4)
parser.add_argument("--depth", type=int, default=2)
parser.add_argument("--repeats", type=int, default=5)
parser.add_argument(
"--offsets",
type=int,
nargs="+",
default=tuple(range(0, 64, 8)),
)
return parser.parse_args()


def main():
"""Benchmark naturally aligned and deliberately offset native inputs."""
arguments = _arguments()
job = read_job(arguments.job)
config = job.config_data
scan = job.scan
payload = scan.get_raw_img(arguments.frame)
image = np.asarray(payload.img)
tile = (
0,
min(arguments.tile_size, image.shape[0]),
0,
min(arguments.tile_size, image.shape[1]),
)
selection = np.s_[tile[0] : tile[1], tile[2] : tile[3]]
correction = _correction_pipeline(
config,
scan,
_load_assets(job),
{},
)
corrected = correction.correct_frame(payload, image, arguments.frame)
inputs = [
np.ascontiguousarray(corrected[0][selection], dtype=np.float64),
np.ascontiguousarray(corrected[1][selection], dtype=np.float64),
np.ascontiguousarray(corrected[2][selection], dtype=bool),
_detector_corner_rays(config.detector, tile),
]
bounds = scan.exposure_angle_bounds(
config,
fallback=job.angle_fallback,
)[arguments.frame]
angles_start = np.ascontiguousarray(bounds[0], dtype=np.float64)
angles_end = np.ascontiguousarray(bounds[1], dtype=np.float64)
spec = replace(
job.internal_spec(),
max_depth=arguments.depth,
)
kernel = _kernel_for_grid(
spec,
spec.grids[0],
config.ub_calculator,
threads=arguments.threads,
memory_budget_bytes=spec.memory_budget_bytes,
)

offsets = tuple(arguments.offsets)
if any(offset < 0 or offset >= 64 for offset in offsets):
raise ValueError("Alignment offsets must be in [0, 64)")
aligned_inputs = {
offset: tuple(_aligned_copy(values, offset) for values in inputs)
for offset in offsets
}
kernel.accumulate(
*aligned_inputs[0],
angles_start,
angles_end,
)
timings = {offset: [] for offset in offsets}
gc.disable()
try:
for repeat in range(arguments.repeats):
order = offsets if repeat % 2 == 0 else tuple(reversed(offsets))
for offset in order:
started = perf_counter()
kernel.accumulate(
*aligned_inputs[offset],
angles_start,
angles_end,
)
timings[offset].append(perf_counter() - started)
finally:
gc.enable()

baseline = median(timings[0])
result = {
"tile": tile,
"threads": arguments.threads,
"depth": arguments.depth,
"repeats": arguments.repeats,
"results": [
{
"address_mod_64": offset,
"median_seconds": median(timings[offset]),
"relative_to_aligned": median(timings[offset]) / baseline,
"samples": timings[offset],
}
for offset in offsets
],
}
print(json.dumps(result, indent=2, sort_keys=True))


if __name__ == "__main__":
main()
Loading