diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 283f26402..0e88201ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,9 +113,7 @@ jobs: env: # Use silx wheelhouse: needed for ppc64le CIBW_ENVIRONMENT_LINUX: "PIP_FIND_LINKS=https://www.silx.org/pub/wheelhouse/ PIP_TRUSTED_HOST=www.silx.org" - # diable OpenMP under windows CIBW_ENVIRONMENT_WINDOWS: "PYFAI_WITH_OPENMP=False" - CIBW_BUILD: cp37-* cp38-* cp39-* cp310-* cp311-* cp312-* # Do not build for pypy and muslinux CIBW_SKIP: pp* *-musllinux_* diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index f7cb86d63..17b760b88 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -1,10 +1,24 @@ :Author: Jérôme Kieffer -:Date: 01/02/2024 +:Date: 17/05/2024 :Keywords: changelog Change-log of versions ====================== +2024.5 XX/05/2024 +----------------- +- Implemented unweighted average for 2D integration + + Integration engines now handle the boolean 'weighted_average' to switch to unweighted mean, similar to legacy methods +- Implementation of pilx (diff-map-view): interactive viewer for pyFAI-diffmap files (thanks Loic Huder) +- Creation of a RingExtraction class based on multi-threading (thanks Emily Massahud) +- Flat-field and dark current corrections for pyFAI-calib2 +- Tunable units and integration methods for fiber/grazing-incidence scattering +- Fix several bugs related to pyFAI-diffmap GUI/no-GUI options +- Compatibility with numpy2 +- Fix numerical precision issue on MacOS/arm64 +- Build system moved from bob to cibuildwheels + + Windows wheels are build with openmp disabled + 2024.2 01/02/2024 ----------------- - Include grazing-incidence capabilities + tutorial (thanks Edgar) diff --git a/src/pyFAI/test/test_geometry.py b/src/pyFAI/test/test_geometry.py index 8b986fc04..ba60bff24 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-2023 European Synchrotron Radiation Facility, Grenoble, France +# Copyright (C) 2015-2024 European Synchrotron Radiation Facility, Grenoble, France # # Principal author: Jérôme Kieffer (Jerome.Kieffer@ESRF.eu) # @@ -34,7 +34,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "07/05/2024" +__date__ = "16/05/2024" import unittest import random @@ -43,7 +43,6 @@ import itertools import logging import os.path -import platform from . import utilstest logger = logging.getLogger(__name__) @@ -54,6 +53,7 @@ from ..detectors import detector_factory from ..third_party import transformations from .utilstest import UtilsTest +from ..utils.mathutil import allclose_mod import fabio @@ -627,72 +627,89 @@ def test_array_from_unit_chi_center(self): self.assertTrue(179 < r4.max() < 180, "Orientation 4 upperrange matches") 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") + r1 = self.ai1.array_from_unit(unit="2th_rad", typ="corner") + r2 = self.ai2.array_from_unit(unit="2th_rad", typ="corner") + r3 = self.ai3.array_from_unit(unit="2th_rad", typ="corner") + r4 = self.ai4.array_from_unit(unit="2th_rad", typ="corner") + + tth1 = r1[..., 0] + tth2 = r2[..., 0] + tth3 = r3[..., 0] + tth4 = r4[..., 0] + + chi1 = r1[..., 1] + chi2 = r2[..., 1] + chi3 = r3[..., 1] + chi4 = r4[..., 1] + + + sin_chi1 = numpy.sin(chi1) + sin_chi2 = numpy.sin(chi2) + sin_chi3 = numpy.sin(chi3) + sin_chi4 = numpy.sin(chi4) + + cos_chi1 = numpy.cos(chi1) + cos_chi2 = numpy.cos(chi2) + cos_chi3 = numpy.cos(chi3) + cos_chi4 = numpy.cos(chi4) + + # Here we use complex numbers + z1 = tth1*cos_chi1 + tth1*sin_chi1 *1j + z2 = tth2*cos_chi2 + tth2*sin_chi2 *1j + z3 = tth3*cos_chi3 + tth3*sin_chi3 *1j + z4 = tth4*cos_chi4 + tth4*sin_chi4 *1j + + # the mean is not sensitive to 2pi discontinuity in azimuthal direction + z1 = z1.mean(axis=-1) + z2 = z2.mean(axis=-1) + z3 = z3.mean(axis=-1) + z4 = z4.mean(axis=-1) + + self.assertFalse(numpy.allclose(z1, z2), "orientation 1,2 differ") + self.assertFalse(numpy.allclose(z1, z3), "orientation 1,3 differ") + self.assertFalse(numpy.allclose(z1, z3), "orientation 1,3 differ") + self.assertFalse(numpy.allclose(z1, z4), "orientation 1,4 differ") + self.assertFalse(numpy.allclose(z2, z3), "orientation 2,3 differ") + self.assertFalse(numpy.allclose(z2, z4), "orientation 2,4 differ") + self.assertFalse(numpy.allclose(z3, z4), "orientation 3,4 differ") + + #Check that the tranformation is OK. This is with complex number thus dense & complicated ! + self.assertTrue(numpy.allclose(z1, -numpy.fliplr(z2.conj())), "orientation 1,2 flipped") + self.assertTrue(numpy.allclose(z1, -z3[-1::-1, -1::-1]), "orientation 1,3 inversed") + self.assertTrue(numpy.allclose(z1, numpy.flipud(z4.conj())), "orientation 1,4 flipped") + self.assertTrue(numpy.allclose(z2, numpy.flipud(z3.conj())), "orientation 2,3 flipped") + self.assertTrue(numpy.allclose(z2, -z4[-1::-1, -1::-1]), "orientation 2,4 inversion") + self.assertTrue(numpy.allclose(z3, -numpy.fliplr(z4.conj())), "orientation 3,4 flipped") - tth1 = r1[..., 0].mean(axis=-1) - chi1 = r1[..., 1].mean(axis=-1) / numpy.pi - tth2 = r2[..., 0].mean(axis=-1) - chi2 = r2[..., 1].mean(axis=-1) / numpy.pi - tth3 = r3[..., 0].mean(axis=-1) - chi3 = r3[..., 1].mean(axis=-1) / numpy.pi - tth4 = r4[..., 0].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") - 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(tth1, numpy.flipud(tth4)), "orientation 1,4 flipped match tth") - 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(tth2, tth4[-1::-1, -1::-1]), "orientation 2,4 inversion match tth") - - # Something fishy on mac-arm64 where this test fails ... correct on all other platforms ! - if platform.system() == "Darwin" and platform.machine()=="arm64": - def angular_distance(a, b, modulo): - return numpy.minimum((a-b)%modulo,(b-a)%modulo) - self.assertLess(angular_distance(chi1 + 1, -numpy.fliplr(chi2), 1).mean(), 0.0001, "orientation 1,2 flipped match chi") - self.assertLess(angular_distance(chi1, -numpy.flipud(chi4), 1).mean(), 0.0001, "orientation 1,4 flipped match chi") - self.assertLess(angular_distance(chi1+1, chi3[-1::-1, -1::-1], 1).mean(), 0.0001, "orientation 1,3 inversion match chi") - self.assertLess(angular_distance(chi2 + 1, chi4[-1::-1, -1::-1], 1).mean(), 0.0001, "orientation 2,4 inversion match chi") - else: - self.assertTrue(numpy.allclose(chi1 + 1, -numpy.fliplr(chi2), atol=0.0001), "orientation 1,2 flipped match chi") - self.assertTrue(numpy.allclose(chi1, -numpy.flipud(chi4)), "orientation 1,4 flipped 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(chi2 + 1, chi4[-1::-1, -1::-1]), "orientation 2,4 inversion match chi") def test_chi(self): + epsilon = 6e-3 orient = {} for i in range(1, 5): - ai = geometry.Geometry.sload({"detector":"pilatus100k", "detector_config":{"orientation":i}, - "poni1":0.01, "poni2":0.01, - "wavelength":1e-10}) + ai = geometry.Geometry.sload({"detector":"Imxpad S10", "detector_config":{"orientation":i}, + "poni1":0.005, "poni2":0.005, "wavelength":1e-10}) chi_c = ai.center_array(unit="chi_rad") / numpy.pi - chi_m = ai.corner_array(unit="chi_rad")[..., 0].mean(axis=-1) / numpy.pi + corners = ai.corner_array(unit="r_mm") + corners_rad = corners[..., 0] + corners_ang = corners[..., 1] + z = corners_rad*numpy.cos(corners_ang) + corners_rad*numpy.sin(corners_ang)*1j + chi_m = numpy.angle(z.mean(axis=-1)) / numpy.pi orient[i] = {"ai": ai, "chi_c": chi_c, "chi_m": chi_m} for o, orien in orient.items(): - self.assertLess(numpy.median(abs(orien["chi_m"] - orien["chi_c"])), 1e-7, f"Orientation {o} matches") + self.assertTrue(allclose_mod(orien["chi_m"], orien["chi_c"], 2), f"Orientation {o} matches") ai = orien["ai"] - self.assertLess(numpy.median(ai.delta_array(unit="chi_rad")) / numpy.pi, 1e-3, f"Orientation {o} delta chi small #0") - self.assertLess(numpy.median(ai.deltaChi()) / numpy.pi, 1e-3, f"Orientation {o} delta chi small #1") + self.assertLess(numpy.median(ai.delta_array(unit="chi_rad")) / numpy.pi, epsilon, f"Orientation {o} delta chi small #0") + self.assertLess(numpy.median(ai.deltaChi()) / numpy.pi, epsilon, f"Orientation {o} delta chi small #1") ai.reset() - self.assertLess(numpy.median(ai.delta_array(unit="chi_rad")) / numpy.pi, 1e-3, f"Orientation {o} delta chi small #2") + self.assertLess(numpy.median(ai.delta_array(unit="chi_rad")) / numpy.pi, epsilon, f"Orientation {o} delta chi small #2") ai.reset() - self.assertLess(numpy.median(ai.deltaChi()) / numpy.pi, 1e-3, f"Orientation {o} delta chi small #3") + self.assertLess(numpy.median(ai.deltaChi()) / numpy.pi, epsilon, f"Orientation {o} delta chi small #3") ai.reset() chiArray = ai.chiArray() / numpy.pi chi_center = orien["chi_c"] - self.assertTrue(numpy.allclose(chiArray, chi_center, atol=1e-5), f"Orientation {o} chiArray == center_array(chi)") + self.assertTrue(allclose_mod(chiArray, chi_center), f"Orientation {o} chiArray == center_array(chi)") class TestOrientation2(unittest.TestCase): @@ -701,7 +718,7 @@ class TestOrientation2(unittest.TestCase): @classmethod def setUpClass(cls) -> None: super(TestOrientation2, cls).setUpClass() - p = detector_factory("Pilatus100k") + p = detector_factory("pilatus100k") c = p.get_pixel_corners() d1 = c[..., 1].max() d2 = c[..., 2].max() diff --git a/src/pyFAI/test/test_utils_mathutil.py b/src/pyFAI/test/test_utils_mathutil.py index 4624be7c0..6529aa7a7 100644 --- a/src/pyFAI/test/test_utils_mathutil.py +++ b/src/pyFAI/test/test_utils_mathutil.py @@ -4,7 +4,7 @@ # Project: Azimuthal integration # https://github.com/silx-kit/pyFAI # -# Copyright (C) 2015-2021 European Synchrotron Radiation Facility, Grenoble, France +# Copyright (C) 2015-2024 European Synchrotron Radiation Facility, Grenoble, France # # Principal author: Jérôme Kieffer (Jerome.Kieffer@ESRF.eu) # @@ -32,7 +32,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "20/02/2024" +__date__ = "16/05/2024" import unittest import numpy @@ -157,6 +157,11 @@ def test_is_far_from_group_cython(self): res = is_far_from_group_cython(pt, pts, dst2) self.assertEqual(ref, res, "cython implementation matches *is_far_from_group*") + def test_allclose_mod(self): + from ..utils.mathutil import allclose_mod + self.assertTrue(allclose_mod(numpy.arctan2(+1e-10, -1), numpy.arctan2(-1e-10, -1)),"angles matches modulo 2pi") + + def suite(): loader = unittest.defaultTestLoader.loadTestsFromTestCase testsuite = unittest.TestSuite() diff --git a/src/pyFAI/utils/mathutil.py b/src/pyFAI/utils/mathutil.py index 4c3aa943f..7bf4ed649 100644 --- a/src/pyFAI/utils/mathutil.py +++ b/src/pyFAI/utils/mathutil.py @@ -4,7 +4,7 @@ # Project: Fast Azimuthal integration # https://github.com/silx-kit/pyFAI # -# Copyright (C) 2017-2018 European Synchrotron Radiation Facility, Grenoble, France +# Copyright (C) 2017-2024 European Synchrotron Radiation Facility, Grenoble, France # # Principal author: Jérôme Kieffer (Jerome.Kieffer@ESRF.eu) # @@ -34,7 +34,7 @@ __contact__ = "Jerome.Kieffer@ESRF.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -__date__ = "20/02/2024" +__date__ = "16/05/2024" __status__ = "production" import logging @@ -394,6 +394,7 @@ def unBinning(*args, **kwargs): return unbinning(*args, **kwargs) + def shift_fft(input_img, shift_val, method="fft"): """Do shift using FFTs @@ -928,3 +929,13 @@ def interp_filter(ary, out=None): out[mask_invalid] = numpy.interp(x[mask_invalid], x[mask_valid], ary[mask_valid], left=first, right=last) return out + + +def allclose_mod(a, b, modulo=2*numpy.pi, **kwargs): + """Returns True if the two arrays a & b are equal within the given + tolerance modulo `modulo`; False otherwise. + + Thanks to "Serguei Sokol" + """ + di = numpy.minimum((a-b)%modulo, (b-a)%modulo) + return numpy.allclose(modulo*0.5, (di+modulo*0.5), **kwargs)