nxs-analysis-tools 0.0.45__tar.gz → 0.0.47__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 (29) hide show
  1. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/LICENSE +1 -1
  2. {nxs_analysis_tools-0.0.45/src/nxs_analysis_tools.egg-info → nxs_analysis_tools-0.0.47}/PKG-INFO +2 -2
  3. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/pyproject.toml +1 -1
  4. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/src/_meta/__init__.py +2 -2
  5. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools/chess.py +54 -12
  6. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools/datareduction.py +100 -29
  7. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools/pairdistribution.py +83 -38
  8. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47/src/nxs_analysis_tools.egg-info}/PKG-INFO +2 -2
  9. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools.egg-info/SOURCES.txt +2 -0
  10. nxs_analysis_tools-0.0.47/tests/test_accurate_highlight.py +388 -0
  11. nxs_analysis_tools-0.0.47/tests/test_sum_axis.py +299 -0
  12. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/MANIFEST.in +0 -0
  13. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/README.md +0 -0
  14. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/setup.cfg +0 -0
  15. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/setup.py +0 -0
  16. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools/__init__.py +0 -0
  17. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools/fitting.py +0 -0
  18. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools.egg-info/dependency_links.txt +0 -0
  19. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools.egg-info/requires.txt +0 -0
  20. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools.egg-info/top_level.txt +0 -0
  21. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/tests/test_chess.py +0 -0
  22. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/tests/test_chess_fitting.py +0 -0
  23. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/tests/test_datareduction.py +0 -0
  24. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/tests/test_fitting.py +0 -0
  25. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/tests/test_lmfit.py +0 -0
  26. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/tests/test_mask_plotting.py +0 -0
  27. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/tests/test_pairdistribution.py +0 -0
  28. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/tests/test_plot_slice_with_ndarray.py +0 -0
  29. {nxs_analysis_tools-0.0.45 → nxs_analysis_tools-0.0.47}/tests/test_symmetrizer_rectangular_plane.py +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 Steven J. Gomez Alvarado
3
+ Copyright (c) 2023-2025 Steven J. Gomez Alvarado
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nxs-analysis-tools
3
- Version: 0.0.45
3
+ Version: 0.0.47
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
7
7
 
8
- Copyright (c) 2023 Steven J. Gomez Alvarado
8
+ Copyright (c) 2023-2025 Steven J. Gomez Alvarado
9
9
 
10
10
  Permission is hereby granted, free of charge, to any person obtaining a copy
11
11
  of this software and associated documentation files (the "Software"), to deal
@@ -6,7 +6,7 @@ build-backend = 'setuptools.build_meta'
6
6
 
7
7
  [project]
8
8
  name = 'nxs-analysis-tools'
9
- version = '0.0.45'
9
+ version = '0.0.47'
10
10
  description = 'Reduce and transform nexus format (.nxs) scattering data.'
11
11
  readme = 'README.md'
12
12
  requires-python = '>=3.7'
@@ -4,7 +4,7 @@
4
4
  __project__ = 'nxs-analysis-tools'
5
5
  __author__ = 'Steven J. Gomez Alvarado'
6
6
  __email__ = 'stevenjgomez@ucsb.edu'
7
- __copyright__ = f"2023, {__author__}"
7
+ __copyright__ = f"2023-2025, {__author__}"
8
8
  __license__ = 'MIT'
9
- __version__ = '0.0.45'
9
+ __version__ = '0.0.47'
10
10
  __repo_url__ = 'https://github.com/stevenjgomez/nxs_analysis_tools'
@@ -58,9 +58,9 @@ class TempDependence:
58
58
  Initialize Scissors and LinecutModel objects for each temperature.
59
59
  set_data(temperature, data):
60
60
  Set the dataset for a specific temperature.
61
- load_transforms(temperatures_list=None):
61
+ load_transforms(temperatures_list=None, print_tree=True):
62
62
  Load transform datasets (from nxrefine) based on temperature.
