nxs-analysis-tools 0.1.3__tar.gz → 0.1.5__tar.gz

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.

Files changed (28) hide show
  1. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/PKG-INFO +2 -24
  2. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/pyproject.toml +4 -4
  3. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/src/_meta/__init__.py +1 -1
  4. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/src/nxs_analysis_tools/chess.py +9 -3
  5. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/src/nxs_analysis_tools/datareduction.py +142 -56
  6. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/src/nxs_analysis_tools/pairdistribution.py +113 -0
  7. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/src/nxs_analysis_tools.egg-info/SOURCES.txt +2 -0
  8. nxs_analysis_tools-0.1.5/tests/test_ellipsoidal_window.py +15 -0
  9. nxs_analysis_tools-0.1.5/tests/test_plot_slice_axes_types.py +40 -0
  10. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/LICENSE +0 -0
  11. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/MANIFEST.in +0 -0
  12. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/README.md +0 -0
  13. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/setup.cfg +0 -0
  14. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/setup.py +0 -0
  15. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/src/nxs_analysis_tools/__init__.py +0 -0
  16. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/src/nxs_analysis_tools/fitting.py +0 -0
  17. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/tests/test_accurate_highlight.py +0 -0
  18. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/tests/test_chess.py +0 -0
  19. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/tests/test_chess_fitting.py +0 -0
  20. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/tests/test_datareduction.py +0 -0
  21. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/tests/test_fitting.py +0 -0
  22. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/tests/test_lmfit.py +0 -0
  23. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/tests/test_mask_plotting.py +0 -0
  24. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/tests/test_pairdistribution.py +0 -0
  25. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/tests/test_plot_slice_with_ndarray.py +0 -0
  26. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/tests/test_rotate_data.py +0 -0
  27. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/tests/test_sum_axis.py +0 -0
  28. {nxs_analysis_tools-0.1.3 → nxs_analysis_tools-0.1.5}/tests/test_symmetrizer_rectangular_plane.py +0 -0
@@ -1,30 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nxs-analysis-tools
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Reduce and transform nexus format (.nxs) scattering data.
5
5
  Author-email: "Steven J. Gomez Alvarado" <stevenjgomez@ucsb.edu>
6
- License: MIT License
7
-
8
- Copyright (c) 2023-2025 Steven J. Gomez Alvarado
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
6
+ License-Expression: MIT
28
7
  Project-URL: Download, https://pypi.org/project/nxs-analysis-tools/
29
8
  Project-URL: Bug Tracker, https://github.com/stevenjgomez/nxs_analysis_tools/issues
30
9
  Project-URL: Documentation, https://nxs-analysis-tools.readthedocs.io/en/stable/
@@ -33,7 +12,6 @@ Project-URL: DOI, https://doi.org/10.5281/zenodo.15186359
33
12
  Keywords: diffraction,xrd,nexusformat,nexus,nxs,scattering
34
13
  Classifier: Development Status :: 5 - Production/Stable
35
14
  Classifier: Intended Audience :: Science/Research
36
- Classifier: License :: OSI Approved :: MIT License
37
15
  Classifier: Programming Language :: Python
38
16
  Classifier: Programming Language :: Python :: 3
39
17
  Classifier: Programming Language :: Python :: 3.9
@@ -6,11 +6,12 @@ build-backend = 'setuptools.build_meta'
6
6
 
7
7
  [project]
8
8
  name = 'nxs-analysis-tools'
9
- version = '0.1.3'
9
+ version = '0.1.5'
10
10
  description = 'Reduce and transform nexus format (.nxs) scattering data.'
11
11
  readme = 'README.md'
12
12
  requires-python = '>=3.7'
13
- license = { file = 'LICENSE' }
13
+ license = "MIT"
14
+ license-files = ['LICENSE']
14
15
  authors = [{ name = 'Steven J. Gomez Alvarado', email = 'stevenjgomez@ucsb.edu' }]
15
16
 
