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 CHANGED
@@ -1,4 +1,4 @@
1
- '''MagentroPy package metadata.'''
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.18'
9
+ __version__= '0.0.20'
10
10
  __repo_url__ = 'https://github.com/stevenjgomez/nxs_analysis_tools'
@@ -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=None
25
- self.linecuts=None
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
- self.scissors = [Scissors() for _ in range(len(self.datasets))]
87
+ # Load dataset at each temperature
88
+ self.datasets[temperature] = load_data(filepath)
87
89
 
88
- for i,dataset in enumerate(self.datasets.values()):
89
- self.scissors[i].set_data(dataset)
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 i,scissors in enumerate(self.scissors):
102
+ """
103
+ for T in self.temperatures:
101
104
  print("----------------------------------")
102
- print("T = " + self.temperatures[i] + " K")
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
- [scissors.set_center(center) for scissors in self.scissors]
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 i,T in enumerate(self.temperatures):
143
+ for T in self.temperatures:
140
144
  print("-------------------------------")
141
145
  print("Cutting T = " + T + " K data...")
142
- self.scissors[i].cut_data(center, window, axis)
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[0].axes[0] if i == self.scissors[0].axis \
170
- else str(c) for i,c in enumerate(self.scissors[0].center)]
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 show_integration_window(self, temperature=None):
188
- '''
189
- Displays the integration window plot for a specific temperature or for all temperatures if
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 all available
198
- temperatures.
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].show_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[0].show_integration_window(data=self.datasets[self.temperatures[0]])
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
- ax=None, xlim=None, ylim=None, xticks=None, yticks=None, cbar=True, logscale=False,
43
- symlogscale=False, cmap='viridis', linthresh = 1, title=None, mdheading=None, cbartitle=None):
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
- ## Apply transform to tick marks
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 = ax.xaxis.get_majorticklines()[i]
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,-1) + Affine2D().skew_deg(skew_angle,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()) # This does nothing
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 = ax.xaxis.get_minorticklines()[i]
236
- if i%2:
237
- m._transform.set(Affine2D().translate(0,-1) + Affine2D().skew_deg(skew_angle,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
- if cbartitle is None:
246
- colorbar.set_label(data.signal)
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
- class Scissors():
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
- axis=self.integrated_axes)
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
- self.integration_volume[self.integration_volume.axes[axis]])
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 show_integration_window(self, data=None, label=None, **kwargs):
448
- '''
449
- Plots integration window highlighted on 2D heatmap full dataset.
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 be used.
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[axis] = center[axis]
475
+ slice_obj = [slice(None)] * data.ndim
476
+ slice_obj[2] = center[2]
470
477
 
471
478
  p1 = plot_slice(data[slice_obj],
472
- X=data[data.axes[integrated_axes[0]]],
473
- Y=data[data.axes[integrated_axes[1]]],
474
- **kwargs)
475
- ax = plt.gca()
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[integrated_axes[0]]-window[integrated_axes[0]],
478
- center[integrated_axes[1]]-window[integrated_axes[1]]),
479
- 2*window[integrated_axes[0]], 2*window[integrated_axes[1]],
480
- linewidth=1, edgecolor='r', facecolor='none', transform=p1.get_transform(), label=label,
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[integrated_axes[1]] = center[integrated_axes[1]]
493
+ slice_obj = [slice(None)] * data.ndim
494
+ slice_obj[1] = center[1]
488
495
 
489
496
  p2 = plot_slice(data[slice_obj],
490
- X=data[data.axes[integrated_axes[0]]],
491
- Y=data[data.axes[axis]],
492
- **kwargs)
493
- ax = plt.gca()
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[integrated_axes[0]]-window[integrated_axes[0]],
496
- center[axis]-window[axis]),
497
- 2*window[integrated_axes[0]], 2*window[axis],
498
- linewidth=1, edgecolor='r', facecolor='none', transform=p2.get_transform(), label=label,
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[integrated_axes[0]] = center[integrated_axes[0]]
511
+ slice_obj = [slice(None)] * data.ndim
512
+ slice_obj[0] = center[0]
506
513
 
507
514
  p3 = plot_slice(data[slice_obj],
508
- X=data[data.axes[integrated_axes[1]]],
509
- Y=data[data.axes[axis]],
510
- **kwargs)
511
- ax = plt.gca()
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[integrated_axes[1]]-window[integrated_axes[1]],
514
- center[axis]-window[axis]),
515
- 2*window[integrated_axes[1]], 2*window[axis],
516
- linewidth=1, edgecolor='r', facecolor='none', transform=p3.get_transform(), label=label,
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 (p1,p2,p3)
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
- # def plot_window(self):
524
- # '''
525
- # Plots 2D heatmap of integration window data on its own.
526
- # '''
527
- # data = self.integration_volume
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
- # # TODO: Adjust code to plot 3 different cross sections, create slice_obj
530
- # p = plot_slice(
531
- # data[slice_obj],
532
- # data[data.axes[self.integrated_axes[0]]],
533
- # data[data.axes[self.integrated_axes[1]]],
534
- # vmin=1, logscale=True,
535
- # )
536
- # plt.show()
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
- # 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
- def symmetrize_2d(q1, q2, counts, theta_min, theta_max, skew_angle=90, mirror=True):
13
+
14
+ class Padder():
6
15
  """
7
- Symmetrizes a 2D dataset within a specified angular range.
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
- # counts=counts.transpose()
24
-
25
- # Define Transformation
26
- skew_angle_adj = 90-skew_angle
27
-
28
- t = Affine2D()
29
- # Scale y-axis to preserve norm while shearing
30
- t += Affine2D().scale(1,np.cos(skew_angle_adj*np.pi/180))
31
- # Shear along x-axis
32
- t += Affine2D().skew_deg(skew_angle_adj,0)
33
- # Return to original y-axis scaling
34
- t += Affine2D().scale(1,np.cos(skew_angle_adj*np.pi/180)).inverted()
35
-
36
- # Calculate the angle for each data point
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nxs-analysis-tools
3
- Version: 0.0.18
3
+ Version: 0.0.20
4
4
  Summary: Reduce and transform nexus format (.nxs) scattering data.
5
5
  Author-email: "Steven J. Gomez Alvarado" <stevenjgomez@ucsb.edu>
6
6
  License: MIT License
@@ -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,,