nxs-analysis-tools 0.0.19__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 +112 -111
- nxs_analysis_tools/pairdistribution.py +430 -72
- {nxs_analysis_tools-0.0.19.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.19.dist-info/RECORD +0 -10
- {nxs_analysis_tools-0.0.19.dist-info → nxs_analysis_tools-0.0.20.dist-info}/LICENSE +0 -0
- {nxs_analysis_tools-0.0.19.dist-info → nxs_analysis_tools-0.0.20.dist-info}/WHEEL +0 -0
- {nxs_analysis_tools-0.0.19.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 highlight_integration_window(self, temperature=None):
|
|
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].highlight_integration_window(data=self.datasets[temperature]
|
|
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,11 +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
|
-
|
|
46
|
-
'''
|
|
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
|
+
"""
|
|
47
47
|
Parameters
|
|
48
48
|
----------
|
|
49
49
|
data : :class:`nexusformat.nexus.NXdata` object
|
|
@@ -108,10 +108,10 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
|
|
|
108
108
|
-------
|
|
109
109
|
p : :class:`matplotlib.collections.QuadMesh`
|
|
110
110
|
|
|
111
|
-
A :class:`matplotlib.collections.QuadMesh` object, to mimick behavior of
|
|
111
|
+
A :class:`matplotlib.collections.QuadMesh` object, to mimick behavior of
|
|
112
112
|
:class:`matplotlib.pyplot.pcolormesh`.
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
"""
|
|
115
115
|
|
|
116
116
|
if X is None:
|
|
117
117
|
X = data[data.axes[0]]
|
|
@@ -133,18 +133,18 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
|
|
|
133
133
|
|
|
134
134
|
# Inherit axes if user provides some
|
|
135
135
|
if ax is not None:
|
|
136
|
-
ax=ax
|
|
137
|
-
fig=ax.get_figure()
|
|
136
|
+
ax = ax
|
|
137
|
+
fig = ax.get_figure()
|
|
138
138
|
# Otherwise set up some default axes
|
|
139
139
|
else:
|
|
140
140
|
fig = plt.figure()
|
|
141
|
-
ax = fig.add_axes([0,0,1,1])
|
|
141
|
+
ax = fig.add_axes([0, 0, 1, 1])
|
|
142
142
|
|
|
143
143
|
# If limits not provided, use extrema
|
|
144
144
|
if vmin is None:
|
|
145
|
-
vmin=data_arr.min()
|
|
145
|
+
vmin = data_arr.min()
|
|
146
146
|
if vmax is None:
|
|
147
|
-
vmax=data_arr.max()
|
|
147
|
+
vmax = data_arr.max()
|
|
148
148
|
|
|
149
149
|
# Set norm (linear scale, logscale, or symlogscale)
|
|
150
150
|
norm = colors.Normalize(vmin=vmin, vmax=vmax) # Default: linear scale
|
|
@@ -154,42 +154,41 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
|
|
|
154
154
|
elif logscale:
|
|
155
155
|
norm = colors.LogNorm(vmin=vmin, vmax=vmax)
|
|
156
156
|
|
|
157
|
-
|
|
158
157
|
# Plot data
|
|
159
158
|
p = ax.pcolormesh(X.nxdata, Y.nxdata, data_arr, shading='auto', norm=norm, cmap=cmap, **kwargs)
|
|
160
159
|
|
|
161
160
|
## Transform data to new coordinate system if necessary
|
|
162
161
|
# Correct skew angle
|
|
163
|
-
skew_angle = 90-skew_angle
|
|
162
|
+
skew_angle = 90 - skew_angle
|
|
164
163
|
# Create blank 2D affine transformation
|
|
165
164
|
t = Affine2D()
|
|
166
165
|
# Scale y-axis to preserve norm while shearing
|
|
167
|
-
t += Affine2D().scale(1,np.cos(skew_angle*np.pi/180))
|
|
166
|
+
t += Affine2D().scale(1, np.cos(skew_angle * np.pi / 180))
|
|
168
167
|
# Shear along x-axis
|
|
169
|
-
t += Affine2D().skew_deg(skew_angle,0)
|
|
168
|
+
t += Affine2D().skew_deg(skew_angle, 0)
|
|
170
169
|
# Return to original y-axis scaling
|
|
171
|
-
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()
|
|
172
171
|
## Correct for x-displacement after shearing
|
|
173
172
|
# If ylims provided, use those
|
|
174
173
|
if ylim is not None:
|
|
175
174
|
# Set ylims
|
|
176
175
|
ax.set(ylim=ylim)
|
|
177
|
-
ymin,ymax = ylim
|
|
176
|
+
ymin, ymax = ylim
|
|
178
177
|
# Else, use current ylims
|
|
179
178
|
else:
|
|
180
|
-
ymin,ymax = ax.get_ylim()
|
|
179
|
+
ymin, ymax = ax.get_ylim()
|
|
181
180
|
# Use ylims to calculate translation (necessary to display axes in correct position)
|
|
182
|
-
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)
|
|
183
182
|
|
|
184
183
|
# Set x limits
|
|
185
184
|
if xlim is not None:
|
|
186
|
-
xmin,xmax = xlim
|
|
185
|
+
xmin, xmax = xlim
|
|
187
186
|
else:
|
|
188
|
-
xmin,xmax = ax.get_xlim()
|
|
189
|
-
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)))
|
|
190
189
|
|
|
191
190
|
# Correct aspect ratio for the x/y axes after transformation
|
|
192
|
-
ax.set(aspect=np.cos(skew_angle*np.pi/180))
|
|
191
|
+
ax.set(aspect=np.cos(skew_angle * np.pi / 180))
|
|
193
192
|
|
|
194
193
|
# Add tick marks all around
|
|
195
194
|
ax.tick_params(direction='in', top=True, right=True, which='both')
|
|
@@ -210,41 +209,41 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
|
|
|
210
209
|
ax.yaxis.set_major_locator(MultipleLocator(yticks))
|
|
211
210
|
ax.yaxis.set_minor_locator(MultipleLocator(1))
|
|
212
211
|
|
|
213
|
-
|
|
214
|
-
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())):
|
|
215
214
|
# Tick marker
|
|
216
215
|
m = MarkerStyle(3)
|
|
217
|
-
line =
|
|
218
|
-
if i%2:
|
|
216
|
+
line = ax.xaxis.get_majorticklines()[i]
|
|
217
|
+
if i % 2:
|
|
219
218
|
# Top ticks (translation here makes their direction="in")
|
|
220
|
-
m._transform.set(Affine2D().translate(0
|
|
219
|
+
m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle, 0))
|
|
221
220
|
# This first method shifts the top ticks horizontally to match the skew angle.
|
|
222
221
|
# This does not look good in all cases.
|
|
223
222
|
# line.set_transform(Affine2D().translate((ymax-ymin)*np.sin(skew_angle*np.pi/180),0) +
|
|
224
223
|
# line.get_transform())
|
|
225
224
|
# This second method skews the tick marks in place and
|
|
226
225
|
# can sometimes lead to them being misaligned.
|
|
227
|
-
line.set_transform(line.get_transform())
|
|
226
|
+
line.set_transform(line.get_transform()) # This does nothing
|
|
228
227
|
else:
|
|
229
228
|
# Bottom ticks
|
|
230
|
-
m._transform.set(Affine2D().skew_deg(skew_angle,0))
|
|
229
|
+
m._transform.set(Affine2D().skew_deg(skew_angle, 0))
|
|
231
230
|
|
|
232
231
|
line.set_marker(m)
|
|
233
232
|
|
|
234
|
-
for i in range(0,len(ax.xaxis.get_minorticklines())):
|
|
233
|
+
for i in range(0, len(ax.xaxis.get_minorticklines())):
|
|
235
234
|
m = MarkerStyle(2)
|
|
236
|
-
line =
|
|
237
|
-
if i%2:
|
|
238
|
-
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))
|
|
239
238
|
else:
|
|
240
|
-
m._transform.set(Affine2D().skew_deg(skew_angle,0))
|
|
239
|
+
m._transform.set(Affine2D().skew_deg(skew_angle, 0))
|
|
241
240
|
|
|
242
241
|
line.set_marker(m)
|
|
243
242
|
|
|
244
243
|
if cbar:
|
|
245
244
|
colorbar = fig.colorbar(p)
|
|
246
|
-
|
|
247
|
-
|
|
245
|
+
if cbartitle is None:
|
|
246
|
+
colorbar.set_label(data.signal)
|
|
248
247
|
|
|
249
248
|
ax.set(
|
|
250
249
|
xlabel=X.nxname,
|
|
@@ -257,8 +256,9 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
|
|
|
257
256
|
# Return the quadmesh object
|
|
258
257
|
return p
|
|
259
258
|
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
|
|
260
|
+
class Scissors:
|
|
261
|
+
"""
|
|
262
262
|
Scissors class provides functionality for reducing data to a 1D linecut using an integration
|
|
263
263
|
window.
|
|
264
264
|
|
|
@@ -299,10 +299,10 @@ class Scissors():
|
|
|
299
299
|
Plot the integration window highlighted on a 2D heatmap of the full dataset.
|
|
300
300
|
plot_window()
|
|
301
301
|
Plot a 2D heatmap of the integration window data.
|
|
302
|
-
|
|
302
|
+
"""
|
|
303
303
|
|
|
304
304
|
def __init__(self, data=None, center=None, window=None, axis=None):
|
|
305
|
-
|
|
305
|
+
"""
|
|
306
306
|
Initializes a Scissors object.
|
|
307
307
|
|
|
308
308
|
Parameters
|
|
@@ -315,7 +315,7 @@ class Scissors():
|
|
|
315
315
|
Extents of the window for integration along each axis. Default is None.
|
|
316
316
|
axis : int or None, optional
|
|
317
317
|
Axis along which to perform the integration. Default is None.
|
|
318
|
-
|
|
318
|
+
"""
|
|
319
319
|
|
|
320
320
|
self.data = data
|
|
321
321
|
self.center = center
|
|
@@ -328,74 +328,74 @@ class Scissors():
|
|
|
328
328
|
self.integration_window = None
|
|
329
329
|
|
|
330
330
|
def set_data(self, data):
|
|
331
|
-
|
|
331
|
+
"""
|
|
332
332
|
Set the input NXdata.
|
|
333
333
|
|
|
334
334
|
Parameters
|
|
335
335
|
----------
|
|
336
336
|
data : :class:`nexusformat.nexus.NXdata`
|
|
337
337
|
Input data array.
|
|
338
|
-
|
|
338
|
+
"""
|
|
339
339
|
self.data = data
|
|
340
340
|
|
|
341
341
|
def get_data(self):
|
|
342
|
-
|
|
342
|
+
"""
|
|
343
343
|
Get the input data array.
|
|
344
344
|
|
|
345
345
|
Returns
|
|
346
346
|
-------
|
|
347
347
|
ndarray or None
|
|
348
348
|
Input data array.
|
|
349
|
-
|
|
349
|
+
"""
|
|
350
350
|
return self.data
|
|
351
351
|
|
|
352
352
|
def set_center(self, center):
|
|
353
|
-
|
|
353
|
+
"""
|
|
354
354
|
Set the central coordinate for the linecut.
|
|
355
355
|
|
|
356
356
|
Parameters
|
|
357
357
|
----------
|
|
358
358
|
center : tuple
|
|
359
359
|
Central coordinate around which to perform the linecut.
|
|
360
|
-
|
|
360
|
+
"""
|
|
361
361
|
self.center = center
|
|
362
362
|
|
|
363
363
|
def set_window(self, window):
|
|
364
|
-
|
|
364
|
+
"""
|
|
365
365
|
Set the extents of the integration window.
|
|
366
366
|
|
|
367
367
|
Parameters
|
|
368
368
|
----------
|
|
369
369
|
window : tuple
|
|
370
370
|
Extents of the window for integration along each axis.
|
|
371
|
-
|
|
371
|
+
"""
|
|
372
372
|
self.window = window
|
|
373
373
|
|
|
374
374
|
# Determine the axis for integration
|
|
375
375
|
self.axis = window.index(max(window))
|
|
376
|
-
print("Linecut axis: "+str(self.data.axes[self.axis]))
|
|
376
|
+
print("Linecut axis: " + str(self.data.axes[self.axis]))
|
|
377
377
|
|
|
378
378
|
# Determine the integrated axes (axes other than the integration axis)
|
|
379
379
|
self.integrated_axes = tuple(i for i in range(self.data.ndim) if i != self.axis)
|
|
380
|
-
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]))
|
|
381
381
|
|
|
382
382
|
def get_window(self):
|
|
383
|
-
|
|
383
|
+
"""
|
|
384
384
|
Get the extents of the integration window.
|
|
385
385
|
|
|
386
386
|
Returns
|
|
387
387
|
-------
|
|
388
388
|
tuple or None
|
|
389
389
|
Extents of the integration window.
|
|
390
|
-
|
|
390
|
+
"""
|
|
391
391
|
return self.window
|
|
392
392
|
|
|
393
393
|
def cut_data(self, center=None, window=None, axis=None):
|
|
394
|
-
|
|
394
|
+
"""
|
|
395
395
|
Reduces data to a 1D linecut with integration extents specified by the window about a central
|
|
396
396
|
coordinate.
|
|
397
397
|
|
|
398
|
-
Parameters
|
|
398
|
+
Parameters
|
|
399
399
|
-----------
|
|
400
400
|
center : float or None, optional
|
|
401
401
|
Central coordinate for the linecut. If not specified, the value from the object's
|
|
@@ -407,11 +407,11 @@ class Scissors():
|
|
|
407
407
|
The axis along which to perform the linecut. If not specified, the value from the
|
|
408
408
|
object's attribute will be used.
|
|
409
409
|
|
|
410
|
-
Returns
|
|
410
|
+
Returns
|
|
411
411
|
--------
|
|
412
412
|
integrated_data : :class:`nexusformat.nexus.NXdata`
|
|
413
413
|
1D linecut data after integration.
|
|
414
|
-
|
|
414
|
+
"""
|
|
415
415
|
|
|
416
416
|
# Extract necessary attributes from the object
|
|
417
417
|
data = self.data
|
|
@@ -436,17 +436,17 @@ class Scissors():
|
|
|
436
436
|
|
|
437
437
|
# Perform integration along the integrated axes
|
|
438
438
|
integrated_data = np.sum(self.integration_volume[self.integration_volume.signal].nxdata,
|
|
439
|
-
|
|
439
|
+
axis=self.integrated_axes)
|
|
440
440
|
|
|
441
441
|
# Create an NXdata object for the linecut data
|
|
442
442
|
self.linecut = NXdata(NXfield(integrated_data, name=self.integration_volume.signal),
|
|
443
|
-
|
|
443
|
+
self.integration_volume[self.integration_volume.axes[axis]])
|
|
444
444
|
self.linecut.nxname = self.integration_volume.nxname
|
|
445
445
|
|
|
446
446
|
return self.linecut
|
|
447
447
|
|
|
448
|
-
def highlight_integration_window(self, data=None, label=None, **kwargs):
|
|
449
|
-
|
|
448
|
+
def highlight_integration_window(self, data=None, label=None, highlight_color='red', **kwargs):
|
|
449
|
+
"""
|
|
450
450
|
Plots integration window highlighted on the three principal cross sections of the first
|
|
451
451
|
temperature dataset.
|
|
452
452
|
|
|
@@ -457,12 +457,13 @@ class Scissors():
|
|
|
457
457
|
be used.
|
|
458
458
|
label : str, optional
|
|
459
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'.
|
|
460
462
|
**kwargs : keyword arguments, optional
|
|
461
463
|
Additional keyword arguments to customize the plot.
|
|
462
464
|
|
|
463
|
-
|
|
465
|
+
"""
|
|
464
466
|
data = self.data if data is None else data
|
|
465
|
-
axis = self.axis
|
|
466
467
|
center = self.center
|
|
467
468
|
window = self.window
|
|
468
469
|
integrated_axes = self.integrated_axes
|
|
@@ -471,75 +472,75 @@ class Scissors():
|
|
|
471
472
|
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
|
472
473
|
|
|
473
474
|
# Plot cross section 1
|
|
474
|
-
slice_obj = [slice(None)]*data.ndim
|
|
475
|
+
slice_obj = [slice(None)] * data.ndim
|
|
475
476
|
slice_obj[2] = center[2]
|
|
476
477
|
|
|
477
478
|
p1 = plot_slice(data[slice_obj],
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
479
|
+
X=data[data.axes[0]],
|
|
480
|
+
Y=data[data.axes[1]],
|
|
481
|
+
ax=axes[0],
|
|
482
|
+
**kwargs)
|
|
482
483
|
ax = axes[0]
|
|
483
484
|
rect_diffuse = patches.Rectangle(
|
|
484
|
-
(center[0]-window[0],
|
|
485
|
-
|
|
486
|
-
2*window[0], 2*window[1],
|
|
487
|
-
linewidth=1, edgecolor=
|
|
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,
|
|
488
489
|
)
|
|
489
490
|
ax.add_patch(rect_diffuse)
|
|
490
491
|
|
|
491
492
|
# Plot cross section 2
|
|
492
|
-
slice_obj = [slice(None)]*data.ndim
|
|
493
|
+
slice_obj = [slice(None)] * data.ndim
|
|
493
494
|
slice_obj[1] = center[1]
|
|
494
495
|
|
|
495
496
|
p2 = plot_slice(data[slice_obj],
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
497
|
+
X=data[data.axes[0]],
|
|
498
|
+
Y=data[data.axes[2]],
|
|
499
|
+
ax=axes[1],
|
|
500
|
+
**kwargs)
|
|
500
501
|
ax = axes[1]
|
|
501
502
|
rect_diffuse = patches.Rectangle(
|
|
502
|
-
(center[0]-window[0],
|
|
503
|
-
|
|
504
|
-
2*window[0], 2*window[2],
|
|
505
|
-
linewidth=1, edgecolor=
|
|
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,
|
|
506
507
|
)
|
|
507
508
|
ax.add_patch(rect_diffuse)
|
|
508
509
|
|
|
509
510
|
# Plot cross section 3
|
|
510
|
-
slice_obj = [slice(None)]*data.ndim
|
|
511
|
+
slice_obj = [slice(None)] * data.ndim
|
|
511
512
|
slice_obj[0] = center[0]
|
|
512
513
|
|
|
513
514
|
p3 = plot_slice(data[slice_obj],
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
515
|
+
X=data[data.axes[1]],
|
|
516
|
+
Y=data[data.axes[2]],
|
|
517
|
+
ax=axes[2],
|
|
518
|
+
**kwargs)
|
|
518
519
|
ax = axes[2]
|
|
519
520
|
rect_diffuse = patches.Rectangle(
|
|
520
|
-
(center[1]-window[1],
|
|
521
|
-
|
|
522
|
-
2*window[1], 2*window[2],
|
|
523
|
-
linewidth=1, edgecolor=
|
|
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,
|
|
524
525
|
)
|
|
525
526
|
ax.add_patch(rect_diffuse)
|
|
526
527
|
|
|
527
528
|
# Adjust subplot padding
|
|
528
529
|
fig.subplots_adjust(wspace=0.5)
|
|
529
530
|
|
|
530
|
-
|
|
531
|
+
if label is not None:
|
|
532
|
+
[ax.legend() for ax in axes]
|
|
531
533
|
|
|
532
|
-
|
|
534
|
+
plt.show()
|
|
533
535
|
|
|
536
|
+
return p1, p2, p3
|
|
534
537
|
|
|
535
|
-
def plot_integration_window(self,
|
|
538
|
+
def plot_integration_window(self, **kwargs):
|
|
536
539
|
"""
|
|
537
|
-
Plots the three principal cross
|
|
540
|
+
Plots the three principal cross-sections of the integration volume on a single figure.
|
|
538
541
|
|
|
539
542
|
Parameters
|
|
540
543
|
----------
|
|
541
|
-
integrated : bool, optional
|
|
542
|
-
Flag indicating whether the integration volume is already integrated.
|
|
543
544
|
**kwargs : keyword arguments, optional
|
|
544
545
|
Additional keyword arguments to customize the plot.
|
|
545
546
|
"""
|
|
@@ -559,31 +560,31 @@ class Scissors():
|
|
|
559
560
|
Y=data[data.axes[1]],
|
|
560
561
|
ax=axes[0],
|
|
561
562
|
**kwargs)
|
|
562
|
-
axes[0].set_aspect(len(data[data.axes[0]].nxdata)/len(data[data.axes[1]].nxdata))
|
|
563
|
+
axes[0].set_aspect(len(data[data.axes[0]].nxdata) / len(data[data.axes[1]].nxdata))
|
|
563
564
|
|
|
564
565
|
# Plot cross section 2
|
|
565
566
|
slice_obj = [slice(None)] * data.ndim
|
|
566
|
-
slice_obj[
|
|
567
|
-
|
|
568
|
-
X=data[data.axes[
|
|
567
|
+
slice_obj[1] = center[1]
|
|
568
|
+
p3 = plot_slice(data[slice_obj],
|
|
569
|
+
X=data[data.axes[0]],
|
|
569
570
|
Y=data[data.axes[2]],
|
|
570
571
|
ax=axes[1],
|
|
571
572
|
**kwargs)
|
|
572
|
-
axes[1].set_aspect(len(data[data.axes[
|
|
573
|
+
axes[1].set_aspect(len(data[data.axes[0]].nxdata) / len(data[data.axes[2]].nxdata))
|
|
573
574
|
|
|
574
575
|
# Plot cross section 3
|
|
575
576
|
slice_obj = [slice(None)] * data.ndim
|
|
576
|
-
slice_obj[
|
|
577
|
-
|
|
578
|
-
X=data[data.axes[
|
|
577
|
+
slice_obj[0] = center[0]
|
|
578
|
+
p2 = plot_slice(data[slice_obj],
|
|
579
|
+
X=data[data.axes[1]],
|
|
579
580
|
Y=data[data.axes[2]],
|
|
580
581
|
ax=axes[2],
|
|
581
582
|
**kwargs)
|
|
582
|
-
axes[2].set_aspect(len(data[data.axes[
|
|
583
|
+
axes[2].set_aspect(len(data[data.axes[1]].nxdata) / len(data[data.axes[2]].nxdata))
|
|
583
584
|
|
|
584
585
|
# Adjust subplot padding
|
|
585
586
|
fig.subplots_adjust(wspace=0.3)
|
|
586
587
|
|
|
587
588
|
plt.show()
|
|
588
589
|
|
|
589
|
-
return
|
|
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=bWxCJb1P-6SdnydWs6NtuN8YoZ1E_LwLdMmj229otkM,343
|
|
2
|
-
nxs_analysis_tools/__init__.py,sha256=guRgN90Lv_DgQ-S7lKWLpFkglFvIJGoDcmQ_pjHVRw0,372
|
|
3
|
-
nxs_analysis_tools/chess.py,sha256=08-NQaui4c9UqZbmlm-_yYgia7HerLNEprm9GN6hgYE,7535
|
|
4
|
-
nxs_analysis_tools/datareduction.py,sha256=OK7BWRd7i7QXPyABuj_8YxLpXEth4X2Al0Q5zqR_CG8,20085
|
|
5
|
-
nxs_analysis_tools/pairdistribution.py,sha256=b4ZvS37OjzTKw8TfRByYYglE__95vD5XoCZfK7b2JRg,3748
|
|
6
|
-
nxs_analysis_tools-0.0.19.dist-info/LICENSE,sha256=tdnoYVH1-ogW_5-gGs9bK-IkCamH1ATJqrdL37kWTHk,1102
|
|
7
|
-
nxs_analysis_tools-0.0.19.dist-info/METADATA,sha256=RF2CaHq-_6LKL4N8ORCX47bB2eWYd8TtGdxtQyv8PyE,3844
|
|
8
|
-
nxs_analysis_tools-0.0.19.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
|
9
|
-
nxs_analysis_tools-0.0.19.dist-info/top_level.txt,sha256=8U000GNPzo6T6pOMjRdgOSO5heMzLMGjkxa1CDtyMHM,25
|
|
10
|
-
nxs_analysis_tools-0.0.19.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|