16
17
  keywords = [
@@ -24,7 +25,6 @@ keywords = [
24
25
  classifiers = [
25
26
  'Development Status :: 5 - Production/Stable',
26
27
  'Intended Audience :: Science/Research',
27
- 'License :: OSI Approved :: MIT License',
28
28
  'Programming Language :: Python',
29
29
  'Programming Language :: Python :: 3',
30
30
  'Programming Language :: Python :: 3.9',
@@ -71,7 +71,7 @@ dev = [
71
71
  'DOI' = 'https://doi.org/10.5281/zenodo.15186359'
72
72
 
73
73
  [tool.bumpver]
74
- current_version = "0.1.3"
74
+ current_version = "0.1.5"
75
75
  version_pattern = "MAJOR.MINOR.PATCH[-TAG]"
76
76
  tag_pattern = "vMAJOR.MINOR.PATCH[-TAG]"
77
77
  commit_message = "Bump version {old_version} -> {new_version}"
@@ -6,5 +6,5 @@ __author__ = 'Steven J. Gomez Alvarado'
6
6
  __email__ = 'stevenjgomez@ucsb.edu'
7
7
  __copyright__ = f"2023-2025, {__author__}"
8
8
  __license__ = 'MIT'
9
- __version__ = '0.1.3'
9
+ __version__ = '0.1.5'
10
10
  __repo_url__ = 'https://github.com/stevenjgomez/nxs_analysis_tools'
@@ -173,7 +173,7 @@ class TempDependence:
173
173
  """
174
174
  for temperature in self.temperatures:
175
175
  self.scissors[temperature] = Scissors()
176
- self.scissors[temperature] = LinecutModel()
176
+ self.linecutmodels[temperature] = LinecutModel()
177
177
 
178
178
  def set_data(self, temperature, data):
179
179
  """
@@ -188,7 +188,7 @@ class TempDependence:
188
188
  """
189
189
  self.datasets[temperature] = data
190
190
 
191
- def load_transforms(self, temperatures_list=None, print_tree=True):
191
+ def load_transforms(self, temperatures_list=None, print_tree=True, use_nxlink=False):
192
192
  """
193
193
  Load transform datasets (from nxrefine) based on temperature.
194
194
 
@@ -196,8 +196,14 @@ class TempDependence:
196
196
  ----------
197
197
  temperatures_list : list of int or None, optional
198
198
  List of temperatures to load. If None, all available temperatures are loaded.
199
+
199
200
  print_tree : bool, optional
200
201
  Whether to print the data tree upon loading. Default True.
202
+
203
+ use_nxlink : bool, optional
204
+ If True, maintains the NXlink defined in the data file, which references
205
+ the raw data in the transform.nxs file. This saves memory when working with
206
+ many datasets. In this case, the axes are in reverse order. Default is False.
201
207
  """
202
208
  # Convert all temperatures to strings
203
209
  if temperatures_list:
@@ -240,7 +246,7 @@ class TempDependence:
240
246
 
241
247
  # Save dataset
242
248
  try:
243
- self.datasets[self.temperatures[i]] = load_transform(path, print_tree)
249
+ self.datasets[self.temperatures[i]] = load_transform(path, print_tree=print_tree, use_nxlink=use_nxlink)
244
250
  except Exception as e:
245
251
  # Report temperature that was unable to load, then raise exception.
246
252
  temp_failed = self.temperatures[i]
@@ -49,7 +49,7 @@ def load_data(path, print_tree=True):
49
49
  return g.entry.data
50
50
 
51
51
 
52
- def load_transform(path, print_tree=True):
52
+ def load_transform(path, print_tree=True, use_nxlink=False):
53
53
  """
54
54
  Load transform data from an nxrefine output file.
55
55
 
@@ -61,16 +61,24 @@ def load_transform(path, print_tree=True):
61
61
  print_tree : bool, optional
62
62
  If True, prints the NeXus data tree upon loading. Default is True.
63
63
 
64
+ use_nxlink : bool, optional
65
+ If True, maintains the NXlink defined in the data file, which references
66
+ the raw data in the transform.nxs file. This saves memory when working with
67
+ many datasets. In this case, the axes are in reverse order. Default is False.
68
+
64
69
  Returns
65
70
  -------
66
71
  data : NXdata
67
72
  The loaded transform data as an NXdata object.
68
73
  """
69
74
 
70
- g = nxload(path)
75
+ root = nxload(path)
71
76
 
72
- data = NXdata(NXfield(g.entry.transform.data.nxdata.transpose(2, 1, 0), name='counts'),
73
- (g.entry.transform.Qh, g.entry.transform.Qk, g.entry.transform.Ql))
77
+ if use_nxlink:
78
+ data = root.entry.transform
79
+ else:
80
+ data = NXdata(NXfield(root.entry.transform.data.nxdata.transpose(2, 1, 0), name='counts'),
81
+ (root.entry.transform.Qh, root.entry.transform.Qk, root.entry.transform.Ql))
74
82
 
75
83
  print(data.tree) if print_tree else None
76
84
 
@@ -138,6 +146,36 @@ def rebin_3d(array):
138
146
 
139
147
  return rebinned
140
148
 
149
+ def rebin_2d(array):
150
+ """
151
+ Rebins a 2D NumPy array by a factor of 2 along each dimension.
152
+
153
+ This function reduces the size of the input array by averaging over non-overlapping
154
+ 2x2 blocks. Each dimension of the input array must be divisible by 2.
155
+
156
+ Parameters
157
+ ----------
158
+ array : np.ndarray
159
+ A 2-dimensional NumPy array to be rebinned.
160
+
161
+ Returns
162
+ -------
163
+ np.ndarray
164
+ A rebinned array with shape (N//2, M//2) if the original shape was (N, M).
165
+ """
166
+
167
+ # Ensure the array shape is divisible by 2 in each dimension
168
+ shape = array.shape
169
+ if any(dim % 2 != 0 for dim in shape):
170
+ raise ValueError("Each dimension of the array must be divisible by 2 to rebin.")
171
+
172
+ # Reshape the array to group the data into 2x2 blocks
173
+ reshaped = array.reshape(shape[0] // 2, 2, shape[1] // 2, 2)
174
+
175
+ # Average over the 2x2 blocks
176
+ rebinned = reshaped.mean(axis=(1, 3))
177
+
178
+ return rebinned
141
179
 
142
180
  def rebin_1d(array):
143
181
  """
@@ -179,8 +217,7 @@ def rebin_nxdata(data):
179
217
  - Then, each axis is rebinned using `rebin_1d`.
180
218
 
181
219
  The signal array is similarly cropped to remove the last element along any dimension
182
- with an odd shape, and then the data is averaged over 2x2x... blocks using the same
183
- `rebin_1d` method (assumed to apply across 1D slices).
220
+ with an odd shape, and then the data is averaged over 2x2x... blocks.
184
221
 
185
222
  Parameters
186
223
  ----------
@@ -224,7 +261,12 @@ def rebin_nxdata(data):
224
261
  data_arr = data_arr[tuple(slice_obj)]
225
262
 
226
263
  # Perform actual rebinning
227
- data_arr = rebin_3d(data_arr)
264
+ if data.ndim == 3:
265
+ data_arr = rebin_3d(data_arr)
266
+ elif data.ndim == 2:
267
+ data_arr = rebin_2d(data_arr)
268
+ elif data.ndim == 1:
269
+ data_arr = rebin_1d(data_arr)
228
270
 
229
271
  return NXdata(NXfield(data_arr, name=data.signal),
230
272
  tuple([axis for axis in new_axes])
@@ -246,13 +288,15 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
246
288
  data : :class:`nexusformat.nexus.NXdata` or ndarray
247
289
  The dataset to plot. Can be an `NXdata` object or a `numpy` array.
248
290
 
249
- X : NXfield, optional
250
- The X axis values. If None, a default range from 0 to the number of
251
- columns in `data` is used.
291
+ X : ndarray or NXfield, optional
292
+ The values for the X axis. If `data` is an NXdata object and `X` is None, the X axis is
293
+ inherited from the NXdata object. If `data` is a NumPy ndarray and `X` is None, a default
294
+ range from 0 to the number of columns in `data` is used.
252
295
 
253
- Y : NXfield, optional
254
- The Y axis values. If None, a default range from 0 to the number of
255
- rows in `data` is used.
296
+ Y : ndarray or NXfield, optional
297
+ The values for the Y axis. If `data` is an NXdata object and `Y` is None, the Y axis is
298
+ inherited from the NXdata object. If `data` is a NumPy ndarray and `Y` is None, a default
299
+ range from 0 to the number of rows in `data` is used.
256
300
 
257
301
  sum_axis : int, optional
258
302
  If the input data is 3D, this specifies the axis to sum over in order
@@ -327,9 +371,17 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
327
371
  p : :class:`matplotlib.collections.QuadMesh`
328
372
  The `matplotlib` QuadMesh object representing the plotted data.
329
373
  """
374
+
375
+ # Some logic to control the processing of the arrays
330
376
  is_array = False
331
377
  is_nxdata = False
378
+ no_xy_provided = True
379
+
380
+ # If X,Y not provided by user
381
+ if X is not None and Y is not None:
382
+ no_xy_provided = False
332
383
 
384
+ # Examine data type to be plotted
333
385
  if isinstance(data, np.ndarray):
334
386
  is_array = True
335
387
  elif isinstance(data, (NXdata, NXfield)):
@@ -339,43 +391,72 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
339
391
  f"Supported types are np.ndarray and NXdata.")
340
392
 
341
393
  # If three-dimensional, demand sum_axis to reduce to two dimensions.
342
- if is_array and len(data.shape) == 3:
343
- assert sum_axis is not None, "sum_axis must be specified when data is 3D."
344
-
345
- data = data.sum(axis=sum_axis)
394
+ if data.ndim == 3:
395
+ assert sum_axis is not None, "sum_axis must be specified when data.ndim == 3."
346
396
 
347
- if is_nxdata and len(data.shape) == 3:
348
- assert sum_axis is not None, "sum_axis must be specified when data is 3D."
397
+ if is_array:
398
+ data = data.sum(axis=sum_axis)
399
+ elif is_nxdata:
400
+ arr = data.nxsignal.nxdata
401
+ arr = arr.sum(axis=sum_axis)
349
402
 
350
- arr = data.nxsignal.nxdata
351
- arr = arr.sum(axis=sum_axis)
403
+ # Create a 2D template from the original nxdata
404
+ slice_obj = [slice(None)] * len(data.shape)
405
+ slice_obj[sum_axis] = 0
352
406
 
353
- # Create a 2D template from the original nxdata
354
- slice_obj = [slice(None)] * len(data.shape)
355
- slice_obj[sum_axis] = 0
356
-
357
- # Use the 2D template to create a new nxdata
358
- data = array_to_nxdata(arr, data[slice_obj])
407
+ # Use the 2D template to create a new nxdata
408
+ data = array_to_nxdata(arr, data[slice_obj])
359
409
 
410
+ # If the data is of type ndarray, then convert to NXdata
360
411
  if is_array:
412
+ # Convert X to NXfield if it is not already
361
413
  if X is None:
362
- X = NXfield(np.linspace(0, data.shape[0], data.shape[0]), name='x')
414
+ X = NXfield(np.arange(data.shape[0]), name='x')
415
+ elif isinstance(X, np.ndarray):
416
+ X = NXfield(X, name='x')
417
+ elif isinstance(X, NXfield):
418
+ pass
419
+ else:
420
+ raise TypeError("X must be of type np.ndarray or NXdata")
421
+
422
+ # Convert Y to NXfield if it is not already
363
423
  if Y is None:
364
- Y = NXfield(np.linspace(0, data.shape[1], data.shape[1]), name='y')
424
+ Y = NXfield(np.arange(data.shape[1]), name='y')
425
+ elif isinstance(Y, np.ndarray):
426
+ Y = NXfield(Y, name='y')
427
+ elif isinstance(Y, NXfield):
428
+ pass
429
+ else:
430
+ raise TypeError("Y must be of type np.ndarray or NXdata")
431
+
365
432
  if transpose:
366
433
  X, Y = Y, X
367
434
  data = data.transpose()
435
+
368
436
  data = NXdata(NXfield(data, name='value'), (X, Y))
369
- data_arr = data[data.signal].nxdata.transpose()
437
+ data_arr = data.nxsignal.nxdata.transpose()
438
+ # Otherwise, if data is of type NXdata, then decide whether to create axes,
439
+ # use provided axes, or inherit axes.
370
440
  elif is_nxdata:
371
441
  if X is None:
372
- X = data[data.axes[0]]
442
+ X = data.nxaxes[0]
443
+ elif isinstance(X, np.ndarray):
444
+ X = NXfield(X, name='x')
445
+ elif isinstance(X, NXdata):
446
+ pass
373
447
  if Y is None:
374
- Y = data[data.axes[1]]
448
+ Y = data.nxaxes[1]
449
+ elif isinstance(Y, np.ndarray):
450
+ Y = NXfield(Y, name='y')
451
+ elif isinstance(Y, NXdata):
452
+ pass
453
+
454
+ # Transpose axes and data if specified
375
455
  if transpose:
376
456
  X, Y = Y, X
377
457
  data = data.transpose()
378
- data_arr = data[data.signal].nxdata.transpose()
458
+
459
+ data_arr = data.nxsignal.nxdata.transpose()
379
460
 
380
461
  # Display Markdown heading
381
462
  if mdheading is None:
@@ -407,6 +488,7 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
407
488
  elif logscale:
408
489
  norm = colors.LogNorm(vmin=vmin, vmax=vmax)
409
490
 
491
+
410
492
  # Plot data
411
493
  p = ax.pcolormesh(X.nxdata, Y.nxdata, data_arr, shading='auto', norm=norm, cmap=cmap, **kwargs)
412
494
 
@@ -448,14 +530,15 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
448
530
  # Correct aspect ratio for the x/y axes after transformation
449
531
  ax.set(aspect=np.cos(skew_angle_adj * np.pi / 180))
450
532
 
451
- # Add tick marks all around
452
- ax.tick_params(direction='in', top=True, right=True, which='both')
453
533
 
454
534
  # Automatically set tick locations, only if NXdata or if X,Y axes are provided for an array
455
- if is_nxdata or (is_array and (X is not None and Y is not None)):
535
+ if is_nxdata or (is_array and (no_xy_provided == False)):
456
536
  # Add default minor ticks on x
457
537
  ax.xaxis.set_minor_locator(MultipleLocator(1))
458
538
 
539
+ # Add tick marks all around
540
+ ax.tick_params(direction='in', top=True, right=True, which='both')
541
+
459
542
  if xticks is not None:
460
543
  # Use user provided values
461
544
  ax.xaxis.set_major_locator(MultipleLocator(xticks))
@@ -466,6 +549,9 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
466
549
  if yticks is not None:
467
550
  # Use user provided values
468
551
  ax.yaxis.set_major_locator(MultipleLocator(yticks))
552
+ else:
553
+ # Add tick marks all around
554
+ ax.tick_params(direction='in', top=True, right=True, which='major')
469
555
 
470
556
  # Apply transform to tick marks
471
557
  for i in range(0, len(ax.xaxis.get_ticklines())):
@@ -753,8 +839,8 @@ class Scissors:
753
839
  slice_obj[2] = center[2]
754
840
 
755
841
  p1 = plot_slice(data[slice_obj],
756
- X=data[data.axes[0]],
757
- Y=data[data.axes[1]],
842
+ X=data.nxaxes[0],
843
+ Y=data.nxaxes[1],
758
844
  ax=axes[0],
759
845
  **kwargs)
760
846
  ax = axes[0]
@@ -777,8 +863,8 @@ class Scissors:
777
863
  slice_obj[1] = center[1]
778
864
 
779
865
  p2 = plot_slice(data[slice_obj],
780
- X=data[data.axes[0]],
781
- Y=data[data.axes[2]],
866
+ X=data.nxaxes[0],
867
+ Y=data.nxaxes[2],
782
868
  ax=axes[1],
783
869
  **kwargs)
784
870
  ax = axes[1]
@@ -801,8 +887,8 @@ class Scissors:
801
887
  slice_obj[0] = center[0]
802
888
 
803
889
  p3 = plot_slice(data[slice_obj],
804
- X=data[data.axes[1]],
805
- Y=data[data.axes[2]],
890
+ X=data.nxaxes[1],
891
+ Y=data.nxaxes[2],
806
892
  ax=axes[2],
807
893
  **kwargs)
808
894
  ax = axes[2]
@@ -849,31 +935,31 @@ class Scissors:
849
935
  slice_obj = [slice(None)] * data.ndim
850
936
  slice_obj[2] = center[2]
851
937
  p1 = plot_slice(data[slice_obj],
852
- X=data[data.axes[0]],
853
- Y=data[data.axes[1]],
938
+ X=data.nxaxes[0],
939
+ Y=data.nxaxes[1],
854
940
  ax=axes[0],
855
941
  **kwargs)
856
- axes[0].set_aspect(len(data[data.axes[0]].nxdata) / len(data[data.axes[1]].nxdata))
942
+ axes[0].set_aspect(len(data.nxaxes[0].nxdata) / len(data.nxaxes[1].nxdata))
857
943
 
858
944
  # Plot cross section 2
859
945
  slice_obj = [slice(None)] * data.ndim
860
946
  slice_obj[1] = center[1]
861
947
  p3 = plot_slice(data[slice_obj],
862
- X=data[data.axes[0]],
863
- Y=data[data.axes[2]],
948
+ X=data.nxaxes[0],
949
+ Y=data.nxaxes[2],
864
950
  ax=axes[1],
865
951
  **kwargs)
866
- axes[1].set_aspect(len(data[data.axes[0]].nxdata) / len(data[data.axes[2]].nxdata))
952
+ axes[1].set_aspect(len(data.nxaxes[0].nxdata) / len(data.nxaxes[2].nxdata))
867
953
 
868
954
  # Plot cross-section 3
869
955
  slice_obj = [slice(None)] * data.ndim
870
956
  slice_obj[0] = center[0]
871
957
  p2 = plot_slice(data[slice_obj],
872
- X=data[data.axes[1]],
873
- Y=data[data.axes[2]],
958
+ X=data.nxaxes[1],
959
+ Y=data.nxaxes[2],
874
960
  ax=axes[2],
875
961
  **kwargs)
876
- axes[2].set_aspect(len(data[data.axes[1]].nxdata) / len(data[data.axes[2]].nxdata))
962
+ axes[2].set_aspect(len(data.nxaxes[1].nxdata) / len(data.nxaxes[2].nxdata))
877
963
 
878
964
  # Adjust subplot padding
879
965
  fig.subplots_adjust(wspace=0.3)
@@ -984,7 +1070,7 @@ def rotate_data(data, lattice_angle, rotation_angle, rotation_axis, printout=Fal
984
1070
  Rotated data as an NXdata object.
985
1071
  """
986
1072
  # Define output array
987
- output_array = np.zeros(data[data.signal].shape)
1073
+ output_array = np.zeros(data.nxsignal.shape)
988
1074
 
989
1075
  # Define shear transformation
990
1076
  skew_angle_adj = 90 - lattice_angle
@@ -1085,7 +1171,7 @@ def rotate_data(data, lattice_angle, rotation_angle, rotation_axis, printout=Fal
1085
1171
  output_array[:, :, i] = counts_unpadded
1086
1172
  print('\nDone.')
1087
1173
  return NXdata(NXfield(output_array, name=p.padded.signal),
1088
- (data[data.axes[0]], data[data.axes[1]], data[data.axes[2]]))
1174
+ (data.nxaxes[0], data.nxaxes[1], data.nxaxes[2]))
1089
1175
 
1090
1176
 
1091
1177
  def rotate_data_2D(data, lattice_angle, rotation_angle):
@@ -1180,7 +1266,7 @@ def rotate_data_2D(data, lattice_angle, rotation_angle):
1180
1266
 
1181
1267
  print('\nDone.')
1182
1268
  return NXdata(NXfield(counts_unpadded, name=p.padded.signal),
1183
- (data[data.axes[0]], data[data.axes[1]]))
1269
+ (data.nxaxes[0], data.nxaxes[1]))
1184
1270
 
1185
1271
 
1186
1272
  class Padder:
@@ -1265,7 +1351,7 @@ class Padder:
1265
1351
  data = self.data
1266
1352
  self.padding = padding
1267
1353
 
1268
- padded_shape = tuple(data[data.signal].nxdata.shape[i]
1354
+ padded_shape = tuple(data.nxsignal.nxdata.shape[i]
1269
1355
  + self.padding[i] * 2 for i in range(data.ndim))
1270
1356
 
1271
1357
  # Create padded dataset
@@ -1275,7 +1361,7 @@ class Padder:
1275
1361
  for i, _ in enumerate(slice_obj):
1276
1362
  slice_obj[i] = slice(self.padding[i], -self.padding[i], None)
1277
1363
  slice_obj = tuple(slice_obj)
1278
- padded[slice_obj] = data[data.signal].nxdata
1364
+ padded[slice_obj] = data.nxsignal.nxdata
1279
1365
 
1280
1366
  padmaxes = tuple(self.maxes[i] + self.padding[i] * self.steps[i]
1281
1367
  for i in range(data.ndim))
@@ -1039,6 +1039,8 @@ class Interpolator:
1039
1039
  The dataset containing the data to be interpolated.
1040
1040
  """
1041
1041
  self.data = data
1042
+ self.interpolated = data
1043
+ self.tapered = data
1042
1044
 
1043
1045
  def set_kernel(self, kernel):
1044
1046
  """
@@ -1198,6 +1200,82 @@ class Interpolator:
1198
1200
 
1199
1201
  self.window = window
1200
1202
 
1203
+ def set_ellipsoidal_tukey_window(self, tukey_alpha=1.0, coeffs=None):
1204
+ """
1205
+ Set an ellipsoidal Tukey window function for data tapering.
1206
+
1207
+ The Tukey window smoothly tapers the data to zero near the edges of the
1208
+ elliptical region defined by quadratic form coefficients. This helps reduce
1209
+ artifacts in Fourier transforms and other operations sensitive to boundary effects.
1210
+
1211
+ Parameters
1212
+ ----------
1213
+ tukey_alpha : float, optional
1214
+ Tapering parameter for the Tukey window, between 0 and 1.
1215
+ - `tukey_alpha = 0` results in a ellipsoidal window (no tapering).
1216
+ - `tukey_alpha = 1` results in a full cosine taper.
1217
+ Default is 1.0.
1218
+
1219
+ coeffs : tuple of float, optional
1220
+ Coefficients `(c0, c1, c2, c3, c4, c5)` defining the ellipsoidal
1221
+ quadratic form:
1222
+ R^2 = c0*H^2 + c1*H*K + c2*K^2 + c3*K*L + c4*L^2 + c5*L*H
1223
+ If None, coefficients are automatically set to match the edges of the
1224
+ reciprocal space axes (H, K, L), which should be appropriate in cases
1225
+ where H, K, and L are orthogonal.
1226
+
1227
+ Notes
1228
+ -----
1229
+ - The maximum allowed radius `Qmax` is determined from the minimum radius
1230
+ value along the edges of reciprocal space.
1231
+ - The Tukey window is applied radially as a function of the distance `R`
1232
+ from the center, defined by the ellipsoidal quadratic form.
1233
+
1234
+ Sets
1235
+ ----
1236
+ self.window : ndarray
1237
+ A 3D array of the same shape as the data, containing the Tukey window
1238
+ values between 0 and 1.
1239
+ """
1240
+
1241
+ # Initialize axes
1242
+ H,K,L = [axis for axis in self.data.nxaxes]
1243
+
1244
+ # Initialize coeffs (default to window reaching edge of array)
1245
+ smallest_extent = np.min([H.max(), K.max(), L.max()])
1246
+ c = coeffs if coeffs is not None else ((smallest_extent / H.max()) ** 2,
1247
+ 0,
1248
+ (smallest_extent / K.max()) ** 2,
1249
+ 0,
1250
+ (smallest_extent / L.max()) ** 2,
1251
+ 0
1252
+ )
1253
+
1254
+ # Create meshgrid
1255
+ HH, KK, LL = np.meshgrid(H,K,L, indexing='ij')
1256
+
1257
+ # Create radius array
1258
+ RR = np.sqrt(
1259
+ c[0] * HH ** 2 +
1260
+ c[1] * HH * KK +
1261
+ c[2] * KK ** 2 +
1262
+ c[3] * KK * LL +
1263
+ c[4] * LL ** 2 +
1264
+ c[5] * LL * HH
1265
+ )
1266
+
1267
+ # Check the edges of reciprocal space to verify Qmax
1268
+ # Create list of pixels where H = H.max() or K = K.max() or L = L.max()
1269
+ edges = np.where(np.logical_or(np.logical_or(HH == H.max(), KK == K.max()), LL == L.max()), RR, RR.max())
1270
+ Qmax = edges.min()
1271
+ alpha = tukey_alpha
1272
+ period = (Qmax * alpha) / np.pi
1273
+
1274
+ window = np.where(RR > Qmax * (1 - alpha), (np.cos((RR - Qmax * (1 - alpha)) / period) + 1) / 2, 1)
1275
+ window = np.where(RR > Qmax, 0, window)
1276
+
1277
+ self.window = window
1278
+
1201
1279
  def set_window(self, window):
1202
1280
  """
1203
1281
  Set a custom window function for data tapering.
@@ -1578,6 +1656,41 @@ class DeltaPDF:
1578
1656
  self.interpolator.set_hexagonal_tukey_window(tukey_alphas)
1579
1657
  self.window = self.interpolator.window
1580
1658
 
1659
+ def set_ellipsoidal_tukey_window(self, tukey_alpha=1.0, coeffs=None):
1660
+ """
1661
+ Set an ellipsoidal Tukey window function for data tapering.
1662
+
1663
+ The Tukey window smoothly tapers the data to zero near the edges of the
1664
+ elliptical region defined by quadratic form coefficients. This helps reduce
1665
+ artifacts in Fourier transforms and other operations sensitive to boundary effects.
1666
+
1667
+ Parameters
1668
+ ----------
1669
+ tukey_alpha : float, optional
1670
+ Tapering parameter for the Tukey window, between 0 and 1.
1671
+ - `tukey_alpha = 0` results in a ellipsoidal window (no tapering).
1672
+ - `tukey_alpha = 1` results in a full cosine taper.
1673
+ Default is 1.0.
1674
+
1675
+ coeffs : tuple of float, optional
1676
+ Coefficients `(c0, c1, c2, c3, c4, c5)` defining the ellipsoidal
1677
+ quadratic form:
1678
+ R^2 = c0*H^2 + c1*H*K + c2*K^2 + c3*K*L + c4*L^2 + c5*L*H
1679
+ If None, coefficients are automatically set to match the edges of the
1680
+ reciprocal space axes (H, K, L), which should be appropriate in cases
1681
+ where H, K, and L are orthogonal.
1682
+
1683
+ Notes
1684
+ -----
1685
+ - The maximum allowed radius `Qmax` is determined from the minimum radius
1686
+ value along the edges of reciprocal space.
1687
+ - The Tukey window is applied radially as a function of the distance `R`
1688
+ from the center, defined by the ellipsoidal quadratic form.
1689
+ """
1690
+ self.interpolator.set_ellipsoidal_tukey_window(tukey_alpha=tukey_alpha, coeffs=coeffs)
1691
+ self.window = self.interpolator.window
1692
+
1693
+
1581
1694
  def set_window(self, window):
1582
1695
  """
1583
1696
  Set a custom window function for data tapering.
@@ -13,10 +13,12 @@ tests/test_accurate_highlight.py
13
13
  tests/test_chess.py
14
14
  tests/test_chess_fitting.py
15
15
  tests/test_datareduction.py
16
+ tests/test_ellipsoidal_window.py
16
17
  tests/test_fitting.py
17
18
  tests/test_lmfit.py
18
19
  tests/test_mask_plotting.py
19
20
  tests/test_pairdistribution.py
21
+ tests/test_plot_slice_axes_types.py
20
22
  tests/test_plot_slice_with_ndarray.py
21
23
  tests/test_rotate_data.py
22
24
  tests/test_sum_axis.py
@@ -0,0 +1,15 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from nxs_analysis_tools import *
4
+
5
+ from nxs_analysis_tools.pairdistribution import Interpolator
6
+
7
+ data = load_data('docs/source/examples/example_data/plot_slice_data/cubic_hkli.nxs')
8
+ h = Interpolator()
9
+ h.set_data(data)
10
+ h.set_ellipsoidal_tukey_window(tukey_alpha=0.0)
11
+ h.apply_window()
12
+ fig,axs = plt.subplots(2,1, figsize=(4,8), dpi=100)
13
+ plot_slice(h.tapered[:,:,0.0], vmin=0, vmax=0.01, ax=axs[0])
14
+ plot_slice(h.tapered[:,0.0,:], vmin=0, vmax=0.01, ax=axs[1])
15
+ plt.show()
@@ -0,0 +1,40 @@
1
+ from src.nxs_analysis_tools import *
2
+ import matplotlib.pyplot as plt
3
+ import numpy as np
4
+
5
+ data = load_data('../docs/source/examples/example_data/plot_slice_data/cubic_hkli.nxs')
6
+
7
+ # NXdata with inherent axes
8
+ _,ax = plt.subplots()
9
+ plot_slice(data[:,:,0.0], ax=ax)
10
+ # plt.show()
11
+
12
+ # NXdata with NXfield axes
13
+ _,ax = plt.subplots()
14
+ plot_slice(data[:,:,0.0],X=data.nxaxes[0]*2,Y=data.nxaxes[1]*2, ax=ax)
15
+ # plt.show()
16
+
17
+ # NXdata with ndarray axes
18
+ _,ax = plt.subplots()
19
+ plot_slice(data[:,:,0.0],X=np.linspace(-0.1,0.1,len(data.nxaxes[0])),Y=np.linspace(-0.3,0.3,len(data.nxaxes[1])), ax=ax)
20
+ # plt.show()
21
+
22
+ # ndarray with inherent axes
23
+ _,ax = plt.subplots()
24
+ plot_slice(data[:,:,0.0].counts.nxdata, ax=ax)
25
+ # plt.show()
26
+
27
+ # ndarray with NXfield axes
28
+ _,ax = plt.subplots()
29
+ plot_slice(data[:,:,0.0].counts.nxdata,X=data.nxaxes[0]*2,Y=data.nxaxes[1]*2, ax=ax)
30
+ # plt.show()
31
+
32
+ # ndarray with ndarray axes
33
+ _,ax = plt.subplots()
34
+ plot_slice(data[:,:,0.0].counts.nxdata,X=np.linspace(-0.1,0.1,len(data.nxaxes[0])),Y=np.linspace(-0.3,0.3,len(data.nxaxes[1])), ax=ax)
35
+ # plt.show()
36
+
37
+ # ndarray with invalid axes
38
+ # _,ax = plt.subplots()
39
+ # plot_slice(data[:,:,0.0].counts.nxdata,X='test',Y='test', ax=ax)
40
+ # plt.show()