nxs-analysis-tools 0.0.46__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.46/src/nxs_analysis_tools.egg-info → nxs_analysis_tools-0.0.47}/PKG-INFO +1 -1
  2. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/pyproject.toml +1 -1
  3. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/src/_meta/__init__.py +1 -1
  4. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools/datareduction.py +76 -21
  5. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47/src/nxs_analysis_tools.egg-info}/PKG-INFO +1 -1
  6. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools.egg-info/SOURCES.txt +2 -0
  7. nxs_analysis_tools-0.0.47/tests/test_accurate_highlight.py +388 -0
  8. nxs_analysis_tools-0.0.47/tests/test_sum_axis.py +299 -0
  9. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/LICENSE +0 -0
  10. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/MANIFEST.in +0 -0
  11. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/README.md +0 -0
  12. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/setup.cfg +0 -0
  13. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/setup.py +0 -0
  14. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools/__init__.py +0 -0
  15. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools/chess.py +0 -0
  16. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools/fitting.py +0 -0
  17. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools/pairdistribution.py +0 -0
  18. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools.egg-info/dependency_links.txt +0 -0
  19. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools.egg-info/requires.txt +0 -0
  20. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/src/nxs_analysis_tools.egg-info/top_level.txt +0 -0
  21. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/tests/test_chess.py +0 -0
  22. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/tests/test_chess_fitting.py +0 -0
  23. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/tests/test_datareduction.py +0 -0
  24. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/tests/test_fitting.py +0 -0
  25. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/tests/test_lmfit.py +0 -0
  26. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/tests/test_mask_plotting.py +0 -0
  27. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/tests/test_pairdistribution.py +0 -0
  28. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/tests/test_plot_slice_with_ndarray.py +0 -0
  29. {nxs_analysis_tools-0.0.46 → nxs_analysis_tools-0.0.47}/tests/test_symmetrizer_rectangular_plane.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nxs-analysis-tools
3
- Version: 0.0.46
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
@@ -6,7 +6,7 @@ build-backend = 'setuptools.build_meta'
6
6
 
7
7
  [project]
8
8
  name = 'nxs-analysis-tools'
9
- version = '0.0.46'
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'
@@ -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.0.46'
9
+ __version__ = '0.0.47'
10
10
  __repo_url__ = 'https://github.com/stevenjgomez/nxs_analysis_tools'
@@ -16,7 +16,7 @@ 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
22
  def load_data(path, print_tree=True):
@@ -107,7 +107,7 @@ def array_to_nxdata(array, data_template, signal_name=None):
107
107
  tuple(d[d.axes[i]] for i in range(len(d.axes))))
108
108
 
109
109
 
110
- 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,
111
111
  skew_angle=90, ax=None, xlim=None, ylim=None,
112
112
  xticks=None, yticks=None, cbar=True, logscale=False,
113
113
  symlogscale=False, cmap='viridis', linthresh=1,
@@ -130,6 +130,10 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None,
130
130
  The Y axis values. If None, a default range from 0 to the number of
131
131
  rows in `data` is used.
132
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
+
133
137
  transpose : bool, optional
134
138
  If True, transpose the dataset and its axes before plotting.
135
139
  Default is False.
@@ -199,7 +203,37 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None,
199
203
  p : :class:`matplotlib.collections.QuadMesh`
200
204
  The `matplotlib` QuadMesh object representing the plotted data.
201
205
  """
206
+ is_array = False
207
+ is_nxdata = False
208
+
202
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:
203
237
  if X is None:
204
238
  X = NXfield(np.linspace(0, data.shape[0], data.shape[0]), name='x')
205
239
  if Y is None:
@@ -209,7 +243,7 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None,
209
243
  data = data.transpose()
210
244
  data = NXdata(NXfield(data, name='value'), (X, Y))
211
245
  data_arr = data[data.signal].nxdata.transpose()
212
- elif isinstance(data, (NXdata, NXfield)):
246
+ elif is_nxdata:
213
247
  if X is None:
214
248
  X = data[data.axes[0]]
215
249
  if Y is None:
@@ -218,9 +252,6 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None,
218
252
  X, Y = Y, X
219
253
  data = data.transpose()
220
254
  data_arr = data[data.signal].nxdata.transpose()
221
- else:
222
- raise TypeError(f"Unexpected data type: {type(data)}. "
223
- f"Supported types are np.ndarray and NXdata.")
224
255
 
225
256
  # Display Markdown heading
226
257
  if mdheading is None:
@@ -559,22 +590,31 @@ class Scissors:
559
590
 
560
591
  return self.linecut
561
592
 
562
- 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):
563
595
  """
564
- Plots integration window highlighted on the three principal cross sections of the first
565
- temperature dataset.
596
+ Plots the integration window highlighted on the three principal 2D cross-sections of a 3D dataset.
566
597
 
