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