Mask and Q-Map Internals

This page explains how XPCS Viewer computes Q-maps from detector geometry and how the mask system manages pixel validity through an undo/redo stack.

From Pixels to Q-Space

Each pixel on a 2D area detector corresponds to a specific scattering vector \(\mathbf{q}\) determined by the experimental geometry. The Q-map is the mapping from pixel coordinates \((i, j)\) to the scattering wavevector magnitude \(q\) and azimuthal angle \(\phi\).

Computing this mapping is the foundation of all XPCS analysis: it determines which pixels belong to which Q-bin for correlation function computation.

Geometry Parameters

The Q-map computation requires six geometry parameters:

Parameter

Unit

Description

energy

keV

X-ray energy, converted to wavevector via \(k_0 = 2\pi / \lambda\)

bcx, bcy

pixels

Beam center position (column, row) on the detector

det_dist

mm

Sample-to-detector distance

pix_dim

mm

Physical size of one detector pixel

shape

(H, W)

Detector dimensions in pixels

Defaults (used when geometry is not available in the HDF5 file): pix_dim=0.075, energy=10.0, det_dist=5000.0, beam center at the image center.

Transmission Geometry

For small-angle X-ray scattering (SAXS) in transmission, the Q-map computation proceeds as follows:

  1. Pixel grid: Create arrays v and h of pixel distances from the beam center, then form a 2D meshgrid.

  2. Radial distance: \(r = \sqrt{v^2 + h^2} \times \text{pix\_dim}\) gives the physical distance from the beam center in mm.

  3. Scattering angle: \(\alpha = \arctan(r / d)\) where \(d\) is the detector distance.

  4. Q magnitude: The scattering wavevector magnitude is:

    \[q = k_0 \sin(\alpha) = \frac{2\pi}{\lambda} \sin\!\left[\arctan\!\left(\frac{r}{d}\right)\right]\]

    where the wavelength is computed from energy: \(\lambda [\text{\AA}] = 12.398 / E [\text{keV}]\).

    For small angles (SAXS regime), this simplifies to:

    \[q \approx \frac{4\pi}{\lambda} \sin\!\left(\frac{\theta}{2}\right)\]

    where \(2\theta = \alpha\) is the full scattering angle.

  5. Azimuthal angle: \(\phi = -\arctan2(v, h)\) gives the azimuthal angle in the detector plane (negated by convention).

  6. Q components: \(q_x = q \cos\phi\), \(q_y = q \sin\phi\).

The output is a dictionary containing arrays sqmap (Q magnitude), phis (azimuthal angle), qr, qx, qy, and angular maps.

Reflection Geometry

For grazing-incidence SAXS (GI-SAXS), the geometry is more complex because the incident beam hits the sample at a shallow angle \(\alpha_i\). The Q-map must account for refraction effects and the tilted scattering geometry. The reflection Q-map additionally requires:

  • alpha_i_deg: Incident angle in degrees (default 0.14)

  • orientation: Detector orientation relative to the sample surface

JIT Compilation of Q-Map

Q-map computation is one of the most performance-critical operations because it runs on every geometry change (beam center adjustment, detector distance update, etc.) during interactive mask editing.

When the JAX backend is active, the core computation is JIT-compiled:

compute_qmap(stype, metadata)
    |
    v
compute_transmission_qmap(energy, beam_center, shape, pix_dim, det_dist)
    |
    v
_compute_transmission_qmap_cached(...)   <-- dict-based cache check
    |
    v (cache miss)
_get_transmission_qmap_jit()             <-- creates @jax.jit function
    |
    v
_transmission_qmap_core(k0, v, h, ...)   <-- JIT-compiled, runs on XLA

The dict-based caching (_JIT_CACHE) is necessary because JAX arrays are not hashable, so functools.lru_cache cannot be used. The first call with a new scattering type creates and caches the compiled function; subsequent calls with different numerical inputs reuse the compiled trace (XLA recompiles only if array shapes change).

Typical performance: ~200 ms first call (compilation), ~5 ms subsequent calls for 1024x1024 detector.

Q-Bin Partition

After computing the Q-map, pixels are grouped into Q-bins for correlation analysis. The partition assigns each pixel to a bin based on its Q value.

The partition generation (simplemask/utils.py) creates:

  • partition_map: An integer array of shape (H, W) where each value is the Q-bin index (0 = unassigned/masked)

  • val_list: Q-bin center values

  • num_list: Number of pixels per bin

  • num_pts: Total number of Q-bins

Binning can be linear or logarithmic in Q-space. Logarithmic binning is preferred for SAXS data because Q resolution varies across the detector (finer at small Q, coarser at large Q).

Mask System Architecture

The mask is a boolean array (stored as int32 with values 0/1) that marks which detector pixels are valid for analysis. Masked pixels (value 0) are excluded from correlation computation.

Common reasons for masking:

  • Dead or hot pixels on the detector

  • Beamstop shadow

  • Gaps between detector modules

  • Regions with parasitic scattering

  • User-defined exclusion zones

MaskAssemble and the History Stack

The MaskAssemble class manages the composite mask through a layered system with undo/redo capability.

Mask layers (applied in order):

  1. Default masks: Loaded from file or computed from detector properties (dead pixels, gaps)

  2. Threshold mask: Pixels above/below intensity thresholds

  3. Parameter masks: Based on Q-range or angle constraints

  4. Drawing masks: User-drawn regions (rectangles, circles, polygons, lines, ellipses)

Each mask layer implements a MaskBase subclass with an evaluate() method that returns a boolean array. The final mask is the logical AND of all layers.

Undo/redo mechanism:

mask_record:  [state_0, state_1, state_2, state_3]
                                    ^
mask_ptr:                           |
                        mask_ptr = 2

Undo: mask_ptr -= 1, apply state_1
Redo: mask_ptr += 1, apply state_3

The history stack (mask_record) stores mask states at each modification point. The pointer (mask_ptr) tracks the current position. mask_ptr_min marks the boundary of the default mask – undo cannot go below this point (the default mask is always present).

Operations:

  • mask_action(key, arr): Pushes a new mask state, truncating any redo history beyond the current pointer

  • redo_undo(step): Moves the pointer by step (+1 for redo, -1 for undo), then applies the state at the new position

  • mask_apply(key): Syncs the kernel’s mask with the current state

Drawing Tools

XPCS Viewer provides six drawing tools for interactive mask editing:

Tool

Description

Rectangle

Axis-aligned rectangular region

Circle

Circular region defined by center and radius

Ellipse

Elliptical region with adjustable axes

Polygon

Arbitrary polygon defined by vertex clicks

Line

Linear selection (custom LineROI from pyqtgraph_mod.py)

Eraser

Removes masked regions (restores pixels)

Drawing is performed through PyQtGraph ROI (Region of Interest) objects. When the user completes a drawing operation:

  1. apply_drawing() converts the ROI to a boolean mask array

  2. The mask is inverted (~mask) and passed to evaluate("mask_draw", arr=~mask)

  3. mask_apply("mask_draw") records the state in the history stack

Signal Export

When the user exports the mask and partition from SimpleMask to the main XPCS Viewer, two Qt signals carry the data:

  • mask_exported(np.ndarray): The final mask array (int32, 0/1)

  • qmap_exported(dict): The partition data (partition_map, val_list, num_list, num_pts)

This signal-based communication maintains loose coupling between the SimpleMask subsystem and the main viewer – neither module imports or directly calls the other. The main viewer connects to these signals and applies the received data to update its analysis state.