nxs-analysis-tools 0.0.18__py3-none-any.whl → 0.0.20__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 +2 -2
- nxs_analysis_tools/chess.py +69 -36
- nxs_analysis_tools/datareduction.py +172 -120
- nxs_analysis_tools/pairdistribution.py +430 -72
- {nxs_analysis_tools-0.0.18.dist-info → nxs_analysis_tools-0.0.20.dist-info}/METADATA +1 -1
- nxs_analysis_tools-0.0.20.dist-info/RECORD +10 -0
- nxs_analysis_tools-0.0.18.dist-info/RECORD +0 -10
- {nxs_analysis_tools-0.0.18.dist-info → nxs_analysis_tools-0.0.20.dist-info}/LICENSE +0 -0
- {nxs_analysis_tools-0.0.18.dist-info → nxs_analysis_tools-0.0.20.dist-info}/WHEEL +0 -0
- {nxs_analysis_tools-0.0.18.dist-info → nxs_analysis_tools-0.0.20.dist-info}/top_level.txt +0 -0
_meta/__init__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
'''
|
|
1
|
+
'''nxs-analysis-tools package metadata.'''
|
|
2
2
|
|
|
3
3
|
# keep consistent with pyproject.toml
|
|
4
4
|
__project__ = 'nxs-analysis-tools'
|
|
@@ -6,5 +6,5 @@ __author__ = 'Steven J. Gomez Alvarado'
|
|
|
6
6
|
__email__ = 'stevenjgomez@ucsb.edu'
|
|
7
7
|
__copyright__ = f"2023, {__author__}"
|
|
8
8
|
__license__ = 'MIT'
|
|
9
|
-
__version__= '0.0.
|
|
9
|
+
__version__= '0.0.20'
|
|
10
10
|
__repo_url__ = 'https://github.com/stevenjgomez/nxs_analysis_tools'
|
nxs_analysis_tools/chess.py
CHANGED
|
@@ -4,6 +4,7 @@ This module provides classes and functions for analyzing scattering datasets col
|
|
|
4
4
|
plotting linecuts.
|
|
5
5
|
'''
|
|
6
6
|
import os
|
|
7
|
+
|
|
7
8
|
import matplotlib.pyplot as plt
|
|
8
9
|
import matplotlib as mpl
|
|
9
10
|
|
|
@@ -14,6 +15,7 @@ class TempDependence():
|
|
|
14
15
|
'''
|
|
15
16
|
Class for analyzing scattering datasets collected at CHESS (ID4B) with temperature dependence.
|
|
16
17
|
'''
|
|
18
|
+
|
|
17
19
|
def __init__(self):
|
|
18
20
|
'''
|
|
19
21
|
Initialize TempDependence class.
|
|
@@ -21,8 +23,8 @@ class TempDependence():
|
|
|
21
23
|
self.datasets={}
|
|
22
24
|
self.folder=None
|
|
23
25
|
self.temperatures=None
|
|
24
|
-
self.scissors=
|
|
25
|
-
self.linecuts=
|
|
26
|
+
self.scissors= {}
|
|
27
|
+
self.linecuts={}
|
|
26
28
|
|
|
27
29
|
def get_folder(self):
|
|
28
30
|
'''
|
|
@@ -81,40 +83,42 @@ class TempDependence():
|
|
|
81
83
|
print('-----------------------------------------------')
|
|
82
84
|
print('Loading ' + temperature + ' K indexed .nxs files...')
|
|
83
85
|
print('Found ' + filepath)
|
|
84
|
-
self.datasets[temperature] = load_data(filepath)
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
# Load dataset at each temperature
|
|
88
|
+
self.datasets[temperature] = load_data(filepath)
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
# Initialize scissors object at each temperature
|
|
91
|
+
self.scissors[temperature] = Scissors()
|
|
92
|
+
self.scissors[temperature].set_data(self.datasets[temperature])
|
|
90
93
|
|
|
91
94
|
def set_window(self, window):
|
|
92
|
-
|
|
95
|
+
"""
|
|
93
96
|
Set the extents of the integration window.
|
|
94
97
|
|
|
95
98
|
Parameters
|
|
96
99
|
----------
|
|
97
100
|
window : tuple
|
|
98
101
|
Extents of the window for integration along each axis.
|
|
99
|
-
|
|
100
|
-
for
|
|
102
|
+
"""
|
|
103
|
+
for T in self.temperatures:
|
|
101
104
|
print("----------------------------------")
|
|
102
|
-
print("T = " +
|
|
103
|
-
scissors.set_window(window)
|
|
105
|
+
print("T = " + T + " K")
|
|
106
|
+
self.scissors[T].set_window(window)
|
|
104
107
|
|
|
105
108
|
def set_center(self, center):
|
|
106
|
-
|
|
109
|
+
"""
|
|
107
110
|
Set the central coordinate for the linecut.
|
|
108
111
|
|
|
109
112
|
Parameters
|
|
110
113
|
----------
|
|
111
114
|
center : tuple
|
|
112
115
|
Central coordinate around which to perform the linecut.
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
"""
|
|
117
|
+
for T in self.temperatures:
|
|
118
|
+
self.scissors[T].set_center(center)
|
|
115
119
|
|
|
116
120
|
def cut_data(self, center=None, window=None, axis=None):
|
|
117
|
-
|
|
121
|
+
"""
|
|
118
122
|
Perform data cutting for each temperature dataset.
|
|
119
123
|
|
|
120
124
|
Parameters
|
|
@@ -131,21 +135,20 @@ class TempDependence():
|
|
|
131
135
|
-------
|
|
132
136
|
list
|
|
133
137
|
A list of linecuts obtained from the cutting operation.
|
|
134
|
-
|
|
138
|
+
"""
|
|
135
139
|
|
|
136
|
-
center = center if center is not None else self.scissors[0].center
|
|
137
|
-
window = window if window is not None else self.scissors[0].window
|
|
140
|
+
center = center if center is not None else self.scissors[self.temperatures[0]].center
|
|
141
|
+
window = window if window is not None else self.scissors[self.temperatures[0]].window
|
|
138
142
|
|
|
139
|
-
for
|
|
143
|
+
for T in self.temperatures:
|
|
140
144
|
print("-------------------------------")
|
|
141
145
|
print("Cutting T = " + T + " K data...")
|
|
142
|
-
self.scissors[
|
|
143
|
-
|
|
144
|
-
self.linecuts = [scissors.linecut for scissors in self.scissors]
|
|
146
|
+
self.scissors[T].cut_data(center, window, axis)
|
|
147
|
+
self.linecuts[T] = self.scissors[T].linecut
|
|
145
148
|
return self.linecuts
|
|
146
149
|
|
|
147
150
|
def plot_linecuts(self, vertical_offset=0, **kwargs):
|
|
148
|
-
|
|
151
|
+
"""
|
|
149
152
|
Plot the linecuts obtained from data cutting.
|
|
150
153
|
|
|
151
154
|
Parameters
|
|
@@ -154,23 +157,24 @@ class TempDependence():
|
|
|
154
157
|
The vertical offset between linecuts on the plot. The default is 0.
|
|
155
158
|
**kwargs
|
|
156
159
|
Additional keyword arguments to be passed to the plot function.
|
|
157
|
-
|
|
160
|
+
"""
|
|
158
161
|
fig, ax = plt.subplots()
|
|
159
162
|
|
|
160
163
|
# Get the Viridis colormap
|
|
161
164
|
cmap = mpl.colormaps.get_cmap('viridis')
|
|
162
165
|
|
|
163
|
-
for i, linecut in enumerate(self.linecuts):
|
|
166
|
+
for i, linecut in enumerate(self.linecuts.values()):
|
|
164
167
|
x_data = linecut[linecut.axes[0]].nxdata
|
|
165
168
|
y_data = linecut[linecut.signal].nxdata + i*vertical_offset
|
|
166
169
|
ax.plot(x_data, y_data, color=cmap(i / len(self.linecuts)), label=self.temperatures[i],
|
|
167
170
|
**kwargs)
|
|
168
171
|
|
|
169
|
-
xlabel_components = [self.linecuts[
|
|
170
|
-
|
|
172
|
+
xlabel_components = [self.linecuts[self.temperatures[0]].axes[0]
|
|
173
|
+
if i == self.scissors[self.temperatures[0]].axis \
|
|
174
|
+
else str(c) for i,c in enumerate(self.scissors[self.temperatures[0]].center)]
|
|
171
175
|
xlabel = ' '.join(xlabel_components)
|
|
172
176
|
ax.set(xlabel=xlabel,
|
|
173
|
-
ylabel=self.linecuts[0].signal)
|
|
177
|
+
ylabel=self.linecuts[self.temperatures[0]].signal)
|
|
174
178
|
|
|
175
179
|
# Get the current legend handles and labels
|
|
176
180
|
handles, labels = plt.gca().get_legend_handles_labels()
|
|
@@ -184,9 +188,9 @@ class TempDependence():
|
|
|
184
188
|
|
|
185
189
|
return fig,ax
|
|
186
190
|
|
|
187
|
-
def
|
|
188
|
-
|
|
189
|
-
Displays the integration window plot for a specific temperature or for
|
|
191
|
+
def highlight_integration_window(self, temperature=None, **kwargs):
|
|
192
|
+
"""
|
|
193
|
+
Displays the integration window plot for a specific temperature, or for the first temperature if
|
|
190
194
|
none is provided.
|
|
191
195
|
|
|
192
196
|
Parameters
|
|
@@ -194,10 +198,39 @@ class TempDependence():
|
|
|
194
198
|
temperature : str, optional
|
|
195
199
|
The temperature at which to display the integration window plot. If provided, the plot
|
|
196
200
|
will be generated using the dataset corresponding to the specified temperature. If not
|
|
197
|
-
provided, the integration window plots will be generated for
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
provided, the integration window plots will be generated for the first temperature.
|
|
202
|
+
**kwargs : keyword arguments, optional
|
|
203
|
+
Additional keyword arguments to customize the plot.
|
|
204
|
+
"""
|
|
205
|
+
|
|
200
206
|
if temperature is not None:
|
|
201
|
-
self.scissors[0].
|
|
207
|
+
p = self.scissors[self.temperatures[0]].highlight_integration_window(data=self.datasets[temperature],
|
|
208
|
+
**kwargs)
|
|
202
209
|
else:
|
|
203
|
-
self.scissors[
|
|
210
|
+
p = self.scissors[self.temperatures[0]].highlight_integration_window(
|
|
211
|
+
data=self.datasets[self.temperatures[0]], **kwargs
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return p
|
|
215
|
+
|
|
216
|
+
def plot_integration_window(self, temperature=None, **kwargs):
|
|
217
|
+
"""
|
|
218
|
+
Plots the three principal cross-sections of the integration volume on a single figure for a specific
|
|
219
|
+
temperature, or for the first temperature if none is provided.
|
|
220
|
+
|
|
221
|
+
Parameters
|
|
222
|
+
----------
|
|
223
|
+
temperature : str, optional
|
|
224
|
+
The temperature at which to plot the integration volume. If provided, the plot
|
|
225
|
+
will be generated using the dataset corresponding to the specified temperature. If not
|
|
226
|
+
provided, the integration window plots will be generated for the first temperature.
|
|
227
|
+
**kwargs : keyword arguments, optional
|
|
228
|
+
Additional keyword arguments to customize the plot.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
if temperature is not None:
|
|
232
|
+
p = self.scissors[self.temperatures[0]].plot_integration_window(**kwargs)
|
|
233
|
+
else:
|
|
234
|
+
p = self.scissors[self.temperatures[0]].plot_integration_window(**kwargs)
|
|
235
|
+
|
|
236
|
+
return p
|
|
@@ -12,10 +12,11 @@ from matplotlib import patches
|
|
|
12
12
|
from IPython.display import display, Markdown
|
|
13
13
|
from nexusformat.nexus import NXfield, NXdata, nxload
|
|
14
14
|
|
|
15
|
-
__all__=['load_data','plot_slice','Scissors']
|
|
15
|
+
__all__ = ['load_data', 'plot_slice', 'Scissors']
|
|
16
|
+
|
|
16
17
|
|
|
17
18
|
def load_data(path):
|
|
18
|
-
|
|
19
|
+
"""
|
|
19
20
|
Load data from a specified path.
|
|
20
21
|
|
|
21
22
|
Parameters
|
|
@@ -28,7 +29,7 @@ def load_data(path):
|
|
|
28
29
|
data : nxdata object
|
|
29
30
|
The loaded data stored in a nxdata object.
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
"""
|
|
32
33
|
g = nxload(path)
|
|
33
34
|
try:
|
|
34
35
|
print(g.entry.data.tree)
|
|
@@ -39,10 +40,10 @@ def load_data(path):
|
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew_angle=90,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
ax=None, xlim=None, ylim=None, xticks=None, yticks=None, cbar=True, logscale=False,
|
|
44
|
+
symlogscale=False, cmap='viridis', linthresh=1, title=None, mdheading=None, cbartitle=None,
|
|
45
|
+
**kwargs):
|
|
46
|
+
"""
|
|
46
47
|
Parameters
|
|
47
48
|
----------
|
|
48
49
|
data : :class:`nexusformat.nexus.NXdata` object
|
|
@@ -107,10 +108,10 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
|
|
|
107
108
|
-------
|
|
108
109
|
p : :class:`matplotlib.collections.QuadMesh`
|
|
109
110
|
|
|
110
|
-
A :class:`matplotlib.collections.QuadMesh` object, to mimick behavior of
|
|
111
|
+
A :class:`matplotlib.collections.QuadMesh` object, to mimick behavior of
|
|
111
112
|
:class:`matplotlib.pyplot.pcolormesh`.
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
"""
|
|
114
115
|
|
|
115
116
|
if X is None:
|
|
116
117
|
X = data[data.axes[0]]
|
|
@@ -132,18 +133,18 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
|
|
|
132
133
|
|
|
133
134
|
# Inherit axes if user provides some
|
|
134
135
|
if ax is not None:
|
|
135
|
-
ax=ax
|
|
136
|
-
fig=ax.get_figure()
|
|
136
|
+
ax = ax
|
|
137
|
+
fig = ax.get_figure()
|
|
137
138
|
# Otherwise set up some default axes
|
|
138
139
|
else:
|
|
139
140
|
fig = plt.figure()
|
|
140
|
-
ax = fig.add_axes([0,0,1,1])
|
|
141
|
+
ax = fig.add_axes([0, 0, 1, 1])
|
|
141
142
|
|
|
142
143
|
# If limits not provided, use extrema
|
|
143
144
|
if vmin is None:
|
|
144
|
-
vmin=data_arr.min()
|
|
145
|
+
vmin = data_arr.min()
|
|
145
146
|
if vmax is None:
|
|
146
|
-
vmax=data_arr.max()
|
|
147
|
+
vmax = data_arr.max()
|
|
147
148
|
|
|
148
149
|
# Set norm (linear scale, logscale, or symlogscale)
|
|
149
150
|
norm = colors.Normalize(vmin=vmin, vmax=vmax) # Default: linear scale
|
|
@@ -153,42 +154,41 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
|
|
|
153
154
|
elif logscale:
|
|
154
155
|
norm = colors.LogNorm(vmin=vmin, vmax=vmax)
|
|
155
156
|
|
|
156
|
-
|
|
157
157
|
# Plot data
|
|
158
|
-
p = ax.pcolormesh(X.nxdata, Y.nxdata, data_arr, shading='auto', norm=norm, cmap=cmap)
|
|
158
|
+
p = ax.pcolormesh(X.nxdata, Y.nxdata, data_arr, shading='auto', norm=norm, cmap=cmap, **kwargs)
|
|
159
159
|
|
|
160
160
|
## Transform data to new coordinate system if necessary
|
|
161
161
|
# Correct skew angle
|
|
162
|
-
skew_angle = 90-skew_angle
|
|
162
|
+
skew_angle = 90 - skew_angle
|
|
163
163
|
# Create blank 2D affine transformation
|
|
164
164
|
t = Affine2D()
|
|
165
165
|
# Scale y-axis to preserve norm while shearing
|
|
166
|
-
t += Affine2D().scale(1,np.cos(skew_angle*np.pi/180))
|
|
166
|
+
t += Affine2D().scale(1, np.cos(skew_angle * np.pi / 180))
|
|
167
167
|
# Shear along x-axis
|
|
168
|
-
t += Affine2D().skew_deg(skew_angle,0)
|
|
168
|
+
t += Affine2D().skew_deg(skew_angle, 0)
|
|
169
169
|
# Return to original y-axis scaling
|
|
170
|
-
t += Affine2D().scale(1,np.cos(skew_angle*np.pi/180)).inverted()
|
|
170
|
+
t += Affine2D().scale(1, np.cos(skew_angle * np.pi / 180)).inverted()
|
|
171
171
|
## Correct for x-displacement after shearing
|
|
172
172
|
# If ylims provided, use those
|
|
173
173
|
if ylim is not None:
|
|
174
174
|
# Set ylims
|
|
175
175
|
ax.set(ylim=ylim)
|
|
176
|
-
ymin,ymax = ylim
|
|
176
|
+
ymin, ymax = ylim
|
|
177
177
|
# Else, use current ylims
|
|
178
178
|
else:
|
|
179
|
-
ymin,ymax = ax.get_ylim()
|
|
179
|
+
ymin, ymax = ax.get_ylim()
|
|
180
180
|
# Use ylims to calculate translation (necessary to display axes in correct position)
|
|
181
|
-
p.set_transform(t + Affine2D().translate(-ymin*np.sin(skew_angle*np.pi/180),0) + ax.transData)
|
|
181
|
+
p.set_transform(t + Affine2D().translate(-ymin * np.sin(skew_angle * np.pi / 180), 0) + ax.transData)
|
|
182
182
|
|
|
183
183
|
# Set x limits
|
|
184
184
|
if xlim is not None:
|
|
185
|
-
xmin,xmax = xlim
|
|
185
|
+
xmin, xmax = xlim
|
|
186
186
|
else:
|
|
187
|
-
xmin,xmax = ax.get_xlim()
|
|
188
|
-
ax.set(xlim=(xmin,xmax+(ymax-ymin)/np.tan((90-skew_angle)*np.pi/180)))
|
|
187
|
+
xmin, xmax = ax.get_xlim()
|
|
188
|
+
ax.set(xlim=(xmin, xmax + (ymax - ymin) / np.tan((90 - skew_angle) * np.pi / 180)))
|
|
189
189
|
|
|
190
190
|
# Correct aspect ratio for the x/y axes after transformation
|
|
191
|
-
ax.set(aspect=np.cos(skew_angle*np.pi/180))
|
|
191
|
+
ax.set(aspect=np.cos(skew_angle * np.pi / 180))
|
|
192
192
|
|
|
193
193
|
# Add tick marks all around
|
|
194
194
|
ax.tick_params(direction='in', top=True, right=True, which='both')
|
|
@@ -209,41 +209,41 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
|
|
|
209
209
|
ax.yaxis.set_major_locator(MultipleLocator(yticks))
|
|
210
210
|
ax.yaxis.set_minor_locator(MultipleLocator(1))
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
for i in range(0,len(ax.xaxis.get_ticklines())):
|
|
212
|
+
# Apply transform to tick marks
|
|
213
|
+
for i in range(0, len(ax.xaxis.get_ticklines())):
|
|
214
214
|
# Tick marker
|
|
215
215
|
m = MarkerStyle(3)
|
|
216
|
-
line =
|
|
217
|
-
if i%2:
|
|
216
|
+
line = ax.xaxis.get_majorticklines()[i]
|
|
217
|
+
if i % 2:
|
|
218
218
|
# Top ticks (translation here makes their direction="in")
|
|
219
|
-
m._transform.set(Affine2D().translate(0
|
|
219
|
+
m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle, 0))
|
|
220
220
|
# This first method shifts the top ticks horizontally to match the skew angle.
|
|
221
221
|
# This does not look good in all cases.
|
|
222
222
|
# line.set_transform(Affine2D().translate((ymax-ymin)*np.sin(skew_angle*np.pi/180),0) +
|
|
223
223
|
# line.get_transform())
|
|
224
224
|
# This second method skews the tick marks in place and
|
|
225
225
|
# can sometimes lead to them being misaligned.
|
|
226
|
-
line.set_transform(line.get_transform())
|
|
226
|
+
line.set_transform(line.get_transform()) # This does nothing
|
|
227
227
|
else:
|
|
228
228
|
# Bottom ticks
|
|
229
|
-
m._transform.set(Affine2D().skew_deg(skew_angle,0))
|
|
229
|
+
m._transform.set(Affine2D().skew_deg(skew_angle, 0))
|
|
230
230
|
|
|
231
231
|
line.set_marker(m)
|
|
232
232
|
|
|
233
|
-
for i in range(0,len(ax.xaxis.get_minorticklines())):
|
|
233
|
+
for i in range(0, len(ax.xaxis.get_minorticklines())):
|
|
234
234
|
m = MarkerStyle(2)
|
|
235
|
-
line =
|
|
236
|
-
if i%2:
|
|
237
|
-
m._transform.set(Affine2D().translate(0
|
|
235
|
+
line = ax.xaxis.get_minorticklines()[i]
|
|
236
|
+
if i % 2:
|
|
237
|
+
m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle, 0))
|
|
238
238
|
else:
|
|
239
|
-
m._transform.set(Affine2D().skew_deg(skew_angle,0))
|
|
239
|
+
m._transform.set(Affine2D().skew_deg(skew_angle, 0))
|
|
240
240
|
|
|
241
241
|
line.set_marker(m)
|
|
242
242
|
|
|
243
243
|
if cbar:
|
|
244
244
|
colorbar = fig.colorbar(p)
|
|
245
|
-
|
|
246
|
-
|
|
245
|
+
if cbartitle is None:
|
|
246
|
+
colorbar.set_label(data.signal)
|
|
247
247
|
|
|
248
248
|
ax.set(
|
|
249
249
|
xlabel=X.nxname,
|
|
@@ -256,8 +256,9 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
|
|
|
256
256
|
# Return the quadmesh object
|
|
257
257
|
return p
|
|
258
258
|
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
|
|
260
|
+
class Scissors:
|
|
261
|
+
"""
|
|
261
262
|
Scissors class provides functionality for reducing data to a 1D linecut using an integration
|
|
262
263
|
window.
|
|
263
264
|
|
|
@@ -298,10 +299,10 @@ class Scissors():
|
|
|
298
299
|
Plot the integration window highlighted on a 2D heatmap of the full dataset.
|
|
299
300
|
plot_window()
|
|
300
301
|
Plot a 2D heatmap of the integration window data.
|
|
301
|
-
|
|
302
|
+
"""
|
|
302
303
|
|
|
303
304
|
def __init__(self, data=None, center=None, window=None, axis=None):
|
|
304
|
-
|
|
305
|
+
"""
|
|
305
306
|
Initializes a Scissors object.
|
|
306
307
|
|
|
307
308
|
Parameters
|
|
@@ -314,7 +315,7 @@ class Scissors():
|
|
|
314
315
|
Extents of the window for integration along each axis. Default is None.
|
|
315
316
|
axis : int or None, optional
|
|
316
317
|
Axis along which to perform the integration. Default is None.
|
|
317
|
-
|
|
318
|
+
"""
|
|
318
319
|
|
|
319
320
|
self.data = data
|
|
320
321
|
self.center = center
|
|
@@ -327,74 +328,74 @@ class Scissors():
|
|
|
327
328
|
self.integration_window = None
|
|
328
329
|
|
|
329
330
|
def set_data(self, data):
|
|
330
|
-
|
|
331
|
+
"""
|
|
331
332
|
Set the input NXdata.
|
|
332
333
|
|
|
333
334
|
Parameters
|
|
334
335
|
----------
|
|
335
336
|
data : :class:`nexusformat.nexus.NXdata`
|
|
336
337
|
Input data array.
|
|
337
|
-
|
|
338
|
+
"""
|
|
338
339
|
self.data = data
|
|
339
340
|
|
|
340
341
|
def get_data(self):
|
|
341
|
-
|
|
342
|
+
"""
|
|
342
343
|
Get the input data array.
|
|
343
344
|
|
|
344
345
|
Returns
|
|
345
346
|
-------
|
|
346
347
|
ndarray or None
|
|
347
348
|
Input data array.
|
|
348
|
-
|
|
349
|
+
"""
|
|
349
350
|
return self.data
|
|
350
351
|
|
|
351
352
|
def set_center(self, center):
|
|
352
|
-
|
|
353
|
+
"""
|
|
353
354
|
Set the central coordinate for the linecut.
|
|
354
355
|
|
|
355
356
|
Parameters
|
|
356
357
|
----------
|
|
357
358
|
center : tuple
|
|
358
359
|
Central coordinate around which to perform the linecut.
|
|
359
|
-
|
|
360
|
+
"""
|
|
360
361
|
self.center = center
|
|
361
362
|
|
|
362
363
|
def set_window(self, window):
|
|
363
|
-
|
|
364
|
+
"""
|
|
364
365
|
Set the extents of the integration window.
|
|
365
366
|
|
|
366
367
|
Parameters
|
|
367
368
|
----------
|
|
368
369
|
window : tuple
|
|
369
370
|
Extents of the window for integration along each axis.
|
|
370
|
-
|
|
371
|
+
"""
|
|
371
372
|
self.window = window
|
|
372
373
|
|
|
373
374
|
# Determine the axis for integration
|
|
374
375
|
self.axis = window.index(max(window))
|
|
375
|
-
print("Linecut axis: "+str(self.data.axes[self.axis]))
|
|
376
|
+
print("Linecut axis: " + str(self.data.axes[self.axis]))
|
|
376
377
|
|
|
377
378
|
# Determine the integrated axes (axes other than the integration axis)
|
|
378
379
|
self.integrated_axes = tuple(i for i in range(self.data.ndim) if i != self.axis)
|
|
379
|
-
print("Integrated axes: "+str([self.data.axes[axis] for axis in self.integrated_axes]))
|
|
380
|
+
print("Integrated axes: " + str([self.data.axes[axis] for axis in self.integrated_axes]))
|
|
380
381
|
|
|
381
382
|
def get_window(self):
|
|
382
|
-
|
|
383
|
+
"""
|
|
383
384
|
Get the extents of the integration window.
|
|
384
385
|
|
|
385
386
|
Returns
|
|
386
387
|
-------
|
|
387
388
|
tuple or None
|
|
388
389
|
Extents of the integration window.
|
|
389
|
-
|
|
390
|
+
"""
|
|
390
391
|
return self.window
|
|
391
392
|
|
|
392
393
|
def cut_data(self, center=None, window=None, axis=None):
|
|
393
|
-
|
|
394
|
+
"""
|
|
394
395
|
Reduces data to a 1D linecut with integration extents specified by the window about a central
|
|
395
396
|
coordinate.
|
|
396
397
|
|
|
397
|
-
Parameters
|
|
398
|
+
Parameters
|
|
398
399
|
-----------
|
|
399
400
|
center : float or None, optional
|
|
400
401
|
Central coordinate for the linecut. If not specified, the value from the object's
|
|
@@ -406,11 +407,11 @@ class Scissors():
|
|
|
406
407
|
The axis along which to perform the linecut. If not specified, the value from the
|
|
407
408
|
object's attribute will be used.
|
|
408
409
|
|
|
409
|
-
Returns
|
|
410
|
+
Returns
|
|
410
411
|
--------
|
|
411
412
|
integrated_data : :class:`nexusformat.nexus.NXdata`
|
|
412
413
|
1D linecut data after integration.
|
|
413
|
-
|
|
414
|
+
"""
|
|
414
415
|
|
|
415
416
|
# Extract necessary attributes from the object
|
|
416
417
|
data = self.data
|
|
@@ -435,104 +436,155 @@ class Scissors():
|
|
|
435
436
|
|
|
436
437
|
# Perform integration along the integrated axes
|
|
437
438
|
integrated_data = np.sum(self.integration_volume[self.integration_volume.signal].nxdata,
|
|
438
|
-
|
|
439
|
+
axis=self.integrated_axes)
|
|
439
440
|
|
|
440
441
|
# Create an NXdata object for the linecut data
|
|
441
442
|
self.linecut = NXdata(NXfield(integrated_data, name=self.integration_volume.signal),
|
|
442
|
-
|
|
443
|
+
self.integration_volume[self.integration_volume.axes[axis]])
|
|
443
444
|
self.linecut.nxname = self.integration_volume.nxname
|
|
444
445
|
|
|
445
446
|
return self.linecut
|
|
446
447
|
|
|
447
|
-
def
|
|
448
|
-
|
|
449
|
-
Plots integration window highlighted on
|
|
448
|
+
def highlight_integration_window(self, data=None, label=None, highlight_color='red', **kwargs):
|
|
449
|
+
"""
|
|
450
|
+
Plots integration window highlighted on the three principal cross sections of the first
|
|
451
|
+
temperature dataset.
|
|
450
452
|
|
|
451
453
|
Parameters
|
|
452
454
|
----------
|
|
453
455
|
data : array-like, optional
|
|
454
|
-
The 2D heatmap dataset to plot. If not provided, the dataset stored in `self.data` will
|
|
456
|
+
The 2D heatmap dataset to plot. If not provided, the dataset stored in `self.data` will
|
|
457
|
+
be used.
|
|
455
458
|
label : str, optional
|
|
456
459
|
The label for the integration window plot.
|
|
460
|
+
highlight_color : str, optional
|
|
461
|
+
The edge color used to highlight the integration window. Default is 'red'.
|
|
457
462
|
**kwargs : keyword arguments, optional
|
|
458
463
|
Additional keyword arguments to customize the plot.
|
|
459
464
|
|
|
460
|
-
|
|
465
|
+
"""
|
|
461
466
|
data = self.data if data is None else data
|
|
462
|
-
axis = self.axis
|
|
463
467
|
center = self.center
|
|
464
468
|
window = self.window
|
|
465
469
|
integrated_axes = self.integrated_axes
|
|
466
470
|
|
|
471
|
+
# Create a figure and subplots
|
|
472
|
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
|
473
|
+
|
|
467
474
|
# Plot cross section 1
|
|
468
|
-
slice_obj = [slice(None)]*data.ndim
|
|
469
|
-
slice_obj[
|
|
475
|
+
slice_obj = [slice(None)] * data.ndim
|
|
476
|
+
slice_obj[2] = center[2]
|
|
470
477
|
|
|
471
478
|
p1 = plot_slice(data[slice_obj],
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
479
|
+
X=data[data.axes[0]],
|
|
480
|
+
Y=data[data.axes[1]],
|
|
481
|
+
ax=axes[0],
|
|
482
|
+
**kwargs)
|
|
483
|
+
ax = axes[0]
|
|
476
484
|
rect_diffuse = patches.Rectangle(
|
|
477
|
-
(center[
|
|
478
|
-
|
|
479
|
-
2*window[
|
|
480
|
-
linewidth=1, edgecolor=
|
|
481
|
-
|
|
485
|
+
(center[0] - window[0],
|
|
486
|
+
center[1] - window[1]),
|
|
487
|
+
2 * window[0], 2 * window[1],
|
|
488
|
+
linewidth=1, edgecolor=highlight_color, facecolor='none', transform=p1.get_transform(), label=label,
|
|
489
|
+
)
|
|
482
490
|
ax.add_patch(rect_diffuse)
|
|
483
|
-
plt.show()
|
|
484
491
|
|
|
485
492
|
# Plot cross section 2
|
|
486
|
-
slice_obj = [slice(None)]*data.ndim
|
|
487
|
-
slice_obj[
|
|
493
|
+
slice_obj = [slice(None)] * data.ndim
|
|
494
|
+
slice_obj[1] = center[1]
|
|
488
495
|
|
|
489
496
|
p2 = plot_slice(data[slice_obj],
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
497
|
+
X=data[data.axes[0]],
|
|
498
|
+
Y=data[data.axes[2]],
|
|
499
|
+
ax=axes[1],
|
|
500
|
+
**kwargs)
|
|
501
|
+
ax = axes[1]
|
|
494
502
|
rect_diffuse = patches.Rectangle(
|
|
495
|
-
(center[
|
|
496
|
-
|
|
497
|
-
2*window[
|
|
498
|
-
linewidth=1, edgecolor=
|
|
499
|
-
|
|
503
|
+
(center[0] - window[0],
|
|
504
|
+
center[2] - window[2]),
|
|
505
|
+
2 * window[0], 2 * window[2],
|
|
506
|
+
linewidth=1, edgecolor=highlight_color, facecolor='none', transform=p2.get_transform(), label=label,
|
|
507
|
+
)
|
|
500
508
|
ax.add_patch(rect_diffuse)
|
|
501
|
-
plt.show()
|
|
502
509
|
|
|
503
510
|
# Plot cross section 3
|
|
504
|
-
slice_obj = [slice(None)]*data.ndim
|
|
505
|
-
slice_obj[
|
|
511
|
+
slice_obj = [slice(None)] * data.ndim
|
|
512
|
+
slice_obj[0] = center[0]
|
|
506
513
|
|
|
507
514
|
p3 = plot_slice(data[slice_obj],
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
515
|
+
X=data[data.axes[1]],
|
|
516
|
+
Y=data[data.axes[2]],
|
|
517
|
+
ax=axes[2],
|
|
518
|
+
**kwargs)
|
|
519
|
+
ax = axes[2]
|
|
512
520
|
rect_diffuse = patches.Rectangle(
|
|
513
|
-
(center[
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
linewidth=1, edgecolor=
|
|
517
|
-
|
|
521
|
+
(center[1] - window[1],
|
|
522
|
+
center[2] - window[2]),
|
|
523
|
+
2 * window[1], 2 * window[2],
|
|
524
|
+
linewidth=1, edgecolor=highlight_color, facecolor='none', transform=p3.get_transform(), label=label,
|
|
525
|
+
)
|
|
518
526
|
ax.add_patch(rect_diffuse)
|
|
527
|
+
|
|
528
|
+
# Adjust subplot padding
|
|
529
|
+
fig.subplots_adjust(wspace=0.5)
|
|
530
|
+
|
|
531
|
+
if label is not None:
|
|
532
|
+
[ax.legend() for ax in axes]
|
|
533
|
+
|
|
519
534
|
plt.show()
|
|
520
535
|
|
|
521
|
-
return
|
|
536
|
+
return p1, p2, p3
|
|
537
|
+
|
|
538
|
+
def plot_integration_window(self, **kwargs):
|
|
539
|
+
"""
|
|
540
|
+
Plots the three principal cross-sections of the integration volume on a single figure.
|
|
541
|
+
|
|
542
|
+
Parameters
|
|
543
|
+
----------
|
|
544
|
+
**kwargs : keyword arguments, optional
|
|
545
|
+
Additional keyword arguments to customize the plot.
|
|
546
|
+
"""
|
|
547
|
+
data = self.integration_volume
|
|
548
|
+
axis = self.axis
|
|
549
|
+
center = self.center
|
|
550
|
+
window = self.window
|
|
551
|
+
integrated_axes = self.integrated_axes
|
|
552
|
+
|
|
553
|
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
|
522
554
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
555
|
+
# Plot cross section 1
|
|
556
|
+
slice_obj = [slice(None)] * data.ndim
|
|
557
|
+
slice_obj[2] = center[2]
|
|
558
|
+
p1 = plot_slice(data[slice_obj],
|
|
559
|
+
X=data[data.axes[0]],
|
|
560
|
+
Y=data[data.axes[1]],
|
|
561
|
+
ax=axes[0],
|
|
562
|
+
**kwargs)
|
|
563
|
+
axes[0].set_aspect(len(data[data.axes[0]].nxdata) / len(data[data.axes[1]].nxdata))
|
|
528
564
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
565
|
+
# Plot cross section 2
|
|
566
|
+
slice_obj = [slice(None)] * data.ndim
|
|
567
|
+
slice_obj[1] = center[1]
|
|
568
|
+
p3 = plot_slice(data[slice_obj],
|
|
569
|
+
X=data[data.axes[0]],
|
|
570
|
+
Y=data[data.axes[2]],
|
|
571
|
+
ax=axes[1],
|
|
572
|
+
**kwargs)
|
|
573
|
+
axes[1].set_aspect(len(data[data.axes[0]].nxdata) / len(data[data.axes[2]].nxdata))
|
|
574
|
+
|
|
575
|
+
# Plot cross section 3
|
|
576
|
+
slice_obj = [slice(None)] * data.ndim
|
|
577
|
+
slice_obj[0] = center[0]
|
|
578
|
+
p2 = plot_slice(data[slice_obj],
|
|
579
|
+
X=data[data.axes[1]],
|
|
580
|
+
Y=data[data.axes[2]],
|
|
581
|
+
ax=axes[2],
|
|
582
|
+
**kwargs)
|
|
583
|
+
axes[2].set_aspect(len(data[data.axes[1]].nxdata) / len(data[data.axes[2]].nxdata))
|
|
584
|
+
|
|
585
|
+
# Adjust subplot padding
|
|
586
|
+
fig.subplots_adjust(wspace=0.3)
|
|
587
|
+
|
|
588
|
+
plt.show()
|
|
537
589
|
|
|
538
|
-
|
|
590
|
+
return p1, p2, p3
|
|
@@ -1,80 +1,438 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tools for generating single crystal pair distribution functions.
|
|
3
|
+
"""
|
|
4
|
+
import time
|
|
5
|
+
import os
|
|
1
6
|
from scipy import ndimage
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
2
8
|
from matplotlib.transforms import Affine2D
|
|
9
|
+
from nexusformat.nexus import nxsave, NXroot, NXentry, NXdata, NXfield
|
|
3
10
|
import numpy as np
|
|
11
|
+
from nxs_analysis_tools import plot_slice
|
|
4
12
|
|
|
5
|
-
|
|
13
|
+
|
|
14
|
+
class Padder():
|
|
6
15
|
"""
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Parameters:
|
|
10
|
-
q1 (ndarray): Array of shape (M,) representing the values of axis q1.
|
|
11
|
-
q2 (ndarray): Array of shape (N,) representing the values of axis q2.
|
|
12
|
-
counts (ndarray): 2D array of shape (M, N) representing the counts at each (q1,q2) coordinate.
|
|
13
|
-
theta_min (float): Minimum angle in degrees for the symmetrization range.
|
|
14
|
-
theta_max (float): Maximum angle in degrees for the symmetrization range.
|
|
15
|
-
skew_angle (float, optional): Skew angle in degrees. Default is 90.
|
|
16
|
-
mirror (bool, optional): Flag indicating whether to include a mirror operation during transformation. Default is True.
|
|
17
|
-
|
|
18
|
-
Returns:
|
|
19
|
-
ndarray: 2D array of shape (M, N) representing the symmetrized dataset.
|
|
16
|
+
A class to pad and unpad datasets with a symmetric region of zeros.
|
|
20
17
|
"""
|
|
21
18
|
|
|
19
|
+
def __init__(self, data=None):
|
|
20
|
+
"""
|
|
21
|
+
Initialize the Symmetrizer3D object.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
data : NXdata, optional
|
|
26
|
+
The input data to be symmetrized. If provided, the `set_data` method is called to set the data.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
if data is not None:
|
|
30
|
+
self.set_data(data)
|
|
31
|
+
|
|
32
|
+
def set_data(self, data):
|
|
33
|
+
"""
|
|
34
|
+
Set the input data for symmetrization.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
data : NXdata
|
|
39
|
+
The input data to be symmetrized.
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
self.data = data
|
|
43
|
+
|
|
44
|
+
self.steps = tuple([(data[axis].nxdata[1] - data[axis].nxdata[0]) for axis in data.axes])
|
|
45
|
+
|
|
46
|
+
# Absolute value of the maximum value; assumes the domain of the input is symmetric (eg, -H_min = H_max)
|
|
47
|
+
self.maxes = tuple([data[axis].nxdata.max() for axis in data.axes])
|
|
48
|
+
|
|
49
|
+
def pad(self, padding):
|
|
50
|
+
"""
|
|
51
|
+
Symmetrically pads the data with zero values.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
padding : tuple
|
|
56
|
+
The number of zero-value pixels to add along each edge of the array.
|
|
57
|
+
"""
|
|
58
|
+
data = self.data
|
|
59
|
+
self.padding = padding
|
|
60
|
+
|
|
61
|
+
padded_shape = tuple([data[data.signal].nxdata.shape[i] + self.padding[i] * 2 for i in range(data.ndim)])
|
|
62
|
+
|
|
63
|
+
# Create padded dataset
|
|
64
|
+
padded = np.zeros(padded_shape)
|
|
65
|
+
|
|
66
|
+
slice_obj = [slice(None)] * data.ndim
|
|
67
|
+
for i, _ in enumerate(slice_obj):
|
|
68
|
+
slice_obj[i] = slice(self.padding[i], -self.padding[i], None)
|
|
69
|
+
slice_obj = tuple(slice_obj)
|
|
70
|
+
padded[slice_obj] = data[data.signal].nxdata
|
|
71
|
+
|
|
72
|
+
padmaxes = tuple([self.maxes[i] + self.padding[i] * self.steps[i] for i in range(data.ndim)])
|
|
73
|
+
|
|
74
|
+
padded = NXdata(NXfield(padded, name=data.signal),
|
|
75
|
+
tuple([NXfield(np.linspace(-padmaxes[i], padmaxes[i], padded_shape[i]),
|
|
76
|
+
name=data.axes[i])
|
|
77
|
+
for i in range(data.ndim)]))
|
|
78
|
+
|
|
79
|
+
self.padded = padded
|
|
80
|
+
return padded
|
|
81
|
+
|
|
82
|
+
def save(self, fout_name=None):
|
|
83
|
+
"""
|
|
84
|
+
Saves the padded dataset to a .nxs file.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
fout_name : str, optional
|
|
89
|
+
The output file name. Default is padded_(Hpadding)_(Kpadding)_(Lpadding).nxs
|
|
90
|
+
"""
|
|
91
|
+
padH, padK, padL = self.padding
|
|
92
|
+
|
|
93
|
+
# Save padded dataset
|
|
94
|
+
print("Saving padded dataset...")
|
|
95
|
+
f = NXroot()
|
|
96
|
+
f['entry'] = NXentry()
|
|
97
|
+
f['entry']['data'] = self.padded
|
|
98
|
+
if fout_name is None:
|
|
99
|
+
fout_name = 'padded_' + str(padH) + '_' + str(padK) + '_' + str(padL) + '.nxs'
|
|
100
|
+
nxsave(fout_name, f)
|
|
101
|
+
print("Output file saved to: " + os.path.join(os.getcwd(), fout_name))
|
|
102
|
+
|
|
103
|
+
def unpad(self, data):
|
|
104
|
+
slice_obj = [slice(None)] * data.ndim
|
|
105
|
+
for i in range(data.ndim):
|
|
106
|
+
slice_obj[i] = slice(self.padding[i], -self.padding[i], None)
|
|
107
|
+
slice_obj = tuple(slice_obj)
|
|
108
|
+
return data[slice_obj]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Symmetrizer2D:
|
|
112
|
+
"""
|
|
113
|
+
A class for symmetrizing 2D datasets.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, **kwargs):
|
|
117
|
+
if kwargs != {}:
|
|
118
|
+
self.set_parameters(**kwargs)
|
|
119
|
+
|
|
120
|
+
def set_parameters(self, theta_min, theta_max, skew_angle=90, mirror=True):
|
|
121
|
+
"""
|
|
122
|
+
Sets the parameters for the symmetrization operation.
|
|
123
|
+
|
|
124
|
+
Parameters
|
|
125
|
+
----------
|
|
126
|
+
theta_min : float
|
|
127
|
+
The minimum angle in degrees for symmetrization.
|
|
128
|
+
theta_max : float
|
|
129
|
+
The maximum angle in degrees for symmetrization.
|
|
130
|
+
skew_angle : float, optional
|
|
131
|
+
The angle in degrees to skew the data during symmetrization (default: 90).
|
|
132
|
+
mirror : bool, optional
|
|
133
|
+
If True, perform mirroring during symmetrization (default: True).
|
|
134
|
+
"""
|
|
135
|
+
self.theta_min = theta_min
|
|
136
|
+
self.theta_max = theta_max
|
|
137
|
+
self.skew_angle = skew_angle
|
|
138
|
+
self.mirror = mirror
|
|
139
|
+
|
|
140
|
+
# Define Transformation
|
|
141
|
+
skew_angle_adj = 90 - skew_angle
|
|
142
|
+
t = Affine2D()
|
|
143
|
+
# Scale y-axis to preserve norm while shearing
|
|
144
|
+
t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
|
|
145
|
+
# Shear along x-axis
|
|
146
|
+
t += Affine2D().skew_deg(skew_angle_adj, 0)
|
|
147
|
+
# Return to original y-axis scaling
|
|
148
|
+
t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
|
|
149
|
+
self.transform = t
|
|
150
|
+
|
|
151
|
+
# Calculate number of rotations needed to reconstructed the dataset
|
|
152
|
+
if mirror:
|
|
153
|
+
rotations = abs(int(360 / (theta_max - theta_min) / 2))
|
|
154
|
+
else:
|
|
155
|
+
rotations = abs(int(360 / (theta_max - theta_min)))
|
|
156
|
+
self.rotations = rotations
|
|
157
|
+
|
|
158
|
+
self.symmetrization_mask = None
|
|
159
|
+
|
|
160
|
+
self.wedges = None
|
|
161
|
+
|
|
162
|
+
self.symmetrized = None
|
|
163
|
+
|
|
164
|
+
def symmetrize_2d(self, data):
|
|
165
|
+
"""
|
|
166
|
+
Symmetrizes a 2D dataset based on the set parameters.
|
|
167
|
+
|
|
168
|
+
Parameters
|
|
169
|
+
----------
|
|
170
|
+
data : NXdata
|
|
171
|
+
The input 2D dataset to be symmetrized.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
symmetrized : NXdata
|
|
176
|
+
The symmetrized 2D dataset.
|
|
177
|
+
"""
|
|
178
|
+
theta_min = self.theta_min
|
|
179
|
+
theta_max = self.theta_max
|
|
180
|
+
mirror = self.mirror
|
|
181
|
+
t = self.transform
|
|
182
|
+
rotations = self.rotations
|
|
183
|
+
|
|
184
|
+
# Pad the dataset so that rotations don't get cutoff if they extend past the extent of the dataset
|
|
185
|
+
p = Padder(data)
|
|
186
|
+
padding = tuple([len(data[axis]) for axis in data.axes])
|
|
187
|
+
data_padded = p.pad(padding)
|
|
188
|
+
|
|
189
|
+
# Define axes that span the plane to be transformed
|
|
190
|
+
q1 = data_padded[data.axes[0]]
|
|
191
|
+
q2 = data_padded[data.axes[1]]
|
|
192
|
+
|
|
193
|
+
# Define signal to be symmetrized
|
|
194
|
+
counts = data_padded[data.signal].nxdata
|
|
195
|
+
|
|
196
|
+
# Calculate the angle for each data point
|
|
197
|
+
theta = np.arctan2(q1.reshape((-1, 1)), q2.reshape((1, -1)))
|
|
198
|
+
# Create a boolean array for the range of angles
|
|
199
|
+
symmetrization_mask = np.logical_and(theta >= theta_min * np.pi / 180, theta <= theta_max * np.pi / 180)
|
|
200
|
+
self.symmetrization_mask = NXdata(NXfield(p.unpad(symmetrization_mask), name='mask'),
|
|
201
|
+
(data[data.axes[0]], data[data.axes[1]]))
|
|
202
|
+
|
|
203
|
+
self.wedge = NXdata(NXfield(p.unpad(counts * symmetrization_mask), name=data.signal),
|
|
204
|
+
(data[data.axes[0]], data[data.axes[1]]))
|
|
205
|
+
|
|
206
|
+
# Scale and skew counts
|
|
207
|
+
skew_angle_adj = 90 - self.skew_angle
|
|
208
|
+
counts_skew = ndimage.affine_transform(counts,
|
|
209
|
+
t.inverted().get_matrix()[:2, :2],
|
|
210
|
+
offset=[counts.shape[0] / 2 * np.sin(skew_angle_adj * np.pi / 180), 0],
|
|
211
|
+
order=0,
|
|
212
|
+
)
|
|
213
|
+
scale1 = np.cos(skew_angle_adj * np.pi / 180)
|
|
214
|
+
wedge = ndimage.affine_transform(counts_skew,
|
|
215
|
+
Affine2D().scale(scale1, 1).get_matrix()[:2, :2],
|
|
216
|
+
offset=[(1 - scale1) * counts.shape[0] / 2, 0],
|
|
217
|
+
order=0,
|
|
218
|
+
) * symmetrization_mask
|
|
219
|
+
|
|
220
|
+
scale2 = counts.shape[0] / counts.shape[1]
|
|
221
|
+
wedge = ndimage.affine_transform(wedge,
|
|
222
|
+
Affine2D().scale(scale2, 1).get_matrix()[:2, :2],
|
|
223
|
+
offset=[(1 - scale2) * counts.shape[0] / 2, 0],
|
|
224
|
+
order=0,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Reconstruct full dataset from wedge
|
|
228
|
+
reconstructed = np.zeros(counts.shape)
|
|
229
|
+
for n in range(0, rotations):
|
|
230
|
+
# The following are attempts to combine images with minimal overlapping pixels
|
|
231
|
+
reconstructed += wedge
|
|
232
|
+
# reconstructed = np.where(reconstructed == 0, reconstructed + wedge, reconstructed)
|
|
233
|
+
|
|
234
|
+
wedge = ndimage.rotate(wedge, 360 / rotations, reshape=False, order=0)
|
|
235
|
+
|
|
236
|
+
# self.rotated_only = NXdata(NXfield(reconstructed, name=data.signal),
|
|
237
|
+
# (q1, q2))
|
|
238
|
+
|
|
239
|
+
if mirror:
|
|
240
|
+
# The following are attempts to combine images with minimal overlapping pixels
|
|
241
|
+
reconstructed = np.where(reconstructed == 0, reconstructed + np.flip(reconstructed, axis=0), reconstructed)
|
|
242
|
+
# reconstructed += np.flip(reconstructed, axis=0)
|
|
243
|
+
|
|
244
|
+
# self.rotated_and_mirrored = NXdata(NXfield(reconstructed, name=data.signal),
|
|
245
|
+
# (q1, q2))
|
|
246
|
+
|
|
247
|
+
reconstructed = ndimage.affine_transform(reconstructed,
|
|
248
|
+
Affine2D().scale(scale2, 1).inverted().get_matrix()[:2, :2],
|
|
249
|
+
offset=[-(1 - scale2) * counts.shape[
|
|
250
|
+
0] / 2 / scale2, 0],
|
|
251
|
+
order=0,
|
|
252
|
+
)
|
|
253
|
+
reconstructed = ndimage.affine_transform(reconstructed,
|
|
254
|
+
Affine2D().scale(scale1,
|
|
255
|
+
1).inverted().get_matrix()[:2, :2],
|
|
256
|
+
offset=[-(1 - scale1) * counts.shape[
|
|
257
|
+
0] / 2 / scale1, 0],
|
|
258
|
+
order=0,
|
|
259
|
+
)
|
|
260
|
+
reconstructed = ndimage.affine_transform(reconstructed,
|
|
261
|
+
t.get_matrix()[:2, :2],
|
|
262
|
+
offset=[(-counts.shape[0] / 2 * np.sin(skew_angle_adj * np.pi / 180)),
|
|
263
|
+
0],
|
|
264
|
+
order=0,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
reconstructed_unpadded = p.unpad(reconstructed)
|
|
268
|
+
|
|
269
|
+
# Fix any overlapping pixels by truncating counts to max
|
|
270
|
+
reconstructed_unpadded[reconstructed_unpadded > data[data.signal].nxdata.max()] = data[data.signal].nxdata.max()
|
|
271
|
+
|
|
272
|
+
symmetrized = NXdata(NXfield(reconstructed_unpadded, name=data.signal),
|
|
273
|
+
(data[data.axes[0]],
|
|
274
|
+
data[data.axes[1]]))
|
|
275
|
+
|
|
276
|
+
return symmetrized
|
|
277
|
+
|
|
278
|
+
def test(self, data):
|
|
279
|
+
"""
|
|
280
|
+
Performs a test visualization of the symmetrization process.
|
|
281
|
+
|
|
282
|
+
Parameters
|
|
283
|
+
----------
|
|
284
|
+
data : ndarray
|
|
285
|
+
The input 2D dataset to be used for the test visualization.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
fig : Figure
|
|
290
|
+
The matplotlib Figure object that contains the test visualization plot.
|
|
291
|
+
axesarr : ndarray
|
|
292
|
+
The numpy array of Axes objects representing the subplots in the test visualization.
|
|
293
|
+
|
|
294
|
+
Notes
|
|
295
|
+
-----
|
|
296
|
+
This method uses the `symmetrize_2d` method to perform the symmetrization on the input data and visualize
|
|
297
|
+
the process.
|
|
298
|
+
|
|
299
|
+
The test visualization plot includes the following subplots:
|
|
300
|
+
- Subplot 1: The original dataset.
|
|
301
|
+
- Subplot 2: The symmetrization mask.
|
|
302
|
+
- Subplot 3: The wedge slice used for reconstruction of the full symmetrized dataset.
|
|
303
|
+
- Subplot 4: The symmetrized dataset.
|
|
304
|
+
|
|
305
|
+
Example usage:
|
|
306
|
+
```
|
|
307
|
+
s = Scissors()
|
|
308
|
+
s.set_parameters(theta_min, theta_max, skew_angle, mirror)
|
|
309
|
+
s.test(data)
|
|
310
|
+
```
|
|
311
|
+
"""
|
|
312
|
+
s = self
|
|
313
|
+
symm_test = s.symmetrize_2d(data)
|
|
314
|
+
fig, axesarr = plt.subplots(2, 2, figsize=(10, 8))
|
|
315
|
+
axes = axesarr.reshape(-1)
|
|
316
|
+
plot_slice(data, skew_angle=s.skew_angle, ax=axes[0], title='data')
|
|
317
|
+
plot_slice(s.symmetrization_mask, skew_angle=s.skew_angle, ax=axes[1], title='mask')
|
|
318
|
+
plot_slice(s.wedge, ax=axes[2], title='wedge')
|
|
319
|
+
plot_slice(symm_test, skew_angle=s.skew_angle, ax=axes[3], title='symmetrized')
|
|
320
|
+
plt.subplots_adjust(wspace=0.4)
|
|
321
|
+
plt.show()
|
|
322
|
+
return fig, axesarr
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class Symmetrizer3D():
|
|
326
|
+
"""
|
|
327
|
+
A class to symmetrize 3D datasets.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
def __init__(self, data):
|
|
331
|
+
"""
|
|
332
|
+
Initialize the Symmetrizer3D object.
|
|
333
|
+
|
|
334
|
+
Parameters
|
|
335
|
+
----------
|
|
336
|
+
data : NXdata
|
|
337
|
+
The input 3D dataset to be symmetrized.
|
|
338
|
+
|
|
339
|
+
"""
|
|
340
|
+
self.data = data
|
|
341
|
+
self.q1 = data[data.axes[0]]
|
|
342
|
+
self.q2 = data[data.axes[1]]
|
|
343
|
+
self.q3 = data[data.axes[2]]
|
|
344
|
+
self.plane1symmetrizer = Symmetrizer2D()
|
|
345
|
+
self.plane2symmetrizer = Symmetrizer2D()
|
|
346
|
+
self.plane3symmetrizer = Symmetrizer2D()
|
|
347
|
+
self.plane1 = self.q1.nxname + self.q2.nxname
|
|
348
|
+
self.plane2 = self.q1.nxname + self.q3.nxname
|
|
349
|
+
self.plane3 = self.q2.nxname + self.q3.nxname
|
|
350
|
+
|
|
351
|
+
print("Plane 1: " + self.plane1)
|
|
352
|
+
print("Plane 2: " + self.plane2)
|
|
353
|
+
print("Plane 3: " + self.plane3)
|
|
354
|
+
|
|
355
|
+
def symmetrize(self):
|
|
356
|
+
"""
|
|
357
|
+
Perform the symmetrization of the 3D dataset.
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
symmetrized : NXdata
|
|
362
|
+
The symmetrized 3D dataset.
|
|
363
|
+
|
|
364
|
+
"""
|
|
365
|
+
starttime = time.time()
|
|
366
|
+
data = self.data
|
|
367
|
+
q1, q2, q3 = self.q1, self.q2, self.q3
|
|
368
|
+
out_array = np.zeros(data[data.signal].shape)
|
|
369
|
+
|
|
370
|
+
print('Symmetrizing ' + self.plane1 + ' planes...')
|
|
371
|
+
for k in range(0, len(q3)):
|
|
372
|
+
print('Symmetrizing ' + q3.nxname + '=' + "{:.02f}".format(q3[k]) + "...", end='\r')
|
|
373
|
+
data_symmetrized = self.plane1symmetrizer.symmetrize_2d(data[:, :, k])
|
|
374
|
+
out_array[:, :, k] = data_symmetrized[data.signal].nxdata
|
|
375
|
+
print('\nSymmetrized ' + self.plane1 + ' planes.')
|
|
376
|
+
|
|
377
|
+
print('Symmetrizing ' + self.plane2 + ' planes...')
|
|
378
|
+
for j in range(0, len(q2)):
|
|
379
|
+
print('Symmetrizing ' + q2.nxname + '=' + "{:.02f}".format(q2[j]) + "...", end='\r')
|
|
380
|
+
data_symmetrized = self.plane2symmetrizer.symmetrize_2d(
|
|
381
|
+
NXdata(NXfield(out_array[:, j, :], name=data.signal),
|
|
382
|
+
(q1, q3)))
|
|
383
|
+
out_array[:, j, :] = data_symmetrized[data.signal].nxdata
|
|
384
|
+
print('\nSymmetrized ' + self.plane2 + ' planes.')
|
|
385
|
+
|
|
386
|
+
print('Symmetrizing ' + self.plane3 + ' planes...')
|
|
387
|
+
for i in range(0, len(q1)):
|
|
388
|
+
print('Symmetrizing ' + q1.nxname + '=' + "{:.02f}".format(q1[i]) + "...", end='\r')
|
|
389
|
+
data_symmetrized = self.plane3symmetrizer.symmetrize_2d(
|
|
390
|
+
NXdata(NXfield(out_array[i, :, :], name=data.signal),
|
|
391
|
+
(q2, q3)))
|
|
392
|
+
out_array[i, :, :] = data_symmetrized[data.signal].nxdata
|
|
393
|
+
print('\nSymmetrized ' + self.plane3 + ' planes.')
|
|
394
|
+
|
|
395
|
+
out_array[out_array < 0] = 0
|
|
396
|
+
|
|
397
|
+
stoptime = time.time()
|
|
398
|
+
print("\nSymmetriztaion finished in " + "{:.02f}".format((stoptime - starttime) / 60) + " minutes.")
|
|
399
|
+
|
|
400
|
+
self.symmetrized = NXdata(NXfield(out_array, name=data.signal), tuple([data[axis] for axis in data.axes]))
|
|
401
|
+
|
|
402
|
+
return self.symmetrized
|
|
403
|
+
|
|
404
|
+
def save(self, fout_name=None):
|
|
405
|
+
"""
|
|
406
|
+
Save the symmetrized dataset to a file.
|
|
407
|
+
|
|
408
|
+
Parameters
|
|
409
|
+
----------
|
|
410
|
+
fout_name : str, optional
|
|
411
|
+
The name of the output file. If not provided, the default name 'symmetrized.nxs' will be used.
|
|
412
|
+
|
|
413
|
+
"""
|
|
414
|
+
print("Saving file...")
|
|
415
|
+
|
|
416
|
+
f = NXroot()
|
|
417
|
+
f['entry'] = NXentry()
|
|
418
|
+
f['entry']['data'] = self.symmetrized
|
|
419
|
+
if fout_name is None:
|
|
420
|
+
fout_name = 'symmetrized.nxs'
|
|
421
|
+
nxsave(fout_name, f)
|
|
422
|
+
print("Output file saved to: " + os.path.join(os.getcwd(), fout_name))
|
|
423
|
+
|
|
22
424
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
theta = np.arctan2(q1.reshape((-1, 1)), q2.reshape((1, -1)))
|
|
38
|
-
|
|
39
|
-
# Create a boolean array for the range of angles
|
|
40
|
-
symm_region = np.logical_and(theta >= theta_min*np.pi/180, theta <= theta_max*np.pi/180)
|
|
41
|
-
|
|
42
|
-
# Calculate number of rotations needed to reconstruct the dataset
|
|
43
|
-
if mirror:
|
|
44
|
-
n_rots = abs(int(360/(theta_max-theta_min)/2))
|
|
45
|
-
else:
|
|
46
|
-
n_rots = abs(int(360/(theta_max-theta_min)))
|
|
47
|
-
|
|
48
|
-
# Scale wedge to preserve norm after skewing
|
|
49
|
-
counts_skew = ndimage.affine_transform(counts,
|
|
50
|
-
t.inverted().get_matrix()[:2,:2],
|
|
51
|
-
offset=[counts.shape[0]/2*np.sin(skew_angle_adj*np.pi/180), 0],
|
|
52
|
-
order=0,
|
|
53
|
-
)
|
|
54
|
-
wedge = ndimage.affine_transform(counts_skew,
|
|
55
|
-
Affine2D().scale(np.cos(skew_angle_adj*np.pi/180),1).get_matrix()[:2,:2],
|
|
56
|
-
offset=[(1-np.cos(skew_angle_adj*np.pi/180))*counts.shape[0]/2, 0],
|
|
57
|
-
order=0,
|
|
58
|
-
)*symm_region
|
|
59
|
-
|
|
60
|
-
reconstruct = np.zeros(counts.shape)
|
|
61
|
-
for n in range(0,n_rots):
|
|
62
|
-
reconstruct += wedge
|
|
63
|
-
wedge = ndimage.rotate(wedge, 360/n_rots, reshape=False, order=0)
|
|
64
|
-
|
|
65
|
-
if mirror:
|
|
66
|
-
reconstruct += np.flip(reconstruct, axis=0)
|
|
67
|
-
|
|
68
|
-
reconstruct = ndimage.affine_transform(reconstruct,
|
|
69
|
-
Affine2D().scale(np.cos(skew_angle_adj*np.pi/180), 1).inverted().get_matrix()[:2,:2],
|
|
70
|
-
offset=[-(1-np.cos(skew_angle_adj*np.pi/180))*counts.shape[0]/2/np.cos(skew_angle_adj*np.pi/180),0],
|
|
71
|
-
order=0,
|
|
72
|
-
)
|
|
73
|
-
reconstruct = ndimage.affine_transform(reconstruct,
|
|
74
|
-
t.get_matrix()[:2,:2],
|
|
75
|
-
offset=[(-counts.shape[0]/2*np.sin(skew_angle_adj*np.pi/180)),0],
|
|
76
|
-
order=0,
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
return reconstruct
|
|
80
|
-
|
|
425
|
+
# class Puncher():
|
|
426
|
+
# pass
|
|
427
|
+
#
|
|
428
|
+
#
|
|
429
|
+
# class Reducer():
|
|
430
|
+
# pass
|
|
431
|
+
#
|
|
432
|
+
#
|
|
433
|
+
# class Interpolator():
|
|
434
|
+
# pass
|
|
435
|
+
#
|
|
436
|
+
#
|
|
437
|
+
# class FourierTransformer():
|
|
438
|
+
# pass
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
_meta/__init__.py,sha256=XSqHTz0m03EA_lG__7ugS0_aLOeAjpT3y0987MxmYIk,351
|
|
2
|
+
nxs_analysis_tools/__init__.py,sha256=guRgN90Lv_DgQ-S7lKWLpFkglFvIJGoDcmQ_pjHVRw0,372
|
|
3
|
+
nxs_analysis_tools/chess.py,sha256=OL-sQ8KZzY9393sWBDYxgpfkpcYhgtCJxonDML9Rwo4,9081
|
|
4
|
+
nxs_analysis_tools/datareduction.py,sha256=nnxBaEWldzD0jbRRb04vBNZs_lPsGMBnjeVyvN7L33Y,20372
|
|
5
|
+
nxs_analysis_tools/pairdistribution.py,sha256=LKHd2dVTMrBXItmtFBmYVAFjX1vK1vMeY8p2SN4UJdM,16692
|
|
6
|
+
nxs_analysis_tools-0.0.20.dist-info/LICENSE,sha256=tdnoYVH1-ogW_5-gGs9bK-IkCamH1ATJqrdL37kWTHk,1102
|
|
7
|
+
nxs_analysis_tools-0.0.20.dist-info/METADATA,sha256=kuXYBt8SKAXXroSf0X7f8nmtUM0zBCAaxIGxZWvTqLc,3844
|
|
8
|
+
nxs_analysis_tools-0.0.20.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
9
|
+
nxs_analysis_tools-0.0.20.dist-info/top_level.txt,sha256=8U000GNPzo6T6pOMjRdgOSO5heMzLMGjkxa1CDtyMHM,25
|
|
10
|
+
nxs_analysis_tools-0.0.20.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
_meta/__init__.py,sha256=4lwV6pfk3hMrZLgiaTqjWQUGRJkD7gAe9lPSfuZx2d4,343
|
|
2
|
-
nxs_analysis_tools/__init__.py,sha256=guRgN90Lv_DgQ-S7lKWLpFkglFvIJGoDcmQ_pjHVRw0,372
|
|
3
|
-
nxs_analysis_tools/chess.py,sha256=A3z1Uxw4pCblZMTWUAMOGU2fRrAMKs1479zIxSA2t4k,7520
|
|
4
|
-
nxs_analysis_tools/datareduction.py,sha256=vyufWH9HmjK_0saUvTVV5RpM2h9oLFoLPeB1UhRKuQ4,18581
|
|
5
|
-
nxs_analysis_tools/pairdistribution.py,sha256=b4ZvS37OjzTKw8TfRByYYglE__95vD5XoCZfK7b2JRg,3748
|
|
6
|
-
nxs_analysis_tools-0.0.18.dist-info/LICENSE,sha256=tdnoYVH1-ogW_5-gGs9bK-IkCamH1ATJqrdL37kWTHk,1102
|
|
7
|
-
nxs_analysis_tools-0.0.18.dist-info/METADATA,sha256=O6wRnMtqzQyMK_XU6JElfSLi9v6aRiCLfDKagO0Tu20,3844
|
|
8
|
-
nxs_analysis_tools-0.0.18.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
9
|
-
nxs_analysis_tools-0.0.18.dist-info/top_level.txt,sha256=8U000GNPzo6T6pOMjRdgOSO5heMzLMGjkxa1CDtyMHM,25
|
|
10
|
-
nxs_analysis_tools-0.0.18.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|