Mask Editor and Q-Map Tutorial =============================== This tutorial shows how to use the SimpleMask module to create detector masks, compute Q-maps from geometry metadata, and generate Q-space partitions for XPCS analysis. .. admonition:: What you'll learn - Initialize the SimpleMask kernel in headless (no-GUI) mode - Load detector data with geometry metadata - Inspect and visualize the Q-map - Create and modify masks programmatically - Save and load masks in HDF5 format - Compute Q-phi partitions for correlation analysis - Validate Q-map data with the schema system For a task-oriented reference of the GUI tools and shortcuts, see :doc:`/how-to/mask_editor_guide`. Overview -------- The mask editor pipeline has three stages: 1. **Load detector data** -- Provide a 2D detector image and geometry metadata. 2. **Create a mask** -- Mark bad pixels, beamstop regions, and detector gaps. 3. **Compute partition** -- Bin the detector into Q-phi regions for correlation. The kernel class :class:`~xpcsviewer.simplemask.simplemask_kernel.SimpleMaskKernel` handles all computation. The GUI-level ``SimpleMaskWindow`` wraps it with interactive drawing tools. Step 1 -- Initialize the Kernel --------------------------------- The kernel can run without a GUI, which is useful for scripted pipelines and batch processing: .. code-block:: python import numpy as np from xpcsviewer.simplemask.simplemask_kernel import SimpleMaskKernel # Create kernel without GUI handles (headless mode) kernel = SimpleMaskKernel(pg_hdl=None, infobar=None) Step 2 -- Load Detector Data ------------------------------ Provide a detector image and a metadata dictionary describing the geometry. The geometry parameters control how pixel positions are converted to Q values: .. code-block:: python # Example: 256x256 detector with synthetic data detector_image = np.random.poisson(lam=100, size=(256, 256)).astype(np.float64) metadata = { "bcx": 128.0, # Beam center X (column), pixels "bcy": 128.0, # Beam center Y (row), pixels "det_dist": 5000.0, # Detector distance, mm "pix_dim": 0.075, # Pixel size, mm "energy": 10.0, # X-ray energy, keV "stype": "Transmission", } success = kernel.read_data(detector_image, metadata) print(f"Data loaded: {success}") print(f"Detector shape: {kernel.shape}") print(f"Q-map keys: {list(kernel.qmap.keys())}") The :meth:`~xpcsviewer.simplemask.simplemask_kernel.SimpleMaskKernel.read_data` method automatically computes the Q-map from the geometry and initializes a mask of all ones (all pixels valid). Geometry Parameters ~~~~~~~~~~~~~~~~~~~~ .. include:: /_includes/geometry_parameters_table.rst Step 3 -- Inspect the Q-Map ----------------------------- The ``qmap`` dictionary contains momentum-transfer and angle maps. Visualizing the Q-map helps verify that the geometry parameters are correct -- the Q rings should be centered on the beam position: .. code-block:: python import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 3, figsize=(14, 4)) # Q magnitude map im0 = axes[0].imshow(kernel.qmap["q"], origin="lower") axes[0].set_title(f"Q ({kernel.qmap_unit['q']})") plt.colorbar(im0, ax=axes[0]) # Azimuthal angle map im1 = axes[1].imshow(kernel.qmap["phi"], origin="lower") axes[1].set_title(f"phi ({kernel.qmap_unit['phi']})") plt.colorbar(im1, ax=axes[1]) # Detector image im2 = axes[2].imshow(np.log1p(detector_image), origin="lower") axes[2].set_title("log(1 + I)") plt.colorbar(im2, ax=axes[2]) plt.tight_layout() plt.show() Step 4 -- Modify the Mask --------------------------- The mask is a boolean array where ``True`` means valid (unmasked). You modify it by setting regions to ``False`` to exclude them from analysis. Common reasons to mask pixels include beamstop shadows, dead pixels, and hot pixels: .. code-block:: python # Mask a rectangular beamstop region kernel.mask[120:136, 120:136] = False # Mask pixels with zero counts kernel.mask[detector_image == 0] = False # Mask hot pixels (intensity > threshold) hot_threshold = np.percentile(detector_image, 99.9) kernel.mask[detector_image > hot_threshold] = False valid_pixels = kernel.mask.sum() total_pixels = kernel.mask.size print(f"Valid pixels: {valid_pixels}/{total_pixels} " f"({100 * valid_pixels / total_pixels:.1f}%)") In the GUI, you use the drawing tools instead of array indexing: .. include:: /_includes/drawing_tools_table.rst Step 5 -- Save and Load Masks ------------------------------- Masks are saved to HDF5 files for reuse across sessions and scripts: .. code-block:: python # Save mask kernel.save_mask("my_mask.h5") # Load mask in a new kernel kernel2 = SimpleMaskKernel(pg_hdl=None, infobar=None) kernel2.read_data(detector_image, metadata) kernel2.load_mask("my_mask.h5", key="mask") Step 6 -- Recompute Q-Map --------------------------- If geometry parameters change (for example, after refining the beam center), recompute the Q-map: .. code-block:: python # Update beam center kernel.metadata["bcx"] = 130.0 kernel.metadata["bcy"] = 126.0 qmap, qmap_unit = kernel.compute_qmap() print(f"Recomputed Q range: [{qmap['q'].min():.4f}, {qmap['q'].max():.4f}] {qmap_unit['q']}") Step 7 -- Compute a Q-Phi Partition ------------------------------------- The partition divides the detector into Q-phi bins for correlation analysis. There are two levels: **dynamic** (fewer bins, used for G2 correlation) and **static** (finer bins, used for SAXS averaging). .. code-block:: python partition = kernel.compute_partition( mode="q-phi", # Partition in Q and phi dq_num=10, # 10 dynamic Q bins sq_num=100, # 100 static Q bins dp_num=1, # 1 dynamic phi bin (full azimuthal average) sp_num=1, # 1 static phi bin style="linear", # Linear Q spacing ("logarithmic" also supported) ) if partition is not None: print(f"Partition keys: {list(partition.keys())}") print(f"Dynamic ROI map shape: {partition['dynamic_roi_map'].shape}") print(f"Number of dynamic bins: {partition['dynamic_roi_map'].max()}") # Visualize the dynamic partition fig, ax = plt.subplots() roi = partition["dynamic_roi_map"].astype(float) roi[roi == 0] = np.nan # Hide masked regions ax.imshow(roi, origin="lower", cmap="tab20") ax.set_title("Dynamic Q-Phi Partition") plt.tight_layout() plt.show() The dynamic partition determines how many G2 curves you get -- one per bin. More bins give finer Q resolution but fewer pixels per bin, increasing statistical noise. A good starting point is 10-20 dynamic Q bins with 1 phi bin (full azimuthal average). Step 8 -- Validate with Schemas --------------------------------- Use :class:`~xpcsviewer.schemas.QMapSchema` to validate Q-map data at I/O boundaries: .. code-block:: python from xpcsviewer.schemas import QMapSchema # Create a validated QMapSchema from the compute_qmap output qmap_schema = QMapSchema.from_compute_qmap(kernel.qmap, kernel.qmap_unit) print(f"Schema sqmap shape: {qmap_schema.sqmap.shape}") print(f"Schema unit: {qmap_schema.sqmap_unit}") # Convert back to dict for legacy interfaces qmap_dict = qmap_schema.to_dict() .. note:: The :class:`~xpcsviewer.schemas.QMapSchema` is a frozen dataclass. It validates shapes, dtypes, NaN values, and units at construction time. Arrays are defensively copied to prevent external mutation. GUI Workflow -------------- In the full GUI application, the mask editor is launched from the **Mask Editor** tab. It provides: - **Drawing tools**: Rectangle, Circle, Polygon, Line, Ellipse, Eraser - **Undo/redo history** via the ``MaskAssemble`` class - **Export signals**: ``mask_exported(np.ndarray)`` and ``qmap_exported(dict)`` The GUI calls the same :class:`~xpcsviewer.simplemask.simplemask_kernel.SimpleMaskKernel` methods demonstrated above. See :doc:`/how-to/mask_editor_guide` for the full GUI reference with keyboard shortcuts and parameter tables. Next Steps ---------- - :doc:`getting_started` -- Load data and perform basic G2 analysis - :doc:`fitting_guide` -- Fit G2 curves with NLSQ and Bayesian inference - :doc:`cookbook` -- Batch processing and advanced patterns