Skip to content

Commit a0904a0

Browse files
committed
♻️ Refactored xtl.diffraction correlate fft CLI
xtl.cli.diffraction.correlate.fft:cli_diffraction_correlate_fft() - Refactored to make use of the new radial unit converters and dataclasses - New `--fill` option to replace NaN values in the CCF to allow calculation of the FFT (will be reworked in the future) xtl.cli.diffraction.correlate.qq:cli_diffraction_correlate_qq() - Fixed the y-axis label on the average CCF plot from CCF_χ to CCF_Δ xtl.cli.diffraction:cli_utils - Removed the `units_repr()` function that is no longer necessary - Updated `get_radial_units_from_header()` to return a `RadialUnit` instance
1 parent 4497299 commit a0904a0

File tree

3 files changed

+69
-57
lines changed

3 files changed

+69
-57
lines changed

src/xtl/cli/diffraction/cli_utils.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pyFAI.detectors import Detector
77

88
from xtl.diffraction.images.images import Image
9+
from xtl.units.crystallography.radial import RadialUnit, RadialUnitType
910

1011

1112
class ZScale(Enum):
@@ -88,20 +89,12 @@ def get_geometry_from_header(header: str) -> Geometry:
8889
return Geometry(**kwargs)
8990

9091

91-
def get_radial_units_from_header(header: str) -> Optional[IntegrationRadialUnits]:
92+
def get_radial_units_from_header(header: str) -> Optional[RadialUnit]:
9293
for line in header.splitlines():
9394
if line.startswith('pyFAI.AzimuthalIntegrator.unit'):
9495
units = line.split(':')[-1].strip()
9596
if units in ['2th_deg', '2theta', '2th']:
96-
return IntegrationRadialUnits.TWOTHETA_DEG
97+
return RadialUnit.from_type(RadialUnitType.TWOTHETA_DEG)
9798
elif units in ['q_nm', 'q', 'q_nm^-1']:
98-
return IntegrationRadialUnits.Q_NM
99-
return None
100-
101-
102-
def units_repr(units: IntegrationRadialUnits) -> Optional[tuple[str, str]]:
103-
if units == IntegrationRadialUnits.TWOTHETA_DEG:
104-
return '2\u03b8', '\u00b0'
105-
elif units == IntegrationRadialUnits.Q_NM:
106-
return 'q', 'nm\u207B\u00B9'
99+
return RadialUnit.from_type(RadialUnitType.Q_NM)
107100
return None

src/xtl/cli/diffraction/correlate/fft.py

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
from pathlib import Path
2-
from typing import Optional
32

43
import matplotlib.pyplot as plt
54
import numpy as np
6-
from rich.progress import Progress, SpinnerColumn, TimeElapsedColumn, MofNCompleteColumn
75
import typer
86

97
from xtl.cli.cliio import Console, epilog
108
from xtl.cli.utils import Timer
11-
from xtl.cli.diffraction.cli_utils import (IntegrationRadialUnits, get_geometry_from_header,
12-
get_radial_units_from_header, units_repr)
13-
from xtl.diffraction.images.correlators import AzimuthalCrossCorrelatorQQ_1
9+
from xtl.cli.diffraction.cli_utils import get_geometry_from_header, get_radial_units_from_header
1410
from xtl.exceptions.utils import Catcher
1511
from xtl.files.npx import NpxFile
16-
17-
import warnings
12+
from xtl.units.crystallography.radial import RadialUnitType, RadialValue
1813

1914

