nxs-analysis-tools 0.0.43__tar.gz → 0.0.45__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 (27) hide show
  1. {nxs_analysis_tools-0.0.43/src/nxs_analysis_tools.egg-info → nxs_analysis_tools-0.0.45}/PKG-INFO +3 -2
  2. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/pyproject.toml +1 -1
  3. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/src/_meta/__init__.py +1 -1
  4. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/src/nxs_analysis_tools/chess.py +31 -9
  5. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/src/nxs_analysis_tools/datareduction.py +2 -2
  6. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/src/nxs_analysis_tools/pairdistribution.py +13 -7
  7. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45/src/nxs_analysis_tools.egg-info}/PKG-INFO +3 -2
  8. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/src/nxs_analysis_tools.egg-info/SOURCES.txt +2 -1
  9. nxs_analysis_tools-0.0.45/tests/test_symmetrizer_rectangular_plane.py +382 -0
  10. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/LICENSE +0 -0
  11. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/MANIFEST.in +0 -0
  12. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/README.md +0 -0
  13. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/setup.cfg +0 -0
  14. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/setup.py +0 -0
  15. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/src/nxs_analysis_tools/__init__.py +0 -0
  16. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/src/nxs_analysis_tools/fitting.py +0 -0
  17. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/src/nxs_analysis_tools.egg-info/dependency_links.txt +0 -0
  18. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/src/nxs_analysis_tools.egg-info/requires.txt +0 -0
  19. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/src/nxs_analysis_tools.egg-info/top_level.txt +0 -0
  20. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/tests/test_chess.py +0 -0
  21. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/tests/test_chess_fitting.py +0 -0
  22. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/tests/test_datareduction.py +0 -0
  23. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/tests/test_fitting.py +0 -0
  24. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/tests/test_lmfit.py +0 -0
  25. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/tests/test_mask_plotting.py +0 -0
  26. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/tests/test_pairdistribution.py +0 -0
  27. {nxs_analysis_tools-0.0.43 → nxs_analysis_tools-0.0.45}/tests/test_plot_slice_with_ndarray.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: nxs-analysis-tools
3
- Version: 0.0.43
3
+ Version: 0.0.45
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
@@ -66,6 +66,7 @@ Requires-Dist: sphinx-autobuild>=2021.3.14; extra == "dev"
66
66
  Requires-Dist: sphinx-copybutton>=0.5.0; extra == "dev"
67
67
  Requires-Dist: sphinxext-opengraph>=0.6.3; extra == "dev"
68
68
  Requires-Dist: twine>=4.0.1; extra == "dev"
69
+ Dynamic: license-file
69
70
 
70
71
  # nxs-analysis-tools
71
72
 
@@ -6,7 +6,7 @@ build-backend = 'setuptools.build_meta'
6
6
 
7
7
  [project]
8
8
  name = 'nxs-analysis-tools'
9
- version = '0.0.43'
9
+ version = '0.0.45'
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, {__author__}"
8
8
  __license__ = 'MIT'
9
- __version__ = '0.0.43'
9
+ __version__ = '0.0.45'
10
10
  __repo_url__ = 'https://github.com/stevenjgomez/nxs_analysis_tools'
@@ -50,6 +50,8 @@ class TempDependence:
50
50
  -------
51
51
  set_temperatures(temperatures):
52
52
  Set the list of temperatures for the datasets.
53
+ find_temperatures():
54
+ Set the list of temperatures by automatically scanning the sample directory.
53
55
  set_sample_directory(path):
54
56
  Set the directory path where the datasets are located.
55
57
  initialize():
@@ -105,7 +107,7 @@ class TempDependence:
105
107
  Initialize the TempDependence class with default values.
106
108
  """
107
109
 
108
- self.sample_directory = ''
110
+ self.sample_directory = None
109
111
  self.xlabel = ''
110
112
  self.datasets = {}
111
113
  self.temperatures = []
@@ -127,6 +129,31 @@ class TempDependence:
127
129
  """
128
130
  self.temperatures = temperatures
129
131
 
