Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

General Timepix3 LATRD format #305

Closed
wants to merge 9 commits into from
228 changes: 228 additions & 0 deletions src/dxtbx/format/FormatNexusTimepix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
"""
Format class and assorted helper functions for the Large Area Time Resolved Detector.

This supports the experimental LATRD 'Tristan'. All measurements are in detector
coordinates, with vector measures having the form (x, y), to match the data set
`/entry/instrument/detector/module/data_size`. In the image data, x is the fast
axis and y is the slow axis.
"""

from __future__ import annotations

Check warning on line 10 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L10

Added line #L10 was not covered by tests

from typing import Sequence, Union

Check warning on line 12 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L12

Added line #L12 was not covered by tests

import h5py
import numpy as np

Check warning on line 15 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L14-L15

Added lines #L14 - L15 were not covered by tests

from dxtbx.format.FormatNexus import FormatNexus
from dxtbx.model import Detector

Check warning on line 18 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L17-L18

Added lines #L17 - L18 were not covered by tests

Vector = Union[int, Sequence[int]]

Check warning on line 20 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L20

Added line #L20 was not covered by tests


def region_size(object_size: Vector, layout: Vector, stride: Vector) -> Vector:

Check warning on line 23 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L23

Added line #L23 was not covered by tests
"""Derive the total size of a grid of objects from their size, layout and stride."""
layout = np.full((2,), layout, dtype=int)
return object_size + (layout - 1) * stride

Check warning on line 26 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L25-L26

Added lines #L25 - L26 were not covered by tests


def object_layout(region_size: Vector, object_size: Vector, stride: Vector) -> Vector:

Check warning on line 29 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L29

Added line #L29 was not covered by tests
"""Derive the layout of a grid of objects from the size of the bounding region."""
region_size = np.full((2,), region_size, dtype=int)
return 1 + (region_size - object_size) // stride

Check warning on line 32 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L31-L32

Added lines #L31 - L32 were not covered by tests


# Each LATRD module consists of an arrangement of 8 × 2 Timepix3 chips.
chip_layout = (8, 2) # Number of chips.

Check warning on line 36 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L36

Added line #L36 was not covered by tests
# Each Timepix3 chip is composed of 256 × 256 pixels, each 55µm × 55µm in size.
chip_size = 256 # Number of pixels.

Check warning on line 38 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L38

Added line #L38 was not covered by tests
# The edge pixels are actually slightly elongated, so there is effectively 3px-worth
# of space between adjacent chips in a LATRD module.
chip_stride = 259 # Number of pixels.

Check warning on line 41 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L41

Added line #L41 was not covered by tests

# From this, we can find the overall module size, including inter-chip spacing.
module_size = region_size(chip_size, chip_layout, chip_stride) # Number of pixels.

Check warning on line 44 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L44

Added line #L44 was not covered by tests
# The LATRD comprises one or more such modules arranged in a grid,
# with some space between adjacent modules.
module_stride = (2114, 632) # Number of pixels.

Check warning on line 47 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L47

Added line #L47 was not covered by tests


def gap_masks(object_size: Vector, layout: Vector, stride: Vector, inflate: Vector = 0):

Check warning on line 50 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L50

Added line #L50 was not covered by tests
"""
Find the extents of the gaps between regularly spaced detector components.

Area detectors often consist of a rectangular array of identical components
(sensor chips, panels, etc.), with gaps in between that we wish to mask. Given a
region containing such an array of components, and knowing their size and
spacing, the coordinates of the masked gap regions are computed. Where the array
consists of n_x × n_y components, there will be N = n_x + n_y - 2 such gap regions.

Optionally, the masked region may be inflated by a specified number of pixels
either side. Sometimes this is necessary when the edge pixels behave in a
different manner to the rest of the component, systematically over- or
under-counting for example. To illustrate, if adjacent components are separated
by a gap three pixels wide and if a value inflate=1 has been specified,
the masked region will be five pixels wide.

Args:
object_size: The dimensions (size x, size y) in pixels of each component of
the detector array. If the components are square, the edge length can be
passed as a scalar.
layout: The number of repeats (number x, number y) of detector components in
the rectangular grid. If there are the same number of components in each
dimension, that number can be passed as a scalar.
stride: The spacing (stride x, stride y) of adjacent components in the grid.
If the spacing is equal in each dimension, it can be passed as a scalar.
The stride in each dimension will be the object size plus the width of the
gap.
inflate: Width in pixels (number x, number y) to be added to each side of
the masked gap region, in order to mask the edges of the components. If
the gaps in both the x and y directions are to be inflated by the same
number of pixels, that number can be passed as a scalar.

Returns:
The parameters of the regions to be masked, a numpy.ndarray of shape (N, 2, 2),
where N is the number of gaps between components. Each of the N elements in
the first dimension is a pair of two-vectors. Each pair represents the
bottom-left and top-right pixel, respectively, of a gap region to be masked.
Each pixel is represented by a (x, y) vector of its position.
"""
# Ensure all the arguments are cast to ndarrays of shape (2,).
x_size, y_size = np.full((2,), object_size, dtype=int)
x_layout, y_layout = np.full((2,), layout, dtype=int) - 1
x_stride, y_stride = stride = np.full((2,), stride, dtype=int)
x_inflate, y_inflate = np.full((2,), inflate, dtype=int)