63
- load_datasets(file_ending='hkli.nxs', temperatures_list=None):
63
+ load_datasets(file_ending='hkli.nxs', temperatures_list=None, print_tree=True):
64
64
  Load datasets (CHESS format) from the specified folder.
65
65
  get_sample_directory():
66
66
  Get the folder path where the datasets are located.
@@ -98,6 +98,8 @@ class TempDependence:
98
98
  Fit the line cut models for each temperature.
99
99
  plot_fit(mdheadings=False, **kwargs):
100
100
  Plot the fit results for each temperature.
101
+ plot_order_parameter(self):
102
+ Plot the temperature dependence of the peakheight parameter.
101
103
  print_fit_report():
102
104
  Print the fit report for each temperature.
103
105
  """
@@ -186,7 +188,7 @@ class TempDependence:
186
188
  """
187
189
  self.datasets[temperature] = data
188
190
 
189
- def load_transforms(self, temperatures_list=None):
191
+ def load_transforms(self, temperatures_list=None, print_tree=True):
190
192
  """
191
193
  Load transform datasets (from nxrefine) based on temperature.
192
194
 
@@ -194,6 +196,8 @@ class TempDependence:
194
196
  ----------
195
197
  temperatures_list : list of int or None, optional
196
198
  List of temperatures to load. If None, all available temperatures are loaded.
199
+ print_tree : bool, optional
200
+ Whether to print the data tree upon loading. Default True.
197
201
  """
198
202
  # Convert all temperatures to strings
199
203
  if temperatures_list:
@@ -231,7 +235,7 @@ class TempDependence:
231
235
 
232
236
  # Save dataset
233
237
  try:
234
- self.datasets[self.temperatures[i]] = load_transform(path)
238
+ self.datasets[self.temperatures[i]] = load_transform(path, print_tree)
235
239
  except Exception as e:
236
240
  # Report temperature that was unable to load, then raise exception.
237
241
  temp_failed = self.temperatures[i]
@@ -245,7 +249,7 @@ class TempDependence:
245
249
  # Initialize linecutmodel object
246
250
  self.linecutmodels[self.temperatures[i]] = LinecutModel()
247
251
 
248
- def load_datasets(self, file_ending='hkli.nxs', temperatures_list=None):
252
+ def load_datasets(self, file_ending='hkli.nxs', temperatures_list=None, print_tree=True):
249
253
  """
250
254
  Load datasets (CHESS format) from the specified folder.
251
255
 
@@ -256,6 +260,8 @@ class TempDependence:
256
260
  temperatures_list : list of int or None, optional
257
261
  The list of specific temperatures to load. If None, all available temperatures are
258
262
  loaded. The default is None.
263
+ print_tree : bool, optional
264
+ Whether to print the data tree upon loading. Default True.
259
265
  """
260
266
  temperature_folders = [] # Empty list to store temperature folder names
261
267
  for item in os.listdir(self.sample_directory):
@@ -278,7 +284,7 @@ class TempDependence:
278
284
  filepath = os.path.join(self.sample_directory, T, file)
279
285
 
280
286
  # Load dataset at each temperature
281
- self.datasets[T] = load_data(filepath)
287
+ self.datasets[T] = load_data(filepath, print_tree)
282
288
 
283
289
  # Initialize scissors object at each temperature
284
290
  self.scissors[T] = Scissors()
@@ -370,15 +376,13 @@ class TempDependence:
370
376
  A dictionary of linecuts obtained from the cutting operation.
