From 4a4aa9bc6de0720b23df716cb1b24c2a7d54326a Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Fri, 1 Dec 2023 13:34:00 +0100 Subject: [PATCH 01/15] fix azimuthal angle when image flipped --- src/pyFAI/ext/_geometry.pyx | 10 ++++++++-- src/pyFAI/geometry/core.py | 20 +++++++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/pyFAI/ext/_geometry.pyx b/src/pyFAI/ext/_geometry.pyx index a2a908526..194339503 100644 --- a/src/pyFAI/ext/_geometry.pyx +++ b/src/pyFAI/ext/_geometry.pyx @@ -37,7 +37,7 @@ coordinates. __author__ = "Jerome Kieffer" __license__ = "MIT" -__date__ = "09/03/2023" +__date__ = "01/12/2023" __copyright__ = "2011-2020, ESRF" __contact__ = "jerome.kieffer@esrf.fr" @@ -574,7 +574,8 @@ def calc_rad_azim(double L, pos3=None, space="2th", wavelength=None, - bint chi_discontinuity_at_pi=True + bint chi_discontinuity_at_pi=True, + bint flip_direction=False, ): """Calculate the radial & azimutal position for each pixel from pos1, pos2, pos3. @@ -589,6 +590,7 @@ def calc_rad_azim(double L, :param pos3: numpy array with distances in meter along Sample->PONI (Z), positive behind the detector :param space: can be "2th", "q" or "r" for radial units. Azimuthal units are radians :param chi_discontinuity_at_pi: set to False to obtain chi in the range [0, 2pi[ instead of [-pi, pi[ + :param flip_direction: set True when orientation in 2,4 :return: ndarray of double with same shape and size as pos1 + (2,), :raise: KeyError when space is bad ! ValueError when wavelength is missing @@ -635,6 +637,8 @@ def calc_rad_azim(double L, elif cspace == 3: out[i, 0] = sqrt(t1 * t1 + t2 * t2) chi = atan2(t1, t2) + if flip_direction: + chi = -chi if chi_discontinuity_at_pi: out[i, 1] = chi else: @@ -653,6 +657,8 @@ def calc_rad_azim(double L, elif cspace == 3: out[i, 0] = sqrt(t1 * t1 + t2 * t2) chi = atan2(t1, t2) + if flip_direction: + chi = -chi if chi_discontinuity_at_pi: out[i, 1] = chi else: diff --git a/src/pyFAI/geometry/core.py b/src/pyFAI/geometry/core.py index 091bf1b70..6ddc9bdef 100644 --- a/src/pyFAI/geometry/core.py +++ b/src/pyFAI/geometry/core.py @@ -40,7 +40,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "23/11/2023" +__date__ = "01/12/2023" __status__ = "production" __docformat__ = 'restructuredtext' @@ -616,6 +616,8 @@ def chi(self, d1, d2, path="cython"): else: _, t1, t2 = self.calc_pos_zyx(d0=None, d1=d1, d2=d2, corners=False, use_cython=True, do_parallax=True) chi = numpy.arctan2(t1, t2) + if self.detector.orientation in (2,4): + chi = -chi return chi def chi_corner(self, d1, d2): @@ -764,7 +766,9 @@ def corner_array(self, shape=None, unit=None, use_cython=True, scale=True): self.rot1, self.rot2, self.rot3, p1, p2, p3, space, self._wavelength, - chi_discontinuity_at_pi=self.chiDiscAtPi) + chi_discontinuity_at_pi=self.chiDiscAtPi, + flip_direction=self.detector.orientation in (2,4)) + #TODO: change orientation if nedded except KeyError: logger.warning("No fast path for space: %s", space) except AttributeError as err: @@ -794,12 +798,20 @@ def corner_array(self, shape=None, unit=None, use_cython=True, scale=True): if numexpr is None: # numpy path chi = numpy.arctan2(y, x) + if self.detector.orientation in (2, 4): + numpy.negative(chi, out=chi) if not self.chiDiscAtPi: numpy.mod(chi, (2.0 * numpy.pi), out=chi) else: # numexpr path twoPi = 2.0 * numpy.pi - chi = numexpr.evaluate("arctan2(y, x)") if self.chiDiscAtPi else numexpr.evaluate("arctan2(y, x)%twoPi") + if self.detector.orientation in (2, 4): + formula = "-arctan2(y, x)" + else: + formula = "arctan2(y, x)" + if self.chiDiscAtPi: + formula += "%twoPi" + chi = numexpr.evaluate(formula) corners = numpy.zeros((shape[0], shape[1], nb_corners, 2), dtype=numpy.float32) if chi.shape[:2] == shape: @@ -911,6 +923,8 @@ def center_array(self, shape=None, unit="2th_deg", scale=True): y = pos[..., 1] z = pos[..., 0] ary = unit.equation(x, y, z, self.wavelength) + if self.detector.orientation in (2,4): + numpy.negative(ary, out=ary) if unit.space == "chi" and not self.chiDiscAtPi: numpy.mod(ary, 2.0 * numpy.pi, out=ary) self._cached_array[key] = ary From 8226ffff9465b55a514319e142ee2cc5b0c726b2 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Fri, 1 Dec 2023 13:35:11 +0100 Subject: [PATCH 02/15] minor improvements --- bootstrap.py | 1 + build-deb.sh | 2 +- run_tests.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bootstrap.py b/bootstrap.py index 4f865e789..6ee8bd6a4 100755 --- a/bootstrap.py +++ b/bootstrap.py @@ -225,6 +225,7 @@ def find_executable(target): else: logger.error("Script %s not found", script) else: + logging.info("Running IPython by default") logger.info("Patch the sys.argv: %s", sys.argv) sys.path.insert(2, "") try: diff --git a/build-deb.sh b/build-deb.sh index 28f10ae4a..1a7430207 100755 --- a/build-deb.sh +++ b/build-deb.sh @@ -42,7 +42,7 @@ then if [ -z $debian_version ] then #we are probably on a ubuntu platform - debian_version=$(cat /etc/debian_version | cut -d/ -f1) + debian_version=$(< /etc/debian_version cut -d/ -f1) case $debian_version in squeeze) debian_version=6 diff --git a/run_tests.py b/run_tests.py index 46d3877b0..74e79a2d0 100755 --- a/run_tests.py +++ b/run_tests.py @@ -276,6 +276,7 @@ def report_rst(cov, package, version="0.0.0", base="", inject_xml=None): else: fn = inject_xml from lxml import etree + xml = etree.parse(fn) classes = xml.xpath("//class") From 952e1bb4c1d58c0f599bb0331981e9a2d3971069 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Fri, 1 Dec 2023 14:16:29 +0100 Subject: [PATCH 03/15] WIP --- src/pyFAI/geometry/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyFAI/geometry/core.py b/src/pyFAI/geometry/core.py index 6ddc9bdef..62d712adc 100644 --- a/src/pyFAI/geometry/core.py +++ b/src/pyFAI/geometry/core.py @@ -753,8 +753,9 @@ def corner_array(self, shape=None, unit=None, use_cython=True, scale=True): corners = None if (_geometry is not None) and use_cython: if self.detector.IS_CONTIGUOUS: - d1 = utils.expand2d(numpy.arange(shape[0] + 1.0), shape[1] + 1.0, False) - d2 = utils.expand2d(numpy.arange(shape[1] + 1.0), shape[0] + 1.0, True) + r1, r2 = self.detector._calc_pixel_index_from_orientation(True) + d1 = utils.expand2d(r1, shape[1]+1, False) + d2 = utils.expand2d(r2, shape[0]+1, True) p1, p2, p3 = self.detector.calc_cartesian_positions(d1, d2, center=False, use_cython=True) else: det_corners = self.detector.get_pixel_corners() @@ -768,7 +769,6 @@ def corner_array(self, shape=None, unit=None, use_cython=True, scale=True): space, self._wavelength, chi_discontinuity_at_pi=self.chiDiscAtPi, flip_direction=self.detector.orientation in (2,4)) - #TODO: change orientation if nedded except KeyError: logger.warning("No fast path for space: %s", space) except AttributeError as err: From 1728d6800d4322d827b35e7f187088c8964359e6 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Fri, 1 Dec 2023 14:31:45 +0100 Subject: [PATCH 04/15] Typo, stupid but important bug --- src/pyFAI/io/ponifile.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pyFAI/io/ponifile.py b/src/pyFAI/io/ponifile.py index 037ca6bb8..dcf674910 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__ = "22/11/2023" +__date__ = "01/12/2023" __docformat__ = 'restructuredtext' import collections @@ -107,8 +107,7 @@ def read_from_dict(self, config): """ version = int(config.get("poni_version", 1)) if "detector_config" in config: - version = min(version, 2) - + version = max(version, 2) if version == 1: # Handle former version of PONI-file if "detector" in config: From b77d824ce1c8df1cbbef86403f458adc9b58fc0c Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Fri, 1 Dec 2023 14:51:20 +0100 Subject: [PATCH 05/15] some tests --- src/pyFAI/test/test_geometry.py | 55 +++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/pyFAI/test/test_geometry.py b/src/pyFAI/test/test_geometry.py index a4a297c93..cd76e645d 100644 --- a/src/pyFAI/test/test_geometry.py +++ b/src/pyFAI/test/test_geometry.py @@ -4,7 +4,7 @@ # Project: Azimuthal integration # https://github.com/silx-kit/pyFAI # -# Copyright (C) 2015-2018 European Synchrotron Radiation Facility, Grenoble, France +# Copyright (C) 2015-2023 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__ = "23/11/2023" +__date__ = "01/12/2023" import unittest import random @@ -549,7 +549,55 @@ def test_regression(self): delta = abs(rp - rc).max() self.assertLess(delta, 1e-5, "error on position is %s" % delta) - +class TestOrientation(unittest.TestCase): + """Simple tests to validate the orientation of the detector""" + @classmethod + def setUpClass(cls)->None: + super(TestOrientation, cls).setUpClass() + cls.ai1 = geometry.Geometry.sload({"detector":"pilatus100k", "detector_config":{"orientation":1}, "wavelength":1e-10}) + cls.ai2 = geometry.Geometry.sload({"detector":"pilatus100k", "detector_config":{"orientation":2}, "wavelength":1e-10}) + cls.ai3 = geometry.Geometry.sload({"detector":"pilatus100k", "detector_config":{"orientation":3}, "wavelength":1e-10}) + cls.ai4 = geometry.Geometry.sload({"detector":"pilatus100k", "detector_config":{"orientation":4}, "wavelength":1e-10}) + print(cls.ai1) + print(cls.ai2) + print(cls.ai3) + print(cls.ai4) + @classmethod + def tearDownClass(cls)->None: + super(TestOrientation, cls).tearDownClass() + cls.ai1 = cls.ai2 = cls.ai3 = cls.ai3 = None + + def test_array_from_unit_tth(self): + r1 = self.ai1.array_from_unit(unit="2th_deg") + r2 = self.ai2.array_from_unit(unit="2th_deg") + r3 = self.ai3.array_from_unit(unit="2th_deg") + r4 = self.ai4.array_from_unit(unit="2th_deg") + + self.assertFalse(numpy.allclose(r1, r2), "orientation 1,2 differ tth") + self.assertFalse(numpy.allclose(r1, r3), "orientation 1,3 differ tth") + self.assertFalse(numpy.allclose(r1, r4), "orientation 1,4 differ tth") + + self.assertTrue(numpy.allclose(r1, numpy.fliplr(r2)), "orientation 1,2 flipped match tth") + self.assertTrue(numpy.allclose(r1, numpy.flipud(r4)), "orientation 1,4 flipped match tth") + self.assertTrue(numpy.allclose(r2, numpy.flipud(r3)), "orientation 2,3 flipped match tth") + self.assertTrue(numpy.allclose(r1, r3[-1::-1,-1::-1]), "orientation 1,3 inversion match tth") + self.assertTrue(numpy.allclose(r2, r4[-1::-1,-1::-1]), "orientation 2,4 inversion match tth") + + def test_array_from_unit_chi(self): + r1 = self.ai1.array_from_unit(unit="chi_deg") + r2 = self.ai2.array_from_unit(unit="chi_deg") + r3 = self.ai3.array_from_unit(unit="chi_deg") + r4 = self.ai4.array_from_unit(unit="chi_deg") + + self.assertFalse(numpy.allclose(r1, r2), "orientation 1,2 differ chi") + self.assertFalse(numpy.allclose(r1, r3), "orientation 1,3 differ chi") + self.assertFalse(numpy.allclose(r1, r4), "orientation 1,4 differ chi") + + self.assertTrue(numpy.allclose(r1, -numpy.fliplr(r2)), "orientation 1,2 flipped match chi") + self.assertTrue(numpy.allclose(r1, -numpy.flipud(r4)), "orientation 1,4 flipped match chi") + self.assertTrue(numpy.allclose(r2, -numpy.flipud(r3)), "orientation 2,3 flipped match chi") + self.assertTrue(numpy.allclose(r1, r3[-1::-1,-1::-1]), "orientation 1,3 inversion match chi") + self.assertTrue(numpy.allclose(r2, r4[-1::-1,-1::-1]), "orientation 2,4 inversion match chi") def suite(): @@ -562,6 +610,7 @@ def suite(): testsuite.addTest(loader(TestCalcFrom)) testsuite.addTest(loader(TestGeometry)) testsuite.addTest(loader(TestFastPath)) + testsuite.addTest(loader(TestOrientation)) return testsuite From dd1adddb9e79cfea979b658fc44e25836787d88b Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Fri, 1 Dec 2023 17:50:36 +0100 Subject: [PATCH 06/15] Test to be validated ... but does not work yet. --- src/pyFAI/_version.py | 7 ++++--- src/pyFAI/geometry/core.py | 16 ++++++++++---- src/pyFAI/test/test_geometry.py | 37 +++++++++++++++++++++++++++++++-- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/pyFAI/_version.py b/src/pyFAI/_version.py index 7b09077b6..447957864 100755 --- a/src/pyFAI/_version.py +++ b/src/pyFAI/_version.py @@ -47,7 +47,7 @@ __authors__ = ["Jérôme Kieffer", "V. Valls"] __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "21/11/2023" +__date__ = "01/12/2023" __status__ = "production" __docformat__ = 'restructuredtext' __all__ = ["date", "version_info", "strictversion", "hexversion", "debianversion", @@ -61,7 +61,7 @@ "final": 15} MAJOR = 2023 -MINOR = 11 +MINOR = 12 MICRO = 0 RELEV = "dev" # <16 SERIAL = 0 # <16 @@ -69,7 +69,6 @@ date = __date__ from collections import namedtuple -from argparse import ArgumentParser _version_info = namedtuple("version_info", ["major", "minor", "micro", "releaselevel", "serial"]) @@ -130,6 +129,8 @@ def calc_hexversion(major=0, minor=0, micro=0, releaselevel="dev", serial=0, str citation = "doi:10.1107/S1600576715004306" if __name__ == "__main__": + from argparse import ArgumentParser + parser = ArgumentParser(usage="print the version of the software") parser.add_argument("--wheel", action="store_true", dest="wheel", default=None, help="print version formated for wheel") diff --git a/src/pyFAI/geometry/core.py b/src/pyFAI/geometry/core.py index 62d712adc..fcf735c82 100644 --- a/src/pyFAI/geometry/core.py +++ b/src/pyFAI/geometry/core.py @@ -617,7 +617,7 @@ def chi(self, d1, d2, path="cython"): _, t1, t2 = self.calc_pos_zyx(d0=None, d1=d1, d2=d2, corners=False, use_cython=True, do_parallax=True) chi = numpy.arctan2(t1, t2) if self.detector.orientation in (2,4): - chi = -chi + numpy.negative(chi, out=chi) return chi def chi_corner(self, d1, d2): @@ -754,8 +754,16 @@ def corner_array(self, shape=None, unit=None, use_cython=True, scale=True): if (_geometry is not None) and use_cython: if self.detector.IS_CONTIGUOUS: r1, r2 = self.detector._calc_pixel_index_from_orientation(True) - d1 = utils.expand2d(r1, shape[1]+1, False) - d2 = utils.expand2d(r2, shape[0]+1, True) + d1 = utils.expand2d(r1, shape[1] + 1, False) + d2 = utils.expand2d(r2, shape[0] + 1, True) + p1, p2, p3 = self.detector.calc_cartesian_positions(d1, d2, center=False) + + + #r1, r2 = self.detector._calc_pixel_index_from_orientation(True) + #d1 = utils.expand2d(r1, shape[1]+1, False) + #d2 = utils.expand2d(r2, shape[0]+1, True) + d1 = utils.expand2d(numpy.arange(shape[0] + 1.0), shape[1] + 1.0, False) + d2 = utils.expand2d(numpy.arange(shape[1] + 1.0), shape[0] + 1.0, True) p1, p2, p3 = self.detector.calc_cartesian_positions(d1, d2, center=False, use_cython=True) else: det_corners = self.detector.get_pixel_corners() @@ -774,7 +782,7 @@ def corner_array(self, shape=None, unit=None, use_cython=True, scale=True): except AttributeError as err: logger.warning("AttributeError: The binary extension _geomety may be missing: %s", err) else: - if self.detector.IS_CONTIGUOUS: + if 0:#self.detector.IS_CONTIGUOUS: if bilinear: # convert_corner_2D_to_4D needs contiguous arrays as input radi = numpy.ascontiguousarray(res[..., 0], numpy.float32) diff --git a/src/pyFAI/test/test_geometry.py b/src/pyFAI/test/test_geometry.py index cd76e645d..abbc91db9 100644 --- a/src/pyFAI/test/test_geometry.py +++ b/src/pyFAI/test/test_geometry.py @@ -567,7 +567,7 @@ def tearDownClass(cls)->None: super(TestOrientation, cls).tearDownClass() cls.ai1 = cls.ai2 = cls.ai3 = cls.ai3 = None - def test_array_from_unit_tth(self): + def test_array_from_unit_tth_center(self): r1 = self.ai1.array_from_unit(unit="2th_deg") r2 = self.ai2.array_from_unit(unit="2th_deg") r3 = self.ai3.array_from_unit(unit="2th_deg") @@ -583,7 +583,7 @@ def test_array_from_unit_tth(self): self.assertTrue(numpy.allclose(r1, r3[-1::-1,-1::-1]), "orientation 1,3 inversion match tth") self.assertTrue(numpy.allclose(r2, r4[-1::-1,-1::-1]), "orientation 2,4 inversion match tth") - def test_array_from_unit_chi(self): + def test_array_from_unit_chi_center(self): r1 = self.ai1.array_from_unit(unit="chi_deg") r2 = self.ai2.array_from_unit(unit="chi_deg") r3 = self.ai3.array_from_unit(unit="chi_deg") @@ -599,6 +599,39 @@ def test_array_from_unit_chi(self): self.assertTrue(numpy.allclose(r1, r3[-1::-1,-1::-1]), "orientation 1,3 inversion match chi") self.assertTrue(numpy.allclose(r2, r4[-1::-1,-1::-1]), "orientation 2,4 inversion match chi") + def test_array_from_unit_tth_corner(self): + r1 = self.ai1.array_from_unit(unit="2th_deg", typ="corner") + r2 = self.ai2.array_from_unit(unit="2th_deg", typ="corner") + r3 = self.ai3.array_from_unit(unit="2th_deg", typ="corner") + r4 = self.ai4.array_from_unit(unit="2th_deg", typ="corner") + + tth1 = r1[...,0].mean(axis=-1) + chi1 = r1[...,1].mean(axis=-1) + tth2 = r2[...,0].mean(axis=-1) + chi2 = r2[...,1].mean(axis=-1) + tth3 = r3[...,0].mean(axis=-1) + chi3 = r3[...,1].mean(axis=-1) + tth4 = r4[...,0].mean(axis=-1) + chi4 = r4[...,1].mean(axis=-1) + + self.assertFalse(numpy.allclose(tth1, tth2), "orientation 1,2 differ tth") + self.assertFalse(numpy.allclose(chi1, chi2), "orientation 1,2 differ chi") + self.assertFalse(numpy.allclose(tth1, tth3), "orientation 1,3 differ tth") + self.assertFalse(numpy.allclose(chi1, chi3), "orientation 1,3 differ chi") + self.assertFalse(numpy.allclose(tth1, tth4), "orientation 1,4 differ tth") + self.assertFalse(numpy.allclose(chi1, chi4), "orientation 1,4 differ chi") + + self.assertTrue(numpy.allclose(tth1, numpy.fliplr(tth2)), "orientation 1,2 flipped match tth") + self.assertTrue(numpy.allclose(chi1, -numpy.fliplr(chi2)), "orientation 1,2 flipped match chi") + self.assertTrue(numpy.allclose(tth1, numpy.flipud(tth4)), "orientation 1,4 flipped match tth") + self.assertTrue(numpy.allclose(chi1, -numpy.flipud(chi4)), "orientation 1,4 flipped match chi") + self.assertTrue(numpy.allclose(tth2, numpy.flipud(tth3)), "orientation 2,3 flipped match tth") + self.assertTrue(numpy.allclose(chi2, -numpy.flipud(chi3)), "orientation 2,3 flipped match chi") + self.assertTrue(numpy.allclose(tth1, tth3[-1::-1,-1::-1]), "orientation 1,3 inversion match tth") + self.assertTrue(numpy.allclose(chi1, chi3[-1::-1,-1::-1]), "orientation 1,3 inversion match chi") + self.assertTrue(numpy.allclose(tth2, tth4[-1::-1,-1::-1]), "orientation 2,4 inversion match tth") + self.assertTrue(numpy.allclose(chi2, chi4[-1::-1,-1::-1]), "orientation 2,4 inversion match chi") + def suite(): loader = unittest.defaultTestLoader.loadTestsFromTestCase From a3d90c8823d9482b1609b6db39856e5294053739 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Mon, 4 Dec 2023 08:34:07 +0100 Subject: [PATCH 07/15] Undo the modification on the azimuthal angle Bad path, one should better invert x/y where needed --- src/pyFAI/ext/_geometry.pyx | 8 +------- src/pyFAI/geometry/core.py | 28 +++------------------------- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/src/pyFAI/ext/_geometry.pyx b/src/pyFAI/ext/_geometry.pyx index 194339503..6a182c2ae 100644 --- a/src/pyFAI/ext/_geometry.pyx +++ b/src/pyFAI/ext/_geometry.pyx @@ -574,8 +574,7 @@ def calc_rad_azim(double L, pos3=None, space="2th", wavelength=None, - bint chi_discontinuity_at_pi=True, - bint flip_direction=False, + bint chi_discontinuity_at_pi=True ): """Calculate the radial & azimutal position for each pixel from pos1, pos2, pos3. @@ -590,7 +589,6 @@ def calc_rad_azim(double L, :param pos3: numpy array with distances in meter along Sample->PONI (Z), positive behind the detector :param space: can be "2th", "q" or "r" for radial units. Azimuthal units are radians :param chi_discontinuity_at_pi: set to False to obtain chi in the range [0, 2pi[ instead of [-pi, pi[ - :param flip_direction: set True when orientation in 2,4 :return: ndarray of double with same shape and size as pos1 + (2,), :raise: KeyError when space is bad ! ValueError when wavelength is missing @@ -637,8 +635,6 @@ def calc_rad_azim(double L, elif cspace == 3: out[i, 0] = sqrt(t1 * t1 + t2 * t2) chi = atan2(t1, t2) - if flip_direction: - chi = -chi if chi_discontinuity_at_pi: out[i, 1] = chi else: @@ -657,8 +653,6 @@ def calc_rad_azim(double L, elif cspace == 3: out[i, 0] = sqrt(t1 * t1 + t2 * t2) chi = atan2(t1, t2) - if flip_direction: - chi = -chi if chi_discontinuity_at_pi: out[i, 1] = chi else: diff --git a/src/pyFAI/geometry/core.py b/src/pyFAI/geometry/core.py index fcf735c82..19eaaff42 100644 --- a/src/pyFAI/geometry/core.py +++ b/src/pyFAI/geometry/core.py @@ -616,8 +616,6 @@ def chi(self, d1, d2, path="cython"): else: _, t1, t2 = self.calc_pos_zyx(d0=None, d1=d1, d2=d2, corners=False, use_cython=True, do_parallax=True) chi = numpy.arctan2(t1, t2) - if self.detector.orientation in (2,4): - numpy.negative(chi, out=chi) return chi def chi_corner(self, d1, d2): @@ -753,15 +751,6 @@ def corner_array(self, shape=None, unit=None, use_cython=True, scale=True): corners = None if (_geometry is not None) and use_cython: if self.detector.IS_CONTIGUOUS: - r1, r2 = self.detector._calc_pixel_index_from_orientation(True) - d1 = utils.expand2d(r1, shape[1] + 1, False) - d2 = utils.expand2d(r2, shape[0] + 1, True) - p1, p2, p3 = self.detector.calc_cartesian_positions(d1, d2, center=False) - - - #r1, r2 = self.detector._calc_pixel_index_from_orientation(True) - #d1 = utils.expand2d(r1, shape[1]+1, False) - #d2 = utils.expand2d(r2, shape[0]+1, True) d1 = utils.expand2d(numpy.arange(shape[0] + 1.0), shape[1] + 1.0, False) d2 = utils.expand2d(numpy.arange(shape[1] + 1.0), shape[0] + 1.0, True) p1, p2, p3 = self.detector.calc_cartesian_positions(d1, d2, center=False, use_cython=True) @@ -775,14 +764,13 @@ def corner_array(self, shape=None, unit=None, use_cython=True, scale=True): self.rot1, self.rot2, self.rot3, p1, p2, p3, space, self._wavelength, - chi_discontinuity_at_pi=self.chiDiscAtPi, - flip_direction=self.detector.orientation in (2,4)) + chi_discontinuity_at_pi=self.chiDiscAtPi) except KeyError: logger.warning("No fast path for space: %s", space) except AttributeError as err: logger.warning("AttributeError: The binary extension _geomety may be missing: %s", err) else: - if 0:#self.detector.IS_CONTIGUOUS: + if self.detector.IS_CONTIGUOUS: if bilinear: # convert_corner_2D_to_4D needs contiguous arrays as input radi = numpy.ascontiguousarray(res[..., 0], numpy.float32) @@ -806,20 +794,12 @@ def corner_array(self, shape=None, unit=None, use_cython=True, scale=True): if numexpr is None: # numpy path chi = numpy.arctan2(y, x) - if self.detector.orientation in (2, 4): - numpy.negative(chi, out=chi) if not self.chiDiscAtPi: numpy.mod(chi, (2.0 * numpy.pi), out=chi) else: # numexpr path twoPi = 2.0 * numpy.pi - if self.detector.orientation in (2, 4): - formula = "-arctan2(y, x)" - else: - formula = "arctan2(y, x)" - if self.chiDiscAtPi: - formula += "%twoPi" - chi = numexpr.evaluate(formula) + chi = numexpr.evaluate("arctan2(y, x)") if self.chiDiscAtPi else numexpr.evaluate("arctan2(y, x)%twoPi") corners = numpy.zeros((shape[0], shape[1], nb_corners, 2), dtype=numpy.float32) if chi.shape[:2] == shape: @@ -931,8 +911,6 @@ def center_array(self, shape=None, unit="2th_deg", scale=True): y = pos[..., 1] z = pos[..., 0] ary = unit.equation(x, y, z, self.wavelength) - if self.detector.orientation in (2,4): - numpy.negative(ary, out=ary) if unit.space == "chi" and not self.chiDiscAtPi: numpy.mod(ary, 2.0 * numpy.pi, out=ary) self._cached_array[key] = ary From df705bd8094bb53520ba7becd2a836bb0241de62 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Wed, 6 Dec 2023 15:17:08 +0100 Subject: [PATCH 08/15] Switch sign of x/y depending on orientation --- src/pyFAI/detectors/orientation.py | 17 +++--- src/pyFAI/ext/_geometry.pyx | 87 +++++++++++++++++++----------- src/pyFAI/geometry/core.py | 11 +++- src/pyFAI/test/test_geometry.py | 15 +++--- 4 files changed, 86 insertions(+), 44 deletions(-) diff --git a/src/pyFAI/detectors/orientation.py b/src/pyFAI/detectors/orientation.py index e28805289..c17cceba7 100644 --- a/src/pyFAI/detectors/orientation.py +++ b/src/pyFAI/detectors/orientation.py @@ -38,20 +38,25 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "21/11/2023" +__date__ = "04/12/2023" __status__ = "stable" from enum import IntEnum class Orientation(IntEnum): - """Names inspired from + """Names come from the position of the origin when looking at the sample from behind the camera. + + When looking from the sample to the detector, the right & left are swapped + + Some names (index 5-8) are inspired from https://learn.microsoft.com/en-us/uwp/api/windows.storage.fileproperties.photoorientation + but unsupported """ Unspecified = 0 - Normal = 1 - FlipHorizontal = 2 - Rotate180 = 3 - FlipVertical = 4 + TopLeft = 1 + TopRight = 2 + BottomRight = 3 + BottomLeft = 4 Transpose = 5 Rotate270 = 6 Transverse = 7 diff --git a/src/pyFAI/ext/_geometry.pyx b/src/pyFAI/ext/_geometry.pyx index 6a182c2ae..8bef2bf23 100644 --- a/src/pyFAI/ext/_geometry.pyx +++ b/src/pyFAI/ext/_geometry.pyx @@ -37,7 +37,7 @@ coordinates. __author__ = "Jerome Kieffer" __license__ = "MIT" -__date__ = "01/12/2023" +__date__ = "04/12/2023" __copyright__ = "2011-2020, ESRF" __contact__ = "jerome.kieffer@esrf.fr" @@ -68,16 +68,19 @@ except Exception: cdef inline double f_t1(double p1, double p2, double p3, double sinRot1, double cosRot1, double sinRot2, double cosRot2, - double sinRot3, double cosRot3) noexcept nogil: - """Calculate t2 (aka y) for 1 pixel + double sinRot3, double cosRot3, + int orientation=0) noexcept nogil: + """Calculate t1 (aka y) for 1 pixel :param p1:distances in meter along dim1 from PONI :param p2: distances in meter along dim2 from PONI :param p3: distance sample - PONI :param sinRot1,sinRot2,sinRot3: sine of the angles :param cosRot1,cosRot2,cosRot3: cosine of the angles + :param orientation: value 1-4 """ - return (p1 * cosRot2 * cosRot3 + + cdef double orient = -1.0 if (orientation==1 or orientation==2) else 1.0 + return orient*(p1 * cosRot2 * cosRot3 + p2 * (cosRot3 * sinRot1 * sinRot2 - cosRot1 * sinRot3) - p3 * (cosRot1 * cosRot3 * sinRot2 + sinRot1 * sinRot3)) @@ -85,16 +88,19 @@ cdef inline double f_t1(double p1, double p2, double p3, cdef inline double f_t2(double p1, double p2, double p3, double sinRot1, double cosRot1, double sinRot2, double cosRot2, - double sinRot3, double cosRot3) noexcept nogil: - """Calculate t2 (aka y) for 1 pixel + double sinRot3, double cosRot3, + int orientation=0) noexcept nogil: + """Calculate t2 (aka x) for 1 pixel :param p1:distances in meter along dim1 from PONI :param p2: distances in meter along dim2 from PONI :param p3: distance sample - PONI :param sinRot1,sinRot2,sinRot3: sine of the angles :param cosRot1,cosRot2,cosRot3: cosine of the angles + :param orientation: value 1-4 """ - return (p1 * cosRot2 * sinRot3 + + cdef double orient = -1.0 if (orientation==1 or orientation==4) else 1.0 + return orient*(p1 * cosRot2 * sinRot3 + p2 * (cosRot1 * cosRot3 + sinRot1 * sinRot2 * sinRot3) - p3 * (-(cosRot3 * sinRot1) + cosRot1 * sinRot2 * sinRot3)) @@ -102,7 +108,8 @@ cdef inline double f_t2(double p1, double p2, double p3, cdef inline double f_t3(double p1, double p2, double p3, double sinRot1, double cosRot1, double sinRot2, double cosRot2, - double sinRot3, double cosRot3) noexcept nogil: + double sinRot3, double cosRot3, + int orientation=0) noexcept nogil: """Calculate t3 (aka -z) for 1 pixel :param p1:distances in meter along dim1 from PONI @@ -110,6 +117,7 @@ cdef inline double f_t3(double p1, double p2, double p3, :param p3: distance sample - PONI :param sinRot1,sinRot2,sinRot3: sine of the angles :param cosRot1,cosRot2,cosRot3: cosine of the angles + :param orientation: unused """ return p1 * sinRot2 - p2 * cosRot2 * sinRot1 + p3 * cosRot1 * cosRot2 @@ -118,7 +126,8 @@ cdef inline double f_t3(double p1, double p2, double p3, cdef inline double f_tth(double p1, double p2, double L, double sinRot1, double cosRot1, double sinRot2, double cosRot2, - double sinRot3, double cosRot3) noexcept nogil: + double sinRot3, double cosRot3, + int orientation=0) noexcept nogil: """Calculate 2 theta for 1 pixel :param p1:distances in meter along dim1 from PONI @@ -126,6 +135,7 @@ cdef inline double f_tth(double p1, double p2, double L, :param L: distance sample - PONI :param sinRot1,sinRot2,sinRot3: sine of the angles :param cosRot1,cosRot2,cosRot3: cosine of the angles + :param orientation: unused :return: 2 theta """ cdef: @@ -139,7 +149,7 @@ cdef inline double f_q(double p1, double p2, double L, double sinRot1, double cosRot1, double sinRot2, double cosRot2, double sinRot3, double cosRot3, - double wavelength) noexcept nogil: + double wavelength, int orientation=0) noexcept nogil: """ Calculate the scattering vector q for 1 pixel @@ -147,7 +157,8 @@ cdef inline double f_q(double p1, double p2, double L, :param p2: distances in meter along dim2 from PONI :param L: distance sample - PONI :param sinRot1,sinRot2,sinRot3: sine of the angles - :param cosRot1,cosRot2,cosRot3: cosine of the angles + :param cosRot1,cosRot2,cosRot3: cosine of the angles, + :param orientation: unused """ return 4.0e-9 * M_PI / wavelength * sin(f_tth(p1, p2, L, sinRot1, cosRot1, sinRot2, cosRot2, sinRot3, cosRot3) / 2.0) @@ -155,25 +166,28 @@ cdef inline double f_q(double p1, double p2, double L, cdef inline double f_chi(double p1, double p2, double L, double sinRot1, double cosRot1, double sinRot2, double cosRot2, - double sinRot3, double cosRot3) noexcept nogil: + double sinRot3, double cosRot3, + int orientation=0) noexcept nogil: """ calculate chi for 1 pixel :param p1:distances in meter along dim1 from PONI :param p2: distances in meter along dim2 from PONI :param L: distance sample - PONI :param sinRot1,sinRot2,sinRot3: sine of the angles - :param cosRot1,cosRot2,cosRot3: cosine of the angles + :param cosRot1,cosRot2,cosRot3: cosine of the angles, + :param orientation: value 1-4 """ cdef: - double t1 = f_t1(p1, p2, L, sinRot1, cosRot1, sinRot2, cosRot2, sinRot3, cosRot3) - double t2 = f_t2(p1, p2, L, sinRot1, cosRot1, sinRot2, cosRot2, sinRot3, cosRot3) + double t1 = f_t1(p1, p2, L, sinRot1, cosRot1, sinRot2, cosRot2, sinRot3, cosRot3, orientation) + double t2 = f_t2(p1, p2, L, sinRot1, cosRot1, sinRot2, cosRot2, sinRot3, cosRot3, orientation) return atan2(t1, t2) cdef inline double f_r(double p1, double p2, double L, double sinRot1, double cosRot1, double sinRot2, double cosRot2, - double sinRot3, double cosRot3) noexcept nogil: + double sinRot3, double cosRot3, + int orientation=0) noexcept nogil: """ calculate r for 1 pixel, radius from beam center to current :param p1:distances in meter along dim1 from PONI @@ -181,6 +195,7 @@ cdef inline double f_r(double p1, double p2, double L, :param L: distance sample - PONI :param sinRot1,sinRot2,sinRot3: sine of the angles :param cosRot1,cosRot2,cosRot3: cosine of the angles + :param orientation: unused """ cdef: double t1 = f_t1(p1, p2, L, sinRot1, cosRot1, sinRot2, cosRot2, sinRot3, cosRot3) @@ -222,7 +237,8 @@ def calc_pos_zyx(double L, double poni1, double poni2, double rot1, double rot2, double rot3, pos1 not None, pos2 not None, - pos3=None): + pos3=None, + int orientation=0): """Calculate the 3D coordinates in the sample's referential :param L: distance sample - PONI @@ -234,6 +250,7 @@ def calc_pos_zyx(double L, double poni1, double poni2, :param pos1: numpy array with distances in meter along dim1 from PONI (Y) :param pos2: numpy array with distances in meter along dim2 from PONI (X) :param pos3: numpy array with distances in meter along Sample->PONI (Z), positive behind the detector + :param orientation: value 1-4 :return: 3-tuple of ndarray of double with same shape and size as pos1 """ @@ -259,8 +276,8 @@ def calc_pos_zyx(double L, double poni1, double poni2, for i in prange(size, nogil=True, schedule="static", num_threads=1 if sizePONI (Z), positive behind the detector + :param orientation: unused :return: ndarray of double with same shape and size as pos1 """ cdef: @@ -340,6 +359,7 @@ def calc_chi(double L, double rot1, double rot2, double rot3, pos1 not None, pos2 not None, pos3=None, + int orientation=0, bint chi_discontinuity_at_pi=True): """Calculate the chi array (azimuthal angles) using OpenMP @@ -356,6 +376,7 @@ def calc_chi(double L, double rot1, double rot2, double rot3, :param pos1: numpy array with distances in meter along dim1 from PONI (Y) :param pos2: numpy array with distances in meter along dim2 from PONI (X) :param pos3: numpy array with distances in meter along Sample->PONI (Z), positive behind the detector + :param orientaion: orientation of the detector, values 1-4 :param chi_discontinuity_at_pi: set to False to obtain chi in the range [0, 2pi[ instead of [-pi, pi[ :return: ndarray of double with same shape and size as pos1 """ @@ -377,12 +398,12 @@ def calc_chi(double L, double rot1, double rot2, double rot3, if pos3 is None: for i in prange(size, nogil=True, schedule="static", num_threads=1 if sizePONI (Z), positive behind the detector :param wavelength: in meter to get q in nm-1 + :param orientation: unused :return: ndarray of double with same shape and size as pos1 """ cdef: @@ -448,7 +471,8 @@ def calc_q(double L, double rot1, double rot2, double rot3, def calc_r(double L, double rot1, double rot2, double rot3, pos1 not None, pos2 not None, - pos3=None): + pos3=None, + int orientation=0): """ Calculate the radius array (radial direction) in parallel @@ -459,6 +483,7 @@ def calc_r(double L, double rot1, double rot2, double rot3, :param pos1: numpy array with distances in meter along dim1 from PONI (Y) :param pos2: numpy array with distances in meter along dim2 from PONI (X) :param pos3: numpy array with distances in meter along Sample->PONI (Z), positive behind the detector + :param orientation: unused :return: ndarray of double with same shape and size as pos1 """ cdef: @@ -574,6 +599,7 @@ def calc_rad_azim(double L, pos3=None, space="2th", wavelength=None, + int orientation=0, bint chi_discontinuity_at_pi=True ): """Calculate the radial & azimutal position for each pixel from pos1, pos2, pos3. @@ -588,6 +614,7 @@ def calc_rad_azim(double L, :param pos2: numpy array with distances in meter along dim2 from PONI (X) :param pos3: numpy array with distances in meter along Sample->PONI (Z), positive behind the detector :param space: can be "2th", "q" or "r" for radial units. Azimuthal units are radians + :param orientation: values from 1 to 4 :param chi_discontinuity_at_pi: set to False to obtain chi in the range [0, 2pi[ instead of [-pi, pi[ :return: ndarray of double with same shape and size as pos1 + (2,), :raise: KeyError when space is bad ! @@ -625,8 +652,8 @@ def calc_rad_azim(double L, if pos3 is None: for i in prange(size, nogil=True, schedule="static", num_threads=1 if size Date: Thu, 7 Dec 2023 13:45:30 +0100 Subject: [PATCH 09/15] ensure the corner calculation is also good --- src/pyFAI/detectors/_common.py | 63 ++++++++++++++++++++++---------- src/pyFAI/detectors/_dectris.py | 26 ++----------- src/pyFAI/detectors/_imxpad.py | 3 +- src/pyFAI/detectors/_non_flat.py | 6 ++- src/pyFAI/detectors/_psi.py | 3 +- src/pyFAI/test/test_detector.py | 21 ++++++++++- 6 files changed, 75 insertions(+), 47 deletions(-) diff --git a/src/pyFAI/detectors/_common.py b/src/pyFAI/detectors/_common.py index d8bb479c9..5be566247 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__ = "24/11/2023" +__date__ = "07/12/2023" __status__ = "stable" import logging @@ -572,7 +572,7 @@ def setFit2D(self, **kwarg): elif kw == "splineFile": self.set_splineFile(kwarg[kw]) - def _calc_pixel_index_from_orientation(self, plus_one=False): + def _calc_pixel_index_from_orientation(self, plus_one=False): #TODO refactor `plus_one` to not `center` """Calculate the pixel index when considering the different orientations""" if plus_one: # used with corners m1 = self.shape[0] + 1 @@ -597,6 +597,30 @@ def _calc_pixel_index_from_orientation(self, plus_one=False): raise RuntimeError(f"Unsuported orientation: {self.orientation.name} ({self.orientation.value})") return r1, r2 + def _reorder_indexes_from_orientation(self, d1, d2, center=True): + """Helper function to recalculate the index of pixels considering orientation + # Not +=: do not mangle in place arrays""" + if self.orientation in (0,3): + return d1, d2 + if center: + shape1 = self.shape[0] - 1 + shape2 = self.shape[1] - 1 + else: #corner + shape1 = self.shape[0] + shape2 = self.shape[1] + + if self.orientation==1: + d1 = shape1 - d1 + d2 = shape2 - d2 + elif self.orientation==2: + d1 = shape1 - d1 + elif self.orientation == 4: + d2 = shape2 - d2 + else: + raise RuntimeError(f"Unsuported orientation: {self.orientation.name} ({self.orientation.value})") + return d1, d2 + + def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=True): """ Calculate the position of each pixel center in cartesian coordinate @@ -618,30 +642,31 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru pos_z is None for flat detectors """ + print("in calc_cartesian_positions") if self.shape: if (d1 is None) or (d2 is None): - r1, r2 = self._calc_pixel_index_from_orientation(False) - d1 = expand2d(r1, self.shape[1], False) - d2 = expand2d(r2, self.shape[0], True) - else: - if self.orientation in (0,3): - pass - elif self.orientation==1: - d1 = self.shape[0] - 1 - d1 - d2 = self.shape[1] - 1 - d2 - elif self.orientation==2: - d1 = self.shape[0] - 1 - d1 - elif self.orientation == 4: - d2 = self.shape[1] - 1 - d2 + if center: + r1, r2 = self._calc_pixel_index_from_orientation(False) + d1 = expand2d(r1, self.shape[1], False) + d2 = expand2d(r2, self.shape[0], True) else: - raise RuntimeError(f"Unsuported orientation: {self.orientation.name} ({self.orientation.value})") - + r1, r2 = self._calc_pixel_index_from_orientation(True) + d1 = expand2d(r1, self.shape[1]+1.0, False) + d2 = expand2d(r2, self.shape[0]+1.0, True) + else: + d1, d2 = self._reorder_indexes_from_orientation(d1, d2, center) elif "ndim" in dir(d1): if d1.ndim == 2: - self.shape = d1.shape + if center: + self.shape = d1.shape + else: #corner + self.shape = tuple(i-1 for i in d1.shape) elif "ndim" in dir(d2): if d2.ndim == 2: - self.shape = d2.shape + if center: + self.shape = d2.shape + else: #corner + self.shape = tuple(i-1 for i in d2.shape) if center: # avoid += It modifies in place then segfaults diff --git a/src/pyFAI/detectors/_dectris.py b/src/pyFAI/detectors/_dectris.py index fd10d3da3..24aa7d3b2 100644 --- a/src/pyFAI/detectors/_dectris.py +++ b/src/pyFAI/detectors/_dectris.py @@ -34,7 +34,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "23/11/2023" +__date__ = "07/12/2023" __status__ = "production" import os @@ -134,17 +134,7 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru d1 = expand2d(r1, self.shape[1], False) d2 = expand2d(r2, self.shape[0], True) else: - if self.orientation in (0,3): - pass - elif self.orientation == 1: - d1 = self.shape[0] - 1 - d1 - d2 = self.shape[1] - 1 - d2 - elif self.orientation == 2: - d1 = self.shape[0] - 1 - d1 - elif self.orientation == 4: - d2 = self.shape[1] - 1 - d2 - else: - raise RuntimeError(f"Unsuported orientation: {self.orientation.name} ({self.orientation.value})") + d1, d2 = self._reorder_indexes_from_orientation(d1, d2, center) if self.offset1 is None or self.offset2 is None: delta1 = delta2 = 0. @@ -539,17 +529,7 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru d1 = expand2d(r1, self.shape[1], False) d2 = expand2d(r2, self.shape[0], True) else: - if self.orientation in (0,3): - pass - elif self.orientation==1: - d1 = self.shape[0] - 1 - d1 - d2 = self.shape[1] - 1 - d2 - elif self.orientation==2: - d1 = self.shape[0] - 1 - d1 - elif self.orientation == 4: - d2 = self.shape[1] - 1 - d2 - else: - raise RuntimeError(f"Unsuported orientation: {self.orientation.name} ({self.orientation.value})") + d1, d2 = self._reorder_indexes_from_orientation(d1, d2, center) if (self.offset1 is None) or (self.offset2 is None): delta1 = delta2 = 0. diff --git a/src/pyFAI/detectors/_imxpad.py b/src/pyFAI/detectors/_imxpad.py index fb04090ee..090c720d0 100644 --- a/src/pyFAI/detectors/_imxpad.py +++ b/src/pyFAI/detectors/_imxpad.py @@ -34,7 +34,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "23/11/2023" +__date__ = "07/12/2023" __status__ = "production" import functools @@ -213,6 +213,7 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru p1 = numpy.outer(d1, numpy.ones(self.shape[1])) p2 = numpy.outer(numpy.ones(self.shape[0]), d2) else: + d1, d2 = self._reorder_indexes_from_orientation(d1, d2, center) if center: # Not +=: do not mangle in place arrays d1 = d1 + 0.5 diff --git a/src/pyFAI/detectors/_non_flat.py b/src/pyFAI/detectors/_non_flat.py index 067e436b3..64f6ee215 100644 --- a/src/pyFAI/detectors/_non_flat.py +++ b/src/pyFAI/detectors/_non_flat.py @@ -36,7 +36,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "23/11/2023" +__date__ = "07/12/2023" __status__ = "production" @@ -178,10 +178,12 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru d1 and d2 must have the same shape, returned array will have the same shape. """ - if (d1 is None) or d2 is None: + if (d1 is None) or (d2 is None): r1, r2 = self._calc_pixel_index_from_orientation(False) d1 = mathutil.expand2d(r1, self.shape[1], False) d2 = mathutil.expand2d(r2, self.shape[0], True) + else: + d1, d2 = self._reorder_indexes_from_orientation(d1, d2, center) corners = self.get_pixel_corners() if center: # avoid += It modifies in place and segfaults diff --git a/src/pyFAI/detectors/_psi.py b/src/pyFAI/detectors/_psi.py index cba92d70b..bd795247f 100644 --- a/src/pyFAI/detectors/_psi.py +++ b/src/pyFAI/detectors/_psi.py @@ -34,7 +34,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "2021 European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "21/11/2023" +__date__ = "07/12/2023" __status__ = "production" import numpy @@ -188,6 +188,7 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru p1 = numpy.outer(d1, numpy.ones(self.shape[1])) p2 = numpy.outer(numpy.ones(self.shape[0]), d2) else: + d1, d2 = self._reorder_indexes_from_orientation(d1, d2, center) if center: # Not +=: do not mangle in place arrays d1 = d1 + 0.5 diff --git a/src/pyFAI/test/test_detector.py b/src/pyFAI/test/test_detector.py index bcaf68e66..c4d9fc8a9 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__ = "24/11/2023" +__date__ = "07/12/2023" import os import shutil @@ -45,6 +45,7 @@ from .. import detectors from ..detectors import detector_factory, ALL_DETECTORS from .. import io +from .. import utils from .utilstest import UtilsTest @@ -434,6 +435,24 @@ def test_corners(self): self.assertTrue(numpy.allclose(p1, numpy.flipud(r1)), "orient 4vs1 dim1,y corner") self.assertTrue(numpy.allclose(p2, numpy.flipud(r2)), "orient 4vs1 dim2,x corner") + def test_corners2(self): + """similar to what is made in geometry ....""" + + shape = self.orient1.shape + d1 = utils.expand2d(numpy.arange(shape[0] + 1.0), shape[1] + 1.0, False) + d2 = utils.expand2d(numpy.arange(shape[1] + 1.0), shape[0] + 1.0, True) + for orient in (self.orient1, self.orient2, self.orient3, self.orient4): + for use_cython in (True, False): + p1, p2, p3 = orient.calc_cartesian_positions(d1, d2, center=False, use_cython=use_cython) + p1/=orient.pixel1 + p2/=orient.pixel2 + self.assertEqual(p3, None, f"P3 is None for {orient} with use_cython={use_cython}") + self.assertEqual(p1.min(), 0, f"P1_min is 0 for {orient} with use_cython={use_cython}") + self.assertEqual(p1.max(), shape[0], f"P1_max is shape for {orient} with use_cython={use_cython}") + self.assertEqual(p2.min(), 0, f"P2_min is 0 for {orient} with use_cython={use_cython}") + self.assertEqual(p2.max(), shape[1], f"P2_max is shape for {orient} with use_cython={use_cython}") + + def test_points(self): npt = 1000 rng = UtilsTest.get_rng() From f05ec5f3db9e360828a6c4a5cebad3a60edc223e Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Thu, 7 Dec 2023 14:28:40 +0100 Subject: [PATCH 10/15] fix tests --- src/pyFAI/test/test_geometry.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pyFAI/test/test_geometry.py b/src/pyFAI/test/test_geometry.py index 5eae86474..496524041 100644 --- a/src/pyFAI/test/test_geometry.py +++ b/src/pyFAI/test/test_geometry.py @@ -34,7 +34,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "04/12/2023" +__date__ = "07/12/2023" import unittest import random @@ -609,13 +609,13 @@ def test_array_from_unit_tth_corner(self): r4 = self.ai4.array_from_unit(unit="2th_deg", typ="corner") tth1 = r1[...,0].mean(axis=-1) - chi1 = r1[...,1].mean(axis=-1) + chi1 = r1[...,1].mean(axis=-1)/numpy.pi tth2 = r2[...,0].mean(axis=-1) - chi2 = r2[...,1].mean(axis=-1) + chi2 = r2[...,1].mean(axis=-1)/numpy.pi tth3 = r3[...,0].mean(axis=-1) - chi3 = r3[...,1].mean(axis=-1) + chi3 = r3[...,1].mean(axis=-1)/numpy.pi tth4 = r4[...,0].mean(axis=-1) - chi4 = r4[...,1].mean(axis=-1) + chi4 = r4[...,1].mean(axis=-1)/numpy.pi self.assertFalse(numpy.allclose(tth1, tth2), "orientation 1,2 differ tth") self.assertFalse(numpy.allclose(chi1, chi2), "orientation 1,2 differ chi") @@ -625,15 +625,15 @@ def test_array_from_unit_tth_corner(self): self.assertFalse(numpy.allclose(chi1, chi4), "orientation 1,4 differ chi") self.assertTrue(numpy.allclose(tth1, numpy.fliplr(tth2)), "orientation 1,2 flipped match tth") - self.assertTrue(numpy.allclose(chi1, -numpy.fliplr(chi2)), "orientation 1,2 flipped match chi") + self.assertTrue(numpy.allclose(chi1+1, -numpy.fliplr(chi2), atol=0.0001), "orientation 1,2 flipped match chi") self.assertTrue(numpy.allclose(tth1, numpy.flipud(tth4)), "orientation 1,4 flipped match tth") self.assertTrue(numpy.allclose(chi1, -numpy.flipud(chi4)), "orientation 1,4 flipped match chi") self.assertTrue(numpy.allclose(tth2, numpy.flipud(tth3)), "orientation 2,3 flipped match tth") self.assertTrue(numpy.allclose(chi2, -numpy.flipud(chi3)), "orientation 2,3 flipped match chi") self.assertTrue(numpy.allclose(tth1, tth3[-1::-1,-1::-1]), "orientation 1,3 inversion match tth") - self.assertTrue(numpy.allclose(chi1, chi3[-1::-1,-1::-1]), "orientation 1,3 inversion match chi") + self.assertTrue(numpy.allclose(chi1+1, chi3[-1::-1,-1::-1], atol=0.0001), "orientation 1,3 inversion match chi") self.assertTrue(numpy.allclose(tth2, tth4[-1::-1,-1::-1]), "orientation 2,4 inversion match tth") - self.assertTrue(numpy.allclose(chi2, chi4[-1::-1,-1::-1]), "orientation 2,4 inversion match chi") + self.assertTrue(numpy.allclose(chi2+1, chi4[-1::-1,-1::-1]), "orientation 2,4 inversion match chi") def suite(): From 773554f82d6555582f683985da803ec5446cbf17 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Thu, 7 Dec 2023 16:51:54 +0100 Subject: [PATCH 11/15] update doc + cleanup --- doc/source/conventions.rst | 50 +++++++++++++++++++ src/pyFAI/detectors/_common.py | 24 ++++----- src/pyFAI/detectors/_dectris.py | 13 ++--- src/pyFAI/detectors/_imxpad.py | 14 +++--- src/pyFAI/detectors/_non_flat.py | 7 +-- src/pyFAI/test/test_geometry.py | 83 ++++++++++++++++++++++++++++++-- 6 files changed, 158 insertions(+), 33 deletions(-) diff --git a/doc/source/conventions.rst b/doc/source/conventions.rst index e581e17dd..dd7b7eb3d 100644 --- a/doc/source/conventions.rst +++ b/doc/source/conventions.rst @@ -30,3 +30,53 @@ Note some specificities: * The origin is at the bottom and differs from the *camera* convention where the origin is at the top. As a consequence, the sign of the ᵪ-angle gets inverted. * The detector is seen from the sample and differs from the *camera* convention where the observer is behind the camera. As a consequence, the sign of the ᵪ-angle gets inverted. * The former 2 inversions cancel each other. + +Detector orientation +-------------------- + +Since 2023.12, it is possible to take into account the detector orientation in pyFAI. +There is a norm in photography for storing this kind of information in an EXIF tag with the following values:: + + 1 2 3 4 5 6 7 8 + + 888888 888888 88 88 8888888888 88 88 8888888888 + 88 88 88 88 88 88 88 88 88 88 88 88 + 8888 8888 8888 8888 88 8888888888 8888888888 88 + 88 88 88 88 + 88 88 888888 888888 + +In photography the observer is behind the camera, thus the image's first pixel of the image (origin) is on the top-left. +This differes in two point with pyFAI's definition:: +* pyFAI considers the frame seen from the sample, so in front of the camera, this change of point of view is equivalent to a flip-right-left of the image +* pyFAI considers the origin is a the bottom (left) of the image, so this corresponds to a flip-up-down of the image +* Those two image flip result in a default orientation identified as *BottomRight (3)* by default on all detectors. + +The *orientation* is a property of the *detector*, it is **intentionnally unmuttable** to prevent errors. +For now, only the geometries 1 to 4 are supported, the rotated geometries can easily be accomodated using the *rot3* parameter during the calibration of the experimental setup + + +Converting the calibration from *Dioptas* to process data with *pyFAI* +...................................................................... + +Despite *Diotas* uses the *pyFAI* library to perform the geometry calculation, refinement and PONI-file management, it offers the ability to actualy flip/rotate the image +and not only the representation of the image on the screen. +As a consequence, the user has to remind the sequence of operations performed on the image and the position of the origin. +Moreover, the image is flipped up-down when loaded in *Dioptas*, making thinks even more complicated (origin: TopRight, orientation 2). +The support for the *orientation* of the image allows to treat with *pyFAI* a dataset calibrated with *Dioptas*. +For this, edit the PONI-file and find the line entitled *detector_config* and declare or modify the value for the orientation like this:: + + Detector_config: {"orientation": 2} + +This will allow to treat the data. +Since the images are flipped, the direction of the azimuthal angle cannot match and the **chi** will not only differ in numeriacal values but most importantly in direction. +This has to be taken into account when performing texture analysis. + +Conventions from the detector manufacturer +.......................................... + +Different detector manufacturer follow different convention, in addition to which acquisition software allow to save the image with some transformation applied. +Here are some information collected for the manufacture:: + * Dectris: orientation 2: TopRight + * ... to be completed. + +For now, all detectors are intialized with the orientation (3), i.e. it does not change with the convention used in pyFAI since 2010. diff --git a/src/pyFAI/detectors/_common.py b/src/pyFAI/detectors/_common.py index 5be566247..9904fbb39 100644 --- a/src/pyFAI/detectors/_common.py +++ b/src/pyFAI/detectors/_common.py @@ -572,14 +572,14 @@ def setFit2D(self, **kwarg): elif kw == "splineFile": self.set_splineFile(kwarg[kw]) - def _calc_pixel_index_from_orientation(self, plus_one=False): #TODO refactor `plus_one` to not `center` + def _calc_pixel_index_from_orientation(self, center=True): """Calculate the pixel index when considering the different orientations""" - if plus_one: # used with corners - m1 = self.shape[0] + 1 - m2 = self.shape[1] + 1 - else: #used with centers + if center: m1 = self.shape[0] m2 = self.shape[1] + else: #corner + m1 = self.shape[0] + 1 + m2 = self.shape[1] + 1 if self.orientation in (0,3): r1 = numpy.arange(m1, dtype="float32") @@ -645,14 +645,10 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru print("in calc_cartesian_positions") if self.shape: if (d1 is None) or (d2 is None): - if center: - r1, r2 = self._calc_pixel_index_from_orientation(False) - d1 = expand2d(r1, self.shape[1], False) - d2 = expand2d(r2, self.shape[0], True) - else: - r1, r2 = self._calc_pixel_index_from_orientation(True) - d1 = expand2d(r1, self.shape[1]+1.0, False) - d2 = expand2d(r2, self.shape[0]+1.0, True) + r1, r2 = self._calc_pixel_index_from_orientation(center) + delta = 0 if center else 1 + d1 = expand2d(r1, self.shape[1] + delta, False) + d2 = expand2d(r2, self.shape[0] + delta, True) else: d1, d2 = self._reorder_indexes_from_orientation(d1, d2, center) elif "ndim" in dir(d1): @@ -761,7 +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: - r1, r2 = self._calc_pixel_index_from_orientation(True) + r1, r2 = self._calc_pixel_index_from_orientation(False) # like numpy.ogrid d1 = expand2d(r1, self.shape[1] + 1, False) d2 = expand2d(r2, self.shape[0] + 1, True) diff --git a/src/pyFAI/detectors/_dectris.py b/src/pyFAI/detectors/_dectris.py index 24aa7d3b2..174f8b30a 100644 --- a/src/pyFAI/detectors/_dectris.py +++ b/src/pyFAI/detectors/_dectris.py @@ -130,9 +130,9 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru """ if self.shape: if (d1 is None) or (d2 is None): - r1, r2 = self._calc_pixel_index_from_orientation(False) - d1 = expand2d(r1, self.shape[1], False) - d2 = expand2d(r2, self.shape[0], True) + r1, r2 = self._calc_pixel_index_from_orientation(center) + d1 = expand2d(r1, self.shape[1]+(0 if center else 1), False) + d2 = expand2d(r2, self.shape[0]+(0 if center else 1), True) else: d1, d2 = self._reorder_indexes_from_orientation(d1, d2, center) @@ -525,9 +525,10 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru """ if self.shape: if ((d1 is None) or (d2 is None)): - r1, r2 = self._calc_pixel_index_from_orientation(False) - d1 = expand2d(r1, self.shape[1], False) - d2 = expand2d(r2, self.shape[0], True) + r1, r2 = self._calc_pixel_index_from_orientation(center) + delta = 0 if center else 1 + d1 = expand2d(r1, self.shape[1] + delta, False) + d2 = expand2d(r2, self.shape[0] + delta, True) else: d1, d2 = self._reorder_indexes_from_orientation(d1, d2, center) diff --git a/src/pyFAI/detectors/_imxpad.py b/src/pyFAI/detectors/_imxpad.py index 090c720d0..50c9c183e 100644 --- a/src/pyFAI/detectors/_imxpad.py +++ b/src/pyFAI/detectors/_imxpad.py @@ -390,9 +390,10 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru """ if self.shape: if (d1 is None) or (d2 is None): - r1, r2 = self._calc_pixel_index_from_orientation(False) - d1 = mathutil.expand2d(r1, self.shape[1], False) - d2 = mathutil.expand2d(r2, self.shape[0], True) + r1, r2 = self._calc_pixel_index_from_orientation(center) + delta = 0 if center else 1 + d1 = mathutil.expand2d(r1, self.shape[1] + delta, False) + d2 = mathutil.expand2d(r2, self.shape[0] + delta, True) corners = self.get_pixel_corners() if center: # note += would make an increment in place which is bad (segfault !) @@ -618,9 +619,10 @@ def get_pixel_corners(self, correct_binning=False): # TODO !!! def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=True): if (d1 is None) or d2 is None: - r1, r2 = self._calc_pixel_index_from_orientation(False) - d1 = mathutil.expand2d(r1, self.MAX_SHAPE[1], False) - d2 = mathutil.expand2d(r2, self.MAX_SHAPE[0], True) + r1, r2 = self._calc_pixel_index_from_orientation(center) + delta = 0 if center else 1 + d1 = mathutil.expand2d(r1, self.MAX_SHAPE[1] + delta, False) + d2 = mathutil.expand2d(r2, self.MAX_SHAPE[0] + delta, True) corners = self.get_pixel_corners() if center: # avoid += It modifies in place and segfaults diff --git a/src/pyFAI/detectors/_non_flat.py b/src/pyFAI/detectors/_non_flat.py index 64f6ee215..9aac00494 100644 --- a/src/pyFAI/detectors/_non_flat.py +++ b/src/pyFAI/detectors/_non_flat.py @@ -179,9 +179,10 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru the same shape. """ if (d1 is None) or (d2 is None): - r1, r2 = self._calc_pixel_index_from_orientation(False) - d1 = mathutil.expand2d(r1, self.shape[1], False) - d2 = mathutil.expand2d(r2, self.shape[0], True) + r1, r2 = self._calc_pixel_index_from_orientation(center) + delta = 0 if center else 1 + d1 = mathutil.expand2d(r1, self.shape[1] + delta, False) + d2 = mathutil.expand2d(r2, self.shape[0] + delta, True) else: d1, d2 = self._reorder_indexes_from_orientation(d1, d2, center) corners = self.get_pixel_corners() diff --git a/src/pyFAI/test/test_geometry.py b/src/pyFAI/test/test_geometry.py index 496524041..81b05b4aa 100644 --- a/src/pyFAI/test/test_geometry.py +++ b/src/pyFAI/test/test_geometry.py @@ -558,10 +558,6 @@ def setUpClass(cls)->None: cls.ai2 = geometry.Geometry.sload({"detector":"pilatus100k", "detector_config":{"orientation":2}, "wavelength":1e-10}) cls.ai3 = geometry.Geometry.sload({"detector":"pilatus100k", "detector_config":{"orientation":3}, "wavelength":1e-10}) cls.ai4 = geometry.Geometry.sload({"detector":"pilatus100k", "detector_config":{"orientation":4}, "wavelength":1e-10}) - print(cls.ai1) - print(cls.ai2) - print(cls.ai3) - print(cls.ai4) @classmethod def tearDownClass(cls)->None: super(TestOrientation, cls).tearDownClass() @@ -635,6 +631,84 @@ def test_array_from_unit_tth_corner(self): self.assertTrue(numpy.allclose(tth2, tth4[-1::-1,-1::-1]), "orientation 2,4 inversion match tth") self.assertTrue(numpy.allclose(chi2+1, chi4[-1::-1,-1::-1]), "orientation 2,4 inversion match chi") +class TestOrientation2(unittest.TestCase): + """Simple tests to validate the orientation of the detector""" + @classmethod + def setUpClass(cls)->None: + super(TestOrientation2, cls).setUpClass() + p = detector_factory("Pilatus100k") + c = p.get_pixel_corners() + d1=c[..., 1].max() + d2=c[..., 2].max() + cls.ai1 = geometry.Geometry.sload({"poni1":3*d1/4,"poni2":3*d2/4,"wavelength":1e-10, + "detector":"pilatus100k", "detector_config":{"orientation":1}}) + cls.ai2 = geometry.Geometry.sload({"poni1":3*d1/4,"poni2":d2/4,"wavelength":1e-10, + "detector":"pilatus100k", "detector_config":{"orientation":2}}) + cls.ai3 = geometry.Geometry.sload({"poni1":d1/4,"poni2":d2/4,"wavelength":1e-10, + "detector":"pilatus100k", "detector_config":{"orientation":3}}) + cls.ai4 = geometry.Geometry.sload({"poni1":d1/4,"poni2":3*d2/4,"wavelength":1e-10, + "detector":"pilatus100k", "detector_config":{"orientation":4}}) + @classmethod + def tearDownClass(cls)->None: + super(TestOrientation2, cls).tearDownClass() + cls.ai1 = cls.ai2 = cls.ai3 = cls.ai3 = None + + def test_center_radius_center(self): + r1 = self.ai1.array_from_unit(unit="r_m", typ="center") + r2 = self.ai2.array_from_unit(unit="r_m", typ="center") + r3 = self.ai3.array_from_unit(unit="r_m", typ="center") + r4 = self.ai4.array_from_unit(unit="r_m", typ="center") + self.assertTrue(numpy.allclose(r1, r2, atol=1e-8)) + self.assertTrue(numpy.allclose(r1, r3, atol=1e-8)) + self.assertTrue(numpy.allclose(r1, r4, atol=1e-8)) + self.assertTrue(numpy.allclose(r2, r3, atol=1e-8)) + self.assertTrue(numpy.allclose(r2, r4, atol=1e-8)) + self.assertTrue(numpy.allclose(r3, r4, atol=1e-8)) + + def test_center_chi_center(self): + r1 = self.ai1.array_from_unit(unit="chi_rad", typ="center")/numpy.pi + r2 = self.ai2.array_from_unit(unit="chi_rad", typ="center")/numpy.pi + r3 = self.ai3.array_from_unit(unit="chi_rad", typ="center")/numpy.pi + r4 = self.ai4.array_from_unit(unit="chi_rad", typ="center")/numpy.pi + self.assertTrue(numpy.allclose(r1[:,200:], r2[:,200:], atol=1e-8)) + self.assertTrue(numpy.allclose(r1[:,200:], r3[:,200:], atol=1e-8)) + self.assertTrue(numpy.allclose(r1[:,200:], r4[:,200:], atol=1e-8)) + self.assertTrue(numpy.allclose(r2[:,200:], r3[:,200:], atol=1e-8)) + self.assertTrue(numpy.allclose(r2[:,200:], r4[:,200:], atol=1e-8)) + self.assertTrue(numpy.allclose(r3[:,200:], r4[:,200:], atol=1e-8)) + + def test_center_tth_center(self): + r1 = self.ai1.array_from_unit(unit="2th_deg", typ="corner") + r2 = self.ai2.array_from_unit(unit="2th_deg", typ="corner") + r3 = self.ai3.array_from_unit(unit="2th_deg", typ="corner") + r4 = self.ai4.array_from_unit(unit="2th_deg", typ="corner") + tth1 = r1[...,0].mean(axis=-1) + chi1 = r1[...,1].mean(axis=-1) + tth2 = r2[...,0].mean(axis=-1) + chi2 = r2[...,1].mean(axis=-1) + tth3 = r3[...,0].mean(axis=-1) + chi3 = r3[...,1].mean(axis=-1) + tth4 = r4[...,0].mean(axis=-1) + chi4 = r4[...,1].mean(axis=-1) + + res = [] + tths = [tth1, tth2, tth3, tth4] + thres = 0.1 + for idx, a1 in enumerate(tths): + for a2 in tths[:idx]: + res.append(numpy.allclose(a1, a2,atol=thres)) + print(res) + self.assertTrue(numpy.all(res), "2th is OK") + + res = [] + tths = [chi1, chi2, chi3, chi4] + thres = 0.1 + for idx, a1 in enumerate(tths): + for a2 in tths[:idx]: + res.append(numpy.allclose(a1[:,200:], a2[:,200:],atol=thres)) + print(res) + self.assertTrue(numpy.all(res), "2th is OK") + def suite(): loader = unittest.defaultTestLoader.loadTestsFromTestCase @@ -647,6 +721,7 @@ def suite(): testsuite.addTest(loader(TestGeometry)) testsuite.addTest(loader(TestFastPath)) testsuite.addTest(loader(TestOrientation)) + testsuite.addTest(loader(TestOrientation2)) return testsuite From 0c3c1f934b1cb9a034bf5fd356de8cc44c748a05 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Thu, 7 Dec 2023 17:25:38 +0100 Subject: [PATCH 12/15] typo in RST --- doc/source/conventions.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/source/conventions.rst b/doc/source/conventions.rst index dd7b7eb3d..ceea12a5c 100644 --- a/doc/source/conventions.rst +++ b/doc/source/conventions.rst @@ -46,10 +46,11 @@ There is a norm in photography for storing this kind of information in an EXIF t 88 88 888888 888888 In photography the observer is behind the camera, thus the image's first pixel of the image (origin) is on the top-left. -This differes in two point with pyFAI's definition:: -* pyFAI considers the frame seen from the sample, so in front of the camera, this change of point of view is equivalent to a flip-right-left of the image -* pyFAI considers the origin is a the bottom (left) of the image, so this corresponds to a flip-up-down of the image -* Those two image flip result in a default orientation identified as *BottomRight (3)* by default on all detectors. +This differes in two point with pyFAI's definition: + + * pyFAI considers the frame seen from the sample, so in front of the camera, this change of point of view is equivalent to a flip-right-left of the image + * pyFAI considers the origin is a the bottom (left) of the image, so this corresponds to a flip-up-down of the image + * Those two image flip result in a default orientation identified as *BottomRight (3)* by default on all detectors. The *orientation* is a property of the *detector*, it is **intentionnally unmuttable** to prevent errors. For now, only the geometries 1 to 4 are supported, the rotated geometries can easily be accomodated using the *rot3* parameter during the calibration of the experimental setup From f0a61adda2c5cf342eb9712c3d1233bc00e8491b Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Thu, 7 Dec 2023 17:33:00 +0100 Subject: [PATCH 13/15] indicate the origin --- doc/source/conventions.rst | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/doc/source/conventions.rst b/doc/source/conventions.rst index ceea12a5c..7acb22855 100644 --- a/doc/source/conventions.rst +++ b/doc/source/conventions.rst @@ -35,17 +35,17 @@ Detector orientation -------------------- Since 2023.12, it is possible to take into account the detector orientation in pyFAI. -There is a norm in photography for storing this kind of information in an EXIF tag with the following values:: - - 1 2 3 4 5 6 7 8 - - 888888 888888 88 88 8888888888 88 88 8888888888 - 88 88 88 88 88 88 88 88 88 88 88 88 - 8888 8888 8888 8888 88 8888888888 8888888888 88 - 88 88 88 88 - 88 88 888888 888888 - -In photography the observer is behind the camera, thus the image's first pixel of the image (origin) is on the top-left. +There is a norme in photography for storing this kind of information in an EXIF tag as an integer with a value from 1 to 8:: + + 1 2 3 4 5 6 7 8 + o o o o + 888888 888888 88 88 8888888888 88 88 8888888888 + 88 88 88 88 88 88 88 88 88 88 88 88 + 8888 8888 8888 8888 88 8888888888 8888888888 88 + 88 88 88 88 o o + 88 88 888888 888888 + o o +In photography the observer is behind the camera, thus the image's first pixel of the image (origin, noted (o)) is by default on the top-left. This differes in two point with pyFAI's definition: * pyFAI considers the frame seen from the sample, so in front of the camera, this change of point of view is equivalent to a flip-right-left of the image @@ -76,7 +76,8 @@ Conventions from the detector manufacturer .......................................... Different detector manufacturer follow different convention, in addition to which acquisition software allow to save the image with some transformation applied. -Here are some information collected for the manufacture:: +Here are some information collected for the manufacture: + * Dectris: orientation 2: TopRight * ... to be completed. From 82b6fd69382fea71d3a3caeee4862a5e4d85982f Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Fri, 8 Dec 2023 10:14:23 +0100 Subject: [PATCH 14/15] remove debug --- src/pyFAI/detectors/_common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pyFAI/detectors/_common.py b/src/pyFAI/detectors/_common.py index 9904fbb39..5bfeee4f6 100644 --- a/src/pyFAI/detectors/_common.py +++ b/src/pyFAI/detectors/_common.py @@ -642,7 +642,6 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru pos_z is None for flat detectors """ - print("in calc_cartesian_positions") if self.shape: if (d1 is None) or (d2 is None): r1, r2 = self._calc_pixel_index_from_orientation(center) From 58db30b80c701d4500484e66edb86c9622d3faec Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Fri, 8 Dec 2023 10:16:19 +0100 Subject: [PATCH 15/15] simplify --- src/pyFAI/detectors/_dectris.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pyFAI/detectors/_dectris.py b/src/pyFAI/detectors/_dectris.py index 174f8b30a..e6ea2acd3 100644 --- a/src/pyFAI/detectors/_dectris.py +++ b/src/pyFAI/detectors/_dectris.py @@ -131,8 +131,9 @@ def calc_cartesian_positions(self, d1=None, d2=None, center=True, use_cython=Tru if self.shape: if (d1 is None) or (d2 is None): r1, r2 = self._calc_pixel_index_from_orientation(center) - d1 = expand2d(r1, self.shape[1]+(0 if center else 1), False) - d2 = expand2d(r2, self.shape[0]+(0 if center else 1), True) + delta = 0 if center else 1 + d1 = expand2d(r1, self.shape[1] + delta, False) + d2 = expand2d(r2, self.shape[0] + delta, True) else: d1, d2 = self._reorder_indexes_from_orientation(d1, d2, center)