Check warning on line 94 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L91-L94

Added lines #L91 - L94 were not covered by tests

x_span, y_span = region_size(object_size, layout, stride)

Check warning on line 96 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L96

Added line #L96 was not covered by tests

# The extents of the first gaps in x & y.
x_gap_extent = np.array(

Check warning on line 99 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L99

Added line #L99 was not covered by tests
(
(x_size - x_inflate, 0), # Bottom-left corner.
(x_stride + x_inflate, y_span), # Top-right corner.
)
)
y_gap_extent = np.array(

Check warning on line 105 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L105

Added line #L105 was not covered by tests
(
(0, y_size - y_inflate), # Bottom-left corner.
(x_span, y_stride + y_inflate), # Top-right corner.
)
)

# Get the positions of the bottom-left corner of each gap region.
x_gap_pos = np.column_stack((np.arange(x_layout), np.zeros(x_layout, dtype=int)))
x_gap_pos *= x_stride
y_gap_pos = np.column_stack((np.zeros(y_layout, dtype=int), np.arange(y_layout)))
y_gap_pos *= y_stride

Check warning on line 116 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L113-L116

Added lines #L113 - L116 were not covered by tests

# Compute the positions and extents of all the gaps in x & y.
x_gap_masks = np.add(x_gap_extent, x_gap_pos[:, np.newaxis])
y_gap_masks = np.add(y_gap_extent, y_gap_pos[:, np.newaxis])

Check warning on line 120 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L119-L120

Added lines #L119 - L120 were not covered by tests

return np.concatenate((x_gap_masks, y_gap_masks))

Check warning on line 122 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L122

Added line #L122 was not covered by tests


def latrd_masks(module_layout: Vector, inflate: Vector = 0):

Check warning on line 125 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L125

Added line #L125 was not covered by tests
"""
Find the regions to be masked on a Large Area Time Resolved Detector.

A LATRD consists of a rectangular array of n_x × n_y LATRD modules, each in turn
consisting of an array of 8 × 2 Timepix3 chips. The gap regions between adjacent
chips and between adjacent modules are calculated so that they can be masked.
The masks for the gaps between adjacent chips can optionally be inflated by a
specified number of pixels.

There will be N = 8 × n_x × n_y + n_x + n_y - 2 rectangular regions to be masked.

See Also:
dxtbx.format.FormatNexusTimepix.gap_masks

Args:
module_layout: The number of repeats (number x, number y) of LATRD modules
in the detector. If there are the same number of components in each
dimension, that number can be passed as a scalar.
inflate: Width in pixels (number x, number y) to be added to each side of
the masked gap region between Timepix3 chips within each LATRD module,
in order to mask the edges of the chips. If the gaps in both the x and y
directions are to be inflated by the same number of pixels, that number
can be passed as a scalar.

Returns:
The parameters of the regions to be masked, a numpy.ndarray of shape (N, 2, 2),
where N is the number of gaps between components. Each of the N elements in
the first dimension is a pair of two-vectors. Each pair represents the
bottom-left and top-right pixel, respectively, of a gap region to be masked.
Each pixel is represented by a (x, y) vector of its position.
"""
# Generate the mask coordinates for the gaps between chips within the first
# module. Inflate the chip gap masks by a width of one pixel either side.
chip_gap_mask_first_module = gap_masks(chip_size, chip_layout, chip_stride, inflate)