371
377
  """
372
378
 
373
- center = center if center is not None else self.scissors[self.temperatures[0]].center
374
- window = window if window is not None else self.scissors[self.temperatures[0]].window
375
- axis = axis if axis is not None else self.scissors[self.temperatures[0]].axis
376
-
377
379
  for T in self.temperatures:
378
380
  if verbose:
379
381
  print("-------------------------------")
380
382
  print("Cutting T = " + T + " K data...")
381
- self.scissors[T].cut_data(center, window, axis, verbose)
383
+ self.scissors[T].set_center(center)
384
+ self.scissors[T].set_window(window)
385
+ self.scissors[T].cut_data(axis=axis, verbose=verbose)
382
386
  self.linecuts[T] = self.scissors[T].linecut
383
387
  self.linecutmodels[T].set_data(self.linecuts[T])
384
388
 
@@ -466,7 +470,7 @@ class TempDependence:
466
470
  _, ax = plt.subplots()
467
471
  p = ax.pcolormesh(X, Y, v_2d, **kwargs)
468
472
  plt.colorbar(p, label='counts')
469
- ax.set(xlabel=cut.axes, ylabel=r'$T$ (K)')
473
+ ax.set(xlabel=self.xlabel, ylabel=r'$T$ (K)')
470
474
 
471
475
  return p
472
476
 
@@ -641,6 +645,44 @@ class TempDependence:
641
645
  title=f"{T} K",
642
646
  **kwargs)
643
647
 
648
+ def plot_order_parameter(self):
649
+ """
650
+ Plot the temperature dependence of the peak height (order parameter).
651
+
652
+ This method extracts the peak height from each temperature-dependent
653
+ line cut fit stored in `linecutmodels` and plots it as a function
654
+ of temperature using matplotlib.
655
+
656
+ Returns
657
+ -------
658
+ Figure
659
+ Matplotlib Figure object containing the peak height vs. temperature plot.
660
+ Axes
661
+ Matplotlib Axes object associated with the figure.
662
+
663
+ Notes
664
+ -----
665
+ - Temperature values are converted to integers for plotting.
666
+ - Peak heights are extracted from the 'peakheight' parameter in the model results.
667
+ - The plot uses standard axes labels with temperature in Kelvin.
668
+ """
669
+
670
+ # Create an array of temperature values
671
+ temperatures = [int(T) for T in self.temperatures]
672
+
673
+ # Create an empty list for the peak heights
674
+ peakheights = []
675
+
676
+ # Extract the peakheight at every temperature
677
+ for T in self.temperatures:
678
+ peakheights.append(self.linecutmodels[T].modelresult.params['peakheight'].value)
679
+
680
+ # Plot the peakheights vs. temperature
681
+ fig, ax = plt.subplots()
682
+ ax.plot(temperatures, peakheights)
683
+ ax.set(xlabel='$T$ (K)', ylabel='peakheight')
684
+ return fig, ax
685
+
644
686
  def print_fit_report(self):
645
687
  """
646
688
  Plot the fit results.
@@ -16,10 +16,10 @@ from scipy import ndimage
16
16
  # Specify items on which users are allowed to perform standalone imports
17
17
  __all__ = ['load_data', 'load_transform', 'plot_slice', 'Scissors',
18
18
  'reciprocal_lattice_params', 'rotate_data', 'rotate_data_2D'
19
- 'array_to_nxdata', 'Padder']
19
+ 'array_to_nxdata', 'Padder']
20
20
 
21
21
 
22
- def load_data(path):
22
+ def load_data(path, print_tree=True):
23
23
  """
24
24
  Load data from a NeXus file at a specified path. It is assumed that the data follows the CHESS
25
25
  file structure (i.e., root/entry/data/counts, etc.).
@@ -29,6 +29,9 @@ def load_data(path):
29
29
  path : str
30
30
  The path to the NeXus data file.
31
31
 
32
+ print_tree : bool, optional
33
+ Whether to print the data tree upon loading. Default True.
34
+
32
35
  Returns
33
36
  -------
34
37
  data : nxdata object
@@ -38,31 +41,42 @@ def load_data(path):
38
41
 
39
42
  g = nxload(path)
40
43
  try:
41
- print(g.entry.data.tree)
44
+ print(g.entry.data.tree) if print_tree else None
42
45
  except NeXusError:
43
46
  pass
44
47
 
45
48
  return g.entry.data
46
49
 
47
50
 
48
- def load_transform(path):
51
+ def load_transform(path, print_tree=True):
49
52
  """
50
53
  Load data obtained from nxrefine output from a specified path.
51
54
 
52
55
  Parameters
53
56
  ----------
