Skip to content

Commit 2760735

Browse files
authored
Merge pull request #2685 from kif/2679_case-insensitive_dataclass
case-insensitive dataclass to prevent some warnings
2 parents 8e55a06 + e081c65 commit 2760735

File tree

7 files changed

+255
-22
lines changed

7 files changed

+255
-22
lines changed

doc/source/changelog.rst

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
:Author: Jérôme Kieffer
2-
:Date: 06/10/2025
2+
:Date: 15/11/2025
33
:Keywords: changelog
44

55
Change-log of versions
66
======================
77

8-
2025.11? YY/11/2025 --> 3.14 comes out in October
8+
2025.11? YY/11/2025 --> 3.14 came out in October, drop 3.9
99
-------------------
1010
- [Parallax] This effect shifts the pixel position when the inclinaison of the beam is large (>30°) making calibration challenging
1111
* Provide absorption coefficients of most common sensor materials
@@ -35,9 +35,8 @@ Change-log of versions
3535
- [integrate1|2d] enforce arguments to be kwargs to limit user errors
3636
- [Doc] Improve the notebook about "flatfield" calculation.
3737
- [Integrate1/2dResult] can now be added or subtracted to perform some basic maths, uncertainties are propagated accordingly.
38-
- [Deprecation]
39-
* `splineFile` --> `splinefile` in most arguments and also as properties
40-
* SPD and FIT2D dataclasses are becoming case insensitive (PEP8 enforcement)
38+
- [Fit2dGeometry] becomes a case-insensitive dataclass (thus mutable, was NamedTuple) which behaves like a dict.
39+
- [Deprecation] `splineFile` --> `splinefile` in most arguments and also as properties
4140
- Prefer the `numexpr` (fallback on `numpy`) function evaluation in favor of the Cython path for geometry initialization, less prone to numerical noise.
4241
Cython is still prefered for geometry optimization where performance is critical.
4342
- Start to support type annotation in the code.

src/pyFAI/geometry/core.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
__contact__ = "Jerome.Kieffer@ESRF.eu"
4141
__license__ = "MIT"
4242
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
43-
__date__ = "11/11/2025"
43+
__date__ = "15/11/2025"
4444
__status__ = "production"
4545
__docformat__ = "restructuredtext"
4646

