{ "cells": [ { "metadata": {}, "cell_type": "markdown", "source": [ "# Tutorial 1: Experimental Parameters\n", "\n", "## 1. Detector Parameters\n", "\n", "### 1.1 PONI File\n", "\n", "The `pygid.ExpParams` class stores the experimental parameters of the detector and the X-ray wavelength. These parameters are needed to calculate the reciprocal-space coordinates of each pixel on the area detector.\n", "\n", "The easiest way to load them is using a PONI file, similar to the **pyFAI** library [https://pyfai.readthedocs.io].\n", "\n", "When you create an `ExpParams` instance, it reads the PONI file from the `poni_path` you provide. It loads the position of the point of normal incidence (in meters) and the detector rotation angles (in radians). You can create a PONI file from a calibrant image using the pyFAI-calib2 GUI: [https://www.silx.org/doc/pyFAI/latest/usage/cookbook/calib-gui/index.html].\n", "\n", "The pixel size is set automatically based on the detector. If your detector is custom or unknown, provide `px_size` in meters.\n", "\n", "**Important:** Make sure the detector orientation is set to `3`. For grazing-incidence experiments, also set the angle of incidence (`ai`) in degrees.\n", "\n", "Before using the detector parameters, download the example files from Zenodo:\n", "\n" ], "id": "d6e11f297256f32e" }, { "metadata": { "ExecuteTime": { "end_time": "2026-03-05T10:30:10.335893900Z", "start_time": "2026-03-05T10:30:06.010798Z" } }, "cell_type": "code", "source": [ "from pygid.datasets import get_dataset\n", "\n", "# Download example dataset from Zenodo\n", "try:\n", " files = get_dataset(\"tutorial_01\")\n", " poni_path = files[\"poni\"]\n", " mask_path = files[\"mask\"]\n", "except:\n", " print(\"Dataset download skipped on Read the Docs.\")" ], "id": "f0c7db1a3153f3e7", "outputs": [], "execution_count": 1 }, { "metadata": {}, "cell_type": "markdown", "source": "Load experimental parameters from PONI:", "id": "27a263f17946617d" }, { "metadata": { "ExecuteTime": { "end_time": "2026-02-09T09:39:37.509673Z", "start_time": "2026-02-09T09:39:37.488652Z" } }, "cell_type": "code", "source": [ "import pygid\n", "params = pygid.ExpParams(\n", " poni_path=poni_path, # path to the PONI file\n", " ai = 0.01, # angle of incidence (in degrees)\n", ")\n", "print(params)" ], "id": "b03e61daea7f7ee7", "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ExpParams(poni_path=WindowsPath('C:/Users/Ainur Abukaev/.cache/pygid/tutorial_01/LaB6_2024_07_ESRF_ID10.poni'), mask_path=None, mask=None, flipud=False, fliplr=False, transp=False, px_size=7.5e-05, img_dim=None, n=None, SDD=0.3271661836504515, wavelength=0.6199209921660013, rot1=-0.0026308680554001304, rot2=-0.004647717026373591, rot3=0.0, poni1=0.1446949272569577, poni2=0.14877525660652482, centerX=None, centerY=None, k=None, count_range=None, ai=0.01, scan=None)\n" ] } ], "execution_count": 4 }, { "metadata": {}, "cell_type": "markdown", "source": "**Note:** The class is associated with a single geometry; therefore, separate class instances must be created for different PONI files. For incident-angle scans, see [Tutorial 7](tutorial_07_angular_scans.ipynb).\n", "id": "88fb4499bf23ac4d" }, { "metadata": {}, "cell_type": "markdown", "source": [ "### 1.2 Direct Loading\n", "\n", "Instead of using a PONI file, you can also load the detector parameters manually. You need to provide:\n", "\n", "- Sample-to-detector distance `SDD` (in meters)\n", "- Point of normal incidence `poni1` (vertical) and `poni2` (horizontal) in meters\n", "- Detector rotation angles `rot1`, `rot2`, `rot3` (in radians)\n", "- Pixel size `px_size` (in meters)\n", "- Wavelength `wavelength` (in angstroms)\n", "- Angle of incidence `ai` (in degrees) " ], "id": "6516348e9071242a" }, { "metadata": { "ExecuteTime": { "end_time": "2026-02-09T09:43:04.594478Z", "start_time": "2026-02-09T09:43:04.522995Z" } }, "cell_type": "code", "source": [ "import pygid\n", "\n", "params = pygid.ExpParams(\n", " SDD=0.327, # Sample-to-detector distance (m)\n", " wavelength=0.6199, # Wavelength (Å)\n", " rot1=-0.0026, # Detector rotation X (radians)\n", " rot2=-0.0046, # Detector rotation Y (radians)\n", " rot3=0, # Detector rotation Z (radians)\n", " poni1=0.145, # Beam vertical position (m)\n", " poni2=0.149, # Beam horizontal position (m)\n", " px_size=75e-6, # Detector pixel size (m)\n", " ai=0.01 # Angle of incidence (degrees)\n", ")\n", "print(params)" ], "id": "1bdcdbb22f37b02c", "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ExpParams(poni_path=None, mask_path=None, mask=None, flipud=False, fliplr=False, transp=False, px_size=7.5e-05, img_dim=None, n=None, SDD=0.327, wavelength=0.6199, rot1=-0.0026, rot2=-0.0046, rot3=0, poni1=0.145, poni2=0.149, centerX=None, centerY=None, k=None, count_range=None, ai=0.01, scan=None)\n" ] } ], "execution_count": 5 }, { "metadata": {}, "cell_type": "markdown", "source": "Instead of poni1 and poni2, you can also provide centerX and centerY in pixels (relative to the bottom-left corner):", "id": "76300b1dd67f0534" }, { "metadata": { "ExecuteTime": { "end_time": "2026-02-09T11:28:52.813715Z", "start_time": "2026-02-09T11:28:52.787431Z" } }, "cell_type": "code", "source": [ "import pygid\n", "\n", "params = pygid.ExpParams(\n", " SDD=0.327, # Sample-to-detector distance (m)\n", " wavelength=0.6199, # Wavelength (Å)\n", " rot1=-0.0026, # Detector rotation X (radians)\n", " rot2=-0.0046, # Detector rotation Y (radians)\n", " rot3=0, # Detector rotation Z (radians)\n", " centerX=1909, # Beam vertical position (m)\n", " centerY=1996, # Beam horizontal position (m)\n", " px_size=75e-6, # Detector pixel size (m)\n", " ai=0.01 # Angle of incidence (degrees)\n", ")\n", "print(params)" ], "id": "5d9b14001eb13be5", "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ExpParams(poni_path=None, mask_path=None, mask=None, flipud=False, fliplr=False, transp=False, px_size=7.5e-05, img_dim=None, n=None, SDD=0.327, wavelength=0.6199, rot1=-0.0026, rot2=-0.0046, rot3=0, poni1=None, poni2=None, centerX=1909, centerY=1996, k=None, count_range=None, ai=0.01, scan=None)\n" ] } ], "execution_count": 6 }, { "metadata": {}, "cell_type": "markdown", "source": [ "## 2. Masking\n", "\n", "### 2.1 Mask from File\n", "\n", "You can load a \"static\" detector mask to remove detector gaps, dead or hot pixels, and the beam stop shadow.\n", "Provide the path to the mask file (`mask_path`) in any supported format: EDF, TIFF, or NPY.\n", "Pixels with value `1` in the mask will be replaced with `numpy.nan`." ], "id": "2f4ed51066f52219" }, { "metadata": { "ExecuteTime": { "end_time": "2026-02-09T12:19:03.259678Z", "start_time": "2026-02-09T12:19:03.210895Z" } }, "cell_type": "code", "source": [ "import pygid\n", "params = pygid.ExpParams(\n", " poni_path=poni_path,\n", " mask_path=mask_path,\n", " ai=0.01\n", ")\n", "print(params.mask.shape)" ], "id": "a0a446798a2ac3c5", "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(2162, 2068)\n" ] } ], "execution_count": 8 }, { "metadata": {}, "cell_type": "markdown", "source": [ "### 2.2 Mask as a NumPy Array\n", "If the mask is already loaded as a NumPy array, you can pass it directly to `ExpParams`:\n" ], "id": "b5e0e4b0d4451985" }, { "metadata": { "ExecuteTime": { "end_time": "2026-02-09T12:26:11.387545Z", "start_time": "2026-02-09T12:26:11.374273Z" } }, "cell_type": "code", "source": [ "import pygid\n", "import numpy as np\n", "\n", "mask_array = np.load(mask_path) # numpy array with the mask\n", "\n", "params = pygid.ExpParams(\n", " poni_path=poni_path,\n", " mask=mask_array,\n", " ai=0.01\n", ")\n", "print(params.mask.shape)\n" ], "id": "c43ec0f4c6939948", "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(2162, 2068)\n" ] } ], "execution_count": 13 }, { "metadata": {}, "cell_type": "markdown", "source": [ "### 2.3 Dynamic Mask\n", "A \"dynamic\" mask can be used to mask very strong or very weak reflections that vary from image to image.\n", "To use it, provide `count_range` as a tuple (min, max) defining the intensity range.\n", "The dynamic mask can be used together with or instead of the static mask:\n" ], "id": "670d0a70e26cfa9" }, { "metadata": { "ExecuteTime": { "end_time": "2026-02-09T12:26:13.294025Z", "start_time": "2026-02-09T12:26:13.281968Z" } }, "cell_type": "code", "source": [ "import pygid\n", "\n", "params = pygid.ExpParams(\n", " poni_path=poni_path,\n", " mask_path=mask_path,\n", " count_range=(10, 10000), # intensity range for dynamic masking\n", " ai=0.01\n", ")\n" ], "id": "41d1c8dd57bb39a7", "outputs": [], "execution_count": 14 }, { "metadata": {}, "cell_type": "markdown", "source": [ "## 3. Image Flipping & Transposition\n", "\n", "In some cases, the detector image must be flipped or transposed prior to conversion.\n", "The following boolean parameters control these operations:\n", "\n", "- `fliplr = True` — horizontal flipping (left-to-right)\n", "- `flipud = True` — vertical flipping (upside-down)\n", "- `transp = True` — transposition (swap of X and Y axes) — can be used when the detector was rotated\n", "\n", "These operations modify the detector orientation and therefore alter the corresponding experimental geometry:\n", "- point of normal incidence:\n", "```python\n", "if transp:\n", " poni1, poni2 = poni2, poni1\n", "if flipud:\n", " poni1 = img_dim[0] * px_size - poni1\n", "if self.fliplr:\n", " poni2 = (img_dim[1] - 1) * px_size - poni2\n", "```\n", "where img_dim - image dimensions; px_size - pixel size\n", "- detector rotation angles:\n", "```python\n", "if transp:\n", " rot1, rot2 = -rot2, -rot1\n", "if flipud:\n", " rot2 = -rot2\n", " rot3 = -rot3\n", "if fliplr:\n", " rot1 = -rot1\n", " rot3 = -rot3\n", "```\n", "\n", "The effect of these options on the conversion result is illustrated below:\n", "