diff --git a/.gitignore b/.gitignore index 15a3129..eabe4d1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ subprojects/.wraplock orgui.egg-info/ orgui/_version.py benchmarks/roi_sum_results.json +# Fingerprint/timing baselines are captured per build and per machine, and +# are only meaningful compared against themselves. +benchmarks/baselines/ **/.ipynb_checkpoints/ # Packages # diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a07610..285b66a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,69 @@ Scientific correctness and performance fixes: corrected atomic-coordinate stacking when splitting unit cells into layers. - Added a C++ electron-density backend and bounded caches for atomic form factors, anomalous scattering factors, and accelerated form-factor lookup. +- Reciprocal-space mapping now maps detector images through a work block + shaped as a brick in (row, column, frame) rather than a run of one + flattened image, and can map several consecutive images in a single native + call. On a rotation scan two adjacent images land as close together in + reciprocal space as two adjacent pixels do, so a brick merges + contributions the per-image block could not see: on a 3651-frame Pilatus + 6M scan, mapping eight images together emits 0.60x the records for the + same samples. Contributions merge inside the kernel rather than in the + checkpoint accumulator, which associates the same sums differently — with + fewer intermediate roundings, so slightly better conditioned. Which voxels + are reached, and how many detector samples reach each of them, are + unchanged. Reciprocal-space reconstruction has not appeared in a release + before this one, so no previously published result is affected; but a job + resumed against a scratch directory written by an earlier development + build will contain checkpoint parts from both association orders. +- Mapping several images per call uses a scheduler of its own, which runs a + few concurrent native calls with the thread budget split between them and + hoists whole-frame correction into the prefetch pool, so one call's + GIL-held Python work overlaps the next call's native work instead of + queueing behind it. Measured 0.87x the mapping wall time of the + per-image pipeline at four images per call on a 3651-frame rotation scan. +- The number of images per call is now measured per job rather than + configured. Grouping only helps while consecutive images land within about + one voxel of each other, so the job's own geometry is probed at three + points in the scan to decide whether it does, and the group size is then + the largest the memory budget can afford without giving up concurrency. + Scans whose angles do not advance monotonically, such as interlaced ones, + map one image per call as before. On a 0.1 deg/frame rotation scan this + chooses four images per call and maps at 0.88x the previous wall time. +- Corrected the detector band height used for reciprocal-space mapping, + which was still derived from the worst-case adaptive leaf count rather + than from the record ceiling the memory prechecks use. At the + ``very_high`` and ``maximum`` accuracy settings this over-estimated a + pixel's cost by four orders of magnitude and split the detector into + extremely thin bands — one row per band on a Pilatus 6M at ``maximum``, + giving 2527 native calls per image instead of six. Band height no longer + depends on the accuracy setting. Measured throughput at ``balanced`` + accuracy is unchanged; the fix matters at the two highest settings. +- Reciprocal-space mapping no longer copies each detector tile out of the + corrected images before mapping it. Detector tiles partition the image, + so gathering every tile into its own buffer copied each corrected image + exactly once — about 105 MB per image on a Pilatus 6M — purely to give + the native call a shape it did not need. It now takes the whole images + plus the rectangle to map and reads that rectangle in place. Measured + 0.84x the mapping wall time at ``Center only`` accuracy on a 3651-frame + rotation scan, with every reconstructed value identical bit for bit. +- Image correction now applies the per-pixel solid-angle and polarization + factor, the exposure and monitor normalizations, and the non-finite + check that masks bad pixels in a single native pass, instead of eight or + nine full-image NumPy passes over the same arrays. The arithmetic, its + order and its uncertainty propagation are unchanged, and the results are + identical bit for bit. Measured 0.92x the mapping wall time at ``Center + only`` accuracy on the same scan; the saving is memory traffic, so it is + largest where the mapping itself is cheap. +- The reciprocal-space reconstruction documentation now says which footprint + accuracy setting to choose, and why. Each setting's effect on a + reconstructed intensity was measured against that voxel's own counting + error: ``Balanced`` is sufficient for a full rotation scan, while + ``High``, ``Very high`` and ``Maximum`` shift intensities by 0.02 sigma or + less and are not distinguishable from ``Balanced`` by the data, at roughly + 4x, 15x and 100x the runtime. ``Center only`` is genuinely lossy and also + misses about 4% of the voxels the other settings reach. No behaviour or + default has changed. A ***critical bug*** was fixed that affects bulk CTR calculations: diff --git a/benchmarks/benchmark_reconstruction_ab.py b/benchmarks/benchmark_reconstruction_ab.py new file mode 100644 index 0000000..4fc7461 --- /dev/null +++ b/benchmarks/benchmark_reconstruction_ab.py @@ -0,0 +1,342 @@ +"""Run two pipeline arms interleaved, and report the paired ratios. + +Everything measured about the mapping pipeline on this machine has had to +be measured this way, and every conclusion drawn any other way has had to +be withdrawn. The rules, which this script exists to make automatic: + +- **Interleave the arms.** A run of one arm followed by a run of the other + confounds the arm with drift and with page-cache state. Arms alternate + here, and the order flips on every repeat so that neither arm is always + the one that inherits the other's warm cache. +- **Only paired ratios carry.** Absolute values drift between sessions by + 30%; the same configuration has measured 183, 202 and 342 ms/frame. The + summary reports ratios and, more importantly, whether the arms separate + cleanly -- every run of one beating every run of the other is worth more + than a difference of means. +- **Verify the machine is quiet.** It carries a persistent 1.8-2.9 cores + of background load. Foreign CPU is measured per run as the sum over + *other user processes*, not as machine-busy minus the benchmark's own: + kernel, interrupt and storage-driver time is charged to System rather + than to the process that caused it, and mapping is I/O-heavy, so the + total-minus-child definition reported 7.2 cores of interference where + per-process accounting showed 2.9. +- **Check what each run actually mapped.** Roughly one mapping run in + twenty routes zero records, writes a checkpoint claiming every frame, + and exits 0 at about a fifth of the usual time. A timing-only harness + reads that as a win. Every run's checkpoint fingerprint is compared + against its arm's first run here, and a mismatch is reported rather + than averaged in. + +An arm is a set of files copied into the checkout before its runs, so a +native change can be measured against its own baseline binary without +rebuilding between runs: capture the baseline ``.pyd`` and ``.py`` before +changing anything, and name both states as arms. + +Plan file (JSON):: + + { + "repeats": 6, + "command": ["python", "benchmarks/benchmark_reconstruction_pipeline.py", + ".json", "--start", "1638", "--count", "234", + "--depth", "0"], + "arms": [ + {"name": "baseline", + "copy": [["saved/baseline.pyd", "orgui/.../_reciprocal_...pyd"]]}, + {"name": "tile-view", + "copy": [["saved/new.pyd", "orgui/.../_reciprocal_...pyd"]], + "arguments": ["--group", "4"]} + ] + } + +``PYTHONPATH`` is set to the checkout for every child, which is the trap +the benchmarks themselves warn about: ``python benchmarks/