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.

@@ -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 .datareduction import plot_slice, reciprocal_lattice_params, Padder
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 symmetrized (default: 90).
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 past the extent of the dataset
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([len(data[axis]) for axis in data.axes])
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, theta <= theta_max * np.pi / 180)
117
- self.symmetrization_mask = NXdata(NXfield(p.unpad(symmetrization_mask), name='mask'),
118
- (data[data.axes[0]], data[data.axes[1]]))
119
-
120
- self.wedge = NXdata(NXfield(p.unpad(counts * symmetrization_mask), name=data.signal),
121
- (data[data.axes[0]], data[data.axes[1]]))
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 * np.sin(skew_angle_adj * np.pi / 180), 0],
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 n in range(0, rotations):
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), reconstructed)
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(scale2, 1).inverted().get_matrix()[:2, :2],
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(scale1,
173
- 1).inverted().get_matrix()[:2, :2],
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 * np.sin(skew_angle_adj * np.pi / 180)),
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()] = 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 visualization.
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 the input data and visualize
217
- the process.
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 = Scissors()
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 data to be symmetrized.
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, self.al_star, self.be_star, self.ga_star = self.reciprocal_lattice_params
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 range(0, len(q3)):
328
- print('Symmetrizing ' + q3.nxname + '=' + "{:.02f}".format(q3[k]) + "...", end='\r')
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 range(0, len(q2)):
335
- print('Symmetrizing ' + q2.nxname + '=' + "{:.02f}".format(q2[j]) + "...", end='\r')
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
- (q1, q3)))
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 range(0, len(q1)):
344
- print('Symmetrizing ' + q1.nxname + '=' + "{:.02f}".format(q1[i]) + "...", end='\r')
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
- (q2, q3)))
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("\nSymmetriztaion finished in " + "{:.02f}".format((stoptime - starttime) / 60) + " minutes.")
464
+ print(f"\nSymmetrization finished in {((stoptime - starttime) / 60):.02f} minutes.")
355
465
 
356
- self.symmetrized = NXdata(NXfield(out_array, name=data.signal), tuple([data[axis] for axis in data.axes]))
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, the default name 'symmetrized.nxs' will be used.
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. Default is [1, 0, 1, 0, 1, 0],
397
- corresponding to (1*H**2 + 0*H*K + 1*K**2 + 0*K*L + 1*L**2 + 0*L*H)
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_, al_, be_, ga_ = reciprocal_lattice_params((a, b, c, al, be, ga))
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.H, self.K, self.L = [None] * 3
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.H, self.K, self.L = np.meshgrid(data[data.axes[0]], data[data.axes[1]], data[data.axes[2]], indexing='ij')
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, self.al_star, self.be_star, self.ga_star = self.reciprocal_lattice_params
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.H, self.K, self.L
456
- a_, b_, c_, al_, be_, ga_ = self.reciprocal_lattice_params
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))) < punch_radius ** 2
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.H, self.K, self.L
475
- a_, b_, c_, al_, be_, ga_ = self.reciprocal_lattice_params
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)) < punch_radius ** 2
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
- data= self.data
491
- self.punched = NXdata(NXfield(np.where(self.mask, np.nan, data[data.signal].nxdata), name=data.signal),
492
- (data[data.axes[0]],data[data.axes[1]],data[data.axes[2]]))
493
- return self.punched
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
- self.punched = None
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.q1, self.q2 = [None] * 2
500
- self.mask = None
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.a, self.b, self.c, self.al, self.be, self.ga = [None] * 6
504
- self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = [None] * 6
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
- if self.mask is None:
509
- self.mask = np.zeros(data[data.signal].nxdata.shape)
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
- self.a, self.b, self.c, self.al, self.be, self.ga = lattice_params
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.reciprocal_lattice_params = reciprocal_lattice_params(lattice_params)
516
- self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = self.reciprocal_lattice_params
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
- self.mask = np.logical_or(self.mask, maskaddition)
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
- self.mask = np.logical_and(self.mask, np.logical_not(masksubtraction))
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
- if coeffs is None:
526
- coeffs = [1, 0, 1]
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
- mask = (coeffs[0] * (q1 - np.rint(q1)) ** 2 +
532
- coeffs[1] * (b_ * a_ / (a_ ** 2)) * (q1 - np.rint(q1)) * (q2 - np.rint(q2)) +
533
- coeffs[2] * (b_ / a_) ** 2 * (q2 - np.rint(q2)) ** 2) < punch_radius ** 2
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
- if thresh:
536
- mask = np.logical_and(mask, data[data.signal] > thresh)
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
- return mask
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
- if coeffs is None:
542
- coeffs = [1, 0, 1]
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
- if thresh:
555
- mask = np.logical_and(mask, data[data.signal] > thresh)
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
- return mask
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
- data= self.data
561
- self.punched = NXdata(NXfield(np.where(self.mask, np.nan, data[data.signal].nxdata), name=data.signal),
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
- # class Reducer():
567
- # pass
568
- #
569
- #
1408
+ def set_kernel(self, kernel):
1409
+ """
1410
+ Set the kernel to be used for interpolation.
570
1411
 
571
- class Interpolator():
572
- def __init__(self):
573
- self.data = None
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 set_data(self, data):
576
- self.data = data
1420
+ def interpolate(self):
1421
+ """
1422
+ Perform interpolation on the dataset using the specified kernel.
577
1423
 
578
- def set_kernel(self, kernel):
579
- self.kernel = kernel
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
- def generate_gaussian_kernel(self, amp, stddev, coeffs=None):
582
- pass
1428
+ Prints the time taken for the interpolation process.
583
1429
 
584
- def interpolate(self):
585
- pass
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)