diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index fd1cca1ca..282082741 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -1,5 +1,5 @@ :Author: Jérôme Kieffer -:Date: 10/01/2024 +:Date: 12/01/2024 :Keywords: changelog Change-log of versions @@ -7,8 +7,9 @@ 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 +- Support pathlib for reading PONI files - Refactor `pyFAI-benchmark` tool (Thanks Edgar) - Possibility to define the detector orientation: + It is the position of the origin of the detector at any of the 4 corner of the image diff --git a/src/pyFAI/detectors/__init__.py b/src/pyFAI/detectors/__init__.py index 82ffbb362..ce968c0e4 100644 --- a/src/pyFAI/detectors/__init__.py +++ b/src/pyFAI/detectors/__init__.py @@ -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 (Jerome.Kieffer@ESRF.eu) # @@ -34,7 +34,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "21/03/2022" +__date__ = "12/01/2024" __status__ = "stable" diff --git a/src/pyFAI/detectors/_adsc.py b/src/pyFAI/detectors/_adsc.py index 96a09d323..78f3555a2 100644 --- a/src/pyFAI/detectors/_adsc.py +++ b/src/pyFAI/detectors/_adsc.py @@ -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 (Jerome.Kieffer@ESRF.eu) # @@ -37,7 +37,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "22/11/2023" +__date__ = "12/01/2024" __status__ = "production" from ._common import Detector, Orientation diff --git a/src/pyFAI/detectors/_common.py b/src/pyFAI/detectors/_common.py index dec5b4439..988360ec8 100644 --- a/src/pyFAI/detectors/_common.py +++ b/src/pyFAI/detectors/_common.py @@ -34,7 +34,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "12/12/2023" +__date__ = "12/01/2024" __status__ = "stable" import logging @@ -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""" @@ -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) @@ -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,:] @@ -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) @@ -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: @@ -1208,6 +1212,7 @@ class NexusDetector(Detector): "aliases", "IS_FLAT", "IS_CONTIGUOUS", + "CORNERS" "force_pixel", "_filename", "uniform_pixel") + Detector._UNMUTABLE_ATTRS + Detector._MUTABLE_ATTRS @@ -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: diff --git a/src/pyFAI/detectors/_hexagonal.py b/src/pyFAI/detectors/_hexagonal.py index 2b1c6f151..cc7a250a8 100644 --- a/src/pyFAI/detectors/_hexagonal.py +++ b/src/pyFAI/detectors/_hexagonal.py @@ -33,7 +33,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "21/11/2023" +__date__ = "12/01/2024" __status__ = "production" @@ -57,9 +57,10 @@ 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) @@ -67,7 +68,7 @@ def build_pixel_coordinates(shape, pitch=1): :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 diff --git a/src/pyFAI/io/ponifile.py b/src/pyFAI/io/ponifile.py index e63899293..180cff2be 100644 --- a/src/pyFAI/io/ponifile.py +++ b/src/pyFAI/io/ponifile.py @@ -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 @@ -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 @@ -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: @@ -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) @@ -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 diff --git a/src/pyFAI/test/test_detector.py b/src/pyFAI/test/test_detector.py index 2ca091b1b..f0a7ed787 100644 --- a/src/pyFAI/test/test_detector.py +++ b/src/pyFAI/test/test_detector.py @@ -33,7 +33,7 @@ __contact__ = "picca@synchrotron-soleil.fr" __license__ = "MIT+" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "12/12/2023" +__date__ = "12/01/2024" import os import shutil @@ -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 @@ -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