Skip to content

Commit

Permalink
Merge pull request #2032 from kif/1957_pixel_corners
Browse files Browse the repository at this point in the history
Register the number of pixel corners
  • Loading branch information
EdgarGF93 authored Jan 17, 2024
2 parents 685e67d + 63067a1 commit f1dbb2d
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 36 deletions.
3 changes: 2 additions & 1 deletion doc/source/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
:Author: Jérôme Kieffer
:Date: 10/01/2024
:Date: 12/01/2024
:Keywords: changelog

Change-log of versions
======================

2024.1 UNRELEASED
-----------------
- Expose the number of corners of a detector pixel
- Support XRDML formt (compatibility with MAUD software)
- Support pathlib for reading PONI files
- Refactor `pyFAI-benchmark` tool (Thanks Edgar)
Expand Down
4 changes: 2 additions & 2 deletions src/pyFAI/detectors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Project: Azimuthal integration
# https://github.com/silx-kit/pyFAI
#
# Copyright (C) 2014-2022 European Synchrotron Radiation Facility, Grenoble, France
# Copyright (C) 2014-2024 European Synchrotron Radiation Facility, Grenoble, France
#
# Principal author: Jérôme Kieffer ([email protected])
#
Expand Down Expand Up @@ -34,7 +34,7 @@
__contact__ = "[email protected]"
__license__ = "MIT"
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
__date__ = "21/03/2022"
__date__ = "12/01/2024"
__status__ = "stable"


Expand Down
4 changes: 2 additions & 2 deletions src/pyFAI/detectors/_adsc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Project: Fast Azimuthal integration
# https://github.com/silx-kit/pyFAI
#
# Copyright (C) 2017-2018 European Synchrotron Radiation Facility, Grenoble, France
# Copyright (C) 2017-2024 European Synchrotron Radiation Facility, Grenoble, France
#
# Principal author: Jérôme Kieffer ([email protected])
#
Expand Down Expand Up @@ -37,7 +37,7 @@
__contact__ = "[email protected]"
__license__ = "MIT"
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
__date__ = "22/11/2023"
__date__ = "12/01/2024"
__status__ = "production"

from ._common import Detector, Orientation
Expand Down
18 changes: 14 additions & 4 deletions src/pyFAI/detectors/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
__contact__ = "[email protected]"
__license__ = "MIT"
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
__date__ = "12/12/2023"
__date__ = "12/01/2024"
__status__ = "stable"