132
+ def find_temperatures(self):
133
+ """
134
+ Set the list of temperatures by automatically scanning the sample directory.
135
+ """
136
+
137
+ # Assert that self.sample_directory must exist
138
+ if self.sample_directory is None:
139
+ raise ValueError("Sample directory is not set. Use set_sample_directory(path) first.")
140
+
141
+ # Clear existing temperatures
142
+ self.temperatures = []
143
+
144
+ # Search for nxrefine .nxs files
145
+ for item in os.listdir(self.sample_directory):
146
+ pattern = r'_(\d+)\.nxs'
147
+ match = re.search(pattern, item)
148
+ if match:
149
+ # Identify temperature
150
+ temperature = match.group(1)
151
+ self.temperatures.append(temperature)
152
+ # Convert all temperatures to int temporarily to sort temperatures list
153
+ self.temperatures = [int(t) for t in self.temperatures]
154
+ self.temperatures.sort()
155
+ self.temperatures = [str(t) for t in self.temperatures]
156
+
130
157
  def set_sample_directory(self, path):
131
158
  """
132
159
  Set the directory path where the datasets are located.
@@ -172,6 +199,9 @@ class TempDependence:
172
199
  if temperatures_list:
173
200
  temperatures_list = [str(t) for t in temperatures_list]
174
201
 
202
+ # Clear existing temperatures before loading files
203
+ self.temperatures = []
204
+
175
205
  # Identify files to load
176
206
  items_to_load = []
177
207
  # Search for nxrefine .nxs files
@@ -179,7 +209,6 @@ class TempDependence:
179
209
  pattern = r'_(\d+)\.nxs'
180
210
  match = re.search(pattern, item)
181
211
  if match:
182
- print(f'Found {item}')
183
212
  # Identify temperature
184
213
  temperature = match.group(1)
185
214
  # print(f'Temperature = {temperature}')
@@ -188,7 +217,6 @@ class TempDependence:
188
217
  self.temperatures.append(temperature)
189
218
  items_to_load.append(item)
190
219
  # print(f'Preparing to load {temperature} K data: {item}')
191
-
192
220
  # Convert all temperatures to int temporarily to sort temperatures list before loading
193
221
  self.temperatures = [int(t) for t in self.temperatures]
194
222
 
@@ -238,9 +266,6 @@ class TempDependence:
238
266
  temperature_folders.sort() # Sort from low to high T
239
267
  temperature_folders = [str(i) for i in temperature_folders] # Convert to strings
240
268
 
241
- print('Found temperature folders:')
242
- [print('[' + str(i) + '] ' + folder) for i, folder in enumerate(temperature_folders)]
243
-
244
269
  self.temperatures = temperature_folders
245
270
 
246
271
  if temperatures_list is not None:
@@ -251,9 +276,6 @@ class TempDependence:
251
276
  for file in os.listdir(os.path.join(self.sample_directory, T)):
252
277
  if file.endswith(file_ending):
253
278
  filepath = os.path.join(self.sample_directory, T, file)
254
- print('-----------------------------------------------')
255
- print('Loading ' + T + ' K indexed .nxs files...')
256
- print('Found ' + filepath)
257
279
 
258
280
  # Load dataset at each temperature
259
281
  self.datasets[T] = load_data(filepath)
@@ -15,7 +15,7 @@ from scipy import ndimage
15
15
 
16
16
  # Specify items on which users are allowed to perform standalone imports
17
17
  __all__ = ['load_data', 'load_transform', 'plot_slice', 'Scissors',
18
- 'reciprocal_lattice_params', 'rotate_data',
18
+ 'reciprocal_lattice_params', 'rotate_data', 'rotate_data_2D'
19
19
  'array_to_nxdata', 'Padder']
20
20
 
21
21
 
@@ -853,7 +853,7 @@ def rotate_data(data, lattice_angle, rotation_angle, rotation_axis, printout=Fal
853
853
  (data[data.axes[0]], data[data.axes[1]], data[data.axes[2]]))
854
854
 
855
855
 
856
- def rotate_data2D(data, lattice_angle, rotation_angle):
856
+ def rotate_data_2D(data, lattice_angle, rotation_angle):
857
857
  """
858
858
  Rotates 2D data.
859
859
 
@@ -197,7 +197,7 @@ class Symmetrizer2D:
197
197
  # Scale and skew counts
198
198
  skew_angle_adj = 90 - self.skew_angle
199
199
 