@@ -1772,9 +1772,7 @@ def getFit2D(self):
17721772
"""
17731773
with self._sem:
17741774
f2d = convert_to_Fit2d(self)
1775-
dico = f2d._asdict()
1776-
dico["splinefile"] = dico.get("splineFile")
1777-
return dico
1775+
return f2d
17781776

17791777
@deprecated_args({"splinefile":"splineFile"}, since_version="2025.10")
17801778
def setFit2D(

src/pyFAI/geometry/fit2d.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Project: Azimuthal integration
55
# https://github.com/silx-kit/pyFAI
66
#
7-
# Copyright (C) 2021-2022 European Synchrotron Radiation Facility, Grenoble, France
7+
# Copyright (C) 2021-2025 European Synchrotron Radiation Facility, Grenoble, France
88
#
99
# Principal author: Jérôme Kieffer (Jerome.Kieffer@ESRF.eu)
1010
#
@@ -33,14 +33,14 @@
3333
__contact__ = "Jerome.Kieffer@ESRF.eu"
3434
__license__ = "MIT"
3535
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
36-
__date__ = "11/11/2025"
36+
__date__ = "15/11/2025"
3737
__status__ = "production"
3838
__docformat__ = 'restructuredtext'
3939

4040
import os
4141
import logging
42-
from typing import NamedTuple
4342
from math import pi, cos, sin, sqrt, acos, asin
43+
from ..utils.dataclasses import case_insensitive_dataclass
4444
from ..detectors import Detector
4545
from ..io.ponifile import PoniFile
4646
logger = logging.getLogger(__name__)
@@ -52,8 +52,11 @@ def radians(deg:float) -> float:
5252
return deg * pi / 180
5353

5454

55-
class Fit2dGeometry(NamedTuple):
56-
""" This object represents the geometry as configured in Fit2D
55+
@case_insensitive_dataclass(slots=True)
56+
class Fit2dGeometry:
57+
""" This object represents the geometry as configured in Fit2D.
58+
59+
It behaves like a dataclass, is case insensitive and can behave like a dict as well but cannnot be extended.
5760
5861
:param directDist: Distance from sample to the detector along the incident beam in mm. The detector may be extrapolated when tilted.
5962
:param centerX: Position of the beam-center on the detector in pixels, along the fastest axis of the image.
@@ -70,23 +73,47 @@ class Fit2dGeometry(NamedTuple):
7073
tiltPlanRotation: float = 0.0
7174
pixelX: float = None
7275
pixelY: float = None
73-
splineFile: str = None
76+
splinefile: str = None
7477
detector: Detector = None
7578
wavelength: float = None
7679

7780
@classmethod
7881
def _fromdict(cls, dico):
7982
"Mirror of _asdict: take the dict and populate the tuple to be returned"
80-
# adaptation of keys, in lower-case
81-
dico_lower = {k.lower():v for k,v in dico.items()}
82-
obj = cls(**{key: dico_lower[key.lower()] for key in [i for i in cls._fields if i.lower() in dico_lower]})
83+
obj = cls(**dico)
8384
return obj
8485

86+
def _asdict(self):
87+
"Mirror of _asdict method from NamedTuple"
88+
return {k: self.__getattr__(k) for k in self.__annotations__}
89+
8590
def __repr__(self):
8691
return f"DirectBeamDist= {self.directDist:.3f} mm\tCenter: x={self.centerX:.3f}, y={self.centerY:.3f} pix\t"\
8792
f"Tilt= {self.tilt:.3f}° tiltPlanRotation= {self.tiltPlanRotation:.3f}°" + \
8893
(f" \N{GREEK SMALL LETTER LAMDA}= {self.wavelength:.3f}\N{LATIN CAPITAL LETTER A WITH RING ABOVE}" if self.wavelength else "")
8994

95+
# dict-like interface:
96+
def __getitem__(self, key:str):
97+
return self.__getattr__(key)
98+
def __setitem__(self, key:str, value):
99+
self.__setattr__(key, value)
100+
def get(self, key:str, default=None):
101+
if key.lower() in self._ci_map:
102+
return self.__getattr__(key)
103+
return default
104+
def __contains__(self, key:str):
105+
return key.lower() in self._ci_map
106+
def __iter__(self):
107+
yield from self._ci_map.values()
108+
def keys(self):
109+
return self._ci_map.values()
110+
def values(self):
111+
return [self.__getattr__(i) for i in self._ci_map.values()]
112+
def items(self):
113+
return [(i, self.__getattr__(i)) for i in self._ci_map.values()]
114+
def __len__(self):
115+
return self._ci_map.__len__()
116+
90117

91118
def convert_to_Fit2d(poni):
92119
"""Convert a Geometry|PONI object to the geometry of Fit2D
@@ -147,7 +174,7 @@ def convert_from_Fit2d(f2d):
147174
"""
148175
if not isinstance(f2d, Fit2dGeometry):
149176
if isinstance(f2d, dict):
150-
f2d = Fit2dGeometry(**f2d)
177+
f2d = Fit2dGeometry._fromdict(f2d)
151178
else:
152179
f2d = Fit2dGeometry(f2d)
153180
res = PoniFile()

src/pyFAI/test/test_geometry.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
__contact__ = "Jerome.Kieffer@ESRF.eu"
3535
__license__ = "MIT"
3636
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
37-
__date__ = "10/11/2025"
37+
__date__ = "14/11/2025"
3838

3939
import unittest
4040
import random
@@ -48,14 +48,16 @@
4848
from math import pi
4949
from . import utilstest
5050
from ..io.ponifile import PoniFile
51-
from .. import geometry
51+
from .. import geometry, load
5252
from ..integrator.azimuthal import AzimuthalIntegrator
5353
from .. import units
5454
from ..detectors import detector_factory
5555
from ..third_party import transformations
5656
from .utilstest import UtilsTest
5757
from ..utils.mathutil import allclose_mod
5858
from ..geometry.crystfel import build_geometry, parse_crystfel_geom
59+
from ..geometry.fit2d import Fit2dGeometry
60+
5961
logger = logging.getLogger(__name__)
6062

6163

@@ -618,6 +620,14 @@ def test_bug2024(self):
618620
self.assertLess(delta_array.max(), numpy.pi, "delta_array is less than pi")
619621
self.assertTrue(numpy.allclose(delta_array, deltaChi, atol=7e-6), "delta_array matches deltaChi")
620622

623+
def test_bug2679(self):
624+
ai = load({"dist":0.1, "rot1":0.1, "detector":"Pilatus100k"})
625+
with utilstest.TestLogging(logger='pyFAI.DEPRECATION', warning=0):
626+
f2d = ai.getFit2D()
627+
f2ddc = Fit2dGeometry(**f2d)
628+
f2ddc.tilt=0
629+
ai.setFit2D(**f2ddc._asdict())
630+
621631

622632
class TestOrientation(unittest.TestCase):
623633
"""Simple tests to validate the orientation of the detector"""

src/pyFAI/test/test_utils.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
__contact__ = "Jerome.Kieffer@ESRF.eu"
3333
__license__ = "MIT"
3434
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
35-
__date__ = "10/10/2025"
35+
__date__ = "14/11/2025"
3636

3737
import os
3838
import unittest
@@ -49,6 +49,7 @@
4949
from ..utils.stringutil import to_scientific_unicode
5050
from ..utils.multiprocessing import cpu_count
5151
from ..utils.mask_utils import search_gaps, build_gaps
52+
from ..utils.dataclasses import case_insensitive_dataclass
5253
logger = logging.getLogger(__name__)
5354

5455

@@ -118,6 +119,35 @@ def test_mask(self):
118119
mask[-1, :] =1
119120
numpy.allclose(build_gaps(mask.shape, search_gaps(mask)), mask)
120121

122+
def test_case_insensitive_dataclass(self):
123+
@case_insensitive_dataclass
124+
class Person:
125+
Name: str
126+
Age: int = 0
127+
a = Person("Alice", 10)
128+
self.assertAlmostEqual(a.Name, "Alice")
129+
self.assertAlmostEqual(a.name, "Alice")
130+
self.assertAlmostEqual(a.Age, 10)
131+
self.assertAlmostEqual(a.age, 10)
132+
133+
a = Person("Alice")
134+
self.assertAlmostEqual(a.Name, "Alice")
135+
self.assertAlmostEqual(a.name, "Alice")
136+
self.assertAlmostEqual(a.Age, 0)
137+
self.assertAlmostEqual(a.age, 0)
138+
139+
a = Person("Alice", age=10)
140+
self.assertAlmostEqual(a.Name, "Alice")
141+
self.assertAlmostEqual(a.name, "Alice")
142+
self.assertAlmostEqual(a.Age, 10)
143+
self.assertAlmostEqual(a.age, 10)
144+
145+
a = Person(age=10, name="Alice")
146+
self.assertAlmostEqual(a.Name, "Alice")
147+
self.assertAlmostEqual(a.name, "Alice")
148+
self.assertAlmostEqual(a.Age, 10)
149+
self.assertAlmostEqual(a.age, 10)
150+
121151

122152
def suite():
123153
loader = unittest.defaultTestLoader.loadTestsFromTestCase

0 commit comments

Comments
 (0)