54
- path : str The path to the transform data file.
57
+ path : str
58
+ The path to the transform data file.
59
+
60
+ print_tree : bool, optional
61
+ Whether to print the data tree upon loading. Default True.
55
62
 
56
63
  Returns
57
64
  -------
58
- data : nxdata object The loaded data stored in a nxdata object.
65
+ data : nxdata object
66
+ The loaded data stored in a nxdata object.
59
67
  """
68
+
60
69
  g = nxload(path)
61
- return NXdata(NXfield(g.entry.transform.data.nxdata.transpose(2, 1, 0), name='counts'),
70
+
71
+ data = NXdata(NXfield(g.entry.transform.data.nxdata.transpose(2, 1, 0), name='counts'),
62
72
  (g.entry.transform.Qh, g.entry.transform.Qk, g.entry.transform.Ql))
63
73
 
74
+ print(data.tree) if print_tree else None
75
+
76
+ return data
77
+
64
78
 
65
- def array_to_nxdata(array, data_template, signal_name='counts'):
79
+ def array_to_nxdata(array, data_template, signal_name=None):
66
80
  """
67
81
  Create an NXdata object from an input array and an NXdata template,
68
82
  with an optional signal name.
@@ -78,7 +92,7 @@ def array_to_nxdata(array, data_template, signal_name='counts'):
78
92
 
79
93
  signal_name : str, optional
80
94
  The name of the signal within the NXdata object. If not provided,
81
- the default signal name 'counts' is used.
95
+ the signal name is inherited from the data_template.
82
96
 
83
97
  Returns
84
98
  -------
@@ -87,11 +101,13 @@ def array_to_nxdata(array, data_template, signal_name='counts'):
87
101
  based on the template.
88
102
  """
89
103
  d = data_template
104
+ if signal_name is None:
105
+ signal_name = d.signal
90
106
  return NXdata(NXfield(array, name=signal_name),
91
107
  tuple(d[d.axes[i]] for i in range(len(d.axes))))
92
108
 
93
109
 
94
- def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None,
110
+ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None, vmax=None,
95
111
  skew_angle=90, ax=None, xlim=None, ylim=None,
96
112
  xticks=None, yticks=None, cbar=True, logscale=False,
97
113
  symlogscale=False, cmap='viridis', linthresh=1,
@@ -114,6 +130,10 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None,
114
130
  The Y axis values. If None, a default range from 0 to the number of
115
131
  rows in `data` is used.
116
132
 
133
+ sum_axis : int, optional
134
+ If the input data is 3D, this specifies the axis to sum over in order
135
+ to reduce the data to 2D for plotting. Required if `data` has three dimensions.
136
+
117
137
  transpose : bool, optional
118
138
  If True, transpose the dataset and its axes before plotting.
119
139
  Default is False.
@@ -183,7 +203,37 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None,
183
203
  p : :class:`matplotlib.collections.QuadMesh`
184
204
  The `matplotlib` QuadMesh object representing the plotted data.
185
205
  """
206
+ is_array = False
207
+ is_nxdata = False
208
+
186
209
  if isinstance(data, np.ndarray):
210
+ is_array = True
211
+ elif isinstance(data, (NXdata, NXfield)):
212
+ is_nxdata = True
213
+ else:
214
+ raise TypeError(f"Unexpected data type: {type(data)}. "
215
+ f"Supported types are np.ndarray and NXdata.")
216
+
217
+ # If three-dimensional, demand sum_axis to reduce to two dimensions.
218
+ if is_array and len(data.shape) == 3:
219
+ assert sum_axis is not None, "sum_axis must be specified when data is a 3D array"
220
+
221
+ data = data.sum(axis=sum_axis)
222
+
223
+ if is_nxdata and len(data.shape) == 3:
224
+ assert sum_axis is not None, "sum_axis must be specified when data is a 3D array"
225
+
226
+ arr = data.nxsignal.nxdata
227
+ arr = arr.sum(axis=sum_axis)
228
+
229
+ # Create a 2D template from the original nxdata
230
+ slice_obj = [slice(None)] * len(data.shape)
231
+ slice_obj[sum_axis] = 0
232
+
233
+ # Use the 2D template to create a new nxdata
234
+ data = array_to_nxdata(arr, data[slice_obj])
235
+
236
+ if is_array:
187
237
  if X is None:
