nxs-analysis-tools 0.0.32__py3-none-any.whl → 0.0.34__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.

@@ -1,515 +1,585 @@
1
- """
2
- Tools for generating single crystal pair distribution functions.
3
- """
4
- import time
5
- import os
6
- from scipy import ndimage
7
- import matplotlib.pyplot as plt
8
- from matplotlib.transforms import Affine2D
9
- from nexusformat.nexus import nxsave, NXroot, NXentry, NXdata, NXfield
10
- import numpy as np
11
- from .datareduction import plot_slice, reciprocal_lattice_params, Padder
12
-
13
- class Symmetrizer2D:
14
- """
15
- A class for symmetrizing 2D datasets.
16
- """
17
- symmetrization_mask: NXdata
18
-
19
- def __init__(self, **kwargs):
20
- self.mirror_axis = None
21
- self.symmetrized = None
22
- self.wedges = None
23
- self.rotations = None
24
- self.transform = None
25
- self.mirror = None
26
- self.skew_angle = None
27
- self.theta_max = None
28
- self.theta_min = None
29
- self.wedge = None
30
- if kwargs:
31
- self.set_parameters(**kwargs)
32
-
33
- def set_parameters(self, theta_min, theta_max, lattice_angle=90, mirror=True, mirror_axis=0):
34
- """
35
- Sets the parameters for the symmetrization operation.
36
-
37
- Parameters
38
- ----------
39
- theta_min : float
40
- The minimum angle in degrees for symmetrization.
41
- theta_max : float
42
- The maximum angle in degrees for symmetrization.
43
- lattice_angle : float, optional
44
- The angle in degrees between the two principal axes of the plane to be symmetrized (default: 90).
45
- mirror : bool, optional
46
- If True, perform mirroring during symmetrization (default: True).
47
- mirror_axis : int, optional
48
- The axis along which to perform mirroring (default: 0).
49
- """
50
- self.theta_min = theta_min
51
- self.theta_max = theta_max
52
- self.skew_angle = lattice_angle
53
- self.mirror = mirror
54
- self.mirror_axis = mirror_axis
55
-
56
- # Define Transformation
57
- skew_angle_adj = 90 - lattice_angle
58
- t = Affine2D()
59
- # Scale y-axis to preserve norm while shearing
60
- t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
61
- # Shear along x-axis
62
- t += Affine2D().skew_deg(skew_angle_adj, 0)
63
- # Return to original y-axis scaling
64
- t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
65
- self.transform = t
66
-
67
- # Calculate number of rotations needed to reconstruct the dataset
68
- if mirror:
69
- rotations = abs(int(360 / (theta_max - theta_min) / 2))
70
- else:
71
- rotations = abs(int(360 / (theta_max - theta_min)))
72
- self.rotations = rotations
73
-
74
- self.symmetrization_mask = None
75
-
76
- self.wedges = None
77
-
78
- self.symmetrized = None
79
-
80
- def symmetrize_2d(self, data):
81
- """
82
- Symmetrizes a 2D dataset based on the set parameters.
83
-
84
- Parameters
85
- ----------
86
- data : NXdata
87
- The input 2D dataset to be symmetrized.
88
-
89
- Returns
90
- -------
91
- symmetrized : NXdata
92
- The symmetrized 2D dataset.
93
- """
94
- theta_min = self.theta_min
95
- theta_max = self.theta_max
96
- mirror = self.mirror
97
- mirror_axis = self.mirror_axis
98
- t = self.transform
99
- rotations = self.rotations
100
-
101
- # Pad the dataset so that rotations don't get cutoff if they extend past the extent of the dataset
102
- p = Padder(data)
103
- padding = tuple([len(data[axis]) for axis in data.axes])
104
- data_padded = p.pad(padding)
105
-
106
- # Define axes that span the plane to be transformed
107
- q1 = data_padded[data.axes[0]]
108
- q2 = data_padded[data.axes[1]]
109
-
110
- # Define signal to be symmetrized
111
- counts = data_padded[data.signal].nxdata
112
-
113
- # Calculate the angle for each data point
114
- theta = np.arctan2(q1.reshape((-1, 1)), q2.reshape((1, -1)))
115
- # 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]]))
122
-
123
- # Scale and skew counts
124
- skew_angle_adj = 90 - self.skew_angle
125
- counts_skew = ndimage.affine_transform(counts,
126
- t.inverted().get_matrix()[:2, :2],
127
- offset=[counts.shape[0] / 2 * np.sin(skew_angle_adj * np.pi / 180), 0],
128
- order=0,
129
- )
130
- scale1 = np.cos(skew_angle_adj * np.pi / 180)
131
- wedge = ndimage.affine_transform(counts_skew,
132
- Affine2D().scale(scale1, 1).get_matrix()[:2, :2],
133
- offset=[(1 - scale1) * counts.shape[0] / 2, 0],
134
- order=0,
135
- ) * symmetrization_mask
136
-
137
- scale2 = counts.shape[0] / counts.shape[1]
138
- wedge = ndimage.affine_transform(wedge,
139
- Affine2D().scale(scale2, 1).get_matrix()[:2, :2],
140
- offset=[(1 - scale2) * counts.shape[0] / 2, 0],
141
- order=0,
142
- )
143
-
144
- # Reconstruct full dataset from wedge
145
- reconstructed = np.zeros(counts.shape)
146
- for n in range(0, rotations):
147
- # The following are attempts to combine images with minimal overlapping pixels
148
- reconstructed += wedge
149
- # reconstructed = np.where(reconstructed == 0, reconstructed + wedge, reconstructed)
150
-
151
- wedge = ndimage.rotate(wedge, 360 / rotations, reshape=False, order=0)
152
-
153
- # self.rotated_only = NXdata(NXfield(reconstructed, name=data.signal),
154
- # (q1, q2))
155
-
156
- if mirror:
157
- # The following are attempts to combine images with minimal overlapping pixels
158
- reconstructed = np.where(reconstructed == 0,
159
- reconstructed + np.flip(reconstructed, axis=mirror_axis), reconstructed)
160
- # reconstructed += np.flip(reconstructed, axis=0)
161
-
162
- # self.rotated_and_mirrored = NXdata(NXfield(reconstructed, name=data.signal),
163
- # (q1, q2))
164
-
165
- reconstructed = ndimage.affine_transform(reconstructed,
166
- Affine2D().scale(scale2, 1).inverted().get_matrix()[:2, :2],
167
- offset=[-(1 - scale2) * counts.shape[
168
- 0] / 2 / scale2, 0],
169
- order=0,
170
- )
171
- reconstructed = ndimage.affine_transform(reconstructed,
172
- Affine2D().scale(scale1,
173
- 1).inverted().get_matrix()[:2, :2],
174
- offset=[-(1 - scale1) * counts.shape[
175
- 0] / 2 / scale1, 0],
176
- order=0,
177
- )
178
- reconstructed = ndimage.affine_transform(reconstructed,
179
- t.get_matrix()[:2, :2],
180
- offset=[(-counts.shape[0] / 2 * np.sin(skew_angle_adj * np.pi / 180)),
181
- 0],
182
- order=0,
183
- )
184
-
185
- reconstructed_unpadded = p.unpad(reconstructed)
186
-
187
- # Fix any overlapping pixels by truncating counts to max
188
- reconstructed_unpadded[reconstructed_unpadded > data[data.signal].nxdata.max()] = data[data.signal].nxdata.max()
189
-
190
- symmetrized = NXdata(NXfield(reconstructed_unpadded, name=data.signal),
191
- (data[data.axes[0]],
192
- data[data.axes[1]]))
193
-
194
- return symmetrized
195
-
196
- def test(self, data, **kwargs):
197
- """
198
- Performs a test visualization of the symmetrization process.
199
-
200
- Parameters
201
- ----------
202
- data : ndarray
203
- The input 2D dataset to be used for the test visualization.
204
- **kwargs : dict
205
- Additional keyword arguments to be passed to the plot_slice function.
206
-
207
- Returns
208
- -------
209
- fig : Figure
210
- The matplotlib Figure object that contains the test visualization plot.
211
- axesarr : ndarray
212
- The numpy array of Axes objects representing the subplots in the test visualization.
213
-
214
- Notes
215
- -----
216
- This method uses the `symmetrize_2d` method to perform the symmetrization on the input data and visualize
217
- the process.
218
-
219
- The test visualization plot includes the following subplots:
220
- - Subplot 1: The original dataset.
221
- - Subplot 2: The symmetrization mask.
222
- - Subplot 3: The wedge slice used for reconstruction of the full symmetrized dataset.
223
- - Subplot 4: The symmetrized dataset.
224
-
225
- Example usage:
226
- ```
227
- s = Scissors()
228
- s.set_parameters(theta_min, theta_max, skew_angle, mirror)
229
- s.test(data)
230
- ```
231
- """
232
- s = self
233
- symm_test = s.symmetrize_2d(data)
234
- fig, axesarr = plt.subplots(2, 2, figsize=(10, 8))
235
- axes = axesarr.reshape(-1)
236
- plot_slice(data, skew_angle=s.skew_angle, ax=axes[0], title='data', **kwargs)
237
- plot_slice(s.symmetrization_mask, skew_angle=s.skew_angle, ax=axes[1], title='mask')
238
- plot_slice(s.wedge, skew_angle=s.skew_angle, ax=axes[2], title='wedge', **kwargs)
239
- plot_slice(symm_test, skew_angle=s.skew_angle, ax=axes[3], title='symmetrized', **kwargs)
240
- plt.subplots_adjust(wspace=0.4)
241
- plt.show()
242
- return fig, axesarr
243
-
244
-
245
- class Symmetrizer3D:
246
- """
247
- A class to symmetrize 3D datasets.
248
- """
249
-
250
- def __init__(self, data=None):
251
- """
252
- Initialize the Symmetrizer3D object.
253
-
254
- Parameters
255
- ----------
256
- data : NXdata, optional
257
- The input 3D dataset to be symmetrized.
258
-
259
- """
260
-
261
- self.a, self.b, self.c, self.al, self.be, self.ga = [None] * 6
262
- self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = [None] * 6
263
- self.lattice_params = None
264
- self.reciprocal_lattice_params = None
265
- self.symmetrized = None
266
- self.data = data
267
- self.plane1symmetrizer = Symmetrizer2D()
268
- self.plane2symmetrizer = Symmetrizer2D()
269
- self.plane3symmetrizer = Symmetrizer2D()
270
-
271
- if data is not None:
272
- self.q1 = data[data.axes[0]]
273
- self.q2 = data[data.axes[1]]
274
- self.q3 = data[data.axes[2]]
275
- self.plane1 = self.q1.nxname + self.q2.nxname
276
- self.plane2 = self.q1.nxname + self.q3.nxname
277
- self.plane3 = self.q2.nxname + self.q3.nxname
278
-
279
- print("Plane 1: " + self.plane1)
280
- print("Plane 2: " + self.plane2)
281
- print("Plane 3: " + self.plane3)
282
-
283
- def set_data(self, data):
284
- """
285
- Sets the data to be symmetrized.
286
-
287
- Parameters
288
- ----------
289
- data : NXdata
290
- The input 3D dataset to be symmetrized.
291
-
292
- """
293
- self.data = data
294
- self.q1 = data[data.axes[0]]
295
- self.q2 = data[data.axes[1]]
296
- self.q3 = data[data.axes[2]]
297
- self.plane1 = self.q1.nxname + self.q2.nxname
298
- self.plane2 = self.q1.nxname + self.q3.nxname
299
- self.plane3 = self.q2.nxname + self.q3.nxname
300
-
301
- print("Plane 1: " + self.plane1)
302
- print("Plane 2: " + self.plane2)
303
- print("Plane 3: " + self.plane3)
304
-
305
- def set_lattice_params(self, lattice_params):
306
- self.a, self.b, self.c, self.al, self.be, self.ga = lattice_params
307
- self.lattice_params = lattice_params
308
- 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
310
-
311
- def symmetrize(self):
312
- """
313
- Perform the symmetrization of the 3D dataset.
314
-
315
- Returns
316
- -------
317
- symmetrized : NXdata
318
- The symmetrized 3D dataset.
319
-
320
- """
321
- starttime = time.time()
322
- data = self.data
323
- q1, q2, q3 = self.q1, self.q2, self.q3
324
- out_array = np.zeros(data[data.signal].shape)
325
-
326
- print('Symmetrizing ' + self.plane1 + ' planes...')
327
- for k in range(0, len(q3)):
328
- print('Symmetrizing ' + q3.nxname + '=' + "{:.02f}".format(q3[k]) + "...", end='\r')
329
- data_symmetrized = self.plane1symmetrizer.symmetrize_2d(data[:, :, k])
330
- out_array[:, :, k] = data_symmetrized[data.signal].nxdata
331
- print('\nSymmetrized ' + self.plane1 + ' planes.')
332
-
333
- print('Symmetrizing ' + self.plane2 + ' planes...')
334
- for j in range(0, len(q2)):
335
- print('Symmetrizing ' + q2.nxname + '=' + "{:.02f}".format(q2[j]) + "...", end='\r')
336
- data_symmetrized = self.plane2symmetrizer.symmetrize_2d(
337
- NXdata(NXfield(out_array[:, j, :], name=data.signal),
338
- (q1, q3)))
339
- out_array[:, j, :] = data_symmetrized[data.signal].nxdata
340
- print('\nSymmetrized ' + self.plane2 + ' planes.')
341
-
342
- print('Symmetrizing ' + self.plane3 + ' planes...')
343
- for i in range(0, len(q1)):
344
- print('Symmetrizing ' + q1.nxname + '=' + "{:.02f}".format(q1[i]) + "...", end='\r')
345
- data_symmetrized = self.plane3symmetrizer.symmetrize_2d(
346
- NXdata(NXfield(out_array[i, :, :], name=data.signal),
347
- (q2, q3)))
348
- out_array[i, :, :] = data_symmetrized[data.signal].nxdata
349
- print('\nSymmetrized ' + self.plane3 + ' planes.')
350
-
351
- out_array[out_array < 0] = 0
352
-
353
- stoptime = time.time()
354
- print("\nSymmetriztaion finished in " + "{:.02f}".format((stoptime - starttime) / 60) + " minutes.")
355
-
356
- self.symmetrized = NXdata(NXfield(out_array, name=data.signal), tuple([data[axis] for axis in data.axes]))
357
-
358
- return self.symmetrized
359
-
360
- def save(self, fout_name=None):
361
- """
362
- Save the symmetrized dataset to a file.
363
-
364
- Parameters
365
- ----------
366
- fout_name : str, optional
367
- The name of the output file. If not provided, the default name 'symmetrized.nxs' will be used.
368
-
369
- """
370
- print("Saving file...")
371
-
372
- f = NXroot()
373
- f['entry'] = NXentry()
374
- f['entry']['data'] = self.symmetrized
375
- if fout_name is None:
376
- fout_name = 'symmetrized.nxs'
377
- nxsave(fout_name, f)
378
- print("Output file saved to: " + os.path.join(os.getcwd(), fout_name))
379
-
380
-
381
- def generate_gaussian(H, K, L, amp, stddev, lattice_params, coeffs=None):
382
- """
383
- Generate a 3D Gaussian distribution.
384
-
385
- Parameters
386
- ----------
387
- H, K, L : ndarray
388
- Arrays specifying the values of H, K, and L coordinates.
389
- amp : float
390
- Amplitude of the Gaussian distribution.
391
- stddev : float
392
- Standard deviation of the Gaussian distribution.
393
- lattice_params : tuple
394
- Tuple of lattice parameters (a, b, c, alpha, beta, gamma).
395
- 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)
398
-
399
- Returns
400
- -------
401
- gaussian : ndarray
402
- 3D Gaussian distribution.
403
- """
404
- if coeffs is None:
405
- coeffs = [1, 0, 1, 0, 1, 0]
406
- a, b, c, al, be, ga = lattice_params
407
- a_, b_, c_, al_, be_, ga_ = reciprocal_lattice_params((a, b, c, al, be, ga))
408
- H, K, L = np.meshgrid(H, K, L, indexing='ij')
409
- gaussian = amp * np.exp(-(coeffs[0] * H ** 2 +
410
- coeffs[1] * (b_ * a_ / (a_ ** 2)) * H * K +
411
- coeffs[2] * (b_ / a_) ** 2 * K ** 2 +
412
- coeffs[3] * (b_ * c_ / (a_ ** 2)) * K * L +
413
- coeffs[4] * (c_ / a_) ** 2 * L ** 2 +
414
- coeffs[5] * (c_ * a_ / (a_ ** 2)) * L * H) / (2 * stddev ** 2))
415
- if gaussian.ndim == 3:
416
- gaussian = gaussian.transpose(1, 0, 2)
417
- elif gaussian.ndim == 2:
418
- gaussian = gaussian.transpose()
419
- return gaussian.transpose(1, 0, 2)
420
-
421
-
422
- class Puncher:
423
- def __init__(self):
424
- self.punched = None
425
- self.data = None
426
- self.H, self.K, self.L = [None] * 3
427
- self.mask = None
428
- self.reciprocal_lattice_params = None
429
- self.lattice_params = None
430
- self.a, self.b, self.c, self.al, self.be, self.ga = [None] * 6
431
- self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = [None] * 6
432
-
433
- def set_data(self, data):
434
- self.data = data
435
- if self.mask is None:
436
- 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')
438
-
439
- def set_lattice_params(self, lattice_params):
440
- self.a, self.b, self.c, self.al, self.be, self.ga = lattice_params
441
- self.lattice_params = lattice_params
442
- 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
444
-
445
- def add_mask(self, maskaddition):
446
- self.mask = np.logical_or(self.mask, maskaddition)
447
-
448
- def subtract_mask(self, masksubtraction):
449
- self.mask = np.logical_and(self.mask, np.logical_not(masksubtraction))
450
-
451
- def generate_bragg_mask(self, punch_radius, coeffs=None, thresh=None):
452
- if coeffs is None:
453
- coeffs = [1, 0, 1, 0, 1, 0]
454
- data = self.data
455
- H, K, L = self.H, self.K, self.L
456
- a_, b_, c_, al_, be_, ga_ = self.reciprocal_lattice_params
457
-
458
- mask = (coeffs[0] * (H - np.rint(H)) ** 2 +
459
- coeffs[1] * (b_ * a_ / (a_ ** 2)) * (H - np.rint(H)) * (K - np.rint(K)) +
460
- coeffs[2] * (b_ / a_) ** 2 * (K - np.rint(K)) ** 2 +
461
- coeffs[3] * (b_ * c_ / (a_ ** 2)) * (K - np.rint(K)) * (L - np.rint(L)) +
462
- 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
464
-
465
- if thresh:
466
- mask = np.logical_and(mask, data[data.signal] > thresh)
467
-
468
- return mask
469
-
470
- def generate_mask_at_coord(self, coordinate, punch_radius, coeffs=None, thresh=None):
471
- if coeffs is None:
472
- coeffs = [1, 0, 1, 0, 1, 0]
473
- data = self.data
474
- H, K, L = self.H, self.K, self.L
475
- a_, b_, c_, al_, be_, ga_ = self.reciprocal_lattice_params
476
- centerH, centerK, centerL = coordinate
477
- mask = (coeffs[0] * (H - centerH) ** 2 +
478
- coeffs[1] * (b_ * a_ / (a_ ** 2)) * (H - centerH) * (K - centerK) +
479
- coeffs[2] * (b_ / a_) ** 2 * (K - centerK) ** 2 +
480
- coeffs[3] * (b_ * c_ / (a_ ** 2)) * (K - centerK) * (L - centerL) +
481
- coeffs[4] * (c_ / a_) ** 2 * (L - centerL) ** 2 +
482
- coeffs[5] * (c_ * a_ / (a_ ** 2)) * (L - centerL) * (H - centerH)) < punch_radius ** 2
483
-
484
- if thresh:
485
- mask = np.logical_and(mask, data[data.signal] > thresh)
486
-
487
- return mask
488
-
489
- 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
494
-
495
-
496
- # class Reducer():
497
- # pass
498
- #
499
- #
500
-
501
- class Interpolator():
502
- def __init__(self):
503
- self.data = None
504
-
505
- def set_data(self, data):
506
- self.data = data
507
-
508
- def set_kernel(self, kernel):
509
- self.kernel = kernel
510
-
511
- def generate_gaussian_kernel(self, amp, stddev, coeffs=None):
512
- pass
513
-
514
- def interpolate(self):
515
- pass
1
+ """
2
+ Tools for generating single crystal pair distribution functions.
3
+ """
4
+ import time
5
+ import os
6
+ from scipy import ndimage
7
+ import matplotlib.pyplot as plt
8
+ from matplotlib.transforms import Affine2D
9
+ from nexusformat.nexus import nxsave, NXroot, NXentry, NXdata, NXfield
10
+ import numpy as np
11
+ from .datareduction import plot_slice, reciprocal_lattice_params, Padder
12
+
13
+ class Symmetrizer2D:
14
+ """
15
+ A class for symmetrizing 2D datasets.
16
+ """
17
+ symmetrization_mask: NXdata
18
+
19
+ def __init__(self, **kwargs):
20
+ self.mirror_axis = None
21
+ self.symmetrized = None
22
+ self.wedges = None
23
+ self.rotations = None
24
+ self.transform = None
25
+ self.mirror = None
26
+ self.skew_angle = None
27
+ self.theta_max = None
28
+ self.theta_min = None
29
+ self.wedge = None
30
+ if kwargs:
31
+ self.set_parameters(**kwargs)
32
+
33
+ def set_parameters(self, theta_min, theta_max, lattice_angle=90, mirror=True, mirror_axis=0):
34
+ """
35
+ Sets the parameters for the symmetrization operation.
36
+
37
+ Parameters
38
+ ----------
39
+ theta_min : float
40
+ The minimum angle in degrees for symmetrization.
41
+ theta_max : float
42
+ The maximum angle in degrees for symmetrization.
43
+ lattice_angle : float, optional
44
+ The angle in degrees between the two principal axes of the plane to be symmetrized (default: 90).
45
+ mirror : bool, optional
46
+ If True, perform mirroring during symmetrization (default: True).
47
+ mirror_axis : int, optional
48
+ The axis along which to perform mirroring (default: 0).
49
+ """
50
+ self.theta_min = theta_min
51
+ self.theta_max = theta_max
52
+ self.skew_angle = lattice_angle
53
+ self.mirror = mirror
54
+ self.mirror_axis = mirror_axis
55
+
56
+ # Define Transformation
57
+ skew_angle_adj = 90 - lattice_angle
58
+ t = Affine2D()
59
+ # Scale y-axis to preserve norm while shearing
60
+ t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
61
+ # Shear along x-axis
62
+ t += Affine2D().skew_deg(skew_angle_adj, 0)
63
+ # Return to original y-axis scaling
64
+ t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
65
+ self.transform = t
66
+
67
+ # Calculate number of rotations needed to reconstruct the dataset
68
+ if mirror:
69
+ rotations = abs(int(360 / (theta_max - theta_min) / 2))
70
+ else:
71
+ rotations = abs(int(360 / (theta_max - theta_min)))
72
+ self.rotations = rotations
73
+
74
+ self.symmetrization_mask = None
75
+
76
+ self.wedges = None
77
+
78
+ self.symmetrized = None
79
+
80
+ def symmetrize_2d(self, data):
81
+ """
82
+ Symmetrizes a 2D dataset based on the set parameters.
83
+
84
+ Parameters
85
+ ----------
86
+ data : NXdata
87
+ The input 2D dataset to be symmetrized.
88
+
89
+ Returns
90
+ -------
91
+ symmetrized : NXdata
92
+ The symmetrized 2D dataset.
93
+ """
94
+ theta_min = self.theta_min
95
+ theta_max = self.theta_max
96
+ mirror = self.mirror
97
+ mirror_axis = self.mirror_axis
98
+ t = self.transform
99
+ rotations = self.rotations
100
+
101
+ # Pad the dataset so that rotations don't get cutoff if they extend past the extent of the dataset
102
+ p = Padder(data)
103
+ padding = tuple([len(data[axis]) for axis in data.axes])
104
+ data_padded = p.pad(padding)
105
+
106
+ # Define axes that span the plane to be transformed
107
+ q1 = data_padded[data.axes[0]]
108
+ q2 = data_padded[data.axes[1]]
109
+
110
+ # Define signal to be symmetrized
111
+ counts = data_padded[data.signal].nxdata
112
+
113
+ # Calculate the angle for each data point
114
+ theta = np.arctan2(q1.reshape((-1, 1)), q2.reshape((1, -1)))
115
+ # 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]]))
122
+
123
+ # Scale and skew counts
124
+ skew_angle_adj = 90 - self.skew_angle
125
+ counts_skew = ndimage.affine_transform(counts,
126
+ t.inverted().get_matrix()[:2, :2],
127
+ offset=[counts.shape[0] / 2 * np.sin(skew_angle_adj * np.pi / 180), 0],
128
+ order=0,
129
+ )
130
+ scale1 = np.cos(skew_angle_adj * np.pi / 180)
131
+ wedge = ndimage.affine_transform(counts_skew,
132
+ Affine2D().scale(scale1, 1).get_matrix()[:2, :2],
133
+ offset=[(1 - scale1) * counts.shape[0] / 2, 0],
134
+ order=0,
135
+ ) * symmetrization_mask
136
+
137
+ scale2 = counts.shape[0] / counts.shape[1]
138
+ wedge = ndimage.affine_transform(wedge,
139
+ Affine2D().scale(scale2, 1).get_matrix()[:2, :2],
140
+ offset=[(1 - scale2) * counts.shape[0] / 2, 0],
141
+ order=0,
142
+ )
143
+
144
+ # Reconstruct full dataset from wedge
145
+ reconstructed = np.zeros(counts.shape)
146
+ for n in range(0, rotations):
147
+ # The following are attempts to combine images with minimal overlapping pixels
148
+ reconstructed += wedge
149
+ # reconstructed = np.where(reconstructed == 0, reconstructed + wedge, reconstructed)
150
+
151
+ wedge = ndimage.rotate(wedge, 360 / rotations, reshape=False, order=0)
152
+
153
+ # self.rotated_only = NXdata(NXfield(reconstructed, name=data.signal),
154
+ # (q1, q2))
155
+
156
+ if mirror:
157
+ # The following are attempts to combine images with minimal overlapping pixels
158
+ reconstructed = np.where(reconstructed == 0,
159
+ reconstructed + np.flip(reconstructed, axis=mirror_axis), reconstructed)
160
+ # reconstructed += np.flip(reconstructed, axis=0)
161
+
162
+ # self.rotated_and_mirrored = NXdata(NXfield(reconstructed, name=data.signal),
163
+ # (q1, q2))
164
+
165
+ reconstructed = ndimage.affine_transform(reconstructed,
166
+ Affine2D().scale(scale2, 1).inverted().get_matrix()[:2, :2],
167
+ offset=[-(1 - scale2) * counts.shape[
168
+ 0] / 2 / scale2, 0],
169
+ order=0,
170
+ )
171
+ reconstructed = ndimage.affine_transform(reconstructed,
172
+ Affine2D().scale(scale1,
173
+ 1).inverted().get_matrix()[:2, :2],
174
+ offset=[-(1 - scale1) * counts.shape[
175
+ 0] / 2 / scale1, 0],
176
+ order=0,
177
+ )
178
+ reconstructed = ndimage.affine_transform(reconstructed,
179
+ t.get_matrix()[:2, :2],
180
+ offset=[(-counts.shape[0] / 2 * np.sin(skew_angle_adj * np.pi / 180)),
181
+ 0],
182
+ order=0,
183
+ )
184
+
185
+ reconstructed_unpadded = p.unpad(reconstructed)
186
+
187
+ # Fix any overlapping pixels by truncating counts to max
188
+ reconstructed_unpadded[reconstructed_unpadded > data[data.signal].nxdata.max()] = data[data.signal].nxdata.max()
189
+
190
+ symmetrized = NXdata(NXfield(reconstructed_unpadded, name=data.signal),
191
+ (data[data.axes[0]],
192
+ data[data.axes[1]]))
193
+
194
+ return symmetrized
195
+
196
+ def test(self, data, **kwargs):
197
+ """
198
+ Performs a test visualization of the symmetrization process.
199
+
200
+ Parameters
201
+ ----------
202
+ data : ndarray
203
+ The input 2D dataset to be used for the test visualization.
204
+ **kwargs : dict
205
+ Additional keyword arguments to be passed to the plot_slice function.
206
+
207
+ Returns
208
+ -------
209
+ fig : Figure
210
+ The matplotlib Figure object that contains the test visualization plot.
211
+ axesarr : ndarray
212
+ The numpy array of Axes objects representing the subplots in the test visualization.
213
+
214
+ Notes
215
+ -----
216
+ This method uses the `symmetrize_2d` method to perform the symmetrization on the input data and visualize
217
+ the process.
218
+
219
+ The test visualization plot includes the following subplots:
220
+ - Subplot 1: The original dataset.
221
+ - Subplot 2: The symmetrization mask.
222
+ - Subplot 3: The wedge slice used for reconstruction of the full symmetrized dataset.
223
+ - Subplot 4: The symmetrized dataset.
224
+
225
+ Example usage:
226
+ ```
227
+ s = Scissors()
228
+ s.set_parameters(theta_min, theta_max, skew_angle, mirror)
229
+ s.test(data)
230
+ ```
231
+ """
232
+ s = self
233
+ symm_test = s.symmetrize_2d(data)
234
+ fig, axesarr = plt.subplots(2, 2, figsize=(10, 8))
235
+ axes = axesarr.reshape(-1)
236
+ plot_slice(data, skew_angle=s.skew_angle, ax=axes[0], title='data', **kwargs)
237
+ plot_slice(s.symmetrization_mask, skew_angle=s.skew_angle, ax=axes[1], title='mask')
238
+ plot_slice(s.wedge, skew_angle=s.skew_angle, ax=axes[2], title='wedge', **kwargs)
239
+ plot_slice(symm_test, skew_angle=s.skew_angle, ax=axes[3], title='symmetrized', **kwargs)
240
+ plt.subplots_adjust(wspace=0.4)
241
+ plt.show()
242
+ return fig, axesarr
243
+
244
+
245
+ class Symmetrizer3D:
246
+ """
247
+ A class to symmetrize 3D datasets.
248
+ """
249
+
250
+ def __init__(self, data=None):
251
+ """
252
+ Initialize the Symmetrizer3D object.
253
+
254
+ Parameters
255
+ ----------
256
+ data : NXdata, optional
257
+ The input 3D dataset to be symmetrized.
258
+
259
+ """
260
+
261
+ self.a, self.b, self.c, self.al, self.be, self.ga = [None] * 6
262
+ self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = [None] * 6
263
+ self.lattice_params = None
264
+ self.reciprocal_lattice_params = None
265
+ self.symmetrized = None
266
+ self.data = data
267
+ self.plane1symmetrizer = Symmetrizer2D()
268
+ self.plane2symmetrizer = Symmetrizer2D()
269
+ self.plane3symmetrizer = Symmetrizer2D()
270
+
271
+ if data is not None:
272
+ self.q1 = data[data.axes[0]]
273
+ self.q2 = data[data.axes[1]]
274
+ self.q3 = data[data.axes[2]]
275
+ self.plane1 = self.q1.nxname + self.q2.nxname
276
+ self.plane2 = self.q1.nxname + self.q3.nxname
277
+ self.plane3 = self.q2.nxname + self.q3.nxname
278
+
279
+ print("Plane 1: " + self.plane1)
280
+ print("Plane 2: " + self.plane2)
281
+ print("Plane 3: " + self.plane3)
282
+
283
+ def set_data(self, data):
284
+ """
285
+ Sets the data to be symmetrized.
286
+
287
+ Parameters
288
+ ----------
289
+ data : NXdata
290
+ The input 3D dataset to be symmetrized.
291
+
292
+ """
293
+ self.data = data
294
+ self.q1 = data[data.axes[0]]
295
+ self.q2 = data[data.axes[1]]
296
+ self.q3 = data[data.axes[2]]
297
+ self.plane1 = self.q1.nxname + self.q2.nxname
298
+ self.plane2 = self.q1.nxname + self.q3.nxname
299
+ self.plane3 = self.q2.nxname + self.q3.nxname
300
+
301
+ print("Plane 1: " + self.plane1)
302
+ print("Plane 2: " + self.plane2)
303
+ print("Plane 3: " + self.plane3)
304
+
305
+ def set_lattice_params(self, lattice_params):
306
+ self.a, self.b, self.c, self.al, self.be, self.ga = lattice_params
307
+ self.lattice_params = lattice_params
308
+ 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
310
+
311
+ def symmetrize(self):
312
+ """
313
+ Perform the symmetrization of the 3D dataset.
314
+
315
+ Returns
316
+ -------
317
+ symmetrized : NXdata
318
+ The symmetrized 3D dataset.
319
+
320
+ """
321
+ starttime = time.time()
322
+ data = self.data
323
+ q1, q2, q3 = self.q1, self.q2, self.q3
324
+ out_array = np.zeros(data[data.signal].shape)
325
+
326
+ print('Symmetrizing ' + self.plane1 + ' planes...')
327
+ for k in range(0, len(q3)):
328
+ print('Symmetrizing ' + q3.nxname + '=' + "{:.02f}".format(q3[k]) + "...", end='\r')
329
+ data_symmetrized = self.plane1symmetrizer.symmetrize_2d(data[:, :, k])
330
+ out_array[:, :, k] = data_symmetrized[data.signal].nxdata
331
+ print('\nSymmetrized ' + self.plane1 + ' planes.')
332
+
333
+ print('Symmetrizing ' + self.plane2 + ' planes...')
334
+ for j in range(0, len(q2)):
335
+ print('Symmetrizing ' + q2.nxname + '=' + "{:.02f}".format(q2[j]) + "...", end='\r')
336
+ data_symmetrized = self.plane2symmetrizer.symmetrize_2d(
337
+ NXdata(NXfield(out_array[:, j, :], name=data.signal),
338
+ (q1, q3)))
339
+ out_array[:, j, :] = data_symmetrized[data.signal].nxdata
340
+ print('\nSymmetrized ' + self.plane2 + ' planes.')
341
+
342
+ print('Symmetrizing ' + self.plane3 + ' planes...')
343
+ for i in range(0, len(q1)):
344
+ print('Symmetrizing ' + q1.nxname + '=' + "{:.02f}".format(q1[i]) + "...", end='\r')
345
+ data_symmetrized = self.plane3symmetrizer.symmetrize_2d(
346
+ NXdata(NXfield(out_array[i, :, :], name=data.signal),
347
+ (q2, q3)))
348
+ out_array[i, :, :] = data_symmetrized[data.signal].nxdata
349
+ print('\nSymmetrized ' + self.plane3 + ' planes.')
350
+
351
+ out_array[out_array < 0] = 0
352
+
353
+ stoptime = time.time()
354
+ print("\nSymmetriztaion finished in " + "{:.02f}".format((stoptime - starttime) / 60) + " minutes.")
355
+
356
+ self.symmetrized = NXdata(NXfield(out_array, name=data.signal), tuple([data[axis] for axis in data.axes]))
357
+
358
+ return self.symmetrized
359
+
360
+ def save(self, fout_name=None):
361
+ """
362
+ Save the symmetrized dataset to a file.
363
+
364
+ Parameters
365
+ ----------
366
+ fout_name : str, optional
367
+ The name of the output file. If not provided, the default name 'symmetrized.nxs' will be used.
368
+
369
+ """
370
+ print("Saving file...")
371
+
372
+ f = NXroot()
373
+ f['entry'] = NXentry()
374
+ f['entry']['data'] = self.symmetrized
375
+ if fout_name is None:
376
+ fout_name = 'symmetrized.nxs'
377
+ nxsave(fout_name, f)
378
+ print("Output file saved to: " + os.path.join(os.getcwd(), fout_name))
379
+
380
+
381
+ def generate_gaussian(H, K, L, amp, stddev, lattice_params, coeffs=None):
382
+ """
383
+ Generate a 3D Gaussian distribution.
384
+
385
+ Parameters
386
+ ----------
387
+ H, K, L : ndarray
388
+ Arrays specifying the values of H, K, and L coordinates.
389
+ amp : float
390
+ Amplitude of the Gaussian distribution.
391
+ stddev : float
392
+ Standard deviation of the Gaussian distribution.
393
+ lattice_params : tuple
394
+ Tuple of lattice parameters (a, b, c, alpha, beta, gamma).
395
+ 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)
398
+
399
+ Returns
400
+ -------
401
+ gaussian : ndarray
402
+ 3D Gaussian distribution.
403
+ """
404
+ if coeffs is None:
405
+ coeffs = [1, 0, 1, 0, 1, 0]
406
+ a, b, c, al, be, ga = lattice_params
407
+ a_, b_, c_, al_, be_, ga_ = reciprocal_lattice_params((a, b, c, al, be, ga))
408
+ H, K, L = np.meshgrid(H, K, L, indexing='ij')
409
+ gaussian = amp * np.exp(-(coeffs[0] * H ** 2 +
410
+ coeffs[1] * (b_ * a_ / (a_ ** 2)) * H * K +
411
+ coeffs[2] * (b_ / a_) ** 2 * K ** 2 +
412
+ coeffs[3] * (b_ * c_ / (a_ ** 2)) * K * L +
413
+ coeffs[4] * (c_ / a_) ** 2 * L ** 2 +
414
+ coeffs[5] * (c_ * a_ / (a_ ** 2)) * L * H) / (2 * stddev ** 2))
415
+ if gaussian.ndim == 3:
416
+ gaussian = gaussian.transpose(1, 0, 2)
417
+ elif gaussian.ndim == 2:
418
+ gaussian = gaussian.transpose()
419
+ return gaussian.transpose(1, 0, 2)
420
+
421
+
422
+ class Puncher:
423
+ def __init__(self):
424
+ self.punched = None
425
+ self.data = None
426
+ self.H, self.K, self.L = [None] * 3
427
+ self.mask = None
428
+ self.reciprocal_lattice_params = None
429
+ self.lattice_params = None
430
+ self.a, self.b, self.c, self.al, self.be, self.ga = [None] * 6
431
+ self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = [None] * 6
432
+
433
+ def set_data(self, data):
434
+ self.data = data
435
+ if self.mask is None:
436
+ 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')
438
+
439
+ def set_lattice_params(self, lattice_params):
440
+ self.a, self.b, self.c, self.al, self.be, self.ga = lattice_params
441
+ self.lattice_params = lattice_params
442
+ 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
444
+
445
+ def add_mask(self, maskaddition):
446
+ self.mask = np.logical_or(self.mask, maskaddition)
447
+
448
+ def subtract_mask(self, masksubtraction):
449
+ self.mask = np.logical_and(self.mask, np.logical_not(masksubtraction))
450
+
451
+ def generate_bragg_mask(self, punch_radius, coeffs=None, thresh=None):
452
+ if coeffs is None:
453
+ coeffs = [1, 0, 1, 0, 1, 0]
454
+ data = self.data
455
+ H, K, L = self.H, self.K, self.L
456
+ a_, b_, c_, al_, be_, ga_ = self.reciprocal_lattice_params
457
+
458
+ mask = (coeffs[0] * (H - np.rint(H)) ** 2 +
459
+ coeffs[1] * (b_ * a_ / (a_ ** 2)) * (H - np.rint(H)) * (K - np.rint(K)) +
460
+ coeffs[2] * (b_ / a_) ** 2 * (K - np.rint(K)) ** 2 +
461
+ coeffs[3] * (b_ * c_ / (a_ ** 2)) * (K - np.rint(K)) * (L - np.rint(L)) +
462
+ 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
464
+
465
+ if thresh:
466
+ mask = np.logical_and(mask, data[data.signal] > thresh)
467
+
468
+ return mask
469
+
470
+ def generate_mask_at_coord(self, coordinate, punch_radius, coeffs=None, thresh=None):
471
+ if coeffs is None:
472
+ coeffs = [1, 0, 1, 0, 1, 0]
473
+ data = self.data
474
+ H, K, L = self.H, self.K, self.L
475
+ a_, b_, c_, al_, be_, ga_ = self.reciprocal_lattice_params
476
+ centerH, centerK, centerL = coordinate
477
+ mask = (coeffs[0] * (H - centerH) ** 2 +
478
+ coeffs[1] * (b_ * a_ / (a_ ** 2)) * (H - centerH) * (K - centerK) +
479
+ coeffs[2] * (b_ / a_) ** 2 * (K - centerK) ** 2 +
480
+ coeffs[3] * (b_ * c_ / (a_ ** 2)) * (K - centerK) * (L - centerL) +
481
+ coeffs[4] * (c_ / a_) ** 2 * (L - centerL) ** 2 +
482
+ coeffs[5] * (c_ * a_ / (a_ ** 2)) * (L - centerL) * (H - centerH)) < punch_radius ** 2
483
+
484
+ if thresh:
485
+ mask = np.logical_and(mask, data[data.signal] > thresh)
486
+
487
+ return mask
488
+
489
+ 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
494
+
495
+ class PuncherHK:
496
+ def __init__(self):
497
+ self.punched = None
498
+ self.data = None
499
+ self.q1, self.q2 = [None] * 2
500
+ self.mask = None
501
+ self.reciprocal_lattice_params = None
502
+ 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
505
+
506
+ def set_data(self, data):
507
+ 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')
511
+
512
+ def set_lattice_params(self, lattice_params):
513
+ self.a, self.b, self.c, self.al, self.be, self.ga = lattice_params
514
+ 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
517
+
518
+ def add_mask(self, maskaddition):
519
+ self.mask = np.logical_or(self.mask, maskaddition)
520
+
521
+ def subtract_mask(self, masksubtraction):
522
+ self.mask = np.logical_and(self.mask, np.logical_not(masksubtraction))
523
+
524
+ 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
530
+
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
534
+
535
+ if thresh:
536
+ mask = np.logical_and(mask, data[data.signal] > thresh)
537
+
538
+ return mask
539
+
540
+ 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
553
+
554
+ if thresh:
555
+ mask = np.logical_and(mask, data[data.signal] > thresh)
556
+
557
+ return mask
558
+
559
+ 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
564
+
565
+
566
+ # class Reducer():
567
+ # pass
568
+ #
569
+ #
570
+
571
+ class Interpolator():
572
+ def __init__(self):
573
+ self.data = None
574
+
575
+ def set_data(self, data):
576
+ self.data = data
577
+
578
+ def set_kernel(self, kernel):
579
+ self.kernel = kernel
580
+
581
+ def generate_gaussian_kernel(self, amp, stddev, coeffs=None):
582
+ pass
583
+
584
+ def interpolate(self):
585
+ pass