200
- scale2 = counts.shape[0] / counts.shape[1]
200
+ scale2 = 1 # q1.max()/q2.max() # TODO: Need to double check this
201
201
  counts_unscaled2 = ndimage.affine_transform(counts,
202
202
  Affine2D().scale(scale2, 1).inverted().get_matrix()[:2, :2],
203
203
  offset=[-(1 - scale2) * counts.shape[
@@ -253,7 +253,7 @@ class Symmetrizer2D:
253
253
  order=0,
254
254
  )
255
255
 
256
- scale2 = counts.shape[0] / counts.shape[1]
256
+ scale2 = counts.shape[0]/counts.shape[1]
257
257
  wedge = ndimage.affine_transform(wedge,
258
258
  Affine2D().scale(scale2, 1).get_matrix()[:2, :2],
259
259
  offset=[(1 - scale2) * counts.shape[0] / 2, 0],
@@ -477,7 +477,7 @@ class Symmetrizer3D:
477
477
  q1, q2, q3 = self.q1, self.q2, self.q3
478
478
  out_array = np.zeros(data[data.signal].shape)
479
479
 
480
- if self.plane1symmetrizer.theta_max:
480
+ if self.plane1symmetrizer.theta_max is not None:
481
481
  print('Symmetrizing ' + self.plane1 + ' planes...')
482
482
  for k, value in enumerate(q3):
483
483
  print(f'Symmetrizing {q3.nxname}={value:.02f}...', end='\r')
@@ -485,7 +485,7 @@ class Symmetrizer3D:
485
485
  out_array[:, :, k] = data_symmetrized[data.signal].nxdata
486
486
  print('\nSymmetrized ' + self.plane1 + ' planes.')
487
487
 
488
- if self.plane2symmetrizer.theta_max:
488
+ if self.plane2symmetrizer.theta_max is not None:
489
489
  print('Symmetrizing ' + self.plane2 + ' planes...')
490
490
  for j, value in enumerate(q2):
491
491
  print(f'Symmetrizing {q2.nxname}={value:.02f}...', end='\r')
@@ -495,7 +495,7 @@ class Symmetrizer3D:
495
495
  out_array[:, j, :] = data_symmetrized[data.signal].nxdata
496
496
  print('\nSymmetrized ' + self.plane2 + ' planes.')
497
497
 
498
- if self.plane3symmetrizer.theta_max:
498
+ if self.plane3symmetrizer.theta_max is not None:
499
499
  print('Symmetrizing ' + self.plane3 + ' planes...')
500
500
  for i, value in enumerate(q1):
501
501
  print(f'Symmetrizing {q1.nxname}={value:.02f}...', end='\r')
@@ -536,7 +536,7 @@ class Symmetrizer3D:
536
536
  print("Output file saved to: " + os.path.join(os.getcwd(), fout_name))
537
537
 
538
538
 
539
- def generate_gaussian(H, K, L, amp, stddev, lattice_params, coeffs=None):
539
+ def generate_gaussian(H, K, L, amp, stddev, lattice_params, coeffs=None, center=None):
540
540
  """
541
541
  Generate a 3D Gaussian distribution.
542
542
 
@@ -558,16 +558,23 @@ def generate_gaussian(H, K, L, amp, stddev, lattice_params, coeffs=None):
558
558
  Coefficients for the Gaussian expression, including cross-terms between axes.
559
559
  Default is [1, 0, 1, 0, 1, 0],
560
560
  corresponding to (1*H**2 + 0*H*K + 1*K**2 + 0*K*L + 1*L**2 + 0*L*H).
561
+ center : tuple
562
+ Tuple of coordinates for the center of the Gaussian. Default is (0,0,0).
561
563
 
562
564
  Returns
563
565
  -------
564
566
  gaussian : ndarray
565
567
  3D Gaussian distribution array.
566
568
  """
569
+ if center is None:
570
+ center=(0,0,0)
567
571
  if coeffs is None:
568
572
  coeffs = [1, 0, 1, 0, 1, 0]
569
573
  a, b, c, al, be, ga = lattice_params
570
574
  a_, b_, c_, _, _, _ = reciprocal_lattice_params((a, b, c, al, be, ga))
575
+ H = H-center[0]
576
+ K = K-center[1]
577
+ L = L-center[2]
571
578
  H, K, L = np.meshgrid(H, K, L, indexing='ij')
572
579
  gaussian = amp * np.exp(-(coeffs[0] * H ** 2 +
573
580
  coeffs[1] * (b_ * a_ / (a_ ** 2)) * H * K +
@@ -581,7 +588,6 @@ def generate_gaussian(H, K, L, amp, stddev, lattice_params, coeffs=None):
581
588
  gaussian = gaussian.transpose()
582
589
  return gaussian.transpose(1, 0, 2)
583
590
 
584
-
585
591
  class Puncher:
586
592
  """
587
593
  A class for applying masks to 3D datasets, typically for data processing in reciprocal space.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: nxs-analysis-tools
3
- Version: 0.0.43
3
+ Version: 0.0.45
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
@@ -66,6 +66,7 @@ Requires-Dist: sphinx-autobuild>=2021.3.14; extra == "dev"
66
66
  Requires-Dist: sphinx-copybutton>=0.5.0; extra == "dev"
67
67
  Requires-Dist: sphinxext-opengraph>=0.6.3; extra == "dev"
68
68
  Requires-Dist: twine>=4.0.1; extra == "dev"
69
+ Dynamic: license-file
69
70
 
70
71
  # nxs-analysis-tools
71
72
 
@@ -21,4 +21,5 @@ tests/test_fitting.py
21
21
  tests/test_lmfit.py
22
22
  tests/test_mask_plotting.py
23
23
  tests/test_pairdistribution.py
24
- tests/test_plot_slice_with_ndarray.py
24
+ tests/test_plot_slice_with_ndarray.py
25
+ tests/test_symmetrizer_rectangular_plane.py
@@ -0,0 +1,382 @@
1
+ from nxs_analysis_tools import load_data, plot_slice
2
+ from nxs_analysis_tools.datareduction import Padder, array_to_nxdata
3
+ from nxs_analysis_tools.pairdistribution import Symmetrizer3D
4
+ import matplotlib.pyplot as plt
5
+
6
+ import time
7
+ import os
8
+ import gc
9
+ import math
10
+ from scipy import ndimage
11
+ import scipy
12
+ import matplotlib.pyplot as plt
13
+ from matplotlib.transforms import Affine2D
14
+ from nexusformat.nexus import nxsave, NXroot, NXentry, NXdata, NXfield
15
+ import numpy as np
16
+ from astropy.convolution import Kernel, convolve_fft
17
+ import pyfftw
18
+
19
+ data = load_data('../docs/source/examples/example_data/pairdistribution_data/test_hkli.nxs')
20
+
21
+
22
+ class Symmetrizer2D:
23
+ """
24
+ A class for symmetrizing 2D datasets.
25
+
26
+ The `Symmetrizer2D` class provides functionality to apply symmetry
27
+ operations such as rotation and mirroring to 2D datasets.
28
+
29
+ Attributes
30
+ ----------
31
+ mirror_axis : int or None
32
+ The axis along which mirroring is performed. Default is None, meaning
33
+ no mirroring is applied.
34
+ symmetrized : NXdata or None
35
+ The symmetrized dataset after applying the symmetrization operations.
36
+ Default is None until symmetrization is performed.
37
+ wedges : NXdata or None
38
+ The wedges extracted from the dataset based on the angular limits.
39
+ Default is None until symmetrization is performed.
40
+ rotations : int or None
41
+ The number of rotations needed to reconstruct the full dataset from
42
+ a single wedge. Default is None until parameters are set.
43
+ transform : Affine2D or None
44
+ The transformation matrix used for skewing and scaling the dataset.
45
+ Default is None until parameters are set.
46
+ mirror : bool or None
47
+ Indicates whether mirroring is performed during symmetrization.
48
+ Default is None until parameters are set.
49
+ skew_angle : float or None
50
+ The skew angle (in degrees) between the principal axes of the plane
51
+ to be symmetrized. Default is None until parameters are set.
52
+ theta_max : float or None
53
+ The maximum angle (in degrees) for symmetrization. Default is None
54
+ until parameters are set.
55
+ theta_min : float or None
56
+ The minimum angle (in degrees) for symmetrization. Default is None
57
+ until parameters are set.
58
+ wedge : NXdata or None
59
+ The dataset wedge used in the symmetrization process. Default is
60
+ None until symmetrization is performed.
61
+ symmetrization_mask : NXdata or None
62
+ The mask used for selecting the region of the dataset to be symmetrized.
63
+ Default is None until symmetrization is performed.
64
+
65
+ Methods
66
+ -------
67
+ __init__(**kwargs):
68
+ Initializes the Symmetrizer2D object and optionally sets the parameters
69
+ using `set_parameters`.
70
+ set_parameters(theta_min, theta_max, lattice_angle=90, mirror=True, mirror_axis=0):
71
+ Sets the parameters for the symmetrization operation, including angle limits,
72
+ lattice angle, and mirroring options.
73
+ symmetrize_2d(data):
74
+ Symmetrizes a 2D dataset based on the set parameters.
75
+ test(data, **kwargs):
76
+ Performs a test visualization of the symmetrization process, displaying the
77
+ original data, mask, wedge, and symmetrized result.
78
+ """
79
+ symmetrization_mask: NXdata
80
+
81
+ def __init__(self, **kwargs):
82
+ """
83
+ Initializes the Symmetrizer2D object.
84
+
85
+ Parameters
86
+ ----------
87
+ **kwargs : dict, optional
88
+ Keyword arguments that can be passed to the `set_parameters` method to
89
+ set the symmetrization parameters during initialization.
90
+ """
91
+ self.mirror_axis = None
92
+ self.symmetrized = None
93
+ self.wedges = None
94
+ self.rotations = None
95
+ self.transform = None
96
+ self.mirror = None
97
+ self.skew_angle = None
98
+ self.theta_max = None
99
+ self.theta_min = None
100
+ self.wedge = None
101
+ if kwargs:
102
+ self.set_parameters(**kwargs)
103
+
104
+ def set_parameters(self, theta_min, theta_max, lattice_angle=90, mirror=True, mirror_axis=0):
105
+ """
106
+ Sets the parameters for the symmetrization operation, and calculates the
107
+ required transformations and rotations.
108
+
109
+ Parameters
110
+ ----------
111
+ theta_min : float
112
+ The minimum angle in degrees for symmetrization.
113
+ theta_max : float
114
+ The maximum angle in degrees for symmetrization.
115
+ lattice_angle : float, optional
116
+ The angle in degrees between the two principal axes of the plane to be
117
+ symmetrized (default: 90).
118
+ mirror : bool, optional
119
+ If True, perform mirroring during symmetrization (default: True).
120
+ mirror_axis : int, optional
121
+ The axis along which to perform mirroring (default: 0).
122
+ """
123
+ self.theta_min = theta_min
124
+ self.theta_max = theta_max
125
+ self.skew_angle = lattice_angle
126
+ self.mirror = mirror
127
+ self.mirror_axis = mirror_axis
128
+
129
+ # Define Transformation
130
+ skew_angle_adj = 90 - lattice_angle
131
+ t = Affine2D()
132
+ # Scale y-axis to preserve norm while shearing
133
+ t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
134
+ # Shear along x-axis
135
+ t += Affine2D().skew_deg(skew_angle_adj, 0)
136
+ # Return to original y-axis scaling
137
+ t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
138
+ self.transform = t
139
+
140
+ # Calculate number of rotations needed to reconstruct the dataset
141
+ if mirror:
142
+ rotations = abs(int(360 / (theta_max - theta_min) / 2))
143
+ else:
144
+ rotations = abs(int(360 / (theta_max - theta_min)))
145
+ self.rotations = rotations
146
+
147
+ self.symmetrization_mask = None
148
+
149
+ self.wedges = None
150
+
151
+ self.symmetrized = None
152
+
153
+ def symmetrize_2d(self, data):
154
+ """
155
+ Symmetrizes a 2D dataset based on the set parameters, applying padding
156
+ to prevent rotation cutoff and handling overlapping pixels.
157
+
158
+ Parameters
159
+ ----------
160
+ data : NXdata
161
+ The input 2D dataset to be symmetrized.
162
+
163
+ Returns
164
+ -------
165
+ symmetrized : NXdata
166
+ The symmetrized 2D dataset.
167
+ """
168
+ theta_min = self.theta_min
169
+ theta_max = self.theta_max
170
+ mirror = self.mirror
171
+ mirror_axis = self.mirror_axis
172
+ t = self.transform
173
+ rotations = self.rotations
174
+
175
+ # Pad the dataset so that rotations don't get cutoff if they extend
176
+ # past the extent of the dataset
177
+ p = Padder(data)
178
+ padding = tuple(len(data[axis]) for axis in data.axes)
179
+ data_padded = p.pad(padding)
180
+
181
+ # Define axes that span the plane to be transformed
182
+ q1 = data_padded[data.axes[0]]
183
+ q2 = data_padded[data.axes[1]]
184
+
185
+ # Calculate the angle for each data point
186
+ theta = np.arctan2(q1.reshape((-1, 1)), q2.reshape((1, -1)))
187
+ # Create a boolean array for the range of angles
188
+ symmetrization_mask = np.logical_and(theta >= theta_min * np.pi / 180,
189
+ theta <= theta_max * np.pi / 180)
190
+
191
+ # Define signal to be transformed
192
+ counts = symmetrization_mask
193
+
194
+ # Scale and skew counts
195
+ skew_angle_adj = 90 - self.skew_angle
196
+
197
+ scale2 = 1 # q1.max()/q2.max()
198
+ counts_unscaled2 = ndimage.affine_transform(counts,
199
+ Affine2D().scale(scale2, 1).inverted().get_matrix()[:2, :2],
200
+ offset=[-(1 - scale2) * counts.shape[
201
+ 0] / 2 / scale2, 0],
202
+ order=0,
203
+ )
204
+
205
+ scale1 = np.cos(skew_angle_adj * np.pi / 180)
206
+ counts_unscaled1 = ndimage.affine_transform(counts_unscaled2,
207
+ Affine2D().scale(scale1, 1).inverted().get_matrix()[:2, :2],
208
+ offset=[-(1 - scale1) * counts.shape[
209
+ 0] / 2 / scale1, 0],
210
+ order=0,
211
+ )
212
+
213
+ mask = ndimage.affine_transform(counts_unscaled1,
214
+ t.get_matrix()[:2, :2],
215
+ offset=[-counts.shape[0] / 2
216
+ * np.sin(skew_angle_adj * np.pi / 180), 0],
217
+ order=0,
218
+ )
219
+
220
+ # Convert mask to nxdata
221
+ mask = array_to_nxdata(mask, data_padded)
222
+
223
+ # Save mask for user interaction
224
+ self.symmetrization_mask = p.unpad(mask)
225
+
226
+ # Perform masking
227
+ wedge = mask * data_padded
228
+
229
+ # Save wedge for user interaction
230
+ self.wedge = p.unpad(wedge)
231
+
232
+ # Convert wedge back to array for further transformations
233
+ wedge = wedge[data.signal].nxdata
234
+
235
+ # Define signal to be transformed
236
+ counts = wedge
237
+
238
+ # Scale and skew counts
239
+ skew_angle_adj = 90 - self.skew_angle
240
+ counts_skew = ndimage.affine_transform(counts,
241
+ t.inverted().get_matrix()[:2, :2],
242
+ offset=[counts.shape[0] / 2
243
+ * np.sin(skew_angle_adj * np.pi / 180), 0],
244
+ order=0,
245
+ )
246
+ scale1 = np.cos(skew_angle_adj * np.pi / 180)
247
+ wedge = ndimage.affine_transform(counts_skew,
248
+ Affine2D().scale(scale1, 1).get_matrix()[:2, :2],
249
+ offset=[(1 - scale1) * counts.shape[0] / 2, 0],
250
+ order=0,
251
+ )
252
+
253
+ scale2 = counts.shape[0]/counts.shape[1]
254
+ wedge = ndimage.affine_transform(wedge,
255
+ Affine2D().scale(scale2, 1).get_matrix()[:2, :2],
256
+ offset=[(1 - scale2) * counts.shape[0] / 2, 0],
257
+ order=0,
258
+ )
259
+
260
+ # Reconstruct full dataset from wedge
261
+ reconstructed = np.zeros(counts.shape)
262
+ for _ in range(0, rotations):
263
+ # The following are attempts to combine images with minimal overlapping pixels
264
+ reconstructed += wedge
265
+ # reconstructed = np.where(reconstructed == 0, reconstructed + wedge, reconstructed)
266
+
267
+ wedge = ndimage.rotate(wedge, 360 / rotations, reshape=False, order=0)
268
+
269
+ # self.rotated_only = NXdata(NXfield(reconstructed, name=data.signal),
270
+ # (q1, q2))
271
+
272
+ if mirror:
273
+ # The following are attempts to combine images with minimal overlapping pixels
274
+ reconstructed = np.where(reconstructed == 0,
275
+ reconstructed + np.flip(reconstructed, axis=mirror_axis),
276
+ reconstructed)
277
+ # reconstructed += np.flip(reconstructed, axis=0)
278
+
279
+ # self.rotated_and_mirrored = NXdata(NXfield(reconstructed, name=data.signal),
280
+ # (q1, q2))
281
+
282
+ reconstructed = ndimage.affine_transform(reconstructed,
283
+ Affine2D().scale(
284
+ scale2, 1
285
+ ).inverted().get_matrix()[:2, :2],
286
+ offset=[-(1 - scale2) * counts.shape[
287
+ 0] / 2 / scale2, 0],
288
+ order=0,
289
+ )
290
+ reconstructed = ndimage.affine_transform(reconstructed,
291
+ Affine2D().scale(
292
+ scale1, 1
293
+ ).inverted().get_matrix()[:2, :2],
294
+ offset=[-(1 - scale1) * counts.shape[
295
+ 0] / 2 / scale1, 0],
296
+ order=0,
297
+ )
298
+ reconstructed = ndimage.affine_transform(reconstructed,
299
+ t.get_matrix()[:2, :2],
300
+ offset=[(-counts.shape[0] / 2
301
+ * np.sin(skew_angle_adj * np.pi / 180)),
302
+ 0],
303
+ order=0,
304
+ )
305
+
306
+ reconstructed_unpadded = p.unpad(reconstructed)
307
+
308
+ # Fix any overlapping pixels by truncating counts to max
309
+ reconstructed_unpadded[reconstructed_unpadded > data[data.signal].nxdata.max()] \
310
+ = data[data.signal].nxdata.max()
311
+
312
+ symmetrized = NXdata(NXfield(reconstructed_unpadded, name=data.signal),
313
+ (data[data.axes[0]],
314
+ data[data.axes[1]]))
315
+
316
+ return symmetrized
317
+
318
+ def test(self, data, **kwargs):
319
+ """
320
+ Performs a test visualization of the symmetrization process to help assess
321
+ the effect of the parameters.
322
+
323
+ Parameters
324
+ ----------
325
+ data : ndarray
326
+ The input 2D dataset to be used for the test visualization.
327
+ **kwargs : dict
328
+ Additional keyword arguments to be passed to the plot_slice function.
329
+
330
+ Returns
331
+ -------
332
+ fig : Figure
333
+ The matplotlib Figure object that contains the test visualization plot.
334
+ axesarr : ndarray
335
+ The numpy array of Axes objects representing the subplots in the test
336
+ visualization.
337
+
338
+ Notes
339
+ -----
340
+ This method uses the `symmetrize_2d` method to perform the symmetrization on
341
+ the input data and visualize the process.
342
+
343
+ The test visualization plot includes the following subplots:
344
+ - Subplot 1: The original dataset.
345
+ - Subplot 2: The symmetrization mask.
346
+ - Subplot 3: The wedge slice used for reconstruction of the full symmetrized dataset.
347
+ - Subplot 4: The symmetrized dataset.
348
+
349
+ Example usage:
350
+ ```
351
+ s = Symmetrizer2D()
352
+ s.set_parameters(theta_min, theta_max, skew_angle, mirror)
353
+ s.test(data)
354
+ ```
355
+ """
356
+ s = self
357
+ symm_test = s.symmetrize_2d(data)
358
+ fig, axesarr = plt.subplots(2, 2, figsize=(10, 8))
359
+ axes = axesarr.reshape(-1)
360
+
361
+ # Plot the data
362
+ plot_slice(data, skew_angle=s.skew_angle, ax=axes[0], title='data', **kwargs)
363
+
364
+ # Filter kwargs to exclude 'vmin' and 'vmax'
365
+ filtered_kwargs = {key: value for key, value in kwargs.items() if key not in ('vmin', 'vmax')}
366
+ # Plot the mask
367
+ plot_slice(s.symmetrization_mask, skew_angle=s.skew_angle, ax=axes[1], title='mask', **filtered_kwargs)
368
+
369
+ # Plot the wedge
370
+ plot_slice(s.wedge, skew_angle=s.skew_angle, ax=axes[2], title='wedge', **kwargs)
371
+
372
+ # Plot the symmetrized data
373
+ plot_slice(symm_test, skew_angle=s.skew_angle, ax=axes[3], title='symmetrized', **kwargs)
374
+ plt.subplots_adjust(wspace=0.4)
375
+ plt.show()
376
+ return fig, axesarr
377
+
378
+
379
+ s2d = Symmetrizer2D(theta_min=0, theta_max=45)
380
+
381
+ symmetrized_data = s2d.test(data[:, :, 0.0])
382
+ plt.show()