Check warning on line 159 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L159

Added line #L159 was not covered by tests

# Find the positions of each of the modules.
module_steps = np.concatenate(np.indices(module_layout).T)
module_displacements = module_stride * module_steps

Check warning on line 163 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L162-L163

Added lines #L162 - L163 were not covered by tests

# Place a copy of the chip gap masks over each module.
chip_gap_mask_first_module = chip_gap_mask_first_module[np.newaxis, ...]
module_displacements = module_displacements[:, np.newaxis, np.newaxis, :]
chip_gap_masks = np.add(chip_gap_mask_first_module, module_displacements)
chip_gap_masks = np.concatenate(chip_gap_masks)

Check warning on line 169 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L166-L169

Added lines #L166 - L169 were not covered by tests

# Generate the mask coordinates for the gaps between modules.
module_gap_mask = gap_masks(module_size, module_layout, module_stride)

Check warning on line 172 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L172

Added line #L172 was not covered by tests

return np.concatenate((chip_gap_masks, module_gap_mask))

Check warning on line 174 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L174

Added line #L174 was not covered by tests


class FormatNexusTimepix(FormatNexus):

Check warning on line 177 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L177

Added line #L177 was not covered by tests
"""Format for binned images from a Timepix3 LATRD 'Tristan' detector."""

@staticmethod

Check warning on line 180 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L180

Added line #L180 was not covered by tests
def understand(image_file):
"""
Check that the image format is recognised as binned images from a LATRD.

The image format is understood if:
• the NeXus tree contains an item
"/entry/instrument/detector/module/data_size", and
• the size recorded there corresponds to a known rectangular arrangement of
LATRD modules.
"""
known_module_layouts = (

Check warning on line 191 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L191

Added line #L191 was not covered by tests
(1, 1), # 1M detector
(1, 2), # 2M detector
(2, 5), # 10M detector
)
with h5py.File(image_file, "r") as handle:
if "/entry/instrument/detector/module/data_size" in handle:
size = handle["/entry/instrument/detector/module/data_size"][()]

Check warning on line 198 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L198

Added line #L198 was not covered by tests
# Note that the data_size is recorded in order slow, fast (i.e. y, x).
# We must therefore reverse it to work in conventional x, y order.
module_layout = object_layout(size[::-1], module_size, module_stride)

Check warning on line 201 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L201

Added line #L201 was not covered by tests
if tuple(module_layout) in known_module_layouts:
return True

Check warning on line 203 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L203

Added line #L203 was not covered by tests

return False

Check warning on line 205 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L205

Added line #L205 was not covered by tests

def __init__(self, image_file, **kwargs):

Check warning on line 207 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L207

Added line #L207 was not covered by tests
with h5py.File(image_file, "r") as handle:
image_size = handle["/entry/instrument/detector/module/data_size"][()]
self.module_layout = object_layout(

Check warning on line 210 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L209-L210

Added lines #L209 - L210 were not covered by tests
image_size[::-1], module_size, module_stride
)

super().__init__(image_file, **kwargs)

Check warning on line 214 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L214

Added line #L214 was not covered by tests

def _detector(self) -> Detector:

Check warning on line 216 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L216

Added line #L216 was not covered by tests
"""Ensure that the detector has appropriate masks for the non-sensitive area."""

detector = super()._detector()

Check warning on line 219 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L219

Added line #L219 was not covered by tests

masks = latrd_masks(self.module_layout, inflate=1)

Check warning on line 221 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L221

Added line #L221 was not covered by tests
# Reshape each mask from a pair of vectors to the four-number list required
# by dxtbx.model.Panel.add_mask()
masks = masks.reshape(masks.shape[0], 4).astype(np.int32)

Check warning on line 224 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L224

Added line #L224 was not covered by tests
# Add all the masks to the detector panel object.
np.apply_along_axis(lambda mask: detector[0].add_mask(*mask), 1, masks)

return detector

Check warning on line 228 in src/dxtbx/format/FormatNexusTimepix.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNexusTimepix.py#L228

Added line #L228 was not covered by tests
52 changes: 0 additions & 52 deletions src/dxtbx/format/FormatNexusTimepix2M.py

This file was deleted.