188
238
  X = NXfield(np.linspace(0, data.shape[0], data.shape[0]), name='x')
189
239
  if Y is None:
@@ -193,7 +243,7 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None,
193
243
  data = data.transpose()
194
244
  data = NXdata(NXfield(data, name='value'), (X, Y))
195
245
  data_arr = data[data.signal].nxdata.transpose()
196
- elif isinstance(data, (NXdata, NXfield)):
246
+ elif is_nxdata:
197
247
  if X is None:
198
248
  X = data[data.axes[0]]
199
249
  if Y is None:
@@ -202,9 +252,6 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None,
202
252
  X, Y = Y, X
203
253
  data = data.transpose()
204
254
  data_arr = data[data.signal].nxdata.transpose()
205
- else:
206
- raise TypeError(f"Unexpected data type: {type(data)}. "
207
- f"Supported types are np.ndarray and NXdata.")
208
255
 
209
256
  # Display Markdown heading
210
257
  if mdheading is None:
@@ -543,22 +590,31 @@ class Scissors:
543
590
 
544
591
  return self.linecut
545
592
 
546
- def highlight_integration_window(self, data=None, label=None, highlight_color='red', **kwargs):
593
+ def highlight_integration_window(self, data=None, width=None, height=None, label=None, highlight_color='red',
594
+ **kwargs):
547
595
  """
548
- Plots integration window highlighted on the three principal cross sections of the first
549
- temperature dataset.
596
+ Plots the integration window highlighted on the three principal 2D cross-sections of a 3D dataset.
550
597
 
551
598
  Parameters
552
599
  ----------
553
600
  data : array-like, optional
554
- The 2D heatmap dataset to plot. If not provided, the dataset stored in `self.data` will
555
- be used.
601
+ The 3D dataset to visualize. If not provided, uses `self.data`.
602
+ width : float, optional
603
+ Width of the visible x-axis range in each subplot. Used to zoom in on the integration region.
604
+ height : float, optional
605
+ Height of the visible y-axis range in each subplot. Used to zoom in on the integration region.
556
606
  label : str, optional
557
- The label for the integration window plot.
607
+ Label for the rectangle patch marking the integration window, used in the legend.
558
608
  highlight_color : str, optional
559
- The edge color used to highlight the integration window. Default is 'red'.
560
- **kwargs : keyword arguments, optional
561
- Additional keyword arguments to customize the plot.
609
+ Color of the rectangle edges highlighting the integration window. Default is 'red'.
610
+ **kwargs : dict, optional
611
+ Additional keyword arguments passed to `plot_slice` for customizing the plot (e.g., colormap, vmin, vmax).
612
+
613
+ Returns
614
+ -------
615
+ p1, p2, p3 : matplotlib.collections.QuadMesh
616
+ The plotted QuadMesh objects for the three cross-sections:
617
+ XY at fixed Z, XZ at fixed Y, and YZ at fixed X.
562
618
 
563
619
  """
564
620
  data = self.data if data is None else data
@@ -568,7 +624,7 @@ class Scissors:
568
624
  # Create a figure and subplots
569
625
  fig, axes = plt.subplots(1, 3, figsize=(15, 4))
570
626
 
571
- # Plot cross section 1
627
+ # Plot cross-section 1
572
628
  slice_obj = [slice(None)] * data.ndim
573
629
  slice_obj[2] = center[2]
574
630
 
@@ -587,7 +643,12 @@ class Scissors:
587
643
  )
588
644
  ax.add_patch(rect_diffuse)
589
645
 
590
- # Plot cross section 2
646
+ if 'xlim' not in kwargs and width is not None:
647
+ ax.set(xlim=(center[0] - width / 2, center[0] + width / 2))
648
+ if 'ylim' not in kwargs and height is not None:
649
+ ax.set(ylim=(center[1] - height / 2, center[1] + height / 2))
650
+
651
+ # Plot cross-section 2
591
652
  slice_obj = [slice(None)] * data.ndim
