Skip to content

Commit eaf5e67

Browse files
authored
Merge pull request #2385 from kif/2346_quality_of_fit
Quality of geometry fit
2 parents 04e2c43 + 9d47314 commit eaf5e67

File tree

2 files changed

+82
-9
lines changed

2 files changed

+82
-9
lines changed

src/pyFAI/test/test_utils_mathutil.py

Lines changed: 18 additions & 8 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) 2015-2024 European Synchrotron Radiation Facility, Grenoble, France
7+
# Copyright (C) 2015-2025 European Synchrotron Radiation Facility, Grenoble, France
88
#
99
# Principal author: Jérôme Kieffer ([email protected])
1010
#
@@ -32,15 +32,20 @@
3232
__contact__ = "[email protected]"
3333
__license__ = "MIT"
3434
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
35-
__date__ = "16/05/2024"
35+
__date__ = "21/01/2025"
3636

3737
import unittest
3838
import numpy
3939
import os
4040
import logging
4141
from . import utilstest
4242
logger = logging.getLogger(__name__)
43+
import fabio
4344
from .. import utils
45+
from .. import load
46+
from .. import calibrant
47+
from ..utils import mathutil
48+
4449

4550
import scipy.ndimage
4651

@@ -148,19 +153,24 @@ def test_interp_filter(self):
148153
self.assertLess(abs(y - utils.mathutil.interp_filter(z)).max(), 0.01, "error is small")
149154

150155
def test_is_far_from_group_cython(self):
151-
from ..utils.mathutil import is_far_from_group_python, is_far_from_group_cython
152156
rng = utilstest.UtilsTest.get_rng()
153157
pt = rng.uniform(size=2)
154158
pts = list(rng.uniform(size=(10,2)))
155159
dst2 = rng.uniform()**2
156-
ref = is_far_from_group_python(pt, pts, dst2)
157-
res = is_far_from_group_cython(pt, pts, dst2)
160+
ref = mathutil.is_far_from_group_python(pt, pts, dst2)
161+
res = mathutil.is_far_from_group_cython(pt, pts, dst2)
158162
self.assertEqual(ref, res, "cython implementation matches *is_far_from_group*")
159163

160164
def test_allclose_mod(self):
161-
from ..utils.mathutil import allclose_mod
162-
self.assertTrue(allclose_mod(numpy.arctan2(+1e-10, -1), numpy.arctan2(-1e-10, -1)),"angles matches modulo 2pi")
163-
165+
self.assertTrue(mathutil.allclose_mod(numpy.arctan2(+1e-10, -1), numpy.arctan2(-1e-10, -1)),"angles matches modulo 2pi")
166+
167+
def test_quality_of_fit(self):
168+
img = fabio.open(utilstest.UtilsTest.getimage("Pilatus1M.edf")).data
169+
ai = load(utilstest.UtilsTest.getimage("Pilatus1M.poni"))
170+
cal = calibrant.get_calibrant("AgBh")
171+
cal.wavelength = ai.wavelength
172+
res = mathutil.quality_of_fit(img, ai, cal, rings=[0,1], npt_azim=36, npt_rad=100)
173+
self.assertLess(res, 0.3, "Fit of good quality")
164174

165175
def suite():
166176
loader = unittest.defaultTestLoader.loadTestsFromTestCase

src/pyFAI/utils/mathutil.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
__contact__ = "[email protected]"
3535
__license__ = "MIT"
3636
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
37-
__date__ = "13/01/2025"
37+
__date__ = "21/01/2025"
3838
__status__ = "production"
3939

4040
import logging
@@ -44,6 +44,7 @@
4444
import numpy
4545
import time
4646
import scipy.ndimage
47+
from scipy.signal import peak_widths
4748
from .decorators import deprecated
4849

