From 036b208f5110ba6ae88b79076b720d13246e09b1 Mon Sep 17 00:00:00 2001 From: Jerome Kieffer Date: Wed, 20 Dec 2023 17:56:28 +0100 Subject: [PATCH 1/2] close #2024 --- src/pyFAI/geometry/core.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/pyFAI/geometry/core.py b/src/pyFAI/geometry/core.py index d05c42b0a..38bf42853 100644 --- a/src/pyFAI/geometry/core.py +++ b/src/pyFAI/geometry/core.py @@ -40,12 +40,13 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "13/12/2023" +__date__ = "20/12/2023" __status__ = "production" __docformat__ = 'restructuredtext' import logging from math import pi +twopi = 2*pi from numpy import arccos, arctan2, sin, cos, sqrt import numpy import os @@ -60,6 +61,7 @@ from .. import utils from ..io import ponifile, integration_config from ..units import CONST_hc, to_unit + logger = logging.getLogger(__name__) try: @@ -250,7 +252,7 @@ def normalize_azimuth_range(self, azimuth_range): return azimuth_range = tuple(deg2rad(azimuth_range[i], self.chiDiscAtPi) for i in (0, -1)) if azimuth_range[1] <= azimuth_range[0]: - azimuth_range = (azimuth_range[0], azimuth_range[1] + 2 * pi) + azimuth_range = (azimuth_range[0], azimuth_range[1] + twopi) self.check_chi_disc(azimuth_range) return azimuth_range @@ -453,7 +455,7 @@ def qFunction(self, d1, d2, param=None, path="cython"): pos3=p3, wavelength=self.wavelength) else: - out = 4.0e-9 * numpy.pi / self.wavelength * \ + out = 4.0e-9 * pi / self.wavelength * \ numpy.sin(self.tth(d1=d1, d2=d2, param=param, path=path) / 2.0) return out @@ -545,7 +547,7 @@ def rd2Array(self, shape=None): if self._cached_array.get("d*2_center") is None: with self._sem: if self._cached_array.get("d*2_center") is None: - self._cached_array["d*2_center"] = (qArray / (2.0 * numpy.pi)) ** 2 + self._cached_array["d*2_center"] = (qArray / (twopi)) ** 2 return self._cached_array["d*2_center"] @deprecated @@ -625,7 +627,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 not self.chiDiscAtPi: - numpy.mod(chi, (2.0 * numpy.pi), out=chi) + numpy.mod(chi, (twopi), out=chi) return chi def chi_corner(self, d1, d2): @@ -662,7 +664,7 @@ def chiArray(self, shape=None): chia = numpy.fromfunction(self.chi, shape, dtype=numpy.float32) if not self.chiDiscAtPi: - chia = chia % (2.0 * numpy.pi) + chia = chia % (twopi) self._cached_array["chi_center"] = chia return self._cached_array["chi_center"] @@ -808,11 +810,10 @@ def corner_array(self, shape=None, unit=None, use_cython=True, scale=True): # numpy path chi = numpy.arctan2(y, x) if not self.chiDiscAtPi: - numpy.mod(chi, (2.0 * numpy.pi), out=chi) + numpy.mod(chi, (twopi), 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") + 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: @@ -925,7 +926,7 @@ def center_array(self, shape=None, unit="2th_deg", scale=True): z = pos[..., 0] ary = unit.equation(x, y, z, self.wavelength) if unit.space == "chi" and not self.chiDiscAtPi: - numpy.mod(ary, 2.0 * numpy.pi, out=ary) + numpy.mod(ary, twopi, out=ary) self._cached_array[key] = ary if scale and unit: return ary * unit.scale @@ -961,6 +962,12 @@ def delta_array(self, shape=None, unit="2th_deg", scale=False): center = self.center_array(shape, unit=unit, scale=False) corners = self.corner_array(shape, unit=unit, scale=False) delta = abs(corners[..., 0] - numpy.atleast_3d(center)) + if space == "chi_delta": + if numexpr: + delta = numexpr.evaluate("where(delta Date: Thu, 21 Dec 2023 08:49:09 +0100 Subject: [PATCH 2/2] regression test for #2024 --- src/pyFAI/test/test_geometry.py | 176 ++++++++++++++++++-------------- 1 file changed, 100 insertions(+), 76 deletions(-) diff --git a/src/pyFAI/test/test_geometry.py b/src/pyFAI/test/test_geometry.py index 34e5529a8..fe3550c4f 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__ = "13/12/2023" +__date__ = "21/12/2023" import unittest import random @@ -537,38 +537,60 @@ def test_calcfrom12d(self): self.assertLess(delta2, 1e-3, "calcfrom2d works delta=%s" % delta2) -class TestBug474(unittest.TestCase): - """This bug is about PONI coordinates not subtracted from x&y coodinates in Cython""" +class TestBugRegression(unittest.TestCase): - def test_regression(self): + @classmethod + def setUpClass(cls) -> None: + super(TestBugRegression, cls).setUpClass() detector = detector_factory("Pilatus100K") # small detectors makes calculation faster - geo = geometry.Geometry(detector=detector) - geo.setFit2D(100, detector.shape[1] // 3, detector.shape[0] // 3, tilt=1) - rc = geo.position_array(use_cython=True) - rp = geo.position_array(use_cython=False) + cls.geo = geometry.Geometry(detector=detector) + cls.geo.setFit2D(100, detector.shape[1] // 3, detector.shape[0] // 3, tilt=1) + + @classmethod + def tearDownClass(cls) -> None: + super(TestBugRegression, cls).tearDownClass() + cls.geo = None + + def test_bug747(self): + """This bug is about PONI coordinates not subtracted from x&y coodinates in Cython""" + # self.geo.reset() + rc = self.geo.position_array(use_cython=True) + rp = self.geo.position_array(use_cython=False) delta = abs(rp - rc).max() self.assertLess(delta, 1e-5, "error on position is %s" % delta) + def test_bug2024(self): + """This bug is about delta chi being sometimes 2pi""" + self.geo.reset() + deltaChi = self.geo.deltaChi() + self.assertLess(deltaChi.max(), numpy.pi, "deltaChi is less than pi") + self.geo.reset() + delta_array = self.geo.delta_array(unit="chi_rad") + self.assertLess(delta_array.max(), numpy.pi, "delta_array is less than pi") + self.assertTrue(numpy.allclose(delta_array, deltaChi, atol=7e-6), "delta_array matches deltaChi") + class TestOrientation(unittest.TestCase): """Simple tests to validate the orientation of the detector""" + @classmethod - def setUpClass(cls)->None: + def setUpClass(cls) -> None: super(TestOrientation, cls).setUpClass() cls.ai1 = geometry.Geometry.sload({"detector":"pilatus100k", "detector_config":{"orientation":1}, - #"poni1":0.1,"poni2":0.1, + # "poni1":0.1,"poni2":0.1, "wavelength":1e-10}) cls.ai2 = geometry.Geometry.sload({"detector":"pilatus100k", "detector_config":{"orientation":2}, - #"poni1":0.1,"poni2":0.1, + # "poni1":0.1,"poni2":0.1, "wavelength":1e-10}) cls.ai3 = geometry.Geometry.sload({"detector":"pilatus100k", "detector_config":{"orientation":3}, - #"poni1":0.1,"poni2":0.1, + # "poni1":0.1,"poni2":0.1, "wavelength":1e-10}) cls.ai4 = geometry.Geometry.sload({"detector":"pilatus100k", "detector_config":{"orientation":4}, - #"poni1":0.1,"poni2":0.1, + # "poni1":0.1,"poni2":0.1, "wavelength":1e-10}) + @classmethod - def tearDownClass(cls)->None: + def tearDownClass(cls) -> None: super(TestOrientation, cls).tearDownClass() cls.ai1 = cls.ai2 = cls.ai3 = cls.ai3 = None @@ -585,8 +607,8 @@ def test_array_from_unit_tth_center(self): 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") + 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_center(self): r1 = self.ai1.array_from_unit(unit="chi_deg") @@ -598,14 +620,14 @@ def test_array_from_unit_chi_center(self): self.assertFalse(numpy.allclose(r1, r3), "orientation 1,3 differ chi") self.assertFalse(numpy.allclose(r1, r4), "orientation 1,4 differ chi") - self.assertTrue(-180None: + 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, + 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, + 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, + 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, + 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: + def tearDownClass(cls) -> None: super(TestOrientation2, cls).tearDownClass() cls.ai1 = cls.ai2 = cls.ai3 = cls.ai3 = None @@ -701,7 +726,6 @@ def test_positions(self): self.assertTrue(numpy.allclose(yc, ym, atol=1e-8), f"check Y on {ai.detector.orientation.name}, cython: center={cc}, corner={cm}") self.assertTrue(numpy.allclose(xc, xm, atol=1e-8), f"check X on {ai.detector.orientation.name}, cython: center={cc}, corner={cm}") - 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") @@ -715,37 +739,37 @@ def test_center_radius_center(self): 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)) + 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) + 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)) + res.append(numpy.allclose(a1, a2, atol=thres)) # print(res) self.assertTrue(numpy.all(res), "2th is OK") @@ -754,7 +778,7 @@ def test_center_tth_center(self): thres = 0.1 for idx, a1 in enumerate(tths): for a2 in tths[:idx]: - res.append(numpy.allclose(a1[:,200:], a2[:,200:],atol=thres)) + res.append(numpy.allclose(a1[:, 200:], a2[:, 200:], atol=thres)) # print(res) self.assertTrue(numpy.all(res), "2th is OK") @@ -762,7 +786,7 @@ def test_center_tth_center(self): def suite(): loader = unittest.defaultTestLoader.loadTestsFromTestCase testsuite = unittest.TestSuite() - testsuite.addTest(loader(TestBug474)) + testsuite.addTest(loader(TestBugRegression)) testsuite.addTest(loader(TestSolidAngle)) testsuite.addTest(loader(TestBug88SolidAngle)) testsuite.addTest(loader(TestRecprocalSpacingSquarred))