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.
- _meta/__init__.py +1 -1
- nxs_analysis_tools/__init__.py +2 -2
- nxs_analysis_tools/chess.py +413 -363
- nxs_analysis_tools/datareduction.py +989 -887
- nxs_analysis_tools/fitting.py +258 -262
- nxs_analysis_tools/pairdistribution.py +585 -515
- {nxs_analysis_tools-0.0.32.dist-info → nxs_analysis_tools-0.0.34.dist-info}/METADATA +1 -1
- nxs_analysis_tools-0.0.34.dist-info/RECORD +11 -0
- {nxs_analysis_tools-0.0.32.dist-info → nxs_analysis_tools-0.0.34.dist-info}/WHEEL +1 -1
- nxs_analysis_tools-0.0.32.dist-info/RECORD +0 -11
- {nxs_analysis_tools-0.0.32.dist-info → nxs_analysis_tools-0.0.34.dist-info}/LICENSE +0 -0
- {nxs_analysis_tools-0.0.32.dist-info → nxs_analysis_tools-0.0.34.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
self.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|