|
34 | 34 | |
35 | 35 | __license__ = "MIT" |
36 | 36 | __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" |
37 | | -__date__ = "13/01/2025" |
| 37 | +__date__ = "21/01/2025" |
38 | 38 | __status__ = "production" |
39 | 39 |
|
40 | 40 | import logging |
|
44 | 44 | import numpy |
45 | 45 | import time |
46 | 46 | import scipy.ndimage |
| 47 | +from scipy.signal import peak_widths |
47 | 48 | from .decorators import deprecated |
48 | 49 |
|
49 | 50 | try: |
@@ -952,3 +953,65 @@ def allclose_mod(a, b, modulo=2*numpy.pi, **kwargs): |
952 | 953 | """ |
953 | 954 | di = numpy.minimum((a-b)%modulo, (b-a)%modulo) |
954 | 955 | 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