592
653
  slice_obj[1] = center[1]
593
654
 
@@ -606,7 +667,12 @@ class Scissors:
606
667
  )
607
668
  ax.add_patch(rect_diffuse)
608
669
 
609
- # Plot cross section 3
670
+ if 'xlim' not in kwargs and width is not None:
671
+ ax.set(xlim=(center[0] - width / 2, center[0] + width / 2))
672
+ if 'ylim' not in kwargs and height is not None:
673
+ ax.set(ylim=(center[2] - height / 2, center[2] + height / 2))
674
+
675
+ # Plot cross-section 3
610
676
  slice_obj = [slice(None)] * data.ndim
611
677
  slice_obj[0] = center[0]
612
678
 
@@ -625,6 +691,11 @@ class Scissors:
625
691
  )
626
692
  ax.add_patch(rect_diffuse)
627
693
 
694
+ if 'xlim' not in kwargs and width is not None:
695
+ ax.set(xlim=(center[1] - width / 2, center[1] + width / 2))
696
+ if 'ylim' not in kwargs and height is not None:
697
+ ax.set(ylim=(center[2] - height / 2, center[2] + height / 2))
698
+
628
699
  # Adjust subplot padding
629
700
  fig.subplots_adjust(wspace=0.5)
630
701
 
@@ -649,7 +720,7 @@ class Scissors:
649
720
 
650
721
  fig, axes = plt.subplots(1, 3, figsize=(15, 4))
651
722
 
652
- # Plot cross section 1
723
+ # Plot cross-section 1
653
724
  slice_obj = [slice(None)] * data.ndim
654
725
  slice_obj[2] = center[2]
655
726
  p1 = plot_slice(data[slice_obj],
@@ -669,7 +740,7 @@ class Scissors:
669
740
  **kwargs)
670
741
  axes[1].set_aspect(len(data[data.axes[0]].nxdata) / len(data[data.axes[2]].nxdata))
671
742
 
672
- # Plot cross section 3
743
+ # Plot cross-section 3
673
744
  slice_obj = [slice(None)] * data.ndim
674
745
  slice_obj[0] = center[0]
675
746
  p2 = plot_slice(data[slice_obj],
@@ -999,7 +1070,7 @@ class Padder:
999
1070
  self.data = data
1000
1071
 
1001
1072
  self.steps = tuple((data[axis].nxdata[1] - data[axis].nxdata[0])
1002
- for axis in data.axes)
1073
+ for axis in data.axes)
1003
1074
 
1004
1075
  # Absolute value of the maximum value; assumes the domain of the input
1005
1076
  # is symmetric (eg, -H_min = H_max)
@@ -458,20 +458,35 @@ class Symmetrizer3D:
458
458
  self.a_star, self.b_star, self.c_star, \
459
459
  self.al_star, self.be_star, self.ga_star = self.reciprocal_lattice_params
460
460
 
461
- def symmetrize(self):
461
+ def symmetrize(self, positive_values=True):
462
462
  """
463
- Perform the symmetrization of the 3D dataset by sequentially applying
464
- 2D symmetrization on the three principal planes.
463
+ Symmetrize the 3D dataset by sequentially applying 2D symmetrization
464
+ on the three principal planes.
465
465
 
466
- This method symmetrizes the dataset on the three principal planes
467
- (q1-q2, q1-q3, q2-q3) and handles any negative values that might result
468
- from the symmetrization process.
466
+ This method performs symmetrization along the (q1-q2), (q1-q3),
467
+ and (q2-q3) planes, ensuring that the dataset maintains expected
468
+ symmetry properties. Optionally, negative values resulting from the
469
+ symmetrization process can be set to zero.
470
+
471
+ Parameters
472
+ ----------
473
+ positive_values : bool, optional
474
+ If True, sets negative symmetrized values to zero (default is True).
469
475
 
470
476
  Returns
471
477
  -------
472
- symmetrized : NXdata
473
- The symmetrized 3D dataset.
478
+ NXdata
479
+ The symmetrized 3D dataset stored in the `symmetrized` attribute.
480
+
481
+ Notes
482
+ -----
483
+ - Symmetrization is performed sequentially across three principal
484
+ planes using corresponding 2D symmetrization methods.
485
+ - The process prints progress updates and timing information.
486
+ - If `theta_max` is not set for a particular plane symmetrizer,
487
+ that plane is skipped.
474
488
  """
