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 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.19'
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 highlight_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].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[0].highlight_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,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
- 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
- **kwargs):
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
- ## Apply transform to tick marks
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 = ax.xaxis.get_majorticklines()[i]
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,-1) + Affine2D().skew_deg(skew_angle,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()) # This does nothing
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 = ax.xaxis.get_minorticklines()[i]
237
- if i%2:
238
- 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))
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
- if cbartitle is None:
247
- colorbar.set_label(data.signal)
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
- class Scissors():
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
- axis=self.integrated_axes)
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
- self.integration_volume[self.integration_volume.axes[axis]])
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
- X=data[data.axes[0]],
479
- Y=data[data.axes[1]],
480
- ax=axes[0],
481
- **kwargs)
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
- center[1]-window[1]),
486
- 2*window[0], 2*window[1],
487
- linewidth=1, edgecolor='r', facecolor='none', transform=p1.get_transform(), label=label,
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
- X=data[data.axes[0]],
497
- Y=data[data.axes[2]],
498
- ax=axes[1],
499
- **kwargs)
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
- center[2]-window[2]),
504
- 2*window[0], 2*window[2],
505
- linewidth=1, edgecolor='r', facecolor='none', transform=p2.get_transform(), label=label,
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
- X=data[data.axes[1]],
515
- Y=data[data.axes[2]],
516
- ax=axes[2],
517
- **kwargs)
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
- center[2]-window[2]),
522
- 2*window[1], 2*window[2],
523
- linewidth=1, edgecolor='r', facecolor='none', transform=p3.get_transform(), label=label,
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
- plt.show()
531
+ if label is not None:
532
+ [ax.legend() for ax in axes]
531
533
 
532
- return (p1, p2, p3)
534
+ plt.show()
533
535
 
536
+ return p1, p2, p3
534
537
 
535
- def plot_integration_window(self, integrated=False, **kwargs):
538
+ def plot_integration_window(self, **kwargs):
536
539
  """
537
- Plots the three principal cross sections of the integration volume on a single figure.
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[0] = center[0]
567
- p2 = plot_slice(data[slice_obj],
568
- X=data[data.axes[1]],
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[1]].nxdata)/len(data[data.axes[2]].nxdata))
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[1] = center[1]
577
- p3 = plot_slice(data[slice_obj],
578
- X=data[data.axes[0]],
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[0]].nxdata)/len(data[data.axes[2]].nxdata))
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 (p1, p2, p3)
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.19
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=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,,