import logging
Expand Down Expand Up @@ -100,14 +100,15 @@ class Detector(metaclass=DetectorMeta):
Generic class representing a 2D detector
"""
MANUFACTURER = None

CORNERS = 4
force_pixel = False # Used to specify pixel size should be defined by the class itself.
aliases = [] # list of alternative names
registry = {} # list of detectors ...
uniform_pixel = True # tells all pixels have the same size
IS_FLAT = True # this detector is flat
IS_CONTIGUOUS = True # No gaps: all pixels are adjacents, speeds-up calculation
API_VERSION = "1.0"
API_VERSION = "1.1"
# 1.1: support for CORNER attribute

HAVE_TAPER = False
"""If true a spline file is mandatory to correct the geometry"""
Expand Down Expand Up @@ -756,6 +757,7 @@ def get_pixel_corners(self, correct_binning=False):
if self._pixel_corners is None:
with self._sem:
if self._pixel_corners is None:
assert self.CORNERS == 4, "overwrite this method when hexagonal !"
# r1, r2 = self._calc_pixel_index_from_orientation(False)
# like numpy.ogrid
# d1 = expand2d(r1, self.shape[1] + 1, False)
Expand Down Expand Up @@ -790,6 +792,7 @@ def _rebin_pixel_corners(self):
r1 = self._pixel_corners.shape[1] // self.shape[1]
if r0 == 0 or r1 == 0:
raise RuntimeError("Cannot unbin an image ")
assert self.CORNERS==4, "not valid with hexagonal pixels"
pixel_corners = numpy.zeros((self.shape[0], self.shape[1], 4, 3), dtype=numpy.float32)
pixel_corners[:,:, 0,:] = self._pixel_corners[::r0,::r1, 0,:]
pixel_corners[:,:, 1,:] = self._pixel_corners[r0 - 1::r0,::r1, 1,:]
Expand All @@ -815,7 +818,7 @@ def set_pixel_corners(self, ary):
# Validation for the array
assert ary.ndim == 4
assert ary.shape[3] == 3 # 3 coordinates in Z Y X
assert ary.shape[2] >= 3 # at least 3 corners per pixel
assert ary.shape[2] == self.CORNERS # at least 3 corners per pixel

z = ary[..., 0]
is_flat = (z.max() == z.min() == 0.0)
Expand Down Expand Up @@ -845,6 +848,7 @@ def save(self, filename):
det_grp["API_VERSION"] = numpy.string_(self.API_VERSION)
det_grp["IS_FLAT"] = self.IS_FLAT
det_grp["IS_CONTIGUOUS"] = self.IS_CONTIGUOUS
det_grp["CORNERS"] = self.CORNERS
if self.dummy is not None:
det_grp["dummy"] = self.dummy
if self.delta_dummy is not None:
Expand Down Expand Up @@ -1208,6 +1212,7 @@ class NexusDetector(Detector):
"aliases",
"IS_FLAT",
"IS_CONTIGUOUS",
"CORNERS"
"force_pixel",
"_filename",
"uniform_pixel") + Detector._UNMUTABLE_ATTRS + Detector._MUTABLE_ATTRS
Expand Down Expand Up @@ -1239,6 +1244,11 @@ def load(self, filename):
raise RuntimeError("No detector definition in this file %s" % filename)
name = posixpath.split(det_grp.name)[-1]
self.aliases = [name.replace("_", " "), det_grp.name]
if "API_VERSION" in det_grp:
self.API_VERSION = det_grp["API_VERSION"][()].decode()
api = [int(i) for i in self.API_VERSION.split(".")]
if api>=[1,1] and "CORNERS" in det_grp:
self.CORNERS = det_grp["CORNERS"][()]
if "IS_FLAT" in det_grp:
self.IS_FLAT = det_grp["IS_FLAT"][()]
if "IS_CONTIGUOUS" in det_grp:
Expand Down
9 changes: 5 additions & 4 deletions src/pyFAI/detectors/_hexagonal.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
__contact__ = "[email protected]"
__license__ = "MIT"
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
__date__ = "21/11/2023"
__date__ = "12/01/2024"
__status__ = "production"


Expand All @@ -57,17 +57,18 @@ class HexDetector(Detector):
uniform_pixel = False # ensures we use the array of position !
IS_CONTIGUOUS = False
IS_FLAT = True
CORNERS = 6

@staticmethod
def build_pixel_coordinates(shape, pitch=1):
@classmethod
def build_pixel_coordinates(cls, shape, pitch=1):
"""Build the 4D array with pixel coordinates for a detector composed of hexagonal-pixels
:param shape: 2-tuple with size of the detector in number of pixels (y, x)
:param pitch: the distance between two pixels
:return: array with pixel coordinates
"""
assert len(shape) == 2
ary = numpy.zeros(shape+(6, 3), dtype=numpy.float32)
ary = numpy.zeros(shape + (cls.CORNERS, 3), dtype=numpy.float32)
sqrt3 = sqrt(3.0)
h = 0.5*sqrt3
r = numpy.linspace(0, 2, 7, endpoint=True)[:-1] - 0.5
Expand Down
68 changes: 49 additions & 19 deletions src/pyFAI/io/ponifile.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
__author__ = "Jérôme Kieffer"
__license__ = "MIT"
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
__date__ = "09/01/2024"
__date__ = "15/01/2024"
__docformat__ = 'restructuredtext'

import collections
Expand All @@ -49,6 +49,7 @@


class PoniFile(object):
API_VERSION = 2.1 # valid version are 1, 2, 2.1

def __init__(self, data=None):
self._detector = None
Expand Down Expand Up @@ -104,10 +105,24 @@ def read_from_dict(self, config):
"""Initialize this object using a dictionary.
.. note:: The dictionary is versionned.
Version:
* 1: Historical version (i.e. unversioned)
* 2: store detector and detector_config instead of pixelsize1, pixelsize2 and splinefile
* 2.1: manage orientation of detector in detector_config
"""
version = int(config.get("poni_version", 1))
version = float(config.get("poni_version", 1))
if "detector_config" in config:
version = max(version, 2)
if "orientation" in config["detector_config"]:
version = max(version, 2.1)
else:
version = max(version, 2)
if version >= 2 and "detector_config" not in config:
_logger.error("PoniFile claim to be version 2 but contains no `detector_config` !!!")

if version == 2.1 and "orientation" not in config.get("detector_config", {}):
_logger.error("PoniFile claim to be version 2.1 but contains no detector orientation !!!")
self.API_VERSION = version

if version == 1:
# Handle former version of PONI-file
if "detector" in config:
Expand All @@ -134,7 +149,7 @@ def read_from_dict(self, config):
if config["splinefile"].lower() != "none":
self._detector.set_splineFile(config["splinefile"])

elif version == 2:
elif version in (2, 2.1):
detector_name = config["detector"]
detector_config = config["detector_config"]
self._detector = detectors.detector_factory(detector_name, detector_config)
Expand Down Expand Up @@ -195,26 +210,41 @@ def read_from_geometryModel(self, model: GeometryModel, detector=None):

def write(self, fd):
"""Write this object to an open stream.
:param fd: file descriptor (opened file)
:return: nothing
"""
fd.write(("# Nota: C-Order, 1 refers to the Y axis,"
" 2 to the X axis \n"))
fd.write("# Calibration done at %s\n" % time.ctime())
fd.write("poni_version: 2\n")
detector = self.detector
fd.write("Detector: %s\n" % detector.__class__.__name__)
fd.write("Detector_config: %s\n" % json.dumps(detector.get_config()))

fd.write("Distance: %s\n" % self._dist)
fd.write("Poni1: %s\n" % self._poni1)
fd.write("Poni2: %s\n" % self._poni2)
fd.write("Rot1: %s\n" % self._rot1)
fd.write("Rot2: %s\n" % self._rot2)
fd.write("Rot3: %s\n" % self._rot3)
txt = ["# Nota: C-Order, 1 refers to the Y axis, 2 to the X axis",
f"# Calibration done at {time.ctime()}",
f"poni_version: {self.API_VERSION}",
f"Detector: {detector.__class__.__name__}"]
if self.API_VERSION == 1:
if not detector.force_pixel:
txt += [f"pixelsize1: {detector.pixel1}",
f"pixelsize2: {detector.pixel2}"]
if detector.splineFile:
txt.append(f"splinefile: {detector.splineFile}")
elif self.API_VERSION >= 2:
detector_config = detector.get_config()
if self.API_VERSION == 2:
detector_config.pop("orientation")
txt.append(f"Detector_config: {json.dumps(detector_config)}")

txt += [f"Distance: {self._dist}",
f"Poni1: {self._poni1}",
f"Poni2: {self._poni2}",
f"Rot1: {self._rot1}",
f"Rot2: {self._rot2}",
f"Rot3: {self._rot3}"
]
if self._wavelength is not None:
fd.write("Wavelength: %s\n" % self._wavelength)
txt.append(f"Wavelength: {self._wavelength}")
txt.append("")
fd.write("\n".join(txt))

def as_dict(self):
config = collections.OrderedDict([("poni_version", 2)])
config = collections.OrderedDict([("poni_version", self.API_VERSION)])
config["detector"] = self.detector.__class__.__name__
config["detector_config"] = self.detector.get_config()
config["dist"] = self._dist
Expand Down
10 changes: 6 additions & 4 deletions src/pyFAI/test/test_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
__contact__ = "[email protected]"
__license__ = "MIT+"
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
__date__ = "12/12/2023"
__date__ = "12/01/2024"

import os
import shutil
Expand Down Expand Up @@ -210,12 +210,12 @@ def test_nexus_detector(self):
if err2 > 1e-6:
logger.error("%s precision on pixel position 1 is better than 1µm, got %e", det_name, err2)

self.assertTrue(err1 < 1e-6, "%s precision on pixel position 1 is better than 1µm, got %e" % (det_name, err1))
self.assertTrue(err2 < 1e-6, "%s precision on pixel position 2 is better than 1µm, got %e" % (det_name, err2))
self.assertLess(err1, 1e-6, f"{det_name} precision on pixel position 1 is better than 1µm, got {err1:e}")
self.assertLess(err2, 1e-6, f"{det_name} precision on pixel position 2 is better than 1µm, got {err1:e}")
if not det.IS_FLAT:
err = abs(r[2] - o[2]).max()
self.assertTrue(err < 1e-6, "%s precision on pixel position 3 is better than 1µm, got %e" % (det_name, err))

self.assertEqual(det.CORNERS, new_det.CORNERS, "Number of pixel corner is consistent")
# check Pilatus with displacement maps
# check spline
# check SPD displacement
Expand Down Expand Up @@ -353,6 +353,8 @@ def test_displacements(self):

def test_hexagonal_detector(self):
pix = detector_factory("Pixirad1")
self.assertEqual(pix.CORNERS, 6, "detector has 6 corners")

wl = 1e-10
from ..calibrant import ALL_CALIBRANTS
from ..azimuthalIntegrator import AzimuthalIntegrator
Expand Down

0 comments on commit f1dbb2d

Please sign in to comment.