nxs-analysis-tools 0.1.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- _meta/__init__.py +10 -0
- nxs_analysis_tools/__init__.py +15 -0
- nxs_analysis_tools/chess.py +866 -0
- nxs_analysis_tools/datareduction.py +1542 -0
- nxs_analysis_tools/datasets.py +137 -0
- nxs_analysis_tools/fitting.py +301 -0
- nxs_analysis_tools/lineartransformations.py +51 -0
- nxs_analysis_tools/pairdistribution.py +1758 -0
- nxs_analysis_tools-0.1.13.dist-info/METADATA +89 -0
- nxs_analysis_tools-0.1.13.dist-info/RECORD +13 -0
- nxs_analysis_tools-0.1.13.dist-info/WHEEL +5 -0
- nxs_analysis_tools-0.1.13.dist-info/licenses/LICENSE +21 -0
- nxs_analysis_tools-0.1.13.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1758 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tools for generating single crystal pair distribution functions.
|
|
3
|
+
"""
|
|
4
|
+
import time
|
|
5
|
+
import os
|
|
6
|
+
import gc
|
|
7
|
+
import math
|
|
8
|
+
import warnings
|
|
9
|
+
from scipy.ndimage import rotate, affine_transform
|
|
10
|
+
import scipy
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
from matplotlib.transforms import Affine2D
|
|
13
|
+
from nexusformat.nexus import nxsave, NXroot, NXentry, NXdata, NXfield
|
|
14
|
+
import numpy as np
|
|
15
|
+
from astropy.convolution import Kernel, convolve_fft
|
|
16
|
+
import pyfftw
|
|
17
|
+
from .datareduction import plot_slice, reciprocal_lattice_params, Padder, \
|
|
18
|
+
array_to_nxdata
|
|
19
|
+
from .lineartransformations import ShearTransformer
|
|
20
|
+
|
|
21
|
+
__all__ = ['Symmetrizer2D', 'Symmetrizer3D', 'Puncher', 'Interpolator',
|
|
22
|
+
'fourier_transform_nxdata', 'Gaussian3DKernel', 'DeltaPDF',
|
|
23
|
+
'generate_gaussian'
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Symmetrizer2D:
|
|
28
|
+
"""
|
|
29
|
+
A class for symmetrizing 2D datasets.
|
|
30
|
+
|
|
31
|
+
The `Symmetrizer2D` class provides functionality to apply symmetry
|
|
32
|
+
operations such as rotation and mirroring to 2D datasets.
|
|
33
|
+
|
|
34
|
+
Attributes
|
|
35
|
+
----------
|
|
36
|
+
mirror_axis : int or None
|
|
37
|
+
The axis along which mirroring is performed. Default is None, meaning
|
|
38
|
+
no mirroring is applied.
|
|
39
|
+
symmetrized : NXdata or None
|
|
40
|
+
The symmetrized dataset after applying the symmetrization operations.
|
|
41
|
+
Default is None until symmetrization is performed.
|
|
42
|
+
rotations : int or None
|
|
43
|
+
The number of rotations needed to reconstruct the full dataset from
|
|
44
|
+
a single wedge. Default is None until parameters are set.
|
|
45
|
+
transform : Affine2D or None
|
|
46
|
+
The transformation matrix used for skewing and scaling the dataset.
|
|
47
|
+
Default is None until parameters are set.
|
|
48
|
+
mirror : bool or None
|
|
49
|
+
Indicates whether mirroring is performed during symmetrization.
|
|
50
|
+
Default is None until parameters are set.
|
|
51
|
+
skew_angle : float or None
|
|
52
|
+
The skew angle (in degrees) between the principal axes of the plane
|
|
53
|
+
to be symmetrized. Default is None until parameters are set.
|
|
54
|
+
theta_max : float or None
|
|
55
|
+
The maximum angle (in degrees) for symmetrization. Default is None
|
|
56
|
+
until parameters are set.
|
|
57
|
+
theta_min : float or None
|
|
58
|
+
The minimum angle (in degrees) for symmetrization. Default is None
|
|
59
|
+
until parameters are set.
|
|
60
|
+
wedge : NXdata or None
|
|
61
|
+
The dataset wedge used in the symmetrization process. Default is
|
|
62
|
+
None until symmetrization is performed.
|
|
63
|
+
symmetrization_mask : NXdata or None
|
|
64
|
+
The mask used for selecting the region of the dataset to be symmetrized.
|
|
65
|
+
Default is None until symmetrization is performed.
|
|
66
|
+
|
|
67
|
+
Methods
|
|
68
|
+
-------
|
|
69
|
+
__init__(**kwargs):
|
|
70
|
+
Initializes the Symmetrizer2D object and optionally sets the parameters
|
|
71
|
+
using `set_parameters`.
|
|
72
|
+
set_parameters(theta_min, theta_max, lattice_angle=90, mirror=True, mirror_axis=0):
|
|
73
|
+
Sets the parameters for the symmetrization operation, including angle limits,
|
|
74
|
+
lattice angle, and mirroring options.
|
|
75
|
+
symmetrize_2d(data):
|
|
76
|
+
Symmetrizes a 2D dataset based on the set parameters.
|
|
77
|
+
test(data, **kwargs):
|
|
78
|
+
Performs a test visualization of the symmetrization process, displaying the
|
|
79
|
+
original data, mask, wedge, and symmetrized result.
|
|
80
|
+
"""
|
|
81
|
+
symmetrization_mask: NXdata
|
|
82
|
+
|
|
83
|
+
def __init__(self, **kwargs):
|
|
84
|
+
"""
|
|
85
|
+
Initializes the Symmetrizer2D object.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
**kwargs : dict, optional
|
|
90
|
+
Keyword arguments that can be passed to the `set_parameters` method to
|
|
91
|
+
set the symmetrization parameters during initialization.
|
|
92
|
+
"""
|
|
93
|
+
self.mirror_axis = None
|
|
94
|
+
self.symmetrized = None
|
|
95
|
+
self.rotations = None
|
|
96
|
+
self.transform = None
|
|
97
|
+
self.mirror = None
|
|
98
|
+
self.skew_angle = None
|
|
99
|
+
self.theta_max = None
|
|
100
|
+
self.theta_min = None
|
|
101
|
+
self.wedge = None
|
|
102
|
+
if kwargs:
|
|
103
|
+
self.set_parameters(**kwargs)
|
|
104
|
+
|
|
105
|
+
def set_parameters(self, theta_min, theta_max, lattice_angle=90, mirror=True, mirror_axis=None):
|
|
106
|
+
"""
|
|
107
|
+
Sets the parameters for the symmetrization operation, and calculates the
|
|
108
|
+
required transformations and rotations.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
theta_min : float
|
|
113
|
+
The minimum angle in degrees for symmetrization.
|
|
114
|
+
theta_max : float
|
|
115
|
+
The maximum angle in degrees for symmetrization.
|
|
116
|
+
lattice_angle : float, optional
|
|
117
|
+
The angle in degrees between the two principal axes of the plane to be
|
|
118
|
+
symmetrized (default: 90).
|
|
119
|
+
mirror : bool, optional
|
|
120
|
+
If True, perform mirroring during symmetrization (default: True).
|
|
121
|
+
mirror_axis : int, optional
|
|
122
|
+
The axis along which to perform mirroring (default: 0).
|
|
123
|
+
"""
|
|
124
|
+
self.theta_min = theta_min
|
|
125
|
+
self.theta_max = theta_max
|
|
126
|
+
self.skew_angle = lattice_angle
|
|
127
|
+
self.mirror = mirror
|
|
128
|
+
if mirror:
|
|
129
|
+
if mirror_axis is None:
|
|
130
|
+
self.mirror_axis = 0
|
|
131
|
+
warnings.warn(
|
|
132
|
+
"mirror_axis not specified. Defaulting to 0. "
|
|
133
|
+
"Set mirror_axis explicitly when using mirror=True.",
|
|
134
|
+
UserWarning,
|
|
135
|
+
stacklevel=2
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
self.mirror_axis = mirror_axis
|
|
139
|
+
|
|
140
|
+
self.transformer = ShearTransformer(lattice_angle)
|
|
141
|
+
self.transform = self.transformer.t
|
|
142
|
+
|
|
143
|
+
# Calculate number of rotations needed to reconstruct the dataset
|
|
144
|
+
if mirror:
|
|
145
|
+
rotations = abs(int(360 / (theta_max - theta_min) / 2))
|
|
146
|
+
else:
|
|
147
|
+
rotations = abs(int(360 / (theta_max - theta_min)))
|
|
148
|
+
self.rotations = rotations
|
|
149
|
+
|
|
150
|
+
self.symmetrization_mask = None
|
|
151
|
+
|
|
152
|
+
self.wedges = None
|
|
153
|
+
|
|
154
|
+
self.symmetrized = None
|
|
155
|
+
|
|
156
|
+
def symmetrize_2d(self, data):
|
|
157
|
+
"""
|
|
158
|
+
Symmetrizes a 2D dataset based on the set parameters, applying padding
|
|
159
|
+
to prevent rotation cutoff and handling overlapping pixels.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
data : NXdata
|
|
164
|
+
The input 2D dataset to be symmetrized.
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
-------
|
|
168
|
+
symmetrized : NXdata
|
|
169
|
+
The symmetrized 2D dataset.
|
|
170
|
+
"""
|
|
171
|
+
theta_min = self.theta_min
|
|
172
|
+
theta_max = self.theta_max
|
|
173
|
+
mirror = self.mirror
|
|
174
|
+
mirror_axis = self.mirror_axis
|
|
175
|
+
rotations = self.rotations
|
|
176
|
+
|
|
177
|
+
# Pad the dataset so that rotations don't get cutoff if they extend
|
|
178
|
+
# past the extent of the dataset
|
|
179
|
+
p = Padder(data)
|
|
180
|
+
padding = tuple(len(axis) for axis in data.nxaxes)
|
|
181
|
+
data_padded = p.pad(padding)
|
|
182
|
+
|
|
183
|
+
# Define axes that span the plane to be transformed
|
|
184
|
+
q1 = data_padded.nxaxes[0]
|
|
185
|
+
q2 = data_padded.nxaxes[1]
|
|
186
|
+
|
|
187
|
+
# Calculate the angle in radians
|
|
188
|
+
theta = np.arctan2(q1.reshape((-1, 1)), q2.reshape((1, -1)))
|
|
189
|
+
theta = np.mod(theta, 2 * np.pi)
|
|
190
|
+
|
|
191
|
+
# Convert min/max to radians and map to [0, 2pi)
|
|
192
|
+
theta_min_rad = np.deg2rad(theta_min % 360)
|
|
193
|
+
theta_max_rad = np.deg2rad(theta_max % 360)
|
|
194
|
+
|
|
195
|
+
# Handle wraparound cases
|
|
196
|
+
if theta_min_rad <= theta_max_rad:
|
|
197
|
+
symmetrization_mask = (theta >= theta_min_rad) & (theta <= theta_max_rad)
|
|
198
|
+
else:
|
|
199
|
+
symmetrization_mask = (theta >= theta_min_rad) | (theta <= theta_max_rad)
|
|
200
|
+
|
|
201
|
+
# Bring mask from skewed basis to data array basis
|
|
202
|
+
mask = array_to_nxdata(self.transformer.invert(symmetrization_mask), data_padded)
|
|
203
|
+
|
|
204
|
+
# Save mask for user interaction
|
|
205
|
+
self.symmetrization_mask = p.unpad(mask)
|
|
206
|
+
|
|
207
|
+
# Perform masking
|
|
208
|
+
wedge = mask * data_padded
|
|
209
|
+
|
|
210
|
+
# Save wedge for user interaction
|
|
211
|
+
self.wedge = p.unpad(wedge)
|
|
212
|
+
|
|
213
|
+
# Convert wedge back to array for further transformations
|
|
214
|
+
wedge = wedge[data.nxsignal.nxname].nxdata
|
|
215
|
+
|
|
216
|
+
# Bring wedge from data array basis to skewed basis for reconstruction
|
|
217
|
+
wedge = self.transformer.apply(wedge)
|
|
218
|
+
|
|
219
|
+
# Apply additional scaling before rotations
|
|
220
|
+
scale = wedge.shape[0]/wedge.shape[1]
|
|
221
|
+
wedge = affine_transform(wedge,
|
|
222
|
+
Affine2D().scale(scale, 1).get_matrix()[:2, :2],
|
|
223
|
+
offset=[(1 - scale) * wedge.shape[0] / 2, 0],
|
|
224
|
+
order=0,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Reconstruct full dataset from wedge
|
|
228
|
+
reconstructed = np.zeros(wedge.shape)
|
|
229
|
+
|
|
230
|
+
for _ in range(0, rotations):
|
|
231
|
+
reconstructed += wedge
|
|
232
|
+
wedge = rotate(wedge, 360 / rotations, reshape=False, order=0)
|
|
233
|
+
|
|
234
|
+
if mirror:
|
|
235
|
+
reconstructed = np.where(reconstructed == 0,
|
|
236
|
+
reconstructed + np.flip(reconstructed, axis=mirror_axis),
|
|
237
|
+
reconstructed)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# Undo scaling transformation
|
|
241
|
+
reconstructed = affine_transform(reconstructed,
|
|
242
|
+
Affine2D().scale(
|
|
243
|
+
scale, 1
|
|
244
|
+
).inverted().get_matrix()[:2, :2],
|
|
245
|
+
offset=[-(1 - scale) * wedge.shape[
|
|
246
|
+
0] / 2 / scale, 0],
|
|
247
|
+
order=0,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
reconstructed = self.transformer.invert(reconstructed)
|
|
251
|
+
|
|
252
|
+
reconstructed = p.unpad(reconstructed)
|
|
253
|
+
|
|
254
|
+
# Fix any overlapping pixels by truncating counts to max
|
|
255
|
+
reconstructed[reconstructed > data.nxsignal.nxdata.max()] \
|
|
256
|
+
= data.nxsignal.nxdata.max()
|
|
257
|
+
|
|
258
|
+
symmetrized = NXdata(NXfield(reconstructed, name=data.nxsignal.nxname),
|
|
259
|
+
(data.nxaxes[0],
|
|
260
|
+
data.nxaxes[1]))
|
|
261
|
+
|
|
262
|
+
return symmetrized
|
|
263
|
+
|
|
264
|
+
def test(self, data, **kwargs):
|
|
265
|
+
"""
|
|
266
|
+
Performs a test visualization of the symmetrization process to help assess
|
|
267
|
+
the effect of the parameters.
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
data : ndarray
|
|
272
|
+
The input 2D dataset to be used for the test visualization.
|
|
273
|
+
**kwargs : dict
|
|
274
|
+
Additional keyword arguments to be passed to the plot_slice function.
|
|
275
|
+
|
|
276
|
+
Returns
|
|
277
|
+
-------
|
|
278
|
+
fig : Figure
|
|
279
|
+
The matplotlib Figure object that contains the test visualization plot.
|
|
280
|
+
axesarr : ndarray
|
|
281
|
+
The numpy array of Axes objects representing the subplots in the test
|
|
282
|
+
visualization.
|
|
283
|
+
|
|
284
|
+
Notes
|
|
285
|
+
-----
|
|
286
|
+
This method uses the `symmetrize_2d` method to perform the symmetrization on
|
|
287
|
+
the input data and visualize the process.
|
|
288
|
+
|
|
289
|
+
The test visualization plot includes the following subplots:
|
|
290
|
+
- Subplot 1: The original dataset.
|
|
291
|
+
- Subplot 2: The symmetrization mask.
|
|
292
|
+
- Subplot 3: The wedge slice used for reconstruction of the full symmetrized dataset.
|
|
293
|
+
- Subplot 4: The symmetrized dataset.
|
|
294
|
+
|
|
295
|
+
Example
|
|
296
|
+
-------
|
|
297
|
+
>>> s = Symmetrizer2D()
|
|
298
|
+
>>> s.set_parameters(theta_min, theta_max, skew_angle, mirror)
|
|
299
|
+
>>> s.test(data)
|
|
300
|
+
"""
|
|
301
|
+
s = self
|
|
302
|
+
symm_test = s.symmetrize_2d(data)
|
|
303
|
+
fig, axesarr = plt.subplots(2, 2, figsize=(10, 8))
|
|
304
|
+
axes = axesarr.reshape(-1)
|
|
305
|
+
|
|
306
|
+
# Plot the data
|
|
307
|
+
plot_slice(data, skew_angle=s.skew_angle, ax=axes[0], title='data', **kwargs)
|
|
308
|
+
|
|
309
|
+
# Filter kwargs to exclude 'vmin' and 'vmax'
|
|
310
|
+
filtered_kwargs = {key: value for key, value in kwargs.items() if key not in ('vmin', 'vmax')}
|
|
311
|
+
# Plot the mask
|
|
312
|
+
plot_slice(s.symmetrization_mask, skew_angle=s.skew_angle, ax=axes[1], title='mask', **filtered_kwargs)
|
|
313
|
+
|
|
314
|
+
# Plot the wedge
|
|
315
|
+
plot_slice(s.wedge, skew_angle=s.skew_angle, ax=axes[2], title='wedge', **kwargs)
|
|
316
|
+
|
|
317
|
+
# Plot the symmetrized data
|
|
318
|
+
plot_slice(symm_test, skew_angle=s.skew_angle, ax=axes[3], title='symmetrized', **kwargs)
|
|
319
|
+
plt.subplots_adjust(wspace=0.4)
|
|
320
|
+
plt.show()
|
|
321
|
+
return fig, axesarr
|
|
322
|
+
|
|
323
|
+
class Symmetrizer3D:
|
|
324
|
+
"""
|
|
325
|
+
A class to symmetrize 3D datasets by performing sequential 2D symmetrization on
|
|
326
|
+
different planes.
|
|
327
|
+
|
|
328
|
+
This class applies 2D symmetrization on the three principal planes of a 3D dataset,
|
|
329
|
+
effectively enhancing the symmetry of the data across all axes.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
def __init__(self, data=None):
|
|
333
|
+
"""
|
|
334
|
+
Initialize the Symmetrizer3D object with an optional 3D dataset.
|
|
335
|
+
|
|
336
|
+
If data is provided, the corresponding q-vectors and planes are automatically
|
|
337
|
+
set up for symmetrization.
|
|
338
|
+
|
|
339
|
+
Parameters
|
|
340
|
+
----------
|
|
341
|
+
data : NXdata, optional
|
|
342
|
+
The input 3D dataset to be symmetrized.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
if data is None:
|
|
346
|
+
raise ValueError("Symmetrizer3D requires a 3D NXdata object for initialization.")
|
|
347
|
+
|
|
348
|
+
self.a, self.b, self.c, self.al, self.be, self.ga = [None] * 6
|
|
349
|
+
self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = [None] * 6
|
|
350
|
+
self.lattice_params = None
|
|
351
|
+
self.reciprocal_lattice_params = None
|
|
352
|
+
self.symmetrized = None
|
|
353
|
+
self.data = data
|
|
354
|
+
self.plane1symmetrizer = Symmetrizer2D()
|
|
355
|
+
self.plane2symmetrizer = Symmetrizer2D()
|
|
356
|
+
self.plane3symmetrizer = Symmetrizer2D()
|
|
357
|
+
|
|
358
|
+
if data is not None:
|
|
359
|
+
self.q1 = data.nxaxes[0]
|
|
360
|
+
self.q2 = data.nxaxes[1]
|
|
361
|
+
self.q3 = data.nxaxes[2]
|
|
362
|
+
self.plane1 = self.q1.nxname + self.q2.nxname
|
|
363
|
+
self.plane2 = self.q1.nxname + self.q3.nxname
|
|
364
|
+
self.plane3 = self.q2.nxname + self.q3.nxname
|
|
365
|
+
|
|
366
|
+
print("Plane 1: " + self.plane1)
|
|
367
|
+
print("Plane 2: " + self.plane2)
|
|
368
|
+
print("Plane 3: " + self.plane3)
|
|
369
|
+
|
|
370
|
+
def set_data(self, data):
|
|
371
|
+
"""
|
|
372
|
+
Sets the 3D dataset to be symmetrized and updates the corresponding q-vectors and planes.
|
|
373
|
+
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
data : NXdata
|
|
377
|
+
The input 3D dataset to be symmetrized.
|
|
378
|
+
"""
|
|
379
|
+
self.data = data
|
|
380
|
+
self.q1 = data.nxaxes[0]
|
|
381
|
+
self.q2 = data.nxaxes[1]
|
|
382
|
+
self.q3 = data.nxaxes[2]
|
|
383
|
+
self.plane1 = self.q1.nxname + self.q2.nxname
|
|
384
|
+
self.plane2 = self.q1.nxname + self.q3.nxname
|
|
385
|
+
self.plane3 = self.q2.nxname + self.q3.nxname
|
|
386
|
+
|
|
387
|
+
print("Plane 1: " + self.plane1)
|
|
388
|
+
print("Plane 2: " + self.plane2)
|
|
389
|
+
print("Plane 3: " + self.plane3)
|
|
390
|
+
|
|
391
|
+
def set_lattice_params(self, lattice_params):
|
|
392
|
+
"""
|
|
393
|
+
Sets the lattice parameters and calculates the reciprocal lattice parameters.
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
lattice_params : tuple of float
|
|
398
|
+
The lattice parameters (a, b, c, alpha, beta, gamma) in real space.
|
|
399
|
+
"""
|
|
400
|
+
self.a, self.b, self.c, self.al, self.be, self.ga = lattice_params
|
|
401
|
+
self.lattice_params = lattice_params
|
|
402
|
+
self.reciprocal_lattice_params = reciprocal_lattice_params(lattice_params)
|
|
403
|
+
self.a_star, self.b_star, self.c_star, \
|
|
404
|
+
self.al_star, self.be_star, self.ga_star = self.reciprocal_lattice_params
|
|
405
|
+
|
|
406
|
+
def symmetrize(self, positive_values=True):
|
|
407
|
+
"""
|
|
408
|
+
Symmetrize the 3D dataset by sequentially applying 2D symmetrization
|
|
409
|
+
on the three principal planes.
|
|
410
|
+
|
|
411
|
+
This method performs symmetrization along the (q1-q2), (q1-q3),
|
|
412
|
+
and (q2-q3) planes, ensuring that the dataset maintains expected
|
|
413
|
+
symmetry properties. Optionally, negative values resulting from the
|
|
414
|
+
symmetrization process can be set to zero.
|
|
415
|
+
|
|
416
|
+
Parameters
|
|
417
|
+
----------
|
|
418
|
+
positive_values : bool, optional
|
|
419
|
+
If True, sets negative symmetrized values to zero (default is True).
|
|
420
|
+
|
|
421
|
+
Returns
|
|
422
|
+
-------
|
|
423
|
+
NXdata
|
|
424
|
+
The symmetrized 3D dataset stored in the `symmetrized` attribute.
|
|
425
|
+
|
|
426
|
+
Notes
|
|
427
|
+
-----
|
|
428
|
+
- Symmetrization is performed sequentially across three principal
|
|
429
|
+
planes using corresponding 2D symmetrization methods.
|
|
430
|
+
- The process prints progress updates and timing information.
|
|
431
|
+
- If `theta_max` is not set for a particular plane symmetrizer,
|
|
432
|
+
that plane is skipped.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
starttime = time.time()
|
|
436
|
+
data = self.data
|
|
437
|
+
q1, q2, q3 = self.q1, self.q2, self.q3
|
|
438
|
+
out_array = np.zeros(data.nxsignal.shape)
|
|
439
|
+
|
|
440
|
+
if self.plane1symmetrizer.theta_max is not None:
|
|
441
|
+
print('Symmetrizing ' + self.plane1 + ' planes...')
|
|
442
|
+
for k, value in enumerate(q3):
|
|
443
|
+
print(f'Symmetrizing {q3.nxname}={value:.02f}.'
|
|
444
|
+
f'..', end='\r')
|
|
445
|
+
data_symmetrized = self.plane1symmetrizer.symmetrize_2d(data[:, :, k])
|
|
446
|
+
out_array[:, :, k] = data_symmetrized[data.nxsignal.nxname].nxdata
|
|
447
|
+
print('\nSymmetrized ' + self.plane1 + ' planes.')
|
|
448
|
+
|
|
449
|
+
if self.plane2symmetrizer.theta_max is not None:
|
|
450
|
+
print('Symmetrizing ' + self.plane2 + ' planes...')
|
|
451
|
+
for j, value in enumerate(q2):
|
|
452
|
+
print(f'Symmetrizing {q2.nxname}={value:.02f}...', end='\r')
|
|
453
|
+
data_symmetrized = self.plane2symmetrizer.symmetrize_2d(
|
|
454
|
+
NXdata(NXfield(out_array[:, j, :], name=data.nxsignal.nxname), (q1, q3))
|
|
455
|
+
)
|
|
456
|
+
out_array[:, j, :] = data_symmetrized[data.nxsignal.nxname].nxdata
|
|
457
|
+
print('\nSymmetrized ' + self.plane2 + ' planes.')
|
|
458
|
+
|
|
459
|
+
if self.plane3symmetrizer.theta_max is not None:
|
|
460
|
+
print('Symmetrizing ' + self.plane3 + ' planes...')
|
|
461
|
+
for i, value in enumerate(q1):
|
|
462
|
+
print(f'Symmetrizing {q1.nxname}={value:.02f}...', end='\r')
|
|
463
|
+
data_symmetrized = self.plane3symmetrizer.symmetrize_2d(
|
|
464
|
+
NXdata(NXfield(out_array[i, :, :], name=data.nxsignal.nxname), (q2, q3))
|
|
465
|
+
)
|
|
466
|
+
out_array[i, :, :] = data_symmetrized[data.nxsignal.nxname].nxdata
|
|
467
|
+
print('\nSymmetrized ' + self.plane3 + ' planes.')
|
|
468
|
+
|
|
469
|
+
if positive_values:
|
|
470
|
+
out_array[out_array < 0] = 0
|
|
471
|
+
|
|
472
|
+
stoptime = time.time()
|
|
473
|
+
print(f"\nSymmetrization finished in {((stoptime - starttime) / 60):.02f} minutes.")
|
|
474
|
+
|
|
475
|
+
self.symmetrized = NXdata(NXfield(out_array, name=data.nxsignal.nxname),
|
|
476
|
+
tuple(axis for axis in data.nxaxes))
|
|
477
|
+
|
|
478
|
+
return self.symmetrized
|
|
479
|
+
|
|
480
|
+
def save(self, fout_name=None):
|
|
481
|
+
"""
|
|
482
|
+
Save the symmetrized dataset to a NeXus file.
|
|
483
|
+
|
|
484
|
+
Parameters
|
|
485
|
+
----------
|
|
486
|
+
fout_name : str, optional
|
|
487
|
+
The name of the output file. If not provided,
|
|
488
|
+
the default name 'symmetrized.nxs' will be used.
|
|
489
|
+
"""
|
|
490
|
+
print("Saving file...")
|
|
491
|
+
|
|
492
|
+
f = NXroot()
|
|
493
|
+
f['entry'] = NXentry()
|
|
494
|
+
f['entry']['data'] = self.symmetrized
|
|
495
|
+
if fout_name is None:
|
|
496
|
+
fout_name = 'symmetrized.nxs'
|
|
497
|
+
nxsave(fout_name, f)
|
|
498
|
+
print("Output file saved to: " + os.path.join(os.getcwd(), fout_name))
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def generate_gaussian(H, K, L, amp, stddev, lattice_params, coeffs=None, center=None):
|
|
502
|
+
"""
|
|
503
|
+
Generate a 3D Gaussian distribution.
|
|
504
|
+
|
|
505
|
+
This function creates a 3D Gaussian distribution in reciprocal space based
|
|
506
|
+
on the specified parameters.
|
|
507
|
+
|
|
508
|
+
Parameters
|
|
509
|
+
----------
|
|
510
|
+
H, K, L : ndarray
|
|
511
|
+
Arrays specifying the values of H, K, and L coordinates in reciprocal space.
|
|
512
|
+
amp : float
|
|
513
|
+
Amplitude of the Gaussian distribution.
|
|
514
|
+
stddev : float
|
|
515
|
+
Standard deviation of the Gaussian distribution.
|
|
516
|
+
lattice_params : tuple
|
|
517
|
+
Tuple of lattice parameters (a, b, c, alpha, beta, gamma) for the
|
|
518
|
+
reciprocal lattice.
|
|
519
|
+
coeffs : list, optional
|
|
520
|
+
Coefficients for the Gaussian expression, including cross-terms between axes.
|
|
521
|
+
Default is [1, 0, 1, 0, 1, 0],
|
|
522
|
+
corresponding to (1*H**2 + 0*H*K + 1*K**2 + 0*K*L + 1*L**2 + 0*L*H).
|
|
523
|
+
center : tuple
|
|
524
|
+
Tuple of coordinates for the center of the Gaussian. Default is (0,0,0).
|
|
525
|
+
|
|
526
|
+
Returns
|
|
527
|
+
-------
|
|
528
|
+
gaussian : ndarray
|
|
529
|
+
3D Gaussian distribution array.
|
|
530
|
+
"""
|
|
531
|
+
if center is None:
|
|
532
|
+
center=(0,0,0)
|
|
533
|
+
if coeffs is None:
|
|
534
|
+
coeffs = [1, 0, 1, 0, 1, 0]
|
|
535
|
+
a, b, c, al, be, ga = lattice_params
|
|
536
|
+
a_, b_, c_, _, _, _ = reciprocal_lattice_params((a, b, c, al, be, ga))
|
|
537
|
+
H = H-center[0]
|
|
538
|
+
K = K-center[1]
|
|
539
|
+
L = L-center[2]
|
|
540
|
+
H, K, L = np.meshgrid(H, K, L, indexing='ij')
|
|
541
|
+
gaussian = amp * np.exp(-(coeffs[0] * H ** 2 +
|
|
542
|
+
coeffs[1] * (b_ * a_ / (a_ ** 2)) * H * K +
|
|
543
|
+
coeffs[2] * (b_ / a_) ** 2 * K ** 2 +
|
|
544
|
+
coeffs[3] * (b_ * c_ / (a_ ** 2)) * K * L +
|
|
545
|
+
coeffs[4] * (c_ / a_) ** 2 * L ** 2 +
|
|
546
|
+
coeffs[5] * (c_ * a_ / (a_ ** 2)) * L * H) / (2 * stddev ** 2))
|
|
547
|
+
if gaussian.ndim == 3:
|
|
548
|
+
gaussian = gaussian.transpose(1, 0, 2)
|
|
549
|
+
elif gaussian.ndim == 2:
|
|
550
|
+
gaussian = gaussian.transpose()
|
|
551
|
+
return gaussian.transpose(1, 0, 2)
|
|
552
|
+
|
|
553
|
+
class Puncher:
|
|
554
|
+
"""
|
|
555
|
+
A class for applying masks to 3D datasets, typically for data processing in reciprocal space.
|
|
556
|
+
|
|
557
|
+
This class provides methods for setting data, applying masks, and generating
|
|
558
|
+
masks based on various criteria. It can be used to "punch" or modify datasets
|
|
559
|
+
by setting specific regions to NaN according to the mask.
|
|
560
|
+
|
|
561
|
+
Attributes
|
|
562
|
+
----------
|
|
563
|
+
punched : NXdata, optional
|
|
564
|
+
The dataset with regions modified (punched) based on the mask.
|
|
565
|
+
data : NXdata, optional
|
|
566
|
+
The input dataset to be processed.
|
|
567
|
+
HH, KK, LL : ndarray
|
|
568
|
+
Meshgrid arrays representing the H, K, and L coordinates in reciprocal space.
|
|
569
|
+
mask : ndarray, optional
|
|
570
|
+
The mask used for identifying and modifying specific regions in the dataset.
|
|
571
|
+
reciprocal_lattice_params : tuple, optional
|
|
572
|
+
The reciprocal lattice parameters derived from the lattice parameters.
|
|
573
|
+
lattice_params : tuple, optional
|
|
574
|
+
The lattice parameters (a, b, c, alpha, beta, gamma).
|
|
575
|
+
a, b, c, al, be, ga : float
|
|
576
|
+
Individual lattice parameters.
|
|
577
|
+
a_star, b_star, c_star, al_star, be_star, ga_star : float
|
|
578
|
+
Individual reciprocal lattice parameters.
|
|
579
|
+
|
|
580
|
+
Methods
|
|
581
|
+
-------
|
|
582
|
+
set_data(data)
|
|
583
|
+
Sets the dataset to be processed and initializes the coordinate arrays and mask.
|
|
584
|
+
set_lattice_params(lattice_params)
|
|
585
|
+
Sets the lattice parameters and computes the reciprocal lattice parameters.
|
|
586
|
+
add_mask(maskaddition)
|
|
587
|
+
Adds regions to the current mask using a logical OR operation.
|
|
588
|
+
subtract_mask(masksubtraction)
|
|
589
|
+
Removes regions from the current mask using a logical AND NOT operation.
|
|
590
|
+
generate_bragg_mask(punch_radius, coeffs=None, thresh=None)
|
|
591
|
+
Generates a mask for Bragg peaks based on a Gaussian distribution in
|
|
592
|
+
reciprocal space.
|
|
593
|
+
generate_intensity_mask(thresh, radius, verbose=True)
|
|
594
|
+
Generates a mask based on intensity thresholds, including a spherical
|
|
595
|
+
region around high-intensity points.
|
|
596
|
+
generate_mask_at_coord(coordinate, punch_radius, coeffs=None, thresh=None)
|
|
597
|
+
Generates a mask centered at a specific coordinate in reciprocal space
|
|
598
|
+
with a specified radius.
|
|
599
|
+
punch()
|
|
600
|
+
Applies the mask to the dataset, setting masked regions to NaN.
|
|
601
|
+
"""
|
|
602
|
+
|
|
603
|
+
def __init__(self):
|
|
604
|
+
"""
|
|
605
|
+
Initialize the Puncher object.
|
|
606
|
+
|
|
607
|
+
This method sets up the initial state of the Puncher instance, including
|
|
608
|
+
attributes for storing the dataset, lattice parameters, and masks.
|
|
609
|
+
It prepares the object for further data processing and masking operations.
|
|
610
|
+
|
|
611
|
+
Attributes
|
|
612
|
+
----------
|
|
613
|
+
punched : NXdata, optional
|
|
614
|
+
The dataset with modified (punched) regions, initialized as None.
|
|
615
|
+
data : NXdata, optional
|
|
616
|
+
The input dataset to be processed, initialized as None.
|
|
617
|
+
HH, KK, LL : ndarray, optional
|
|
618
|
+
Arrays representing the H, K, and L coordinates in reciprocal space,
|
|
619
|
+
initialized as None.
|
|
620
|
+
mask : ndarray, optional
|
|
621
|
+
The mask for identifying and modifying specific regions in the dataset,
|
|
622
|
+
initialized as None.
|
|
623
|
+
reciprocal_lattice_params : tuple, optional
|
|
624
|
+
The reciprocal lattice parameters, initialized as None.
|
|
625
|
+
lattice_params : tuple, optional
|
|
626
|
+
The lattice parameters (a, b, c, alpha, beta, gamma),
|
|
627
|
+
initialized as None.
|
|
628
|
+
a, b, c, al, be, ga : float
|
|
629
|
+
Individual lattice parameters, initialized as None.
|
|
630
|
+
a_star, b_star, c_star, al_star, be_star, ga_star : float
|
|
631
|
+
Individual reciprocal lattice parameters, initialized as None.
|
|
632
|
+
"""
|
|
633
|
+
self.punched = None
|
|
634
|
+
self.data = None
|
|
635
|
+
self.HH, self.KK, self.LL = [None] * 3
|
|
636
|
+
self.mask = None
|
|
637
|
+
self.reciprocal_lattice_params = None
|
|
638
|
+
self.lattice_params = None
|
|
639
|
+
self.a, self.b, self.c, self.al, self.be, self.ga = [None] * 6
|
|
640
|
+
self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = [None] * 6
|
|
641
|
+
|
|
642
|
+
def set_data(self, data):
|
|
643
|
+
"""
|
|
644
|
+
Set the 3D dataset and initialize the mask if not already set.
|
|
645
|
+
|
|
646
|
+
Parameters
|
|
647
|
+
----------
|
|
648
|
+
data : NXdata
|
|
649
|
+
The dataset to be processed.
|
|
650
|
+
|
|
651
|
+
Notes
|
|
652
|
+
-----
|
|
653
|
+
This method also sets up the H, K, and L coordinate grids for the dataset.
|
|
654
|
+
"""
|
|
655
|
+
self.data = data
|
|
656
|
+
if self.mask is None:
|
|
657
|
+
self.mask = np.zeros(data.nxsignal.nxdata.shape)
|
|
658
|
+
self.HH, self.KK, self.LL = np.meshgrid(data.nxaxes[0],
|
|
659
|
+
data.nxaxes[1],
|
|
660
|
+
data.nxaxes[2],
|
|
661
|
+
indexing='ij')
|
|
662
|
+
|
|
663
|
+
def set_lattice_params(self, lattice_params):
|
|
664
|
+
"""
|
|
665
|
+
Set the lattice parameters and compute the reciprocal lattice parameters.
|
|
666
|
+
|
|
667
|
+
Parameters
|
|
668
|
+
----------
|
|
669
|
+
lattice_params : tuple
|
|
670
|
+
Tuple of lattice parameters (a, b, c, alpha, beta, gamma).
|
|
671
|
+
"""
|
|
672
|
+
self.a, self.b, self.c, self.al, self.be, self.ga = lattice_params
|
|
673
|
+
self.lattice_params = lattice_params
|
|
674
|
+
self.reciprocal_lattice_params = reciprocal_lattice_params(lattice_params)
|
|
675
|
+
self.a_star, self.b_star, self.c_star, \
|
|
676
|
+
self.al_star, self.be_star, self.ga_star = self.reciprocal_lattice_params
|
|
677
|
+
|
|
678
|
+
def add_mask(self, maskaddition):
|
|
679
|
+
"""
|
|
680
|
+
Add regions to the current mask using a logical OR operation.
|
|
681
|
+
|
|
682
|
+
Parameters
|
|
683
|
+
----------
|
|
684
|
+
maskaddition : ndarray
|
|
685
|
+
The mask to be added.
|
|
686
|
+
"""
|
|
687
|
+
self.mask = np.logical_or(self.mask, maskaddition)
|
|
688
|
+
|
|
689
|
+
def subtract_mask(self, masksubtraction):
|
|
690
|
+
"""
|
|
691
|
+
Remove regions from the current mask using a logical AND NOT operation.
|
|
692
|
+
|
|
693
|
+
Parameters
|
|
694
|
+
----------
|
|
695
|
+
masksubtraction : ndarray
|
|
696
|
+
The mask to be subtracted.
|
|
697
|
+
"""
|
|
698
|
+
self.mask = np.logical_and(self.mask, np.logical_not(masksubtraction))
|
|
699
|
+
|
|
700
|
+
def generate_bragg_mask(self, punch_radius, coeffs=None, thresh=None):
|
|
701
|
+
"""
|
|
702
|
+
Generate a mask for Bragg peaks.
|
|
703
|
+
|
|
704
|
+
Parameters
|
|
705
|
+
----------
|
|
706
|
+
punch_radius : float
|
|
707
|
+
Radius for the Bragg peak mask.
|
|
708
|
+
coeffs : list, optional
|
|
709
|
+
Coefficients for the expression of the sphere to be removed around
|
|
710
|
+
each Bragg position, corresponding to coefficients for H, HK, K, KL, L, and LH terms.
|
|
711
|
+
Default is [1, 0, 1, 0, 1, 0].
|
|
712
|
+
thresh : float, optional
|
|
713
|
+
Intensity threshold for applying the mask.
|
|
714
|
+
|
|
715
|
+
Returns
|
|
716
|
+
-------
|
|
717
|
+
mask : ndarray
|
|
718
|
+
Boolean mask identifying the Bragg peaks.
|
|
719
|
+
"""
|
|
720
|
+
if coeffs is None:
|
|
721
|
+
coeffs = [1, 0, 1, 0, 1, 0]
|
|
722
|
+
data = self.data
|
|
723
|
+
H, K, L = self.HH, self.KK, self.LL
|
|
724
|
+
a_, b_, c_, _, _, _ = self.reciprocal_lattice_params
|
|
725
|
+
|
|
726
|
+
mask = (coeffs[0] * (H - np.rint(H)) ** 2 +
|
|
727
|
+
coeffs[1] * (b_ * a_ / (a_ ** 2)) * (H - np.rint(H)) * (K - np.rint(K)) +
|
|
728
|
+
coeffs[2] * (b_ / a_) ** 2 * (K - np.rint(K)) ** 2 +
|
|
729
|
+
coeffs[3] * (b_ * c_ / (a_ ** 2)) * (K - np.rint(K)) * (L - np.rint(L)) +
|
|
730
|
+
coeffs[4] * (c_ / a_) ** 2 * (L - np.rint(L)) ** 2 +
|
|
731
|
+
coeffs[5] * (c_ * a_ / (a_ ** 2)) * (L - np.rint(L)) * (H - np.rint(H))) \
|
|
732
|
+
< punch_radius ** 2
|
|
733
|
+
|
|
734
|
+
if thresh:
|
|
735
|
+
mask = np.logical_and(mask, data.nxsignal > thresh)
|
|
736
|
+
|
|
737
|
+
return mask
|
|
738
|
+
|
|
739
|
+
def generate_intensity_mask(self, thresh, radius, verbose=True):
|
|
740
|
+
"""
|
|
741
|
+
Generate a mask based on intensity thresholds.
|
|
742
|
+
|
|
743
|
+
Parameters
|
|
744
|
+
----------
|
|
745
|
+
thresh : float
|
|
746
|
+
Intensity threshold for creating the mask.
|
|
747
|
+
radius : int
|
|
748
|
+
Radius around high-intensity points to include in the mask.
|
|
749
|
+
verbose : bool, optional
|
|
750
|
+
Whether to print progress information.
|
|
751
|
+
|
|
752
|
+
Returns
|
|
753
|
+
-------
|
|
754
|
+
mask : ndarray
|
|
755
|
+
Boolean mask highlighting regions with high intensity.
|
|
756
|
+
"""
|
|
757
|
+
data = self.data
|
|
758
|
+
counts = data.nxsignal.nxdata
|
|
759
|
+
mask = np.zeros(counts.shape)
|
|
760
|
+
|
|
761
|
+
print(f"Shape of data is {counts.shape}") if verbose else None
|
|
762
|
+
for i in range(counts.shape[0]):
|
|
763
|
+
for j in range(counts.shape[1]):
|
|
764
|
+
for k in range(counts.shape[2]):
|
|
765
|
+
if counts[i, j, k] > thresh:
|
|
766
|
+
# Set the pixels within the sphere to NaN
|
|
767
|
+
for x in range(max(i - radius, 0),
|
|
768
|
+
min(i + radius + 1, counts.shape[0])):
|
|
769
|
+
for y in range(max(j - radius, 0),
|
|
770
|
+
min(j + radius + 1, counts.shape[1])):
|
|
771
|
+
for z in range(max(k - radius, 0),
|
|
772
|
+
min(k + radius + 1, counts.shape[2])):
|
|
773
|
+
mask[x, y, z] = 1
|
|
774
|
+
print(f"Found high intensity at ({i}, {j}, {k}).\t\t", end='\r') \
|
|
775
|
+
if verbose else None
|
|
776
|
+
print("\nDone.")
|
|
777
|
+
return mask
|
|
778
|
+
|
|
779
|
+
def generate_mask_at_coord(self, coordinate, punch_radius, coeffs=None, thresh=None):
|
|
780
|
+
"""
|
|
781
|
+
Generate a mask centered at a specific coordinate.
|
|
782
|
+
|
|
783
|
+
Parameters
|
|
784
|
+
----------
|
|
785
|
+
coordinate : tuple of float
|
|
786
|
+
Center coordinate (H, K, L) for the mask.
|
|
787
|
+
punch_radius : float
|
|
788
|
+
Radius for the mask.
|
|
789
|
+
coeffs : list, optional
|
|
790
|
+
Coefficients for the expression of the sphere to be removed around
|
|
791
|
+
each Bragg position,
|
|
792
|
+
corresponding to coefficients for H, HK, K, KL, L, and LH terms.
|
|
793
|
+
Default is [1, 0, 1, 0, 1, 0].
|
|
794
|
+
thresh : float, optional
|
|
795
|
+
Intensity threshold for applying the mask.
|
|
796
|
+
|
|
797
|
+
Returns
|
|
798
|
+
-------
|
|
799
|
+
mask : ndarray
|
|
800
|
+
Boolean mask for the specified coordinate.
|
|
801
|
+
"""
|
|
802
|
+
if coeffs is None:
|
|
803
|
+
coeffs = [1, 0, 1, 0, 1, 0]
|
|
804
|
+
data = self.data
|
|
805
|
+
H, K, L = self.HH, self.KK, self.LL
|
|
806
|
+
a_, b_, c_, _, _, _ = self.reciprocal_lattice_params
|
|
807
|
+
centerH, centerK, centerL = coordinate
|
|
808
|
+
mask = (coeffs[0] * (H - centerH) ** 2 +
|
|
809
|
+
coeffs[1] * (b_ * a_ / (a_ ** 2)) * (H - centerH) * (K - centerK) +
|
|
810
|
+
coeffs[2] * (b_ / a_) ** 2 * (K - centerK) ** 2 +
|
|
811
|
+
coeffs[3] * (b_ * c_ / (a_ ** 2)) * (K - centerK) * (L - centerL) +
|
|
812
|
+
coeffs[4] * (c_ / a_) ** 2 * (L - centerL) ** 2 +
|
|
813
|
+
coeffs[5] * (c_ * a_ / (a_ ** 2)) * (L - centerL) * (H - centerH)) \
|
|
814
|
+
< punch_radius ** 2
|
|
815
|
+
|
|
816
|
+
if thresh:
|
|
817
|
+
mask = np.logical_and(mask, data.nxsignal > thresh)
|
|
818
|
+
|
|
819
|
+
return mask
|
|
820
|
+
|
|
821
|
+
def punch(self):
|
|
822
|
+
"""
|
|
823
|
+
Apply the mask to the dataset, setting masked regions to NaN.
|
|
824
|
+
|
|
825
|
+
This method creates a new dataset where the masked regions are set to
|
|
826
|
+
NaN, effectively "punching" those regions.
|
|
827
|
+
"""
|
|
828
|
+
data = self.data
|
|
829
|
+
self.punched = NXdata(NXfield(
|
|
830
|
+
np.where(self.mask, np.nan, data.nxsignal.nxdata),
|
|
831
|
+
name=data.nxsignal.nxname),
|
|
832
|
+
(data.nxaxes[0], data.nxaxes[1], data.nxaxes[2])
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _round_up_to_odd_integer(value):
|
|
837
|
+
"""
|
|
838
|
+
Round up a given number to the nearest odd integer.
|
|
839
|
+
|
|
840
|
+
This function takes a floating-point value and rounds it up to the smallest
|
|
841
|
+
odd integer that is greater than or equal to the given value.
|
|
842
|
+
|
|
843
|
+
Parameters
|
|
844
|
+
----------
|
|
845
|
+
value : float
|
|
846
|
+
The input floating-point number to be rounded up.
|
|
847
|
+
|
|
848
|
+
Returns
|
|
849
|
+
-------
|
|
850
|
+
int
|
|
851
|
+
The nearest odd integer greater than or equal to the input value.
|
|
852
|
+
|
|
853
|
+
Examples
|
|
854
|
+
--------
|
|
855
|
+
>>> _round_up_to_odd_integer(4.2)
|
|
856
|
+
5
|
|
857
|
+
|
|
858
|
+
>>> _round_up_to_odd_integer(5.0)
|
|
859
|
+
5
|
|
860
|
+
|
|
861
|
+
>>> _round_up_to_odd_integer(6.7)
|
|
862
|
+
7
|
|
863
|
+
"""
|
|
864
|
+
i = int(math.ceil(value))
|
|
865
|
+
if i % 2 == 0:
|
|
866
|
+
return i + 1
|
|
867
|
+
|
|
868
|
+
return i
|
|
869
|
+
|
|
870
|
+
class Gaussian2DKernel(Kernel):
|
|
871
|
+
"""
|
|
872
|
+
Initialize a 2D Gaussian kernel.
|
|
873
|
+
|
|
874
|
+
This constructor creates a 2D Gaussian kernel with the specified
|
|
875
|
+
standard deviation and size. The Gaussian kernel is generated based on
|
|
876
|
+
the provided coefficients and is then normalized.
|
|
877
|
+
|
|
878
|
+
Parameters
|
|
879
|
+
----------
|
|
880
|
+
stddev : float
|
|
881
|
+
The standard deviation of the Gaussian distribution, which controls
|
|
882
|
+
the width of the kernel.
|
|
883
|
+
|
|
884
|
+
size : tuple of int
|
|
885
|
+
The dimensions of the kernel, given as (x_dim, y_dim).
|
|
886
|
+
|
|
887
|
+
coeffs : list of float, optional
|
|
888
|
+
Coefficients for the Gaussian expression.
|
|
889
|
+
The default is [1, 0, 1] corresponding to the Gaussian form:
|
|
890
|
+
(1 * X^2 + 0 * X * Y + 1 * Y^2).
|
|
891
|
+
|
|
892
|
+
Raises
|
|
893
|
+
------
|
|
894
|
+
ValueError
|
|
895
|
+
If the dimensions in `size` are not positive integers.
|
|
896
|
+
|
|
897
|
+
Notes
|
|
898
|
+
-----
|
|
899
|
+
The kernel is generated over a grid that spans twice the size of
|
|
900
|
+
each dimension, and the resulting array is normalized.
|
|
901
|
+
"""
|
|
902
|
+
_separable = True
|
|
903
|
+
_is_bool = False
|
|
904
|
+
|
|
905
|
+
def __init__(self, stddev, size, coeffs=None):
|
|
906
|
+
if not coeffs:
|
|
907
|
+
coeffs = [1, 0, 1]
|
|
908
|
+
x_dim, y_dim = size
|
|
909
|
+
x = np.linspace(-x_dim, x_dim, int(x_dim) + 1)
|
|
910
|
+
y = np.linspace(-y_dim, y_dim, int(y_dim) + 1)
|
|
911
|
+
X, Y = np.meshgrid(x, y)
|
|
912
|
+
array = np.exp(-(coeffs[0] * X ** 2 +
|
|
913
|
+
coeffs[1] * X * Y +
|
|
914
|
+
coeffs[2] * Y ** 2) / (2 * stddev ** 2)
|
|
915
|
+
)
|
|
916
|
+
self._default_size = _round_up_to_odd_integer(stddev)
|
|
917
|
+
super().__init__(array)
|
|
918
|
+
self.normalize()
|
|
919
|
+
self._truncation = np.abs(1. - self._array.sum())
|
|
920
|
+
|
|
921
|
+
class Gaussian3DKernel(Kernel):
|
|
922
|
+
"""
|
|
923
|
+
Initialize a 3D Gaussian kernel.
|
|
924
|
+
|
|
925
|
+
This constructor creates a 3D Gaussian kernel with the specified
|
|
926
|
+
standard deviation and size. The Gaussian kernel is generated based on
|
|
927
|
+
the provided coefficients and is then normalized.
|
|
928
|
+
|
|
929
|
+
Parameters
|
|
930
|
+
----------
|
|
931
|
+
stddev : float
|
|
932
|
+
The standard deviation of the Gaussian distribution, which controls
|
|
933
|
+
the width of the kernel.
|
|
934
|
+
|
|
935
|
+
size : tuple of int
|
|
936
|
+
The dimensions of the kernel, given as (x_dim, y_dim, z_dim).
|
|
937
|
+
|
|
938
|
+
coeffs : list of float, optional
|
|
939
|
+
Coefficients for the Gaussian expression.
|
|
940
|
+
The default is [1, 0, 1, 0, 1, 0], corresponding to the Gaussian form:
|
|
941
|
+
(1 * X^2 + 0 * X * Y + 1 * Y^2 + 0 * Y * Z + 1 * Z^2 + 0 * Z * X).
|
|
942
|
+
|
|
943
|
+
Raises
|
|
944
|
+
------
|
|
945
|
+
ValueError
|
|
946
|
+
If the dimensions in `size` are not positive integers.
|
|
947
|
+
|
|
948
|
+
Notes
|
|
949
|
+
-----
|
|
950
|
+
The kernel is generated over a grid that spans twice the size of
|
|
951
|
+
each dimension, and the resulting array is normalized.
|
|
952
|
+
"""
|
|
953
|
+
_separable = True
|
|
954
|
+
_is_bool = False
|
|
955
|
+
|
|
956
|
+
def __init__(self, stddev, size, coeffs=None):
|
|
957
|
+
if not coeffs:
|
|
958
|
+
coeffs = [1, 0, 1, 0, 1, 0]
|
|
959
|
+
x_dim, y_dim, z_dim = size
|
|
960
|
+
x = np.linspace(-x_dim, x_dim, int(x_dim) + 1)
|
|
961
|
+
y = np.linspace(-y_dim, y_dim, int(y_dim) + 1)
|
|
962
|
+
z = np.linspace(-z_dim, z_dim, int(z_dim) + 1)
|
|
963
|
+
X, Y, Z = np.meshgrid(x, y, z)
|
|
964
|
+
array = np.exp(-(coeffs[0] * X ** 2 +
|
|
965
|
+
coeffs[1] * X * Y +
|
|
966
|
+
coeffs[2] * Y ** 2 +
|
|
967
|
+
coeffs[3] * Y * Z +
|
|
968
|
+
coeffs[4] * Z ** 2 +
|
|
969
|
+
coeffs[5] * Z * X) / (2 * stddev ** 2)
|
|
970
|
+
)
|
|
971
|
+
self._default_size = _round_up_to_odd_integer(stddev)
|
|
972
|
+
super().__init__(array)
|
|
973
|
+
self.normalize()
|
|
974
|
+
self._truncation = np.abs(1. - self._array.sum())
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
class Interpolator:
|
|
978
|
+
"""
|
|
979
|
+
A class to perform data interpolation using convolution with a specified
|
|
980
|
+
kernel.
|
|
981
|
+
|
|
982
|
+
Attributes
|
|
983
|
+
----------
|
|
984
|
+
interp_time : float or None
|
|
985
|
+
Time taken for the last interpolation operation. Defaults to None.
|
|
986
|
+
|
|
987
|
+
window : ndarray or None
|
|
988
|
+
Window function to be applied to the interpolated data. Defaults to None.
|
|
989
|
+
|
|
990
|
+
interpolated : ndarray or None
|
|
991
|
+
The result of the interpolation operation. Defaults to None.
|
|
992
|
+
|
|
993
|
+
data : NXdata or None
|
|
994
|
+
The dataset to be interpolated. Defaults to None.
|
|
995
|
+
|
|
996
|
+
kernel : ndarray or None
|
|
997
|
+
The kernel used for convolution during interpolation. Defaults to None.
|
|
998
|
+
|
|
999
|
+
tapered : ndarray or None
|
|
1000
|
+
The interpolated data after applying the window function. Defaults to None.
|
|
1001
|
+
"""
|
|
1002
|
+
|
|
1003
|
+
def __init__(self):
|
|
1004
|
+
"""
|
|
1005
|
+
Initialize an Interpolator object.
|
|
1006
|
+
|
|
1007
|
+
Sets up an instance of the Interpolator class with the
|
|
1008
|
+
following attributes initialized to None:
|
|
1009
|
+
|
|
1010
|
+
- interp_time
|
|
1011
|
+
- window
|
|
1012
|
+
- interpolated
|
|
1013
|
+
- data
|
|
1014
|
+
- kernel
|
|
1015
|
+
- tapered
|
|
1016
|
+
|
|
1017
|
+
"""
|
|
1018
|
+
self.interp_time = None
|
|
1019
|
+
self.window = None
|
|
1020
|
+
self.interpolated = None
|
|
1021
|
+
self.data = None
|
|
1022
|
+
self.kernel = None
|
|
1023
|
+
self.tapered = None
|
|
1024
|
+
|
|
1025
|
+
def set_data(self, data):
|
|
1026
|
+
"""
|
|
1027
|
+
Set the dataset to be interpolated.
|
|
1028
|
+
|
|
1029
|
+
Parameters
|
|
1030
|
+
----------
|
|
1031
|
+
data : NXdata
|
|
1032
|
+
The dataset containing the data to be interpolated.
|
|
1033
|
+
"""
|
|
1034
|
+
self.data = data
|
|
1035
|
+
self.interpolated = data
|
|
1036
|
+
self.tapered = data
|
|
1037
|
+
|
|
1038
|
+
def set_kernel(self, kernel):
|
|
1039
|
+
"""
|
|
1040
|
+
Set the kernel to be used for interpolation.
|
|
1041
|
+
|
|
1042
|
+
Parameters
|
|
1043
|
+
----------
|
|
1044
|
+
kernel : ndarray
|
|
1045
|
+
The kernel to be used for convolution during interpolation.
|
|
1046
|
+
"""
|
|
1047
|
+
self.kernel = kernel
|
|
1048
|
+
|
|
1049
|
+
def interpolate(self, verbose=True, positive_values=True):
|
|
1050
|
+
"""
|
|
1051
|
+
Perform interpolation on the dataset using the specified kernel.
|
|
1052
|
+
|
|
1053
|
+
This method convolves the dataset with a kernel using `convolve_fft`
|
|
1054
|
+
to perform interpolation. The resulting interpolated data is stored
|
|
1055
|
+
in the `interpolated` attribute.
|
|
1056
|
+
|
|
1057
|
+
Parameters
|
|
1058
|
+
----------
|
|
1059
|
+
verbose : bool, optional
|
|
1060
|
+
If True, prints progress messages and timing information
|
|
1061
|
+
(default is True).
|
|
1062
|
+
positive_values : bool, optional
|
|
1063
|
+
If True, sets negative interpolated values to zero
|
|
1064
|
+
(default is True).
|
|
1065
|
+
|
|
1066
|
+
Notes
|
|
1067
|
+
-----
|
|
1068
|
+
- The convolution operation is performed in Fourier space.
|
|
1069
|
+
- If a previous interpolation time is recorded, it is displayed
|
|
1070
|
+
before starting a new interpolation.
|
|
1071
|
+
|
|
1072
|
+
Returns
|
|
1073
|
+
-------
|
|
1074
|
+
None
|
|
1075
|
+
"""
|
|
1076
|
+
start = time.time()
|
|
1077
|
+
|
|
1078
|
+
if self.interp_time and verbose:
|
|
1079
|
+
print(f"Last interpolation took {self.interp_time / 60:.2f} minutes.")
|
|
1080
|
+
|
|
1081
|
+
print("Running interpolation...") if verbose else None
|
|
1082
|
+
result = np.real(
|
|
1083
|
+
convolve_fft(self.data.nxsignal.nxdata,
|
|
1084
|
+
self.kernel, allow_huge=True, return_fft=False))
|
|
1085
|
+
print("Interpolation finished.") if verbose else None
|
|
1086
|
+
|
|
1087
|
+
end = time.time()
|
|
1088
|
+
interp_time = end - start
|
|
1089
|
+
|
|
1090
|
+
print(f'Interpolation took {interp_time / 60:.2f} minutes.') if verbose else None
|
|
1091
|
+
|
|
1092
|
+
if positive_values:
|
|
1093
|
+
result[result < 0] = 0
|
|
1094
|
+
self.interpolated = array_to_nxdata(result, self.data)
|
|
1095
|
+
|
|
1096
|
+
def set_tukey_window(self, tukey_alphas=(1.0, 1.0, 1.0)):
|
|
1097
|
+
"""
|
|
1098
|
+
Set a Tukey window function for data tapering.
|
|
1099
|
+
|
|
1100
|
+
Parameters
|
|
1101
|
+
----------
|
|
1102
|
+
tukey_alphas : tuple of floats, optional
|
|
1103
|
+
The alpha parameters for the Tukey window in each
|
|
1104
|
+
dimension (H, K, L). Default is (1.0, 1.0, 1.0).
|
|
1105
|
+
|
|
1106
|
+
Notes
|
|
1107
|
+
-----
|
|
1108
|
+
The window function is generated based on the size of the dataset in each dimension.
|
|
1109
|
+
"""
|
|
1110
|
+
data = self.data
|
|
1111
|
+
tukey_H = np.tile(
|
|
1112
|
+
scipy.signal.windows.tukey(len(data.nxaxes[0]), alpha=tukey_alphas[0])[:, None, None],
|
|
1113
|
+
(1, len(data.nxaxes[1]), len(data.nxaxes[2]))
|
|
1114
|
+
)
|
|
1115
|
+
tukey_K = np.tile(
|
|
1116
|
+
scipy.signal.windows.tukey(len(data.nxaxes[1]), alpha=tukey_alphas[1])[None, :, None],
|
|
1117
|
+
(len(data.nxaxes[0]), 1, len(data.nxaxes[2]))
|
|
1118
|
+
)
|
|
1119
|
+
window = tukey_H * tukey_K
|
|
1120
|
+
|
|
1121
|
+
del tukey_H, tukey_K
|
|
1122
|
+
gc.collect()
|
|
1123
|
+
|
|
1124
|
+
tukey_L = np.tile(
|
|
1125
|
+
scipy.signal.windows.tukey(len(data.nxaxes[2]), alpha=tukey_alphas[2])[None, None, :],
|
|
1126
|
+
(len(data.nxaxes[0]), len(data.nxaxes[1]), 1))
|
|
1127
|
+
window = window * tukey_L
|
|
1128
|
+
|
|
1129
|
+
self.window = window
|
|
1130
|
+
|
|
1131
|
+
def set_hexagonal_tukey_window(self, tukey_alphas=(1.0, 1.0, 1.0, 1.0)):
|
|
1132
|
+
"""
|
|
1133
|
+
Set a hexagonal Tukey window function for data tapering.
|
|
1134
|
+
|
|
1135
|
+
Parameters
|
|
1136
|
+
----------
|
|
1137
|
+
tukey_alphas : tuple of floats, optional
|
|
1138
|
+
The alpha parameters for the Tukey window in each dimension and
|
|
1139
|
+
for the hexagonal truncation (H, HK, K, L).
|
|
1140
|
+
Default is (1.0, 1.0, 1.0, 1.0).
|
|
1141
|
+
|
|
1142
|
+
Notes
|
|
1143
|
+
-----
|
|
1144
|
+
The hexagonal Tukey window is applied to the dataset in a manner that
|
|
1145
|
+
preserves hexagonal symmetry.
|
|
1146
|
+
|
|
1147
|
+
"""
|
|
1148
|
+
data = self.data
|
|
1149
|
+
H_ = data.nxaxes[0]
|
|
1150
|
+
K_ = data.nxaxes[1]
|
|
1151
|
+
L_ = data.nxaxes[2]
|
|
1152
|
+
|
|
1153
|
+
tukey_H = np.tile(
|
|
1154
|
+
scipy.signal.windows.tukey(len(data.nxaxes[0]), alpha=tukey_alphas[0])[:, None, None],
|
|
1155
|
+
(1, len(data.nxaxes[1]), len(data.nxaxes[2]))
|
|
1156
|
+
)
|
|
1157
|
+
tukey_K = np.tile(
|
|
1158
|
+
scipy.signal.windows.tukey(len(data.nxaxes[1]), alpha=tukey_alphas[1])[None, :, None],
|
|
1159
|
+
(len(data.nxaxes[0]), 1, len(data.nxaxes[2]))
|
|
1160
|
+
)
|
|
1161
|
+
window = tukey_H * tukey_K
|
|
1162
|
+
|
|
1163
|
+
del tukey_H, tukey_K
|
|
1164
|
+
gc.collect()
|
|
1165
|
+
|
|
1166
|
+
truncation = int((len(H_) - int(len(H_) * np.sqrt(2) / 2)) / 2)
|
|
1167
|
+
|
|
1168
|
+
tukey_HK = scipy.ndimage.rotate(
|
|
1169
|
+
np.tile(
|
|
1170
|
+
np.concatenate(
|
|
1171
|
+
(np.zeros(truncation)[:, None, None],
|
|
1172
|
+
scipy.signal.windows.tukey(len(H_) - 2 * truncation,
|
|
1173
|
+
alpha=tukey_alphas[2])[:, None, None],
|
|
1174
|
+
np.zeros(truncation)[:, None, None])),
|
|
1175
|
+
(1, len(K_), len(L_))
|
|
1176
|
+
),
|
|
1177
|
+
angle=45, reshape=False, mode='nearest',
|
|
1178
|
+
)[0:len(H_), 0:len(K_), :]
|
|
1179
|
+
tukey_HK = np.nan_to_num(tukey_HK)
|
|
1180
|
+
window = window * tukey_HK
|
|
1181
|
+
|
|
1182
|
+
del tukey_HK
|
|
1183
|
+
gc.collect()
|
|
1184
|
+
|
|
1185
|
+
tukey_L = np.tile(
|
|
1186
|
+
scipy.signal.windows.tukey(len(data.nxaxes[2]), alpha=tukey_alphas[3])[None, None, :],
|
|
1187
|
+
(len(data.nxaxes[0]), len(data.nxaxes[1]), 1)
|
|
1188
|
+
)
|
|
1189
|
+
window = window * tukey_L
|
|
1190
|
+
|
|
1191
|
+
del tukey_L
|
|
1192
|
+
gc.collect()
|
|
1193
|
+
|
|
1194
|
+
self.window = window
|
|
1195
|
+
|
|
1196
|
+
def set_ellipsoidal_tukey_window(self, tukey_alpha=1.0, coeffs=None):
|
|
1197
|
+
"""
|
|
1198
|
+
Set an ellipsoidal Tukey window function for data tapering.
|
|
1199
|
+
|
|
1200
|
+
The Tukey window smoothly tapers the data to zero near the edges of the
|
|
1201
|
+
elliptical region defined by quadratic form coefficients. This helps reduce
|
|
1202
|
+
artifacts in Fourier transforms and other operations sensitive to boundary effects.
|
|
1203
|
+
|
|
1204
|
+
Parameters
|
|
1205
|
+
----------
|
|
1206
|
+
tukey_alpha : float, optional
|
|
1207
|
+
Tapering parameter for the Tukey window, between 0 and 1.
|
|
1208
|
+
- `tukey_alpha = 0` results in a ellipsoidal window (no tapering).
|
|
1209
|
+
- `tukey_alpha = 1` results in a full cosine taper.
|
|
1210
|
+
Default is 1.0.
|
|
1211
|
+
|
|
1212
|
+
coeffs : tuple of float, optional
|
|
1213
|
+
Coefficients `(c0, c1, c2, c3, c4, c5)` defining the ellipsoidal
|
|
1214
|
+
quadratic form:
|
|
1215
|
+
R^2 = c0*H^2 + c1*H*K + c2*K^2 + c3*K*L + c4*L^2 + c5*L*H
|
|
1216
|
+
If None, coefficients are automatically set to match the edges of the
|
|
1217
|
+
reciprocal space axes (H, K, L), which should be appropriate in cases
|
|
1218
|
+
where H, K, and L are orthogonal.
|
|
1219
|
+
|
|
1220
|
+
Notes
|
|
1221
|
+
-----
|
|
1222
|
+
- The maximum allowed radius `Qmax` is determined from the minimum radius
|
|
1223
|
+
value along the edges of reciprocal space.
|
|
1224
|
+
- The Tukey window is applied radially as a function of the distance `R`
|
|
1225
|
+
from the center, defined by the ellipsoidal quadratic form.
|
|
1226
|
+
|
|
1227
|
+
Sets
|
|
1228
|
+
----
|
|
1229
|
+
self.window : ndarray
|
|
1230
|
+
A 3D array of the same shape as the data, containing the Tukey window
|
|
1231
|
+
values between 0 and 1.
|
|
1232
|
+
"""
|
|
1233
|
+
|
|
1234
|
+
# Initialize axes
|
|
1235
|
+
H,K,L = [axis for axis in self.data.nxaxes]
|
|
1236
|
+
|
|
1237
|
+
# Initialize coeffs (default to window reaching edge of array)
|
|
1238
|
+
smallest_extent = np.min([H.max(), K.max(), L.max()])
|
|
1239
|
+
c = coeffs if coeffs is not None else ((smallest_extent / H.max()) ** 2,
|
|
1240
|
+
0,
|
|
1241
|
+
(smallest_extent / K.max()) ** 2,
|
|
1242
|
+
0,
|
|
1243
|
+
(smallest_extent / L.max()) ** 2,
|
|
1244
|
+
0
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
# Create meshgrid
|
|
1248
|
+
HH, KK, LL = np.meshgrid(H,K,L, indexing='ij')
|
|
1249
|
+
|
|
1250
|
+
# Create radius array
|
|
1251
|
+
RR = np.sqrt(
|
|
1252
|
+
c[0] * HH ** 2 +
|
|
1253
|
+
c[1] * HH * KK +
|
|
1254
|
+
c[2] * KK ** 2 +
|
|
1255
|
+
c[3] * KK * LL +
|
|
1256
|
+
c[4] * LL ** 2 +
|
|
1257
|
+
c[5] * LL * HH
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
# Check the edges of reciprocal space to verify Qmax
|
|
1261
|
+
# Create list of pixels where H = H.max() or K = K.max() or L = L.max()
|
|
1262
|
+
edges = np.where(np.logical_or(np.logical_or(HH == H.max(), KK == K.max()), LL == L.max()), RR, RR.max())
|
|
1263
|
+
Qmax = edges.min()
|
|
1264
|
+
alpha = tukey_alpha
|
|
1265
|
+
period = (Qmax * alpha) / np.pi
|
|
1266
|
+
|
|
1267
|
+
window = np.where(RR > Qmax * (1 - alpha), (np.cos((RR - Qmax * (1 - alpha)) / period) + 1) / 2, 1)
|
|
1268
|
+
window = np.where(RR > Qmax, 0, window)
|
|
1269
|
+
|
|
1270
|
+
self.window = window
|
|
1271
|
+
|
|
1272
|
+
def set_window(self, window):
|
|
1273
|
+
"""
|
|
1274
|
+
Set a custom window function for data tapering.
|
|
1275
|
+
|
|
1276
|
+
Parameters
|
|
1277
|
+
----------
|
|
1278
|
+
window : ndarray
|
|
1279
|
+
A custom window function to be applied to the interpolated data.
|
|
1280
|
+
"""
|
|
1281
|
+
self.window = window
|
|
1282
|
+
|
|
1283
|
+
def apply_window(self):
|
|
1284
|
+
"""
|
|
1285
|
+
Apply the window function to the interpolated data.
|
|
1286
|
+
|
|
1287
|
+
The window function, if set, is applied to the `interpolated` data
|
|
1288
|
+
to produce the `tapered` result.
|
|
1289
|
+
|
|
1290
|
+
Returns
|
|
1291
|
+
-------
|
|
1292
|
+
None
|
|
1293
|
+
"""
|
|
1294
|
+
self.tapered = self.interpolated * self.window
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
def fourier_transform_nxdata(data, is_2d=False):
|
|
1298
|
+
"""
|
|
1299
|
+
Perform a 3D Fourier Transform on the given NXdata object.
|
|
1300
|
+
|
|
1301
|
+
This function applies an inverse Fourier Transform to the input data
|
|
1302
|
+
using the `pyfftw` library to optimize performance. The result is a
|
|
1303
|
+
transformed array with spatial frequency components calculated along
|
|
1304
|
+
each axis.
|
|
1305
|
+
|
|
1306
|
+
Parameters
|
|
1307
|
+
----------
|
|
1308
|
+
data : NXdata
|
|
1309
|
+
An NXdata object containing the data to be transformed. It should
|
|
1310
|
+
include the `signal` field for the data and `axes` fields
|
|
1311
|
+
specifying the coordinate axes.
|
|
1312
|
+
|
|
1313
|
+
is_2d : bool
|
|
1314
|
+
If true, skip FFT on out-of-plane direction and only do FFT
|
|
1315
|
+
on axes 0 and 1. Default False.
|
|
1316
|
+
|
|
1317
|
+
Returns
|
|
1318
|
+
-------
|
|
1319
|
+
NXdata
|
|
1320
|
+
A new NXdata object containing the Fourier Transformed data. The
|
|
1321
|
+
result includes:
|
|
1322
|
+
|
|
1323
|
+
- `dPDF`: The transformed data array.
|
|
1324
|
+
- `x`, `y`, `z`: Arrays representing the real-space components along each axis.
|
|
1325
|
+
|
|
1326
|
+
Notes
|
|
1327
|
+
-----
|
|
1328
|
+
- The FFT is performed in two stages: first along the last dimension of the input array and then along the first two dimensions.
|
|
1329
|
+
- The function uses `pyfftw` for efficient computation of the Fourier Transform.
|
|
1330
|
+
- The output frequency components are computed based on the step sizes of the original data axes.
|
|
1331
|
+
|
|
1332
|
+
"""
|
|
1333
|
+
start = time.time()
|
|
1334
|
+
print("Starting FFT.")
|
|
1335
|
+
|
|
1336
|
+
padded = data.nxsignal.nxdata
|
|
1337
|
+
|
|
1338
|
+
fft_array = np.zeros(padded.shape)
|
|
1339
|
+
|
|
1340
|
+
print("FFT on axes 1,2")
|
|
1341
|
+
|
|
1342
|
+
for k in range(0, padded.shape[2]):
|
|
1343
|
+
fft_array[:, :, k] = np.real(
|
|
1344
|
+
np.fft.fftshift(
|
|
1345
|
+
pyfftw.interfaces.numpy_fft.ifftn(np.fft.fftshift(padded[:, :, k]),
|
|
1346
|
+
planner_effort='FFTW_MEASURE'))
|
|
1347
|
+
)
|
|
1348
|
+
print(f'k={k} ', end='\r')
|
|
1349
|
+
|
|
1350
|
+
if not is_2d:
|
|
1351
|
+
print("FFT on axis 3")
|
|
1352
|
+
for i in range(0, padded.shape[0]):
|
|
1353
|
+
for j in range(0, padded.shape[1]):
|
|
1354
|
+
f_slice = fft_array[i, j, :]
|
|
1355
|
+
print(f'i={i} ', end='\r')
|
|
1356
|
+
fft_array[i, j, :] = np.real(
|
|
1357
|
+
np.fft.fftshift(
|
|
1358
|
+
pyfftw.interfaces.numpy_fft.ifftn(np.fft.fftshift(f_slice),
|
|
1359
|
+
planner_effort='FFTW_MEASURE')
|
|
1360
|
+
)
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
end = time.time()
|
|
1364
|
+
print("FFT complete.")
|
|
1365
|
+
print('FFT took ' + str(end - start) + ' seconds.')
|
|
1366
|
+
|
|
1367
|
+
H_step = data.nxaxes[0].nxdata[1] - data.nxaxes[0].nxdata[0]
|
|
1368
|
+
K_step = data.nxaxes[1].nxdata[1] - data.nxaxes[1].nxdata[0]
|
|
1369
|
+
L_step = data.nxaxes[2].nxdata[1] - data.nxaxes[2].nxdata[0]
|
|
1370
|
+
|
|
1371
|
+
fft = NXdata(NXfield(fft_array, name='dPDF'),
|
|
1372
|
+
(NXfield(np.linspace(-0.5 / H_step, 0.5 / H_step, padded.shape[0]), name='x'),
|
|
1373
|
+
NXfield(np.linspace(-0.5 / K_step, 0.5 / K_step, padded.shape[1]), name='y'),
|
|
1374
|
+
NXfield(np.linspace(-0.5 / L_step, 0.5 / L_step, padded.shape[2]), name='z'),
|
|
1375
|
+
)
|
|
1376
|
+
)
|
|
1377
|
+
return fft
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
class DeltaPDF:
|
|
1381
|
+
"""
|
|
1382
|
+
A class for processing and analyzing 3D diffraction data using various\
|
|
1383
|
+
operations, including masking, interpolation, padding, and Fourier
|
|
1384
|
+
transformation.
|
|
1385
|
+
|
|
1386
|
+
Attributes
|
|
1387
|
+
----------
|
|
1388
|
+
fft : NXdata or None
|
|
1389
|
+
The Fourier transformed data.
|
|
1390
|
+
data : NXdata or None
|
|
1391
|
+
The input diffraction data.
|
|
1392
|
+
lattice_params : tuple or None
|
|
1393
|
+
Lattice parameters (a, b, c, al, be, ga).
|
|
1394
|
+
reciprocal_lattice_params : tuple or None
|
|
1395
|
+
Reciprocal lattice parameters (a*, b*, c*, al*, be*, ga*).
|
|
1396
|
+
puncher : Puncher
|
|
1397
|
+
An instance of the Puncher class for generating masks and punching
|
|
1398
|
+
the data.
|
|
1399
|
+
interpolator : Interpolator
|
|
1400
|
+
An instance of the Interpolator class for interpolating and applying
|
|
1401
|
+
windows to the data.
|
|
1402
|
+
padder : Padder
|
|
1403
|
+
An instance of the Padder class for padding the data.
|
|
1404
|
+
mask : ndarray or None
|
|
1405
|
+
The mask used for data processing.
|
|
1406
|
+
kernel : Kernel or None
|
|
1407
|
+
The kernel used for interpolation.
|
|
1408
|
+
window : ndarray or None
|
|
1409
|
+
The window applied to the interpolated data.
|
|
1410
|
+
padded : ndarray or None
|
|
1411
|
+
The padded data.
|
|
1412
|
+
tapered : ndarray or None
|
|
1413
|
+
The data after applying the window.
|
|
1414
|
+
interpolated : NXdata or None
|
|
1415
|
+
The interpolated data.
|
|
1416
|
+
punched : NXdata or None
|
|
1417
|
+
The punched data.
|
|
1418
|
+
|
|
1419
|
+
"""
|
|
1420
|
+
|
|
1421
|
+
def __init__(self):
|
|
1422
|
+
"""
|
|
1423
|
+
Initialize a DeltaPDF object with default attributes.
|
|
1424
|
+
"""
|
|
1425
|
+
self.reciprocal_lattice_params = None
|
|
1426
|
+
self.fft = None
|
|
1427
|
+
self.data = None
|
|
1428
|
+
self.lattice_params = None
|
|
1429
|
+
self.puncher = Puncher()
|
|
1430
|
+
self.interpolator = Interpolator()
|
|
1431
|
+
self.padder = Padder()
|
|
1432
|
+
self.mask = None
|
|
1433
|
+
self.kernel = None
|
|
1434
|
+
self.window = None
|
|
1435
|
+
self.padded = None
|
|
1436
|
+
self.tapered = None
|
|
1437
|
+
self.interpolated = None
|
|
1438
|
+
self.punched = None
|
|
1439
|
+
|
|
1440
|
+
def set_data(self, data):
|
|
1441
|
+
"""
|
|
1442
|
+
Set the input diffraction data and update the Puncher and Interpolator
|
|
1443
|
+
with the data.
|
|
1444
|
+
|
|
1445
|
+
Parameters
|
|
1446
|
+
----------
|
|
1447
|
+
data : NXdata
|
|
1448
|
+
The diffraction data to be processed.
|
|
1449
|
+
"""
|
|
1450
|
+
self.data = data
|
|
1451
|
+
self.puncher.set_data(data)
|
|
1452
|
+
self.interpolator.set_data(data)
|
|
1453
|
+
self.padder.set_data(data)
|
|
1454
|
+
self.tapered = data
|
|
1455
|
+
self.padded = data
|
|
1456
|
+
self.interpolated = data
|
|
1457
|
+
self.punched = data
|
|
1458
|
+
|
|
1459
|
+
def set_lattice_params(self, lattice_params):
|
|
1460
|
+
"""
|
|
1461
|
+
Sets the lattice parameters and calculates the reciprocal lattice
|
|
1462
|
+
parameters.
|
|
1463
|
+
|
|
1464
|
+
Parameters
|
|
1465
|
+
----------
|
|
1466
|
+
lattice_params : tuple of float
|
|
1467
|
+
The lattice parameters (a, b, c, alpha, beta, gamma) in real space.
|
|
1468
|
+
"""
|
|
1469
|
+
self.lattice_params = lattice_params
|
|
1470
|
+
self.puncher.set_lattice_params(lattice_params)
|
|
1471
|
+
self.reciprocal_lattice_params = self.puncher.reciprocal_lattice_params
|
|
1472
|
+
|
|
1473
|
+
def add_mask(self, maskaddition):
|
|
1474
|
+
"""
|
|
1475
|
+
Add regions to the current mask using a logical OR operation.
|
|
1476
|
+
|
|
1477
|
+
Parameters
|
|
1478
|
+
----------
|
|
1479
|
+
maskaddition : ndarray
|
|
1480
|
+
The mask to be added.
|
|
1481
|
+
"""
|
|
1482
|
+
self.puncher.add_mask(maskaddition)
|
|
1483
|
+
self.mask = self.puncher.mask
|
|
1484
|
+
|
|
1485
|
+
def subtract_mask(self, masksubtraction):
|
|
1486
|
+
"""
|
|
1487
|
+
Remove regions from the current mask using a logical AND NOT operation.
|
|
1488
|
+
|
|
1489
|
+
Parameters
|
|
1490
|
+
----------
|
|
1491
|
+
masksubtraction : ndarray
|
|
1492
|
+
The mask to be subtracted.
|
|
1493
|
+
"""
|
|
1494
|
+
self.puncher.subtract_mask(masksubtraction)
|
|
1495
|
+
self.mask = self.puncher.mask
|
|
1496
|
+
|
|
1497
|
+
def generate_bragg_mask(self, punch_radius, coeffs=None, thresh=None):
|
|
1498
|
+
"""
|
|
1499
|
+
Generate a mask for Bragg peaks.
|
|
1500
|
+
|
|
1501
|
+
Parameters
|
|
1502
|
+
----------
|
|
1503
|
+
punch_radius : float
|
|
1504
|
+
Radius for the Bragg peak mask.
|
|
1505
|
+
coeffs : list, optional
|
|
1506
|
+
Coefficients for the expression of the sphere to be removed
|
|
1507
|
+
around each Bragg position, corresponding to coefficients
|
|
1508
|
+
for H, HK, K, KL, L, and LH terms. Default is [1, 0, 1, 0, 1, 0].
|
|
1509
|
+
thresh : float, optional
|
|
1510
|
+
Intensity threshold for applying the mask.
|
|
1511
|
+
|
|
1512
|
+
Returns
|
|
1513
|
+
-------
|
|
1514
|
+
mask : ndarray
|
|
1515
|
+
Boolean mask identifying the Bragg peaks.
|
|
1516
|
+
"""
|
|
1517
|
+
return self.puncher.generate_bragg_mask(punch_radius, coeffs, thresh)
|
|
1518
|
+
|
|
1519
|
+
def generate_intensity_mask(self, thresh, radius, verbose=True):
|
|
1520
|
+
"""
|
|
1521
|
+
Generate a mask based on intensity thresholds.
|
|
1522
|
+
|
|
1523
|
+
Parameters
|
|
1524
|
+
----------
|
|
1525
|
+
thresh : float
|
|
1526
|
+
Intensity threshold for creating the mask.
|
|
1527
|
+
radius : int
|
|
1528
|
+
Radius around high-intensity points to include in the mask.
|
|
1529
|
+
verbose : bool, optional
|
|
1530
|
+
Whether to print progress information.
|
|
1531
|
+
|
|
1532
|
+
Returns
|
|
1533
|
+
-------
|
|
1534
|
+
mask : ndarray
|
|
1535
|
+
Boolean mask highlighting regions with high intensity.
|
|
1536
|
+
"""
|
|
1537
|
+
return self.puncher.generate_intensity_mask(thresh, radius, verbose)
|
|
1538
|
+
|
|
1539
|
+
def generate_mask_at_coord(self, coordinate, punch_radius, coeffs=None, thresh=None):
|
|
1540
|
+
"""
|
|
1541
|
+
Generate a mask centered at a specific coordinate.
|
|
1542
|
+
|
|
1543
|
+
Parameters
|
|
1544
|
+
----------
|
|
1545
|
+
coordinate : tuple of float
|
|
1546
|
+
Center coordinate (H, K, L) for the mask.
|
|
1547
|
+
punch_radius : float
|
|
1548
|
+
Radius for the mask.
|
|
1549
|
+
coeffs : list, optional
|
|
1550
|
+
Coefficients for the expression of the sphere to be removed around
|
|
1551
|
+
each Bragg position, corresponding to coefficients for
|
|
1552
|
+
H, HK, K, KL, L, and LH terms. Default is [1, 0, 1, 0, 1, 0].
|
|
1553
|
+
thresh : float, optional
|
|
1554
|
+
Intensity threshold for applying the mask.
|
|
1555
|
+
|
|
1556
|
+
Returns
|
|
1557
|
+
-------
|
|
1558
|
+
mask : ndarray
|
|
1559
|
+
Boolean mask for the specified coordinate.
|
|
1560
|
+
"""
|
|
1561
|
+
return self.puncher.generate_mask_at_coord(coordinate, punch_radius, coeffs, thresh)
|
|
1562
|
+
|
|
1563
|
+
def punch(self):
|
|
1564
|
+
"""
|
|
1565
|
+
Apply the mask to the dataset, setting masked regions to NaN.
|
|
1566
|
+
|
|
1567
|
+
This method creates a new dataset where the masked regions are set to
|
|
1568
|
+
NaN, effectively "punching" those regions.
|
|
1569
|
+
"""
|
|
1570
|
+
self.puncher.punch()
|
|
1571
|
+
self.punched = self.puncher.punched
|
|
1572
|
+
self.interpolator.set_data(self.punched)
|
|
1573
|
+
|
|
1574
|
+
def set_kernel(self, kernel):
|
|
1575
|
+
"""
|
|
1576
|
+
Set the kernel to be used for interpolation.
|
|
1577
|
+
|
|
1578
|
+
Parameters
|
|
1579
|
+
----------
|
|
1580
|
+
kernel : ndarray
|
|
1581
|
+
The kernel to be used for convolution during interpolation.
|
|
1582
|
+
"""
|
|
1583
|
+
self.interpolator.set_kernel(kernel)
|
|
1584
|
+
self.kernel = kernel
|
|
1585
|
+
|
|
1586
|
+
def interpolate(self, verbose=True, positive_values=True):
|
|
1587
|
+
"""
|
|
1588
|
+
Perform interpolation on the dataset using the specified kernel.
|
|
1589
|
+
|
|
1590
|
+
This method convolves the dataset with a kernel using `convolve_fft`
|
|
1591
|
+
to perform interpolation. The resulting interpolated data is stored
|
|
1592
|
+
in the `interpolated` attribute.
|
|
1593
|
+
|
|
1594
|
+
Parameters
|
|
1595
|
+
----------
|
|
1596
|
+
verbose : bool, optional
|
|
1597
|
+
If True, prints progress messages and timing information
|
|
1598
|
+
(default is True).
|
|
1599
|
+
positive_values : bool, optional
|
|
1600
|
+
If True, sets negative interpolated values to zero
|
|
1601
|
+
(default is True).
|
|
1602
|
+
|
|
1603
|
+
Notes
|
|
1604
|
+
-----
|
|
1605
|
+
- The convolution operation is performed in Fourier space.
|
|
1606
|
+
- If a previous interpolation time is recorded, it is displayed
|
|
1607
|
+
before starting a new interpolation.
|
|
1608
|
+
|
|
1609
|
+
Returns
|
|
1610
|
+
-------
|
|
1611
|
+
None
|
|
1612
|
+
"""
|
|
1613
|
+
self.interpolator.interpolate(verbose, positive_values)
|
|
1614
|
+
self.interpolated = self.interpolator.interpolated
|
|
1615
|
+
|
|
1616
|
+
def set_tukey_window(self, tukey_alphas=(1.0, 1.0, 1.0)):
|
|
1617
|
+
"""
|
|
1618
|
+
Set a Tukey window function for data tapering.
|
|
1619
|
+
|
|
1620
|
+
Parameters
|
|
1621
|
+
----------
|
|
1622
|
+
tukey_alphas : tuple of floats, optional
|
|
1623
|
+
The alpha parameters for the Tukey window in each dimension
|
|
1624
|
+
(H, K, L). Default is (1.0, 1.0, 1.0).
|
|
1625
|
+
|
|
1626
|
+
Notes
|
|
1627
|
+
-----
|
|
1628
|
+
The window function is generated based on the size of the dataset
|
|
1629
|
+
in each dimension.
|
|
1630
|
+
"""
|
|
1631
|
+
self.interpolator.set_tukey_window(tukey_alphas)
|
|
1632
|
+
self.window = self.interpolator.window
|
|
1633
|
+
|
|
1634
|
+
def set_hexagonal_tukey_window(self, tukey_alphas=(1.0, 1.0, 1.0, 1.0)):
|
|
1635
|
+
"""
|
|
1636
|
+
Set a hexagonal Tukey window function for data tapering.
|
|
1637
|
+
|
|
1638
|
+
Parameters
|
|
1639
|
+
----------
|
|
1640
|
+
tukey_alphas : tuple of floats, optional
|
|
1641
|
+
The alpha parameters for the Tukey window in each dimension and
|
|
1642
|
+
for the hexagonal truncation (H, HK, K, L). Default is (1.0, 1.0, 1.0, 1.0).
|
|
1643
|
+
|
|
1644
|
+
Notes
|
|
1645
|
+
-----
|
|
1646
|
+
The hexagonal Tukey window is applied to the dataset in a manner that
|
|
1647
|
+
preserves hexagonal symmetry.
|
|
1648
|
+
"""
|
|
1649
|
+
self.interpolator.set_hexagonal_tukey_window(tukey_alphas)
|
|
1650
|
+
self.window = self.interpolator.window
|
|
1651
|
+
|
|
1652
|
+
def set_ellipsoidal_tukey_window(self, tukey_alpha=1.0, coeffs=None):
|
|
1653
|
+
"""
|
|
1654
|
+
Set an ellipsoidal Tukey window function for data tapering.
|
|
1655
|
+
|
|
1656
|
+
The Tukey window smoothly tapers the data to zero near the edges of the
|
|
1657
|
+
elliptical region defined by quadratic form coefficients. This helps reduce
|
|
1658
|
+
artifacts in Fourier transforms and other operations sensitive to boundary effects.
|
|
1659
|
+
|
|
1660
|
+
Parameters
|
|
1661
|
+
----------
|
|
1662
|
+
tukey_alpha : float, optional
|
|
1663
|
+
Tapering parameter for the Tukey window, between 0 and 1.
|
|
1664
|
+
- `tukey_alpha = 0` results in a ellipsoidal window (no tapering).
|
|
1665
|
+
- `tukey_alpha = 1` results in a full cosine taper.
|
|
1666
|
+
Default is 1.0.
|
|
1667
|
+
|
|
1668
|
+
coeffs : tuple of float, optional
|
|
1669
|
+
Coefficients `(c0, c1, c2, c3, c4, c5)` defining the ellipsoidal
|
|
1670
|
+
quadratic form:
|
|
1671
|
+
R^2 = c0*H^2 + c1*H*K + c2*K^2 + c3*K*L + c4*L^2 + c5*L*H
|
|
1672
|
+
If None, coefficients are automatically set to match the edges of the
|
|
1673
|
+
reciprocal space axes (H, K, L), which should be appropriate in cases
|
|
1674
|
+
where H, K, and L are orthogonal.
|
|
1675
|
+
|
|
1676
|
+
Notes
|
|
1677
|
+
-----
|
|
1678
|
+
- The maximum allowed radius `Qmax` is determined from the minimum radius
|
|
1679
|
+
value along the edges of reciprocal space.
|
|
1680
|
+
- The Tukey window is applied radially as a function of the distance `R`
|
|
1681
|
+
from the center, defined by the ellipsoidal quadratic form.
|
|
1682
|
+
"""
|
|
1683
|
+
self.interpolator.set_ellipsoidal_tukey_window(tukey_alpha=tukey_alpha, coeffs=coeffs)
|
|
1684
|
+
self.window = self.interpolator.window
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
def set_window(self, window):
|
|
1688
|
+
"""
|
|
1689
|
+
Set a custom window function for data tapering.
|
|
1690
|
+
|
|
1691
|
+
Parameters
|
|
1692
|
+
----------
|
|
1693
|
+
window : ndarray
|
|
1694
|
+
A custom window function to be applied to the interpolated data.
|
|
1695
|
+
"""
|
|
1696
|
+
self.interpolator.set_window(window)
|
|
1697
|
+
|
|
1698
|
+
def apply_window(self):
|
|
1699
|
+
"""
|
|
1700
|
+
Apply the window function to the interpolated data.
|
|
1701
|
+
|
|
1702
|
+
The window function, if set, is applied to the `interpolated` data to
|
|
1703
|
+
produce the `tapered` result.
|
|
1704
|
+
|
|
1705
|
+
Returns
|
|
1706
|
+
-------
|
|
1707
|
+
None
|
|
1708
|
+
"""
|
|
1709
|
+
self.interpolator.apply_window()
|
|
1710
|
+
self.tapered = self.interpolator.tapered
|
|
1711
|
+
self.padder.set_data(self.tapered)
|
|
1712
|
+
|
|
1713
|
+
def pad(self, padding):
|
|
1714
|
+
"""
|
|
1715
|
+
Symmetrically pads the data with zero values.
|
|
1716
|
+
|
|
1717
|
+
Parameters
|
|
1718
|
+
----------
|
|
1719
|
+
padding : tuple
|
|
1720
|
+
The number of zero-value pixels to add along each edge of the array.
|
|
1721
|
+
|
|
1722
|
+
Returns
|
|
1723
|
+
-------
|
|
1724
|
+
NXdata
|
|
1725
|
+
The padded data with symmetric zero padding.
|
|
1726
|
+
"""
|
|
1727
|
+
self.padded = self.padder.pad(padding)
|
|
1728
|
+
|
|
1729
|
+
def perform_fft(self, is_2d=False):
|
|
1730
|
+
"""
|
|
1731
|
+
Perform a 3D Fourier Transform on the padded data.
|
|
1732
|
+
|
|
1733
|
+
This method applies an inverse Fourier Transform to the padded data
|
|
1734
|
+
using `pyfftw` for optimized performance. The result is stored in
|
|
1735
|
+
the `fft` attribute as an NXdata object containing the transformed
|
|
1736
|
+
spatial frequency components.
|
|
1737
|
+
|
|
1738
|
+
Parameters
|
|
1739
|
+
----------
|
|
1740
|
+
is_2d : bool, optional
|
|
1741
|
+
If True, performs the FFT only along the first two axes,
|
|
1742
|
+
skipping the out-of-plane direction (default is False).
|
|
1743
|
+
|
|
1744
|
+
Returns
|
|
1745
|
+
-------
|
|
1746
|
+
None
|
|
1747
|
+
|
|
1748
|
+
Notes
|
|
1749
|
+
-----
|
|
1750
|
+
- Calls `fourier_transform_nxdata` to perform the transformation.
|
|
1751
|
+
- The FFT is computed in two stages: first along the last dimension,
|
|
1752
|
+
then along the first two dimensions.
|
|
1753
|
+
- The output includes frequency components computed from the step
|
|
1754
|
+
sizes of the original data axes.
|
|
1755
|
+
|
|
1756
|
+
"""
|
|
1757
|
+
|
|
1758
|
+
self.fft = fourier_transform_nxdata(self.padded, is_2d=is_2d)
|