4950
try:
@@ -952,3 +953,65 @@ def allclose_mod(a, b, modulo=2*numpy.pi, **kwargs):
952953
"""
953954
di = numpy.minimum((a-b)%modulo, (b-a)%modulo)
954955
return numpy.allclose(modulo*0.5, (di+modulo*0.5), **kwargs)
956+
957+
958+
def quality_of_fit(img, ai, calibrant,
959+
npt_rad=1000, npt_azim=360,
960+
unit="q_nm^-1",
961+
method=("full", "csr", "cython"),
962+
empty = numpy.nan, rings=None):
963+
"""Provide an indicator for the quality of fit of a given geometry for an image
964+
965+
:param img: 2D image with a calibration image (containing rings)
966+
:param ai: azimuthal integrator object (instance of pyFAI.integrator.azimuthal.AzimuthalIntegrator)
967+
:param calibrant: calibration object, instance of pyFAI.calibrant.Calibrant
968+
:param npt_rad: int with the number of radial bins
969+
:param npt_azim: int with the number of azimuthal bins
970+
:param unit: typically "2th_deg" or "q_nm^-1", the quality of fit should be largely independant from the space.
971+
:param method: integration method
972+
:param empty: value of the empy bins, discarded values
973+
:param rings: list of rings to evaluate (0-based)
974+
:return: QoF indicator, similar to reduced χ², the smaller, the better
975+
"""
976+
977+
ai.empty = empty
978+
q_theo = calibrant.get_peaks(unit=unit)
979+
res = ai.integrate2d(img, npt_rad, npt_azim, method=method, unit=unit)
980+
if rings is None:
981+
rings = list(range(len(calibrant.get_2th())))
982+
q_theo = q_theo[rings]
983+
idx_theo = abs(numpy.add.outer(res.radial,-q_theo)).argmin(axis=0)
984+
idx_maxi = numpy.empty((res.azimuthal.size, q_theo.size))+numpy.nan
985+
idx_fwhm = numpy.empty((res.azimuthal.size, q_theo.size))+numpy.nan
986+
signal = res.intensity
987+
gradient = numpy.gradient(signal, axis=-1)
988+
minima = numpy.where(numpy.logical_and(gradient[:,:-1]<0, gradient[:,1:]>=0))
989+
maxima = numpy.where(numpy.logical_and(gradient[:,:-1]>0, gradient[:,1:]<0))
990+
for idx in range(res.azimuthal.size):
991+
for ring in rings:
992+
q_th = q_theo[ring]
993+
idx_th = idx_theo[ring]
994+
if (q_th<=res.radial[0]) or (q_th>=res.radial[-1]):
995+
continue
996+
maxi = maxima[1][maxima[0]==idx]
997+
mini = minima[1][minima[0]==idx]
998+
idx_max = maxi[abs(maxi-idx_th).argmin()]
999+
idx_inf = mini[mini<idx_max]
1000+
if idx_inf.size:
1001+
idx_inf = idx_inf[-1]
1002+
idx_sup = mini[mini>idx_max]
1003+
if idx_sup.size:
1004+
idx_sup = idx_sup[0]
1005+
if idx_inf< idx_th< idx_sup:
1006+
sub = signal[idx, idx_inf:idx_sup+1] - numpy.linspace(signal[idx, idx_inf],signal[idx, idx_sup], 1+idx_sup-idx_inf)
1007+
com = (sub*numpy.linspace(idx_inf, idx_sup, 1+idx_sup-idx_inf)).sum()/sub.sum()
1008+
if numpy.isfinite(com):
1009+
width = peak_widths(sub, [numpy.argmax(sub)])[0][0]
1010+
if width==0:
1011+
print(f" #{idx},{ring}: {idx_inf} < th:{idx_th} max:{idx_max} com:{com:.3f} < {idx_sup}; fwhm={width}")
1012+
print(signal[idx, idx_inf:idx_sup+1])
1013+
print(sub)
1014+
else:
1015+
idx_fwhm[idx, ring] = width
1016+
idx_maxi[idx, ring] = idx_max
1017+
return numpy.nanmean((2.355*(idx_maxi-idx_theo)/idx_fwhm)**2)

0 commit comments

Comments
 (0)