489
+
475
490
  starttime = time.time()
476
491
  data = self.data
477
492
  q1, q2, q3 = self.q1, self.q2, self.q3
@@ -480,7 +495,8 @@ class Symmetrizer3D:
480
495
  if self.plane1symmetrizer.theta_max is not None:
481
496
  print('Symmetrizing ' + self.plane1 + ' planes...')
482
497
  for k, value in enumerate(q3):
483
- print(f'Symmetrizing {q3.nxname}={value:.02f}...', end='\r')
498
+ print(f'Symmetrizing {q3.nxname}={value:.02f}.'
499
+ f'..', end='\r')
484
500
  data_symmetrized = self.plane1symmetrizer.symmetrize_2d(data[:, :, k])
485
501
  out_array[:, :, k] = data_symmetrized[data.signal].nxdata
486
502
  print('\nSymmetrized ' + self.plane1 + ' planes.')
@@ -505,7 +521,8 @@ class Symmetrizer3D:
505
521
  out_array[i, :, :] = data_symmetrized[data.signal].nxdata
506
522
  print('\nSymmetrized ' + self.plane3 + ' planes.')
507
523
 
508
- out_array[out_array < 0] = 0
524
+ if positive_values:
525
+ out_array[out_array < 0] = 0
509
526
 
510
527
  stoptime = time.time()
511
528
  print(f"\nSymmetrization finished in {((stoptime - starttime) / 60):.02f} minutes.")
@@ -1032,15 +1049,28 @@ class Interpolator:
1032
1049
  """
1033
1050
  self.kernel = kernel
1034
1051
 
1035
- def interpolate(self):
1052
+ def interpolate(self, verbose=True, positive_values=True):
1036
1053
  """
1037
1054
  Perform interpolation on the dataset using the specified kernel.
1038
1055
 
1039
- The interpolation is done by convolving the data with the kernel
1040
- using the `convolve_fft` function. Updates the `interpolated`
1041
- attribute with the result.
1056
+ This method convolves the dataset with a kernel using `convolve_fft`
1057
+ to perform interpolation. The resulting interpolated data is stored
1058
+ in the `interpolated` attribute.
1059
+
1060
+ Parameters
1061
+ ----------
1062
+ verbose : bool, optional
1063
+ If True, prints progress messages and timing information
1064
+ (default is True).
1065
+ positive_values : bool, optional
1066
+ If True, sets negative interpolated values to zero
1067
+ (default is True).
1042
1068
 
1043
- Prints the time taken for the interpolation process.
1069
+ Notes
1070
+ -----
1071
+ - The convolution operation is performed in Fourier space.
1072
+ - If a previous interpolation time is recorded, it is displayed
1073
+ before starting a new interpolation.
1044
1074
 
1045
1075
  Returns
1046
1076
  -------
@@ -1048,21 +1078,22 @@ class Interpolator:
1048
1078
  """
1049
1079
  start = time.time()
1050
1080
 
1051
- if self.interp_time:
1081
+ if self.interp_time and verbose:
1052
1082
  print(f"Last interpolation took {self.interp_time / 60:.2f} minutes.")
1053
1083
 
1054
- print("Running interpolation...")
1084
+ print("Running interpolation...") if verbose else None
1055
1085
  result = np.real(
1056
1086
  convolve_fft(self.data[self.data.signal].nxdata,
1057
1087
  self.kernel, allow_huge=True, return_fft=False))
1058
- print("Interpolation finished.")
1088
+ print("Interpolation finished.") if verbose else None
1059
1089
 
1060
1090
  end = time.time()
1061
1091
  interp_time = end - start
1062
1092
 
1063
- print(f'Interpolation took {interp_time / 60:.2f} minutes.')
1093
+ print(f'Interpolation took {interp_time / 60:.2f} minutes.') if verbose else None
1064
1094
 
1065
- result[result < 0] = 0
1095
+ if positive_values:
1096
+ result[result < 0] = 0
1066
1097
  self.interpolated = array_to_nxdata(result, self.data)
1067
1098
 
1068
1099
  def set_tukey_window(self, tukey_alphas=(1.0, 1.0, 1.0)):
@@ -1479,21 +1510,34 @@ class DeltaPDF:
1479
1510
  self.interpolator.set_kernel(kernel)
1480
1511
  self.kernel = kernel
1481
1512
 
1482
- def interpolate(self):
1513
+ def interpolate(self, verbose=True, positive_values=True):
1483
1514
  """
