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

Register the number of pixel corners #2032

Merged
merged 4 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions doc/source/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
: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
- 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
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
Loading