567
598
  Parameters
568
599
  ----------
569
600
  data : array-like, optional
570
- The 2D heatmap dataset to plot. If not provided, the dataset stored in `self.data` will
571
- 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.
572
606
  label : str, optional
573
- The label for the integration window plot.
607
+ Label for the rectangle patch marking the integration window, used in the legend.
574
608
  highlight_color : str, optional
575
- The edge color used to highlight the integration window. Default is 'red'.
576
- **kwargs : keyword arguments, optional
577
- 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.
578
618
 
579
619
  """
580
620
  data = self.data if data is None else data
@@ -584,7 +624,7 @@ class Scissors:
584
624
  # Create a figure and subplots
585
625
  fig, axes = plt.subplots(1, 3, figsize=(15, 4))
586
626
 
587
- # Plot cross section 1
627
+ # Plot cross-section 1
588
628
  slice_obj = [slice(None)] * data.ndim
589
629
  slice_obj[2] = center[2]
590
630
 
@@ -603,7 +643,12 @@ class Scissors:
603
643
  )
604
644
  ax.add_patch(rect_diffuse)
605
645
 
606
- # 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
607
652
  slice_obj = [slice(None)] * data.ndim
608
653
  slice_obj[1] = center[1]
609
654
 
@@ -622,7 +667,12 @@ class Scissors:
622
667
  )
623
668
  ax.add_patch(rect_diffuse)
624
669
 
625
- # 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
626
676
  slice_obj = [slice(None)] * data.ndim
627
677
  slice_obj[0] = center[0]
628
678
 
@@ -641,6 +691,11 @@ class Scissors:
641
691
  )
642
692
  ax.add_patch(rect_diffuse)
643
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
+
644
699
  # Adjust subplot padding
645
700
  fig.subplots_adjust(wspace=0.5)
646
701
 
@@ -665,7 +720,7 @@ class Scissors:
665
720
 
666
721
  fig, axes = plt.subplots(1, 3, figsize=(15, 4))
667
722
 
668
- # Plot cross section 1
723
+ # Plot cross-section 1
669
724
  slice_obj = [slice(None)] * data.ndim
670
725
  slice_obj[2] = center[2]
671
726
  p1 = plot_slice(data[slice_obj],
@@ -685,7 +740,7 @@ class Scissors:
685
740
  **kwargs)
686
741
  axes[1].set_aspect(len(data[data.axes[0]].nxdata) / len(data[data.axes[2]].nxdata))
687
742
 
688
- # Plot cross section 3
743
+ # Plot cross-section 3
689
744
  slice_obj = [slice(None)] * data.ndim
690
745
  slice_obj[0] = center[0]
691
746
  p2 = plot_slice(data[slice_obj],
@@ -1015,7 +1070,7 @@ class Padder:
1015
1070
  self.data = data
1016
1071
 
1017
1072
  self.steps = tuple((data[axis].nxdata[1] - data[axis].nxdata[0])
1018
- for axis in data.axes)
1073
+ for axis in data.axes)
1019
1074
 
1020
1075
  # Absolute value of the maximum value; assumes the domain of the input
1021
1076
  # is symmetric (eg, -H_min = H_max)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nxs-analysis-tools
3
- Version: 0.0.46
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
@@ -14,6 +14,7 @@ src/nxs_analysis_tools.egg-info/SOURCES.txt
14
14
  src/nxs_analysis_tools.egg-info/dependency_links.txt
15
15
  src/nxs_analysis_tools.egg-info/requires.txt
16
16
  src/nxs_analysis_tools.egg-info/top_level.txt
17
+ tests/test_accurate_highlight.py
17
18
  tests/test_chess.py
18
19
  tests/test_chess_fitting.py
19
20
  tests/test_datareduction.py
@@ -22,4 +23,5 @@ tests/test_lmfit.py
22
23
  tests/test_mask_plotting.py
23
24
  tests/test_pairdistribution.py
24
25
  tests/test_plot_slice_with_ndarray.py
26
+ tests/test_sum_axis.py
25
27
  tests/test_symmetrizer_rectangular_plane.py
@@ -0,0 +1,388 @@
1
+ import os
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from matplotlib.transforms import Affine2D
5
+ from matplotlib.markers import MarkerStyle
6
+ from matplotlib.ticker import MultipleLocator
7
+ from matplotlib import colors
8
+ from matplotlib import patches
9
+ from IPython.display import display, Markdown
10
+ from nexusformat.nexus import NXfield, NXdata, nxload, NeXusError, NXroot, NXentry, nxsave
11
+ from scipy import ndimage
12
+ from nxs_analysis_tools.datareduction import plot_slice, load_data
13
+
14
+
15
+ class Scissors:
16
+ """
17
+ Scissors class provides functionality for reducing data to a 1D linecut using an integration
18
+ window.
19
+
20
+ Attributes
21
+ ----------
22
+ data : :class:`nexusformat.nexus.NXdata` or None
23
+ Input :class:`nexusformat.nexus.NXdata`.
24
+ center : tuple or None
25
+ Central coordinate around which to perform the linecut.
26
+ window : tuple or None
27
+ Extents of the window for integration along each axis.
28
+ axis : int or None
29
+ Axis along which to perform the integration.
30
+ integration_volume : :class:`nexusformat.nexus.NXdata` or None
31
+ Data array after applying the integration window.
32
+ integrated_axes : tuple or None
33
+ Indices of axes that were integrated.
34
+ linecut : :class:`nexusformat.nexus.NXdata` or None
35
+ 1D linecut data after integration.
36
+ integration_window : tuple or None
37
+ Slice object representing the integration window in the data array.
38
+
39
+ Methods
40
+ -------
41
+ set_data(data)
42
+ Set the input :class:`nexusformat.nexus.NXdata`.
43
+ get_data()
44
+ Get the input :class:`nexusformat.nexus.NXdata`.
45
+ set_center(center)
46
+ Set the central coordinate for the linecut.
47
+ set_window(window, axis=None, verbose=False)
48
+ Set the extents of the integration window.
49
+ get_window()
50
+ Get the extents of the integration window.
51
+ cut_data(center=None, window=None, axis=None, verbose=False)
52
+ Reduce data to a 1D linecut using the integration window.
53
+ highlight_integration_window(data=None, label=None, highlight_color='red', **kwargs)
54
+ Plot the integration window highlighted on a 2D heatmap of the full dataset.
55
+ plot_integration_window(**kwargs)
56
+ Plot a 2D heatmap of the integration window data.
57
+ """
58
+
59
+ def __init__(self, data=None, center=None, window=None, axis=None):
60
+ """
61
+ Initializes a Scissors object.
62
+
63
+ Parameters
64
+ ----------
65
+ data : :class:`nexusformat.nexus.NXdata` or None, optional
66
+ Input NXdata. Default is None.
67
+ center : tuple or None, optional
68
+ Central coordinate around which to perform the linecut. Default is None.
69
+ window : tuple or None, optional
70
+ Extents of the window for integration along each axis. Default is None.
71
+ axis : int or None, optional
72
+ Axis along which to perform the integration. Default is None.
73
+ """
74
+
75
+ self.data = data
76
+ self.center = tuple(float(i) for i in center) if center is not None else None
77
+ self.window = tuple(float(i) for i in window) if window is not None else None
78
+ self.axis = axis
79
+
80
+ self.integration_volume = None
81
+ self.integrated_axes = None
82
+ self.linecut = None
83
+ self.integration_window = None
84
+
85
+ def set_data(self, data):
86
+ """
87
+ Set the input NXdata.
88
+
89
+ Parameters
90
+ ----------
91
+ data : :class:`nexusformat.nexus.NXdata`
92
+ Input data array.
93
+ """
94
+ self.data = data
95
+
96
+ def get_data(self):
97
+ """
98
+ Get the input data array.
99
+
100
+ Returns
101
+ -------
102
+ ndarray or None
103
+ Input data array.
104
+ """
105
+ return self.data
106
+
107
+ def set_center(self, center):
108
+ """
109
+ Set the central coordinate for the linecut.
110
+
111
+ Parameters
112
+ ----------
113
+ center : tuple
114
+ Central coordinate around which to perform the linecut.
115
+ """
116
+ self.center = tuple(float(i) for i in center) if center is not None else None
117
+
118
+ def set_window(self, window, axis=None, verbose=False):
119
+ """
120
+ Set the extents of the integration window.
121
+
122
+ Parameters
123
+ ----------
124
+ window : tuple
125
+ Extents of the window for integration along each axis.
126
+ axis : int or None, optional
127
+ The axis along which to perform the linecut. If not specified, the value from the
128
+ object's attribute will be used.
129
+ verbose : bool, optional
130
+ Enables printout of linecut axis and integrated axes. Default False.
131
+
132
+ """
133
+ self.window = tuple(float(i) for i in window) if window is not None else None
134
+
135
+ # Determine the axis for integration
136
+ self.axis = window.index(max(window)) if axis is None else axis
137
+
138
+ # Determine the integrated axes (axes other than the integration axis)
139
+ self.integrated_axes = tuple(i for i in range(self.data.ndim) if i != self.axis)
140
+
141
+ if verbose:
142
+ print("Linecut axis: " + str(self.data.axes[self.axis]))
143
+ print("Integrated axes: " + str([self.data.axes[axis]
144
+ for axis in self.integrated_axes]))
145
+
146
+ def get_window(self):
147
+ """
148
+ Get the extents of the integration window.
149
+
150
+ Returns
151
+ -------
152
+ tuple or None
153
+ Extents of the integration window.
154
+ """
155
+ return self.window
156
+
157
+ def cut_data(self, center=None, window=None, axis=None, verbose=False):
158
+ """
159
+ Reduces data to a 1D linecut with integration extents specified by the
160
+ window about a central coordinate.
161
+
162
+ Parameters
163
+ ----------
164
+ center : float or None, optional
165
+ Central coordinate for the linecut. If not specified, the value from the object's
166
+ attribute will be used.
167
+ window : tuple or None, optional
168
+ Integration window extents around the central coordinate. If not specified, the value
169
+ from the object's attribute will be used.
170
+ axis : int or None, optional
171
+ The axis along which to perform the linecut. If not specified, the value from the
172
+ object's attribute will be used.
173
+ verbose : bool
174
+ Enables printout of linecut axis and integrated axes. Default False.
175
+
176
+ Returns
177
+ -------
178
+ integrated_data : :class:`nexusformat.nexus.NXdata`
179
+ 1D linecut data after integration.
180
+
181
+ """
182
+
183
+ # Extract necessary attributes from the object
184
+ data = self.data
185
+ center = center if center is not None else self.center
186
+ self.set_center(center)
187
+ window = window if window is not None else self.window
188
+ self.set_window(window, axis, verbose)
189
+
190
+ # Convert the center to a tuple of floats
191
+ center = tuple(float(c) for c in center)
192
+
193
+ # Calculate the start and stop indices for slicing the data
194
+ start = np.subtract(center, window)
195
+ stop = np.add(center, window)
196
+ slice_obj = tuple(slice(s, e) for s, e in zip(start, stop))
197
+ self.integration_window = slice_obj
198
+
199
+ # Perform the data cut
200
+ self.integration_volume = data[slice_obj]
201
+ self.integration_volume.nxname = data.nxname
202
+
203
+ # Perform integration along the integrated axes
204
+ integrated_data = np.sum(self.integration_volume[self.integration_volume.signal].nxdata,
205
+ axis=self.integrated_axes)
206
+
207
+ # Create an NXdata object for the linecut data
208
+ self.linecut = NXdata(NXfield(integrated_data, name=self.integration_volume.signal),
209
+ self.integration_volume[self.integration_volume.axes[self.axis]])
210
+ self.linecut.nxname = self.integration_volume.nxname
211
+
212
+ return self.linecut
213
+
214
+ def highlight_integration_window(self, data=None, width=None, height=None, label=None, highlight_color='red', **kwargs):
215
+ """
216
+ Plots the integration window highlighted on the three principal 2D cross-sections of a 3D dataset.
217
+
218
+ Parameters
219
+ ----------
220
+ data : array-like, optional
221
+ The 3D dataset to visualize. If not provided, uses `self.data`.
222
+ width : float, optional
223
+ Width of the visible x-axis range in each subplot. Used to zoom in on the integration region.
224
+ height : float, optional
225
+ Height of the visible y-axis range in each subplot. Used to zoom in on the integration region.
226
+ label : str, optional
227
+ Label for the rectangle patch marking the integration window, used in the legend.
228
+ highlight_color : str, optional
229
+ Color of the rectangle edges highlighting the integration window. Default is 'red'.
230
+ **kwargs : dict, optional
231
+ Additional keyword arguments passed to `plot_slice` for customizing the plot (e.g., colormap, vmin, vmax).
232
+
233
+ Returns
234
+ -------
235
+ p1, p2, p3 : matplotlib.collections.QuadMesh
236
+ The plotted QuadMesh objects for the three cross-sections:
237
+ XY at fixed Z, XZ at fixed Y, and YZ at fixed X.
238
+
239
+ """
240
+ data = self.data if data is None else data
241
+ center = self.center
242
+ window = self.window
243
+
244
+ # Create a figure and subplots
245
+ fig, axes = plt.subplots(1, 3, figsize=(15, 4))
246
+
247
+ # Plot cross-section 1
248
+ slice_obj = [slice(None)] * data.ndim
249
+ slice_obj[2] = center[2]
250
+
251
+ p1 = plot_slice(data[slice_obj],
252
+ X=data[data.axes[0]],
253
+ Y=data[data.axes[1]],
254
+ ax=axes[0],
255
+ **kwargs)
256
+ ax = axes[0]
257
+ rect_diffuse = patches.Rectangle(
258
+ (center[0] - window[0],
259
+ center[1] - window[1]),
260
+ 2 * window[0], 2 * window[1],
261
+ linewidth=1, edgecolor=highlight_color,
262
+ facecolor='none', transform=p1.get_transform(), label=label,
263
+ )
264
+ ax.add_patch(rect_diffuse)
265
+
266
+ if 'xlim' not in kwargs and width is not None:
267
+ ax.set(xlim=(center[0]-width/2,center[0]+width/2))
268
+ if 'ylim' not in kwargs and height is not None:
269
+ ax.set(ylim=(center[1]-height/2,center[1]+height/2))
270
+
271
+ # Plot cross-section 2
272
+ slice_obj = [slice(None)] * data.ndim
273
+ slice_obj[1] = center[1]
274
+
275
+ p2 = plot_slice(data[slice_obj],
276
+ X=data[data.axes[0]],
277
+ Y=data[data.axes[2]],
278
+ ax=axes[1],
279
+ **kwargs)
280
+ ax = axes[1]
281
+ rect_diffuse = patches.Rectangle(
282
+ (center[0] - window[0],
283
+ center[2] - window[2]),
284
+ 2 * window[0], 2 * window[2],
285
+ linewidth=1, edgecolor=highlight_color,
286
+ facecolor='none', transform=p2.get_transform(), label=label,
287
+ )
288
+ ax.add_patch(rect_diffuse)
289
+
290
+ if 'xlim' not in kwargs and width is not None:
291
+ ax.set(xlim=(center[0]-width/2,center[0]+width/2))
292
+ if 'ylim' not in kwargs and height is not None:
293
+ ax.set(ylim=(center[2]-height/2,center[2]+height/2))
294
+
295
+
296
+ # Plot cross-section 3
297
+ slice_obj = [slice(None)] * data.ndim
298
+ slice_obj[0] = center[0]
299
+
300
+ p3 = plot_slice(data[slice_obj],
301
+ X=data[data.axes[1]],
302
+ Y=data[data.axes[2]],
303
+ ax=axes[2],
304
+ **kwargs)
305
+ ax = axes[2]
306
+ rect_diffuse = patches.Rectangle(
307
+ (center[1] - window[1],
308
+ center[2] - window[2]),
309
+ 2 * window[1], 2 * window[2],
310
+ linewidth=1, edgecolor=highlight_color,
311
+ facecolor='none', transform=p3.get_transform(), label=label,
312
+ )
313
+ ax.add_patch(rect_diffuse)
314
+
315
+ if 'xlim' not in kwargs and width is not None:
316
+ ax.set(xlim=(center[1]-width/2,center[1]+width/2))
317
+ if 'ylim' not in kwargs and height is not None:
318
+ ax.set(ylim=(center[2]-height/2,center[2]+height/2))
319
+
320
+
321
+ # Adjust subplot padding
322
+ fig.subplots_adjust(wspace=0.5)
323
+
324
+ if label is not None:
325
+ [ax.legend() for ax in axes]
326
+
327
+ plt.show()
328
+
329
+ return p1, p2, p3
330
+
331
+ def plot_integration_window(self, **kwargs):
332
+ """
333
+ Plots the three principal cross-sections of the integration volume on a single figure.
334
+
335
+ Parameters
336
+ ----------
337
+ **kwargs : keyword arguments, optional
338
+ Additional keyword arguments to customize the plot.
339
+ """
340
+ data = self.integration_volume
341
+ center = self.center
342
+
343
+ fig, axes = plt.subplots(1, 3, figsize=(15, 4))
344
+
345
+ # Plot cross section 1
346
+ slice_obj = [slice(None)] * data.ndim
347
+ slice_obj[2] = center[2]
348
+ p1 = plot_slice(data[slice_obj],
349
+ X=data[data.axes[0]],
350
+ Y=data[data.axes[1]],
351
+ ax=axes[0],
352
+ **kwargs)
353
+ axes[0].set_aspect(len(data[data.axes[0]].nxdata) / len(data[data.axes[1]].nxdata))
354
+
355
+ # Plot cross section 2
356
+ slice_obj = [slice(None)] * data.ndim
357
+ slice_obj[1] = center[1]
358
+ p3 = plot_slice(data[slice_obj],
359
+ X=data[data.axes[0]],
360
+ Y=data[data.axes[2]],
361
+ ax=axes[1],
362
+ **kwargs)
363
+ axes[1].set_aspect(len(data[data.axes[0]].nxdata) / len(data[data.axes[2]].nxdata))
364
+
365
+ # Plot cross section 3
366
+ slice_obj = [slice(None)] * data.ndim
367
+ slice_obj[0] = center[0]
368
+ p2 = plot_slice(data[slice_obj],
369
+ X=data[data.axes[1]],
370
+ Y=data[data.axes[2]],
371
+ ax=axes[2],
372
+ **kwargs)
373
+ axes[2].set_aspect(len(data[data.axes[1]].nxdata) / len(data[data.axes[2]].nxdata))
374
+
375
+ # Adjust subplot padding
376
+ fig.subplots_adjust(wspace=0.3)
377
+
378
+ plt.show()
379
+
380
+ return p1, p2, p3
381
+
382
+
383
+ data = load_data(r'C:\Users\steve\OneDrive\Documents\UCSB\Projects\RECd3P3\mini\15\3rot_hkli.nxs')
384
+ s = Scissors(data)
385
+ s.cut_data(center=(1,1,0), window=(0.5,0.2,0.2))
386
+ print(s.highlight_integration_window(vmin=0, vmax=100, width=10, height=10))
387
+
388
+ print(data.shape)
@@ -0,0 +1,299 @@
1
+ import os
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ from matplotlib.transforms import Affine2D
5
+ from matplotlib.markers import MarkerStyle
6
+ from matplotlib.ticker import MultipleLocator
7
+ from matplotlib import colors
8
+ from matplotlib import patches
9
+ from IPython.display import display, Markdown
10
+ from nexusformat.nexus import NXfield, NXdata, nxload, NeXusError, NXroot, NXentry, nxsave
11
+ from scipy import ndimage
12
+ from nxs_analysis_tools.datareduction import load_data, array_to_nxdata, plot_slice
13
+
14
+ # def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None, vmax=None,
15
+ # skew_angle=90, ax=None, xlim=None, ylim=None,
16
+ # xticks=None, yticks=None, cbar=True, logscale=False,
17
+ # symlogscale=False, cmap='viridis', linthresh=1,
18
+ # title=None, mdheading=None, cbartitle=None,
19
+ # **kwargs):
20
+ # """
21
+ # Plot a 2D slice of the provided dataset, with optional transformations
22
+ # and customizations.
23
+ #
24
+ # Parameters
25
+ # ----------
26
+ # data : :class:`nexusformat.nexus.NXdata` or ndarray
27
+ # The dataset to plot. Can be an `NXdata` object or a `numpy` array.
28
+ #
29
+ # X : NXfield, optional
30
+ # The X axis values. If None, a default range from 0 to the number of
31
+ # columns in `data` is used.
32
+ #
33
+ # Y : NXfield, optional
34
+ # The Y axis values. If None, a default range from 0 to the number of
35
+ # rows in `data` is used.
36
+ #
37
+ # sum_axis : int, optional
38
+ # If the input data is 3D, this specifies the axis to sum over in order
39
+ # to reduce the data to 2D for plotting. Required if `data` has three dimensions.
40
+ #
41
+ # transpose : bool, optional
42
+ # If True, transpose the dataset and its axes before plotting.
43
+ # Default is False.
44
+ #
45
+ # vmin : float, optional
46
+ # The minimum value for the color scale. If not provided, the minimum
47
+ # value of the dataset is used.
48
+ #
49
+ # vmax : float, optional
50
+ # The maximum value for the color scale. If not provided, the maximum
51
+ # value of the dataset is used.
52
+ #
53
+ # skew_angle : float, optional
54
+ # The angle in degrees to shear the plot. Default is 90 degrees (no skew).
55
+ #
56
+ # ax : matplotlib.axes.Axes, optional
57
+ # The `matplotlib` axis to plot on. If None, a new figure and axis will
58
+ # be created.
59
+ #
60
+ # xlim : tuple, optional
61
+ # The limits for the x-axis. If None, the limits are set automatically
62
+ # based on the data.
63
+ #
64
+ # ylim : tuple, optional
65
+ # The limits for the y-axis. If None, the limits are set automatically
66
+ # based on the data.
67
+ #
68
+ # xticks : float or list of float, optional
69
+ # The major tick interval or specific tick locations for the x-axis.
70
+ # Default is to use a minor tick interval of 1.
71
+ #
72
+ # yticks : float or list of float, optional
73
+ # The major tick interval or specific tick locations for the y-axis.
74
+ # Default is to use a minor tick interval of 1.
75
+ #
76
+ # cbar : bool, optional
77
+ # Whether to include a colorbar. Default is True.
78
+ #
79
+ # logscale : bool, optional
80
+ # Whether to use a logarithmic color scale. Default is False.
81
+ #
82
+ # symlogscale : bool, optional
83
+ # Whether to use a symmetrical logarithmic color scale. Default is False.
84
+ #
85
+ # cmap : str or Colormap, optional
86
+ # The colormap to use for the plot. Default is 'viridis'.
87
+ #
88
+ # linthresh : float, optional
89
+ # The linear threshold for symmetrical logarithmic scaling. Default is 1.
90
+ #
91
+ # title : str, optional
92
+ # The title for the plot. If None, no title is set.
93
+ #
94
+ # mdheading : str, optional
95
+ # A Markdown heading to display above the plot. If 'None' or not provided,
96
+ # no heading is displayed.
97
+ #
98
+ # cbartitle : str, optional
99
+ # The title for the colorbar. If None, the colorbar label will be set to
100
+ # the name of the signal.
101
+ #
102
+ # **kwargs
103
+ # Additional keyword arguments passed to `pcolormesh`.
104
+ #
105
+ # Returns
106
+ # -------
107
+ # p : :class:`matplotlib.collections.QuadMesh`
108
+ # The `matplotlib` QuadMesh object representing the plotted data.
109
+ # """
110
+ # is_array = False
111
+ # is_nxdata = False
112
+ #
113
+ # if isinstance(data, np.ndarray):
114
+ # is_array = True
115
+ # elif isinstance(data, (NXdata, NXfield)):
116
+ # is_nxdata = True
117
+ # else:
118
+ # raise TypeError(f"Unexpected data type: {type(data)}. "
119
+ # f"Supported types are np.ndarray and NXdata.")
120
+ #
121
+ # # If three-dimensional, demand sum_axis to reduce to two dimensions.
122
+ # if is_array and len(data.shape) == 3:
123
+ # assert sum_axis is not None, "sum_axis must be specified when data is a 3D array"
124
+ #
125
+ # data = data.sum(axis=sum_axis)
126
+ #
127
+ # if is_nxdata and len(data.shape) == 3:
128
+ # assert sum_axis is not None, "sum_axis must be specified when data is a 3D array"
129
+ #
130
+ # arr = data.nxsignal.nxdata
131
+ # arr = arr.sum(axis=sum_axis)
132
+ #
133
+ # # Create a 2D template from the original nxdata
134
+ # slice_obj = [slice(None)] * len(data.shape)
135
+ # slice_obj[sum_axis] = 0
136
+ #
137
+ # # Use the 2D template to create a new nxdata
138
+ # data = array_to_nxdata(arr, data[slice_obj])
139
+ #
140
+ # if is_array:
141
+ # if X is None:
142
+ # X = NXfield(np.linspace(0, data.shape[0], data.shape[0]), name='x')
143
+ # if Y is None:
144
+ # Y = NXfield(np.linspace(0, data.shape[1], data.shape[1]), name='y')
145
+ # if transpose:
146
+ # X, Y = Y, X
147
+ # data = data.transpose()
148
+ # data = NXdata(NXfield(data, name='value'), (X, Y))
149
+ # data_arr = data[data.signal].nxdata.transpose()
150
+ # elif is_nxdata:
151
+ # if X is None:
152
+ # X = data[data.axes[0]]
153
+ # if Y is None:
154
+ # Y = data[data.axes[1]]
155
+ # if transpose:
156
+ # X, Y = Y, X
157
+ # data = data.transpose()
158
+ # data_arr = data[data.signal].nxdata.transpose()
159
+ #
160
+ # # Display Markdown heading
161
+ # if mdheading is None:
162
+ # pass
163
+ # elif mdheading == "None":
164
+ # display(Markdown('### Figure'))
165
+ # else:
166
+ # display(Markdown('### Figure - ' + mdheading))
167
+ #
168
+ # # Inherit axes if user provides some
169
+ # if ax is not None:
170
+ # fig = ax.get_figure()
171
+ # # Otherwise set up some default axes
172
+ # else:
173
+ # fig = plt.figure()
174
+ # ax = fig.add_axes([0, 0, 1, 1])
175
+ #
176
+ # # If limits not provided, use extrema
177
+ # if vmin is None:
178
+ # vmin = data_arr.min()
179
+ # if vmax is None:
180
+ # vmax = data_arr.max()
181
+ #
182
+ # # Set norm (linear scale, logscale, or symlogscale)
183
+ # norm = colors.Normalize(vmin=vmin, vmax=vmax) # Default: linear scale
184
+ #
185
+ # if symlogscale:
186
+ # norm = colors.SymLogNorm(linthresh=linthresh, vmin=-1 * vmax, vmax=vmax)
187
+ # elif logscale:
188
+ # norm = colors.LogNorm(vmin=vmin, vmax=vmax)
189
+ #
190
+ # # Plot data
191
+ # p = ax.pcolormesh(X.nxdata, Y.nxdata, data_arr, shading='auto', norm=norm, cmap=cmap, **kwargs)
192
+ #
193
+ # ## Transform data to new coordinate system if necessary
194
+ # # Correct skew angle
195
+ # skew_angle_adj = 90 - skew_angle
196
+ # # Create blank 2D affine transformation
197
+ # t = Affine2D()
198
+ # # Scale y-axis to preserve norm while shearing
199
+ # t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
200
+ # # Shear along x-axis
201
+ # t += Affine2D().skew_deg(skew_angle_adj, 0)
202
+ # # Return to original y-axis scaling
203
+ # t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
204
+ # ## Correct for x-displacement after shearing
205
+ # # If ylims provided, use those
206
+ # if ylim is not None:
207
+ # # Set ylims
208
+ # ax.set(ylim=ylim)
209
+ # ymin, ymax = ylim
210
+ # # Else, use current ylims
211
+ # else:
212
+ # ymin, ymax = ax.get_ylim()
213
+ # # Use ylims to calculate translation (necessary to display axes in correct position)
214
+ # p.set_transform(t
215
+ # + Affine2D().translate(-ymin * np.sin(skew_angle_adj * np.pi / 180), 0)
216
+ # + ax.transData)
217
+ #
218
+ # # Set x limits
219
+ # if xlim is not None:
220
+ # xmin, xmax = xlim
221
+ # else:
222
+ # xmin, xmax = ax.get_xlim()
223
+ # if skew_angle <= 90:
224
+ # ax.set(xlim=(xmin, xmax + (ymax - ymin) / np.tan((90 - skew_angle_adj) * np.pi / 180)))
225
+ # else:
226
+ # ax.set(xlim=(xmin - (ymax - ymin) / np.tan((skew_angle_adj - 90) * np.pi / 180), xmax))
227
+ #
228
+ # # Correct aspect ratio for the x/y axes after transformation
229
+ # ax.set(aspect=np.cos(skew_angle_adj * np.pi / 180))
230
+ #
231
+ # # Add tick marks all around
232
+ # ax.tick_params(direction='in', top=True, right=True, which='both')
233
+ #
234
+ # # Set tick locations
235
+ # if xticks is None:
236
+ # # Add default minor ticks
237
+ # ax.xaxis.set_minor_locator(MultipleLocator(1))
238
+ # else:
239
+ # # Otherwise use user provided values
240
+ # ax.xaxis.set_major_locator(MultipleLocator(xticks))
241
+ # ax.xaxis.set_minor_locator(MultipleLocator(1))
242
+ # if yticks is None:
243
+ # # Add default minor ticks
244
+ # ax.yaxis.set_minor_locator(MultipleLocator(1))
245
+ # else:
246
+ # # Otherwise use user provided values
247
+ # ax.yaxis.set_major_locator(MultipleLocator(yticks))
248
+ # ax.yaxis.set_minor_locator(MultipleLocator(1))
249
+ #
250
+ # # Apply transform to tick marks
251
+ # for i in range(0, len(ax.xaxis.get_ticklines())):
252
+ # # Tick marker
253
+ # m = MarkerStyle(3)
254
+ # line = ax.xaxis.get_majorticklines()[i]
255
+ # if i % 2:
256
+ # # Top ticks (translation here makes their direction="in")
257
+ # m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle_adj, 0))
258
+ # # This first method shifts the top ticks horizontally to match the skew angle.
259
+ # # This does not look good in all cases.
260
+ # # line.set_transform(Affine2D().translate((ymax-ymin)*np.sin(skew_angle*np.pi/180),0) +
261
+ # # line.get_transform())
262
+ # # This second method skews the tick marks in place and
263
+ # # can sometimes lead to them being misaligned.
264
+ # line.set_transform(line.get_transform()) # This does nothing
265
+ # else:
266
+ # # Bottom ticks
267
+ # m._transform.set(Affine2D().skew_deg(skew_angle_adj, 0))
268
+ #
269
+ # line.set_marker(m)
270
+ #
271
+ # for i in range(0, len(ax.xaxis.get_minorticklines())):
272
+ # m = MarkerStyle(2)
273
+ # line = ax.xaxis.get_minorticklines()[i]
274
+ # if i % 2:
275
+ # m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle_adj, 0))
276
+ # else:
277
+ # m._transform.set(Affine2D().skew_deg(skew_angle_adj, 0))
278
+ #
279
+ # line.set_marker(m)
280
+ #
281
+ # if cbar:
282
+ # colorbar = fig.colorbar(p)
283
+ # if cbartitle is None:
284
+ # colorbar.set_label(data.signal)
285
+ #
286
+ # ax.set(
287
+ # xlabel=X.nxname,
288
+ # ylabel=Y.nxname,
289
+ # )
290
+ #
291
+ # if title is not None:
292
+ # ax.set_title(title)
293
+ #
294
+ # # Return the quadmesh object
295
+ # return p
296
+
297
+ data = load_data(r'C:\Users\steve\OneDrive\Documents\UCSB\Projects\RECd3P3\mini\15\3rot_hkli.nxs')
298
+ plot_slice(data[:,-0.2:0.2,:], vmin=0, vmax=100*10, sum_axis=1)
299
+ plt.show()