# Sign of the odd aberrations in SSB reconstruction (2026-10-21)

> **Historical:** describes the code as of 2026-10-21; the `mean_phase` estimator is retired. See the [current fit methods](../../../api/ssb.md).

## Question

For a thin object the bright-field spectrum is

G(q, k) ∝ φ̃(q) [P(k − q) P*(k) − P(k) P*(k + q)]

(Pennycook et al., Ultramicroscopy 151, 2015; probe at p(r − r₀), scan FFT
e^{−2πi q·r₀}). Every backend evaluated the first term's probe phase at q − k.
For C10, C12 and every even order χ(q − k) = χ(k − q); for the odd orders
(C21, C23, C41, C43, C45) the sign reverses on one sideband only. Which one do
the data follow, and does each backend now evaluate it?

## The oracle

An independent float64 model: χ in polar form (α, atan2), the backends' soft
aperture, the depth weights, both estimators (mean phase, phase of the mean), and
either sign. It shares no code with the kernels.

- **Closed form.** On an exact thin-object 4D-STEM simulation (one transmission
  function, probe with C21 50 nm, C23 40 nm, C30 3 µm, 64 × 64 scan), the linear
  part of the simulated G, ½[G(φ) − G(−φ)], matches the k − q form to
  1.7 × 10⁻⁵ (relative, one complex amplitude per q). The q − k form misses
  by 0.59.
- **Data symmetry.** Real intensities give G(−q, k) = conj G(q, k), and the
  k − q model keeps Γ(−q, k) = −conj Γ(q, k) for any probe phase. The q − k
  model does not. Applied to a measured 4D-STEM acquisition (a 128 × 128 crop,
  2,287 bright-field pixels) with C21 104 nm, C23 21 nm and C30 5.3 µm, the
  corrected spectra break the symmetry by 2 × 10⁻⁸ (relative) with k − q and by
  1.05 with q − k (Nyquist row and column excluded). The half-plane fit
  objectives rely on this symmetry.

## Every backend against the oracle

Largest phase difference (rad) from the oracle at either sign: before
(quantem.gpu e25bb089) and after the fix. "Simulation": a weak periodic lattice,
one slice, the probe above, tiled to the backend's scan size. "Measured": the
crop above. Each backend reconstructs from its own G(q, k) and k, and the
oracle uses the same inputs. A backend has the right sign when it matches k − q
to float32 precision. The two signs differ by 5 × 10⁻³ to 0.13 rad.

| backend, path | data | before: vs k − q | before: vs q − k | after: vs k − q | after: vs q − k |
| --- | --- | --- | --- | --- | --- |
| MPS, phase of mean | simulation | 8.4e-3 | 1.2e-6 | 1.5e-6 | 8.4e-3 |
| MPS, mean phase | simulation | 8.4e-3 | 1.6e-7 | 1.3e-7 | 8.4e-3 |
| MPS, phase of mean | measured | 0.125 | 6.5e-5 | 8.6e-5 | 0.125 |
| MPS, mean phase | measured | 0.110 | 5.4e-4 | 7.5e-5 | 0.110 |
| CUDA, wave sum (phase of mean) | simulation, 256² | 8.5e-3 | 1.3e-7 | 9.2e-8 | 8.5e-3 |
| CUDA, fused FFT (mean phase) | simulation, 256² | 8.4e-3 | 1.6e-7 | 1.3e-7 | 8.4e-3 |
| CUDA, wave sum | measured, 256² | 0.130 | 5.7e-5 | 8.6e-5 | 0.130 |
| CUDA, fused FFT | measured, 256² | 0.118 | 9.1e-4 | 7.7e-5 | 0.118 |
| WebGPU, phase of mean | simulation, 128² | 8.5e-3 | 8.1e-8 | 6.2e-8 | 8.5e-3 |
| WebGPU, mean phase | simulation, 128² | 8.4e-3 | 4.9e-8 | 1.2e-8 | 8.4e-3 |
| WebGPU, both | measured | 0.080–0.092 | 2.7e-6–4.2e-6 | 4.6e-6–4.9e-6 | 0.080–0.092 |
| native Metal (MetalSSBKernels) | simulation, 128² | 8.5e-3 | 3.3e-8 | 2.9e-8 | 8.5e-3 |
| native Metal | measured | 0.092 | 2.7e-6 | 4.3e-6 | 0.092 |

The measured data's float32 floor is set by the low-order terms alone: there
the two signs agree, and every path matches the oracle to 3 × 10⁻⁵ rad (MPS) or
9 × 10⁻⁷ (native). WebGPU ran headless in Chrome on the Apple GPU (adapter
`apple / metal-3`, subgroup size 32, not a fallback adapter). CUDA ran on an
RTX PRO 6000 Blackwell.

The paths include CUDA's fused full-aberration FFT kernels
(`cuda/kernels/common.py`, `gamma_mul_pk_onthefly_full`), which
`preview(..., phase_estimator="mean_phase")` runs with higher orders and no
depth spread on 256, 512 and 1024 scans.

## Effect on the image

With the probe's own coefficients, the reconstructed phase of the simulation
correlates with the object as follows (both estimators, every backend; 64², 128²
and 256² scans of the same periodic specimen):

| | k − q (after) | q − k (before) |
| --- | --- | --- |
| true C10 … C30 | 0.963–0.964 | 0.941–0.942 |
| C10, C12 only | 0.942 | 0.942 |
| odd terms reversed | 0.875 | 0.880–0.881 |

Before the fix, entering the true coma and trefoil did not improve the image.

## Tests

- `tests/test_ssb_higher_order_sign.py` (CPU): the closed form against an exact
  thin-object simulation, and the symmetry of each form.
- `tests/hardware/test_ssb_higher_order_sign.py` (MPS or CUDA): reconstruction of
  a weak-phase simulation, true coefficients against none and against reversed
  odd terms, for both estimators. On CUDA the scan is tiled to 256², so it runs
  the fused FFT path.
- `tests/hardware/cuda/test_ssb_higher_order.py` and
  `tests/hardware/test_ssb_coherent_higher_order.py`: their float64 references
  now use k − q. On the old kernels 9 of their 16 cases fail.
- `tests/webgpu/ssb-higher-order-sign.ts` (headed browser gate,
  `tests/webgpu/run_qem_browser.py`): an exact weak-phase simulation, 128² scan,
  293 BF pixels, reconstructed with the true coefficients, none above C12 and the
  odd ones reversed, both estimators, against a float64 k − q reference. Fixed
  WGSL: within 7.5 × 10⁻⁸ rad. Old WGSL: 2.8 × 10⁻² rad from the reference and
  7.5 × 10⁻⁸ from the q − k form.
- `tests/metal/ssb_workflow_check.swift` (`scripts/check_metal_ssb.sh`): three
  weak phase gratings, cached and streamed engines. With the true coefficients
  the corrected Fourier sum at each grating's q must equal
  e^{i(π/2 + θ)} mean_k |G_k(q)|. Fixed `ssb.metal`: within 9.4 × 10⁻⁷ of its
  size. Old: 0.14–0.18.

The fused CUDA test's tolerance rises from 5e-6 to 1e-5 rad. Against the
independent oracle, the fused path (float32 Chebyshev χ, all 14 orders active)
is 3.5 × 10⁻⁶ off and the test's own float64-χ reference 3.6 × 10⁻⁶. Before
the fix the two errors partly cancelled (2.0 × 10⁻⁶). With the wrong sign the
difference is 2.6 × 10⁻³.

The saved-result signature's `higher_order_model` becomes `"krivanek14 k-q"`.
Reconstructions saved with odd orders under the old sign are recomputed.