2015
app = typer.Typer()
@@ -25,13 +20,18 @@ def cli_diffraction_correlate_fft(
2520
ccf_file: Path = typer.Argument(..., metavar='ccf.npx', help='CCF file to analyze'),
2621
ai2_file: Path = typer.Argument(None, metavar='ai2.npx', help='Azimuthal integration file'),
2722
# Selection parameters
28-
selection_2theta: float = typer.Option(None, '-tt', '--2theta', help='2\u03b8 angle to inspect (in \u00b0)',
23+
selection_2theta: float = typer.Option(None, '-t', '--2theta', help='2\u03b8 angle to inspect (in \u00b0)',
2924
rich_help_panel='Selection parameters'),
3025
selection_q: float = typer.Option(None, '-q', '--q', help='Q value to inspect (in nm\u207B\u00B9)',
3126
rich_help_panel='Selection parameters'),
3227
# FFT parameters
3328
no_coeffs: int = typer.Option(24, '-n', '--no-coeffs', help='Number of Fourier components to display',
3429
rich_help_panel='FFT parameters'),
30+
fill_value: float = typer.Option(None, '--fill', help='Fill NaN values in CCF with a given value',
31+
rich_help_panel='FFT parameters'),
32+
# Plotting parameters
33+
# polar_plots: bool = typer.Option(False, '--polar', help='Plot CCF and azimuthal integration in polar coordinates',
34+
# rich_help_panel='Plotting parameters'),
3535
# Debugging
3636
verbose: int = typer.Option(0, '-v', '--verbose', count=True, help='Print additional information',
3737
rich_help_panel='Debugging'),
@@ -47,8 +47,11 @@ def cli_diffraction_correlate_fft(
4747
elif selection_2theta is not None and selection_q is not None:
4848
cli.print('Select only one parameter to calculate FFT (--2theta, --q)', style='red')
4949
raise typer.Abort()
50-
selection_units = IntegrationRadialUnits.Q_NM if selection_q is not None else IntegrationRadialUnits.TWOTHETA_DEG
51-
selection = selection_q if selection_q is not None else selection_2theta
50+
51+
if selection_2theta is not None:
52+
selection = RadialValue(value=selection_2theta, type=RadialUnitType.TWOTHETA_DEG)
53+
else:
54+
selection = RadialValue(value=selection_q, type=RadialUnitType.Q_NM)
5255

5356
# Load CCF data
5457
with Catcher(echo_func=cli.print, traceback_func=cli.print_traceback) as catcher:
@@ -86,89 +89,104 @@ def cli_diffraction_correlate_fft(
8689
raise typer.Abort()
8790

8891
# Get the radial units of the CCF
89-
radial_units = get_radial_units_from_header(acc.header)
90-
if radial_units is None:
92+
r = get_radial_units_from_header(acc.header)
93+
if r is None:
9194
cli.print('Error: Failed to get radial units from CCF file header', style='red')
9295
raise typer.Abort()
93-
u = units_repr(radial_units)
94-
cli.print(f'Radial units in CCF file: {u[0]} ({u[1]})')
96+
cli.print(f'Radial units in CCF file: {r.name.latex} ({r.unit.latex})')
9597

9698
# Convert selection units to the units of CCF if necessary
97-
if radial_units != selection_units:
98-
previous_selection = selection
99-
if radial_units == IntegrationRadialUnits.Q_NM: # Convert 2theta to q
100-
selection = 4 * np.pi / (geometry.wavelength / 1e-10) * np.sin(np.radians(selection / 2))
101-
selection *= 10 # Convert 1/A to nm^-1
102-
elif radial_units == IntegrationRadialUnits.TWOTHETA_DEG: # Convert q to 2theta
103-
selection /= 10 # Convert nm^-1 to 1/A
104-
selection = 2 * np.degrees(np.arcsin(selection * (geometry.wavelength / 1e-10) / (4 * np.pi)))
105-
else:
106-
cli.print(f'Error: Invalid radial units {radial_units!r}', style='red')
107-
raise typer.Abort()
108-
u0 = units_repr(selection_units)
109-
u1 = units_repr(radial_units)
110-
cli.print(f'Converted selection from {u0[0]}={previous_selection:.4f} ({u0[1]}) '
111-
f'to {u1[0]}={selection:.4f} ({u1[1]}) using \u03bb={geometry.wavelength / 1e-10:.4f} (\u212b)')
99+
wavelength = geometry.wavelength / 1e-10
100+
if r != selection.units:
101+
with Catcher(echo_func=cli.print, traceback_func=cli.print_traceback) as catcher:
102+
n0, v0, u0 = selection.name.latex, selection.value, selection.units.latex
103+
selection = selection.to(units=r, wavelength=wavelength)
104+
n1, v1, u1 = selection.name.latex, selection.value, selection.units.latex
105+
106+
u0 = u0 if u0 == '\u00b0' else f' {u0}'
107+
u1 = u1 if u1 == '\u00b0' else f' {u1}'
108+
cli.print(f'Converted selection from {n0}={v0:.4f}{u0} '
109+
f'to {n1}={v1:.4f}{u1} using \u03bb={wavelength:.4f} \u212b')
112110

113111
# Check if selection is within the radial range of the CCF
114112
radial_min, radial_max = acc.data['radial'].min(), acc.data['radial'].max()
115-
if selection < radial_min or selection > radial_max:
113+
if selection.value < radial_min or selection.value > radial_max:
116114
cli.print(f'Error: Selection is outside the radial range of the CCF: {[radial_min, radial_max]}', style='red')
117115
raise typer.Abort()
118116

119117
# Get the radial index of the selection
120-
ccf_i = np.argmin(np.abs(acc.data['radial'] - selection))
118+
ccf_i = np.argmin(np.abs(acc.data['radial'] - selection.value))
121119

122120
# Calculate the FFT of the CCF
123121
if verbose:
124-
cli.print(f'Calculating FFT(CCF)... ')
125-
with Timer(silent=verbose<=1, echo_func=cli.print):
122+
cli.print(f'Calculating FFT... ')
123+
with Timer(silent=verbose<1, echo_func=cli.print):
126124
ccf = acc.data['ccf']
127125
ccf_delta = ccf[:, ccf_i]
126+
if np.any(np.isnan(ccf_delta)):
127+
if fill_value is not None:
128+
cli.print(f'Replacing missing values in CCF with {fill_value}', style='yellow')
129+
ccf_delta = np.nan_to_num(ccf_delta, nan=fill_value)
130+
else:
131+
cli.print('Warning: NaN values found in CCF data. FFT will be empty. '
132+
'Use --fill to replace the NaNs.', style='yellow')
128133
fc = np.fft.fft(ccf_delta) / len(ccf_delta)
129134
fc = np.abs(fc)
130135

136+
# Calculate selection in 2theta, q and d
137+
tth = selection.to(RadialUnitType.TWOTHETA_DEG, wavelength=wavelength)
138+
q = selection.to(RadialUnitType.Q_NM, wavelength=wavelength)
139+
d = selection.to(RadialUnitType.D_A, wavelength=wavelength)
140+
subtitle = (f'{tth.name.latex}={tth.value:.4f}{tth.units.latex} | '
141+
f'{q.name.latex}={q.value:.4f} {q.units.latex} | '
142+
f'{d.name.latex}={d.value:.2f} {d.units.latex}')
143+
131144
# Prepare plots
132145
fig = plt.figure('XCCA overview', figsize=(16 / 1.2, 9 / 1.2))
133-
fig.suptitle(f'{ccf_file.name} {u[0]}={selection:.4f} {u[1]}')
146+
fig.suptitle(f'{ccf_file.name}\n{subtitle}')
134147
gs = fig.add_gridspec(2, 3, wspace=0.2,)
135148
ax0 = fig.add_subplot(gs[0, 0]) # CCF 2D
136149
ax1 = fig.add_subplot(gs[1, 0]) # Azimuthal integration 2D
137150
ax2 = fig.add_subplot(gs[0, 1]) # CCF 1D
151+
# ax2 = fig.add_subplot(gs[0, 1], projection='polar') # CCF 1D
138152
ax3 = fig.add_subplot(gs[1, 1]) # Azimuthal integration 1D
153+
# ax3 = fig.add_subplot(gs[1, 1], projection='polar') # Azimuthal integration 1D
139154
ax4 = fig.add_subplot(gs[:, 2]) # FFT
140155

141156
ccf, radial, delta = acc.data['ccf'], acc.data['radial'], acc.data['delta']
142157
ax0.imshow(ccf, origin='lower', aspect='auto', interpolation='nearest', cmap='Spectral',
143158
#norm=norm(vmin=zmin, vmax=zmax),
144159
extent=(radial.min(), radial.max(), delta.min(), delta.max()))
145-
ax0.vlines(selection, delta.min(), delta.max(), 'r', '--')
160+
ax0.vlines(selection.value, delta.min(), delta.max(), 'r', '--')
146161
ax0.set_title('2D Cross-correlation function')
147-
ax0.set_ylabel(f'Angular offset / \u0394 ({u[1]})')
148-
ax0.set_xlabel(f'Radial angle / {u[0]} ({u[1]})')
162+
ax0.set_ylabel(f'\u0394 (\u00b0)')
163+
ax0.set_xlabel(f'{selection.name.latex} ({selection.units.latex})')
149164

150-
ax2.plot(delta, ccf[:, ccf_i])
165+
ax2.plot(delta, ccf[:, ccf_i], color='xkcd:light brown')
166+
# ax2.plot(delta * np.pi / 180., ccf[:, ccf_i], color='xkcd:light brown')
151167
ax2.set_title('1D Cross-correlation function')
152168
ax2.set_ylabel('Cross-correlation function')
153-
ax2.set_xlabel(f'Angular offset / \u0394 ({u[1]})')
169+
ax2.set_xlabel('\u03c7 (\u00b0)')
154170

155171
if has_ai2:
156172
intensities, radial, azimuthal = ai2.data['intensities'], ai2.data['radial'], ai2.data['azimuthal']
157173
ax1.imshow(intensities, origin='lower', aspect='auto', interpolation='nearest', cmap='magma',
158174
#norm=norm(vmin=zmin, vmax=zmax),
159175
extent=(radial.min(), radial.max(), azimuthal.min(), azimuthal.max()))
160-
ax1.vlines(selection, azimuthal.min(), azimuthal.max(), 'r', '--')
176+
ax1.vlines(selection.value, azimuthal.min(), azimuthal.max(), 'r', '--')
161177
ax1.set_title('2D Azimuthal integration')
162-
ax1.set_ylabel(f'Azimuthal angle / \u03c7 {u[1]}')
163-
ax1.set_xlabel(f'Radial angle / {u[0]} ({u[1]})')
178+
ax1.set_ylabel('\u03c7 (\u00b0)')
179+
ax1.set_xlabel(f'{selection.name.latex} ({selection.units.latex})')
164180

165-
ax3.plot(azimuthal, intensities[:, ccf_i])
181+
ax3.plot(azimuthal, intensities[:, ccf_i], color='xkcd:crimson')
182+
# ax3.plot(azimuthal * np.pi / 180., intensities[:, ccf_i], color='xkcd:crimson')
183+
# ax3.set_ylim(intensities[:, ccf_i].min(), intensities[:, ccf_i].max())
166184
ax3.set_title('1D Azimuthal integration')
167185
ax3.set_ylabel('Intensity')
168-
ax3.set_xlabel(f'Azimuthal angle / \u03c7 ({u[1]})')
186+
ax3.set_xlabel('\u03c7 (\u00b0)')
169187

170188
ax4.bar(range(1, no_coeffs + 1), fc[1:no_coeffs+1])
171-
ax4.set_title('FFT(CCF)')
189+
ax4.set_title('$\u2131\{\mathrm{CCF}(\u0394)\}$')
172190
ax4.set_ylabel('Distribution')
173191
ax4.set_xlabel('Fourier coefficient')
174192

src/xtl/cli/diffraction/correlate/qq.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def cli_diffraction_correlate_qq(
2323
geometry: Path = typer.Option(..., '-g', '--geometry', help='Geometry .PONI file',
2424
exists=True),
2525
# mask: Path = typer.Option(None, '-m', '--mask', help='Mask file'),
26+
# blemishes: Path = typer.Option(None, '-b', '--blemishes', help='Blemishes file'),
2627
# Integration parameters
2728
points_radial: int = typer.Option(300, '-pR', '--points-radial', help='Number of points along the radial axis',
2829
min=50, rich_help_panel='Integration parameters'),
@@ -181,7 +182,7 @@ def cli_diffraction_correlate_qq(
181182
ccf_mean = np.nanmean(accf.ccf, axis=0)
182183
ax4.plot(img.ai1.results.radial, ccf_mean, color='xkcd:plum')
183184
ax4.set_xlabel(accf.units_radial_repr)
184-
ax4.set_ylabel('\u27e8CCF\u27e9$_{\u03c7}$')
185+
ax4.set_ylabel('\u27e8CCF\u27e9$_{\u0394}$')
185186
ax4.set_title('Average CCF')
186187

187188
fig.suptitle(f'{img.file.name} frame #{img.frame}', y=0.95)

0 commit comments

Comments
 (0)