1484
1515
  Perform interpolation on the dataset using the specified kernel.
1485
1516
 
1486
- The interpolation is done by convolving the data with the kernel using
1487
- the `convolve_fft` function. Updates the `interpolated` attribute with
1488
- the result.
1517
+ This method convolves the dataset with a kernel using `convolve_fft`
1518
+ to perform interpolation. The resulting interpolated data is stored
1519
+ in the `interpolated` attribute.
1520
+
1521
+ Parameters
1522
+ ----------
1523
+ verbose : bool, optional
1524
+ If True, prints progress messages and timing information
1525
+ (default is True).
1526
+ positive_values : bool, optional
1527
+ If True, sets negative interpolated values to zero
1528
+ (default is True).
1489
1529
 
1490
- Prints the time taken for the interpolation process.
1530
+ Notes
1531
+ -----
1532
+ - The convolution operation is performed in Fourier space.
1533
+ - If a previous interpolation time is recorded, it is displayed
1534
+ before starting a new interpolation.
1491
1535
 
1492
1536
  Returns
1493
1537
  -------
1494
1538
  None
1495
1539
  """
1496
- self.interpolator.interpolate()
1540
+ self.interpolator.interpolate(verbose, positive_values)
1497
1541
  self.interpolated = self.interpolator.interpolated
1498
1542
 
1499
1543
  def set_tukey_window(self, tukey_alphas=(1.0, 1.0, 1.0)):
@@ -1578,14 +1622,16 @@ class DeltaPDF:
1578
1622
  """
1579
1623
  Perform a 3D Fourier Transform on the padded data.
1580
1624
 
1581
- This function applies an inverse Fourier Transform to the padded data
1582
- using the `pyfftw` library to optimize performance. The result is a
1583
- transformed array with spatial frequency components calculated along
1584
- each axis.
1625
+ This method applies an inverse Fourier Transform to the padded data
1626
+ using `pyfftw` for optimized performance. The result is stored in
1627
+ the `fft` attribute as an NXdata object containing the transformed
1628
+ spatial frequency components.
1585
1629
 
1586
1630
  Parameters
1587
1631
  ----------
1588
- None
1632
+ is_2d : bool, optional
1633
+ If True, performs the FFT only along the first two axes,
1634
+ skipping the out-of-plane direction (default is False).
1589
1635
 
1590
1636
  Returns
1591
1637
  -------
@@ -1593,13 +1639,12 @@ class DeltaPDF:
1593
1639
 
1594
1640
  Notes
1595
1641
  -----
1596
- - The FFT is performed in two stages: first along the last dimension of
1597
- the input array and then along the first two dimensions.
1598
- - The function uses `pyfftw` for efficient computation of the Fourier
1599
- Transform.
1600
- - The output frequency components are computed based on the step sizes
1601
- of the original data axes.
1602
-
1642
+ - Calls `fourier_transform_nxdata` to perform the transformation.
1643
+ - The FFT is computed in two stages: first along the last dimension,
1644
+ then along the first two dimensions.
1645
+ - The output includes frequency components computed from the step
1646
+ sizes of the original data axes.
1603
1647
 
1604
1648
  """
1649
+
1605
1650
  self.fft = fourier_transform_nxdata(self.padded, is_2d=is_2d)