nxs-analysis-tools 0.0.41__tar.gz → 0.0.43__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 (26) hide show
  1. {nxs_analysis_tools-0.0.41/src/nxs_analysis_tools.egg-info → nxs_analysis_tools-0.0.43}/PKG-INFO +2 -2
  2. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/pyproject.toml +1 -1
  3. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/src/_meta/__init__.py +1 -1
  4. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/src/nxs_analysis_tools/chess.py +13 -2
  5. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/src/nxs_analysis_tools/datareduction.py +9 -7
  6. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/src/nxs_analysis_tools/pairdistribution.py +58 -14
  7. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43/src/nxs_analysis_tools.egg-info}/PKG-INFO +2 -2
  8. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/src/nxs_analysis_tools.egg-info/SOURCES.txt +3 -1
  9. nxs_analysis_tools-0.0.43/tests/test_mask_plotting.py +388 -0
  10. nxs_analysis_tools-0.0.43/tests/test_plot_slice_with_ndarray.py +277 -0
  11. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/LICENSE +0 -0
  12. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/MANIFEST.in +0 -0
  13. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/README.md +0 -0
  14. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/setup.cfg +0 -0
  15. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/setup.py +0 -0
  16. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/src/nxs_analysis_tools/__init__.py +0 -0
  17. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/src/nxs_analysis_tools/fitting.py +0 -0
  18. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/src/nxs_analysis_tools.egg-info/dependency_links.txt +0 -0
  19. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/src/nxs_analysis_tools.egg-info/requires.txt +0 -0
  20. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/src/nxs_analysis_tools.egg-info/top_level.txt +0 -0
  21. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/tests/test_chess.py +0 -0
  22. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/tests/test_chess_fitting.py +0 -0
  23. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/tests/test_datareduction.py +0 -0
  24. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/tests/test_fitting.py +0 -0
  25. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/tests/test_lmfit.py +0 -0
  26. {nxs_analysis_tools-0.0.41 → nxs_analysis_tools-0.0.43}/tests/test_pairdistribution.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: nxs-analysis-tools
3
- Version: 0.0.41
3
+ Version: 0.0.43
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.41'
9
+ version = '0.0.43'
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.41'
9
+ __version__ = '0.0.43'
10
10
  __repo_url__ = 'https://github.com/stevenjgomez/nxs_analysis_tools'
@@ -8,6 +8,7 @@ import re
8
8
 
9
9
  import matplotlib.pyplot as plt
10
10
  import matplotlib as mpl
11
+ import pandas as pd
11
12
  import numpy as np
12
13
  from IPython.display import display, Markdown
13
14
  from nxs_analysis_tools import load_data, Scissors
@@ -190,14 +191,24 @@ class TempDependence:
190
191
 
191
192
  # Convert all temperatures to int temporarily to sort temperatures list before loading
192
193
  self.temperatures = [int(t) for t in self.temperatures]
193
- self.temperatures.sort()
194
+
195
+ loading_template = pd.DataFrame({'temperature': self.temperatures, 'filename': items_to_load})
196
+ loading_template = loading_template.sort_values(by='temperature')
197
+ self.temperatures = loading_template['temperature']
194
198
  self.temperatures = [str(t) for t in self.temperatures]
199
+ items_to_load = loading_template['filename'].to_list()
195
200
 
196
201
  for i, item in enumerate(items_to_load):
197
202
  path = os.path.join(self.sample_directory, item)
198
203
 
199
204
  # Save dataset
200
- self.datasets[self.temperatures[i]] = load_transform(path)
205
+ try:
206
+ self.datasets[self.temperatures[i]] = load_transform(path)
207
+ except Exception as e:
208
+ # Report temperature that was unable to load, then raise exception.
209
+ temp_failed = self.temperatures[i]
210
+ print(f"Failed to load data for temperature {temp_failed} K from file {item}. Error: {e}")
211
+ raise # Re-raise the exception
201
212
 
202
213
  # Initialize scissors object
203
214
  self.scissors[self.temperatures[i]] = Scissors()
@@ -185,14 +185,14 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None,
185
185
  """
186
186
  if isinstance(data, np.ndarray):
187
187
  if X is None:
188
- X = NXfield(np.linspace(0, data.shape[1], data.shape[1]), name='x')
188
+ X = NXfield(np.linspace(0, data.shape[0], data.shape[0]), name='x')
189
189
  if Y is None:
190
- Y = NXfield(np.linspace(0, data.shape[0], data.shape[0]), name='y')
190
+ Y = NXfield(np.linspace(0, data.shape[1], data.shape[1]), name='y')
191
191
  if transpose:
192
192
  X, Y = Y, X
193
193
  data = data.transpose()
194
194
  data = NXdata(NXfield(data, name='value'), (X, Y))
195
- data_arr = data
195
+ data_arr = data[data.signal].nxdata.transpose()
196
196
  elif isinstance(data, (NXdata, NXfield)):
197
197
  if X is None:
198
198
  X = data[data.axes[0]]
@@ -787,7 +787,8 @@ def rotate_data(data, lattice_angle, rotation_angle, rotation_axis, printout=Fal
787
787
 
788
788
  p = Padder(sliced_data)
789
789
  padding = tuple(len(sliced_data[axis]) for axis in sliced_data.axes)
790
- counts = p.pad(padding).counts
790
+ counts = p.pad(padding)
791
+ counts = p.padded[p.padded.signal]
791
792
 
792
793
  counts_skewed = ndimage.affine_transform(counts,
793
794
  t.inverted().get_matrix()[:2, :2],
@@ -848,7 +849,7 @@ def rotate_data(data, lattice_angle, rotation_angle, rotation_axis, printout=Fal
848
849
  elif rotation_axis == 2:
849
850
  output_array[:, :, i] = counts_unpadded
850
851
  print('\nDone.')
851
- return NXdata(NXfield(output_array, name='counts'),
852
+ return NXdata(NXfield(output_array, name=p.padded.signal),
852
853
  (data[data.axes[0]], data[data.axes[1]], data[data.axes[2]]))
853
854
 
854
855
 
@@ -884,7 +885,8 @@ def rotate_data2D(data, lattice_angle, rotation_angle):
884
885
 
885
886
  p = Padder(data)
886
887
  padding = tuple(len(data[axis]) for axis in data.axes)
887
- counts = p.pad(padding).counts
888
+ counts = p.pad(padding)
889
+ counts = p.padded[p.padded.signal]
888
890
 
889
891
  counts_skewed = ndimage.affine_transform(counts,
890
892
  t.inverted().get_matrix()[:2, :2],
@@ -937,7 +939,7 @@ def rotate_data2D(data, lattice_angle, rotation_angle):
937
939
  counts_unpadded = p.unpad(counts_unskewed)
938
940
 
939
941
  print('\nDone.')
940
- return NXdata(NXfield(counts_unpadded, name='counts'),
942
+ return NXdata(NXfield(counts_unpadded, name=p.padded.signal),
941
943
  (data[data.axes[0]], data[data.axes[1]]))
942
944
 
943
945
 
@@ -185,23 +185,58 @@ class Symmetrizer2D:
185
185
  q1 = data_padded[data.axes[0]]
186
186
  q2 = data_padded[data.axes[1]]
187
187
 
188
- # Define signal to be symmetrized
189
- counts = data_padded[data.signal].nxdata
190
-
191
188
  # Calculate the angle for each data point
192
189
  theta = np.arctan2(q1.reshape((-1, 1)), q2.reshape((1, -1)))
193
190
  # Create a boolean array for the range of angles
194
191
  symmetrization_mask = np.logical_and(theta >= theta_min * np.pi / 180,
195
192
  theta <= theta_max * np.pi / 180)
196
- self.symmetrization_mask = NXdata(NXfield(p.unpad(symmetrization_mask),
197
- name='mask'),
198
- (data[data.axes[0]], data[data.axes[1]])
199
- )
200
193
 
201
- self.wedge = NXdata(NXfield(p.unpad(counts * symmetrization_mask),
202
- name=data.signal),
203
- (data[data.axes[0]], data[data.axes[1]])
204
- )
194
+ # Define signal to be transformed
195
+ counts = symmetrization_mask
196
+
197
+ # Scale and skew counts
198
+ skew_angle_adj = 90 - self.skew_angle
199
+
200
+ scale2 = counts.shape[0] / counts.shape[1]
201
+ counts_unscaled2 = ndimage.affine_transform(counts,
202
+ Affine2D().scale(scale2, 1).inverted().get_matrix()[:2, :2],
203
+ offset=[-(1 - scale2) * counts.shape[
204
+ 0] / 2 / scale2, 0],
205
+ order=0,
206
+ )
207
+
208
+ scale1 = np.cos(skew_angle_adj * np.pi / 180)
209
+ counts_unscaled1 = ndimage.affine_transform(counts_unscaled2,
210
+ Affine2D().scale(scale1, 1).inverted().get_matrix()[:2, :2],
211
+ offset=[-(1 - scale1) * counts.shape[
212
+ 0] / 2 / scale1, 0],
213
+ order=0,
214
+ )
215
+
216
+ mask = ndimage.affine_transform(counts_unscaled1,
217
+ t.get_matrix()[:2, :2],
218
+ offset=[-counts.shape[0] / 2
219
+ * np.sin(skew_angle_adj * np.pi / 180), 0],
220
+ order=0,
221
+ )
222
+
223
+ # Convert mask to nxdata
224
+ mask = array_to_nxdata(mask, data_padded)
225
+
226
+ # Save mask for user interaction
227
+ self.symmetrization_mask = p.unpad(mask)
228
+
229
+ # Perform masking
230
+ wedge = mask * data_padded
231
+
232
+ # Save wedge for user interaction
233
+ self.wedge = p.unpad(wedge)
234
+
235
+ # Convert wedge back to array for further transformations
236
+ wedge = wedge[data.signal].nxdata
237
+
238
+ # Define signal to be transformed
239
+ counts = wedge
205
240
 
206
241
  # Scale and skew counts
207
242
  skew_angle_adj = 90 - self.skew_angle
@@ -216,7 +251,7 @@ class Symmetrizer2D:
216
251
  Affine2D().scale(scale1, 1).get_matrix()[:2, :2],
217
252
  offset=[(1 - scale1) * counts.shape[0] / 2, 0],
218
253
  order=0,
219
- ) * symmetrization_mask
254
+ )
220
255
 
221
256
  scale2 = counts.shape[0] / counts.shape[1]
222
257
  wedge = ndimage.affine_transform(wedge,
@@ -325,15 +360,24 @@ class Symmetrizer2D:
325
360
  symm_test = s.symmetrize_2d(data)
326
361
  fig, axesarr = plt.subplots(2, 2, figsize=(10, 8))
327
362
  axes = axesarr.reshape(-1)
363
+
364
+ # Plot the data
328
365
  plot_slice(data, skew_angle=s.skew_angle, ax=axes[0], title='data', **kwargs)
329
- plot_slice(s.symmetrization_mask, skew_angle=s.skew_angle, ax=axes[1], title='mask')
366
+
367
+ # Filter kwargs to exclude 'vmin' and 'vmax'
368
+ filtered_kwargs = {key: value for key, value in kwargs.items() if key not in ('vmin', 'vmax')}
369
+ # Plot the mask
370
+ plot_slice(s.symmetrization_mask, skew_angle=s.skew_angle, ax=axes[1], title='mask', **filtered_kwargs)
371
+
372
+ # Plot the wedge
330
373
  plot_slice(s.wedge, skew_angle=s.skew_angle, ax=axes[2], title='wedge', **kwargs)
374
+
375
+ # Plot the symmetrized data
331
376
  plot_slice(symm_test, skew_angle=s.skew_angle, ax=axes[3], title='symmetrized', **kwargs)
332
377
  plt.subplots_adjust(wspace=0.4)
333
378
  plt.show()
334
379
  return fig, axesarr
335
380
 
336
-
337
381
  class Symmetrizer3D:
338
382
  """
339
383
  A class to symmetrize 3D datasets by performing sequential 2D symmetrization on
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: nxs-analysis-tools
3
- Version: 0.0.41
3
+ Version: 0.0.43
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
@@ -19,4 +19,6 @@ tests/test_chess_fitting.py
19
19
  tests/test_datareduction.py
20
20
  tests/test_fitting.py
21
21
  tests/test_lmfit.py
22
- tests/test_pairdistribution.py
22
+ tests/test_mask_plotting.py
23
+ tests/test_pairdistribution.py
24
+ tests/test_plot_slice_with_ndarray.py
@@ -0,0 +1,388 @@
1
+ import time
2
+ import os
3
+ import gc
4
+ import math
5
+ from scipy import ndimage
6
+ import scipy
7
+ import matplotlib.pyplot as plt
8
+ from matplotlib.transforms import Affine2D
9
+ from nexusformat.nexus import nxsave, NXroot, NXentry, NXdata, NXfield
10
+ import numpy as np
11
+ from astropy.convolution import Kernel, convolve_fft
12
+ import pyfftw
13
+ from nxs_analysis_tools import *
14
+ from nxs_analysis_tools.datareduction import Padder, array_to_nxdata
15
+
16
+ """
17
+ Tools for generating single crystal pair distribution functions.
18
+ """
19
+ import time
20
+ import os
21
+ import gc
22
+ import math
23
+ from scipy import ndimage
24
+ import scipy
25
+ import matplotlib.pyplot as plt
26
+ from matplotlib.transforms import Affine2D
27
+ from nexusformat.nexus import nxsave, NXroot, NXentry, NXdata, NXfield
28
+ import numpy as np
29
+ from astropy.convolution import Kernel, convolve_fft
30
+ import pyfftw
31
+ # from .datareduction import plot_slice, reciprocal_lattice_params, Padder, \
32
+ # array_to_nxdata
33
+
34
+ # __all__ = ['Symmetrizer2D', 'Symmetrizer3D', 'Puncher', 'Interpolator',
35
+ # 'fourier_transform_nxdata', 'Gaussian3DKernel', 'DeltaPDF',
36
+ # 'generate_gaussian'
37
+ # ]
38
+
39
+
40
+ class Symmetrizer2D:
41
+ """
42
+ A class for symmetrizing 2D datasets.
43
+
44
+ The `Symmetrizer2D` class provides functionality to apply symmetry
45
+ operations such as rotation and mirroring to 2D datasets.
46
+
47
+ Attributes
48
+ ----------
49
+ mirror_axis : int or None
50
+ The axis along which mirroring is performed. Default is None, meaning
51
+ no mirroring is applied.
52
+ symmetrized : NXdata or None
53
+ The symmetrized dataset after applying the symmetrization operations.
54
+ Default is None until symmetrization is performed.
55
+ wedges : NXdata or None
56
+ The wedges extracted from the dataset based on the angular limits.
57
+ Default is None until symmetrization is performed.
58
+ rotations : int or None
59
+ The number of rotations needed to reconstruct the full dataset from
60
+ a single wedge. Default is None until parameters are set.
61
+ transform : Affine2D or None
62
+ The transformation matrix used for skewing and scaling the dataset.
63
+ Default is None until parameters are set.
64
+ mirror : bool or None
65
+ Indicates whether mirroring is performed during symmetrization.
66
+ Default is None until parameters are set.
67
+ skew_angle : float or None
68
+ The skew angle (in degrees) between the principal axes of the plane
69
+ to be symmetrized. Default is None until parameters are set.
70
+ theta_max : float or None
71
+ The maximum angle (in degrees) for symmetrization. Default is None
72
+ until parameters are set.
73
+ theta_min : float or None
74
+ The minimum angle (in degrees) for symmetrization. Default is None
75
+ until parameters are set.
76
+ wedge : NXdata or None
77
+ The dataset wedge used in the symmetrization process. Default is
78
+ None until symmetrization is performed.
79
+ symmetrization_mask : NXdata or None
80
+ The mask used for selecting the region of the dataset to be symmetrized.
81
+ Default is None until symmetrization is performed.
82
+
83
+ Methods
84
+ -------
85
+ __init__(**kwargs):
86
+ Initializes the Symmetrizer2D object and optionally sets the parameters
87
+ using `set_parameters`.
88
+ set_parameters(theta_min, theta_max, lattice_angle=90, mirror=True, mirror_axis=0):
89
+ Sets the parameters for the symmetrization operation, including angle limits,
90
+ lattice angle, and mirroring options.
91
+ symmetrize_2d(data):
92
+ Symmetrizes a 2D dataset based on the set parameters.
93
+ test(data, **kwargs):
94
+ Performs a test visualization of the symmetrization process, displaying the
95
+ original data, mask, wedge, and symmetrized result.
96
+ """
97
+ symmetrization_mask: NXdata
98
+
99
+ def __init__(self, **kwargs):
100
+ """
101
+ Initializes the Symmetrizer2D object.
102
+
103
+ Parameters
104
+ ----------
105
+ **kwargs : dict, optional
106
+ Keyword arguments that can be passed to the `set_parameters` method to
107
+ set the symmetrization parameters during initialization.
108
+ """
109
+ self.mirror_axis = None
110
+ self.symmetrized = None
111
+ self.wedges = None
112
+ self.rotations = None
113
+ self.transform = None
114
+ self.mirror = None
115
+ self.skew_angle = None
116
+ self.theta_max = None
117
+ self.theta_min = None
118
+ self.wedge = None
119
+ if kwargs:
120
+ self.set_parameters(**kwargs)
121
+
122
+ def set_parameters(self, theta_min, theta_max, lattice_angle=90, mirror=True, mirror_axis=0):
123
+ """
124
+ Sets the parameters for the symmetrization operation, and calculates the
125
+ required transformations and rotations.
126
+
127
+ Parameters
128
+ ----------
129
+ theta_min : float
130
+ The minimum angle in degrees for symmetrization.
131
+ theta_max : float
132
+ The maximum angle in degrees for symmetrization.
133
+ lattice_angle : float, optional
134
+ The angle in degrees between the two principal axes of the plane to be
135
+ symmetrized (default: 90).
136
+ mirror : bool, optional
137
+ If True, perform mirroring during symmetrization (default: True).
138
+ mirror_axis : int, optional
139
+ The axis along which to perform mirroring (default: 0).
140
+ """
141
+ self.theta_min = theta_min
142
+ self.theta_max = theta_max
143
+ self.skew_angle = lattice_angle
144
+ self.mirror = mirror
145
+ self.mirror_axis = mirror_axis
146
+
147
+ # Define Transformation
148
+ skew_angle_adj = 90 - lattice_angle
149
+ t = Affine2D()
150
+ # Scale y-axis to preserve norm while shearing
151
+ t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
152
+ # Shear along x-axis
153
+ t += Affine2D().skew_deg(skew_angle_adj, 0)
154
+ # Return to original y-axis scaling
155
+ t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
156
+ self.transform = t
157
+
158
+ # Calculate number of rotations needed to reconstruct the dataset
159
+ if mirror:
160
+ rotations = abs(int(360 / (theta_max - theta_min) / 2))
161
+ else:
162
+ rotations = abs(int(360 / (theta_max - theta_min)))
163
+ self.rotations = rotations
164
+
165
+ self.symmetrization_mask = None
166
+
167
+ self.wedges = None
168
+
169
+ self.symmetrized = None
170
+
171
+ def symmetrize_2d(self, data):
172
+ """
173
+ Symmetrizes a 2D dataset based on the set parameters, applying padding
174
+ to prevent rotation cutoff and handling overlapping pixels.
175
+
176
+ Parameters
177
+ ----------
178
+ data : NXdata
179
+ The input 2D dataset to be symmetrized.
180
+
181
+ Returns
182
+ -------
183
+ symmetrized : NXdata
184
+ The symmetrized 2D dataset.
185
+ """
186
+ theta_min = self.theta_min
187
+ theta_max = self.theta_max
188
+ mirror = self.mirror
189
+ mirror_axis = self.mirror_axis
190
+ t = self.transform
191
+ rotations = self.rotations
192
+
193
+ # Pad the dataset so that rotations don't get cutoff if they extend
194
+ # past the extent of the dataset
195
+ p = Padder(data)
196
+ padding = tuple(len(data[axis]) for axis in data.axes)
197
+ data_padded = p.pad(padding)
198
+
199
+ # Define axes that span the plane to be transformed
200
+ q1 = data_padded[data.axes[0]]
201
+ q2 = data_padded[data.axes[1]]
202
+
203
+ # Calculate the angle for each data point
204
+ theta = np.arctan2(q1.reshape((-1, 1)), q2.reshape((1, -1)))
205
+ # Create a boolean array for the range of angles
206
+ symmetrization_mask = np.logical_and(theta >= theta_min * np.pi / 180,
207
+ theta <= theta_max * np.pi / 180)
208
+
209
+ # Define signal to be transformed
210
+ counts = symmetrization_mask
211
+
212
+ # Scale and skew counts
213
+ skew_angle_adj = 90 - self.skew_angle
214
+ counts_skew = ndimage.affine_transform(counts,
215
+ t.get_matrix()[:2, :2],
216
+ offset=[-counts.shape[0] / 2
217
+ * np.sin(skew_angle_adj * np.pi / 180), 0],
218
+ order=0,
219
+ )
220
+ scale1 = np.cos(skew_angle_adj * np.pi / 180)
221
+ mask = ndimage.affine_transform(counts_skew,
222
+ Affine2D().scale(scale1, 1).inverted().get_matrix()[:2, :2],
223
+ offset=[-(1 - scale1) * counts.shape[0] / 2, 0],
224
+ order=0,
225
+ )
226
+
227
+ scale2 = counts.shape[0] / counts.shape[1]
228
+ mask = ndimage.affine_transform(mask,
229
+ Affine2D().scale(scale2, 1).inverted().get_matrix()[:2, :2],
230
+ offset=[-(1 - scale2) * counts.shape[0] / 2, 0],
231
+ order=0,
232
+ )
233
+
234
+ # Convert mask to nxdata
235
+ mask = array_to_nxdata(mask, data_padded)
236
+
237
+ # Save mask for user interaction
238
+ self.symmetrization_mask = p.unpad(mask)
239
+
240
+ # Perform masking
241
+ wedge = mask*data_padded
242
+
243
+ # Save wedge for user interaction
244
+ self.wedge = p.unpad(wedge)
245
+
246
+ # Convert wedge back to array for further transformations
247
+ wedge = wedge[data.signal].nxdata
248
+
249
+ # Define signal to be transformed
250
+ counts = wedge
251
+
252
+ # Scale and skew counts
253
+ skew_angle_adj = 90 - self.skew_angle
254
+ counts_skew = ndimage.affine_transform(counts,
255
+ t.inverted().get_matrix()[:2, :2],
256
+ offset=[counts.shape[0] / 2
257
+ * np.sin(skew_angle_adj * np.pi / 180), 0],
258
+ order=0,
259
+ )
260
+ scale1 = np.cos(skew_angle_adj * np.pi / 180)
261
+ wedge = ndimage.affine_transform(counts_skew,
262
+ Affine2D().scale(scale1, 1).get_matrix()[:2, :2],
263
+ offset=[(1 - scale1) * counts.shape[0] / 2, 0],
264
+ order=0,
265
+ )
266
+
267
+ scale2 = counts.shape[0] / counts.shape[1]
268
+ wedge = ndimage.affine_transform(wedge,
269
+ Affine2D().scale(scale2, 1).get_matrix()[:2, :2],
270
+ offset=[(1 - scale2) * counts.shape[0] / 2, 0],
271
+ order=0,
272
+ )
273
+
274
+ # Reconstruct full dataset from wedge
275
+ reconstructed = np.zeros(counts.shape)
276
+ for _ in range(0, rotations):
277
+ # The following are attempts to combine images with minimal overlapping pixels
278
+ reconstructed += wedge
279
+ # reconstructed = np.where(reconstructed == 0, reconstructed + wedge, reconstructed)
280
+
281
+ wedge = ndimage.rotate(wedge, 360 / rotations, reshape=False, order=0)
282
+
283
+ # self.rotated_only = NXdata(NXfield(reconstructed, name=data.signal),
284
+ # (q1, q2))
285
+
286
+ if mirror:
287
+ # The following are attempts to combine images with minimal overlapping pixels
288
+ reconstructed = np.where(reconstructed == 0,
289
+ reconstructed + np.flip(reconstructed, axis=mirror_axis),
290
+ reconstructed)
291
+ # reconstructed += np.flip(reconstructed, axis=0)
292
+
293
+ # self.rotated_and_mirrored = NXdata(NXfield(reconstructed, name=data.signal),
294
+ # (q1, q2))
295
+
296
+ reconstructed = ndimage.affine_transform(reconstructed,
297
+ Affine2D().scale(
298
+ scale2, 1
299
+ ).inverted().get_matrix()[:2, :2],
300
+ offset=[-(1 - scale2) * counts.shape[
301
+ 0] / 2 / scale2, 0],
302
+ order=0,
303
+ )
304
+ reconstructed = ndimage.affine_transform(reconstructed,
305
+ Affine2D().scale(
306
+ scale1, 1
307
+ ).inverted().get_matrix()[:2, :2],
308
+ offset=[-(1 - scale1) * counts.shape[
309
+ 0] / 2 / scale1, 0],
310
+ order=0,
311
+ )
312
+ reconstructed = ndimage.affine_transform(reconstructed,
313
+ t.get_matrix()[:2, :2],
314
+ offset=[(-counts.shape[0] / 2
315
+ * np.sin(skew_angle_adj * np.pi / 180)),
316
+ 0],
317
+ order=0,
318
+ )
319
+
320
+ reconstructed_unpadded = p.unpad(reconstructed)
321
+
322
+ # Fix any overlapping pixels by truncating counts to max
323
+ reconstructed_unpadded[reconstructed_unpadded > data[data.signal].nxdata.max()] \
324
+ = data[data.signal].nxdata.max()
325
+
326
+ symmetrized = NXdata(NXfield(reconstructed_unpadded, name=data.signal),
327
+ (data[data.axes[0]],
328
+ data[data.axes[1]]))
329
+
330
+ return symmetrized
331
+
332
+ def test(self, data, **kwargs):
333
+ """
334
+ Performs a test visualization of the symmetrization process to help assess
335
+ the effect of the parameters.
336
+
337
+ Parameters
338
+ ----------
339
+ data : ndarray
340
+ The input 2D dataset to be used for the test visualization.
341
+ **kwargs : dict
342
+ Additional keyword arguments to be passed to the plot_slice function.
343
+
344
+ Returns
345
+ -------
346
+ fig : Figure
347
+ The matplotlib Figure object that contains the test visualization plot.
348
+ axesarr : ndarray
349
+ The numpy array of Axes objects representing the subplots in the test
350
+ visualization.
351
+
352
+ Notes
353
+ -----
354
+ This method uses the `symmetrize_2d` method to perform the symmetrization on
355
+ the input data and visualize the process.
356
+
357
+ The test visualization plot includes the following subplots:
358
+ - Subplot 1: The original dataset.
359
+ - Subplot 2: The symmetrization mask.
360
+ - Subplot 3: The wedge slice used for reconstruction of the full symmetrized dataset.
361
+ - Subplot 4: The symmetrized dataset.
362
+
363
+ Example usage:
364
+ ```
365
+ s = Symmetrizer2D()
366
+ s.set_parameters(theta_min, theta_max, skew_angle, mirror)
367
+ s.test(data)
368
+ ```
369
+ """
370
+ s = self
371
+ symm_test = s.symmetrize_2d(data)
372
+ fig, axesarr = plt.subplots(2, 2, figsize=(10, 8))
373
+ axes = axesarr.reshape(-1)
374
+ plot_slice(data, skew_angle=s.skew_angle, ax=axes[0], title='data', **kwargs)
375
+ plot_slice(s.symmetrization_mask, skew_angle=s.skew_angle, ax=axes[1], title='mask')
376
+ plot_slice(s.wedge, skew_angle=s.skew_angle, ax=axes[2], title='wedge', **kwargs)
377
+ plot_slice(symm_test, skew_angle=s.skew_angle, ax=axes[3], title='symmetrized', **kwargs)
378
+ plt.subplots_adjust(wspace=0.4)
379
+ plt.show()
380
+ return fig, axesarr
381
+
382
+
383
+ data = load_transform(r'K:\wilson-3947-a\nxrefine\LaCd3P3\MLA1\LaCd3P3_300.nxs')[:,:,7.9:8.1]
384
+ data = rotate_data(data, lattice_angle=60, rotation_angle=120, rotation_axis=2, printout=True)
385
+
386
+ # from nxs_analysis_tools.pairdistribution import Symmetrizer2D
387
+ s2d = Symmetrizer2D(theta_min=-90, theta_max=-90+60, mirror=True, lattice_angle=60, mirror_axis=1)
388
+ s2d.test(data[:,:,8.0], vmin=0, vmax=100, xlim=(-3,2))
@@ -0,0 +1,277 @@
1
+ from nxs_analysis_tools import *
2
+ from nxs_analysis_tools.datareduction import load_transform
3
+
4
+ import os
5
+ import numpy as np
6
+ import matplotlib.pyplot as plt
7
+ from matplotlib.transforms import Affine2D
8
+ from matplotlib.markers import MarkerStyle
9
+ from matplotlib.ticker import MultipleLocator
10
+ from matplotlib import colors
11
+ from matplotlib import patches
12
+ from IPython.display import display, Markdown
13
+ from nexusformat.nexus import NXfield, NXdata, nxload, NeXusError, NXroot, NXentry, nxsave
14
+ from scipy import ndimage
15
+
16
+
17
+ # def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None,
18
+ # skew_angle=90, ax=None, xlim=None, ylim=None,
19
+ # xticks=None, yticks=None, cbar=True, logscale=False,
20
+ # symlogscale=False, cmap='viridis', linthresh=1,
21
+ # title=None, mdheading=None, cbartitle=None,
22
+ # **kwargs):
23
+ # """
24
+ # Plot a 2D slice of the provided dataset, with optional transformations
25
+ # and customizations.
26
+ #
27
+ # Parameters
28
+ # ----------
29
+ # data : :class:`nexusformat.nexus.NXdata` or ndarray
30
+ # The dataset to plot. Can be an `NXdata` object or a `numpy` array.
31
+ #
32
+ # X : NXfield, optional
33
+ # The X axis values. If None, a default range from 0 to the number of
34
+ # columns in `data` is used.
35
+ #
36
+ # Y : NXfield, optional
37
+ # The Y axis values. If None, a default range from 0 to the number of
38
+ # rows in `data` is used.
39
+ #
40
+ # transpose : bool, optional
41
+ # If True, transpose the dataset and its axes before plotting.
42
+ # Default is False.
43
+ #
44
+ # vmin : float, optional
45
+ # The minimum value for the color scale. If not provided, the minimum
46
+ # value of the dataset is used.
47
+ #
48
+ # vmax : float, optional
49
+ # The maximum value for the color scale. If not provided, the maximum
50
+ # value of the dataset is used.
51
+ #
52
+ # skew_angle : float, optional
53
+ # The angle in degrees to shear the plot. Default is 90 degrees (no skew).
54
+ #
55
+ # ax : matplotlib.axes.Axes, optional
56
+ # The `matplotlib` axis to plot on. If None, a new figure and axis will
57
+ # be created.
58
+ #
59
+ # xlim : tuple, optional
60
+ # The limits for the x-axis. If None, the limits are set automatically
61
+ # based on the data.
62
+ #
63
+ # ylim : tuple, optional
64
+ # The limits for the y-axis. If None, the limits are set automatically
65
+ # based on the data.
66
+ #
67
+ # xticks : float or list of float, optional
68
+ # The major tick interval or specific tick locations for the x-axis.
69
+ # Default is to use a minor tick interval of 1.
70
+ #
71
+ # yticks : float or list of float, optional
72
+ # The major tick interval or specific tick locations for the y-axis.
73
+ # Default is to use a minor tick interval of 1.
74
+ #
75
+ # cbar : bool, optional
76
+ # Whether to include a colorbar. Default is True.
77
+ #
78
+ # logscale : bool, optional
79
+ # Whether to use a logarithmic color scale. Default is False.
80
+ #
81
+ # symlogscale : bool, optional
82
+ # Whether to use a symmetrical logarithmic color scale. Default is False.
83
+ #
84
+ # cmap : str or Colormap, optional
85
+ # The colormap to use for the plot. Default is 'viridis'.
86
+ #
87
+ # linthresh : float, optional
88
+ # The linear threshold for symmetrical logarithmic scaling. Default is 1.
89
+ #
90
+ # title : str, optional
91
+ # The title for the plot. If None, no title is set.
92
+ #
93
+ # mdheading : str, optional
94
+ # A Markdown heading to display above the plot. If 'None' or not provided,
95
+ # no heading is displayed.
96
+ #
97
+ # cbartitle : str, optional
98
+ # The title for the colorbar. If None, the colorbar label will be set to
99
+ # the name of the signal.
100
+ #
101
+ # **kwargs
102
+ # Additional keyword arguments passed to `pcolormesh`.
103
+ #
104
+ # Returns
105
+ # -------
106
+ # p : :class:`matplotlib.collections.QuadMesh`
107
+ # The `matplotlib` QuadMesh object representing the plotted data.
108
+ # """
109
+ # if isinstance(data, np.ndarray):
110
+ # if X is None:
111
+ # X = NXfield(np.linspace(0, data.shape[1], data.shape[1]), name='x')
112
+ # if Y is None:
113
+ # Y = NXfield(np.linspace(0, data.shape[0], data.shape[0]), name='y')
114
+ # if transpose:
115
+ # X, Y = Y, X
116
+ # data = data.transpose()
117
+ # data = NXdata(NXfield(data, name='value'), (X, Y))
118
+ # data_arr = data
119
+ # elif isinstance(data, (NXdata, NXfield)):
120
+ # if X is None:
121
+ # X = data[data.axes[0]]
122
+ # if Y is None:
123
+ # Y = data[data.axes[1]]
124
+ # if transpose:
125
+ # X, Y = Y, X
126
+ # data = data.transpose()
127
+ # data_arr = data[data.signal].nxdata.transpose()
128
+ # else:
129
+ # raise TypeError(f"Unexpected data type: {type(data)}. "
130
+ # f"Supported types are np.ndarray and NXdata.")
131
+ #
132
+ # # Display Markdown heading
133
+ # if mdheading is None:
134
+ # pass
135
+ # elif mdheading == "None":
136
+ # display(Markdown('### Figure'))
137
+ # else:
138
+ # display(Markdown('### Figure - ' + mdheading))
139
+ #
140
+ # # Inherit axes if user provides some
141
+ # if ax is not None:
142
+ # fig = ax.get_figure()
143
+ # # Otherwise set up some default axes
144
+ # else:
145
+ # fig = plt.figure()
146
+ # ax = fig.add_axes([0, 0, 1, 1])
147
+ #
148
+ # # If limits not provided, use extrema
149
+ # if vmin is None:
150
+ # vmin = data_arr.min()
151
+ # if vmax is None:
152
+ # vmax = data_arr.max()
153
+ #
154
+ # # Set norm (linear scale, logscale, or symlogscale)
155
+ # norm = colors.Normalize(vmin=vmin, vmax=vmax) # Default: linear scale
156
+ #
157
+ # if symlogscale:
158
+ # norm = colors.SymLogNorm(linthresh=linthresh, vmin=-1 * vmax, vmax=vmax)
159
+ # elif logscale:
160
+ # norm = colors.LogNorm(vmin=vmin, vmax=vmax)
161
+ #
162
+ # # Plot data
163
+ # p = ax.pcolormesh(X.nxdata, Y.nxdata, data_arr, shading='auto', norm=norm, cmap=cmap, **kwargs)
164
+ #
165
+ # ## Transform data to new coordinate system if necessary
166
+ # # Correct skew angle
167
+ # skew_angle_adj = 90 - skew_angle
168
+ # # Create blank 2D affine transformation
169
+ # t = Affine2D()
170
+ # # Scale y-axis to preserve norm while shearing
171
+ # t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
172
+ # # Shear along x-axis
173
+ # t += Affine2D().skew_deg(skew_angle_adj, 0)
174
+ # # Return to original y-axis scaling
175
+ # t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
176
+ # ## Correct for x-displacement after shearing
177
+ # # If ylims provided, use those
178
+ # if ylim is not None:
179
+ # # Set ylims
180
+ # ax.set(ylim=ylim)
181
+ # ymin, ymax = ylim
182
+ # # Else, use current ylims
183
+ # else:
184
+ # ymin, ymax = ax.get_ylim()
185
+ # # Use ylims to calculate translation (necessary to display axes in correct position)
186
+ # p.set_transform(t
187
+ # + Affine2D().translate(-ymin * np.sin(skew_angle_adj * np.pi / 180), 0)
188
+ # + ax.transData)
189
+ #
190
+ # # Set x limits
191
+ # if xlim is not None:
192
+ # xmin, xmax = xlim
193
+ # else:
194
+ # xmin, xmax = ax.get_xlim()
195
+ # if skew_angle <= 90:
196
+ # ax.set(xlim=(xmin, xmax + (ymax - ymin) / np.tan((90 - skew_angle_adj) * np.pi / 180)))
197
+ # else:
198
+ # ax.set(xlim=(xmin - (ymax - ymin) / np.tan((skew_angle_adj - 90) * np.pi / 180), xmax))
199
+ #
200
+ # # Correct aspect ratio for the x/y axes after transformation
201
+ # ax.set(aspect=np.cos(skew_angle_adj * np.pi / 180))
202
+ #
203
+ # # Add tick marks all around
204
+ # ax.tick_params(direction='in', top=True, right=True, which='both')
205
+ #
206
+ # # Set tick locations
207
+ # if xticks is None:
208
+ # # Add default minor ticks
209
+ # ax.xaxis.set_minor_locator(MultipleLocator(1))
210
+ # else:
211
+ # # Otherwise use user provided values
212
+ # ax.xaxis.set_major_locator(MultipleLocator(xticks))
213
+ # ax.xaxis.set_minor_locator(MultipleLocator(1))
214
+ # if yticks is None:
215
+ # # Add default minor ticks
216
+ # ax.yaxis.set_minor_locator(MultipleLocator(1))
217
+ # else:
218
+ # # Otherwise use user provided values
219
+ # ax.yaxis.set_major_locator(MultipleLocator(yticks))
220
+ # ax.yaxis.set_minor_locator(MultipleLocator(1))
221
+ #
222
+ # # Apply transform to tick marks
223
+ # for i in range(0, len(ax.xaxis.get_ticklines())):
224
+ # # Tick marker
225
+ # m = MarkerStyle(3)
226
+ # line = ax.xaxis.get_majorticklines()[i]
227
+ # if i % 2:
228
+ # # Top ticks (translation here makes their direction="in")
229
+ # m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle_adj, 0))
230
+ # # This first method shifts the top ticks horizontally to match the skew angle.
231
+ # # This does not look good in all cases.
232
+ # # line.set_transform(Affine2D().translate((ymax-ymin)*np.sin(skew_angle*np.pi/180),0) +
233
+ # # line.get_transform())
234
+ # # This second method skews the tick marks in place and
235
+ # # can sometimes lead to them being misaligned.
236
+ # line.set_transform(line.get_transform()) # This does nothing
237
+ # else:
238
+ # # Bottom ticks
239
+ # m._transform.set(Affine2D().skew_deg(skew_angle_adj, 0))
240
+ #
241
+ # line.set_marker(m)
242
+ #
243
+ # for i in range(0, len(ax.xaxis.get_minorticklines())):
244
+ # m = MarkerStyle(2)
245
+ # line = ax.xaxis.get_minorticklines()[i]
246
+ # if i % 2:
247
+ # m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle_adj, 0))
248
+ # else:
249
+ # m._transform.set(Affine2D().skew_deg(skew_angle_adj, 0))
250
+ #
251
+ # line.set_marker(m)
252
+ #
253
+ # if cbar:
254
+ # colorbar = fig.colorbar(p)
255
+ # if cbartitle is None:
256
+ # colorbar.set_label(data.signal)
257
+ #
258
+ # ax.set(
259
+ # xlabel=X.nxname,
260
+ # ylabel=Y.nxname,
261
+ # )
262
+ #
263
+ # if title is not None:
264
+ # ax.set_title(title)
265
+ #
266
+ # # Return the quadmesh object
267
+ # return p
268
+
269
+
270
+ data = load_transform(r'K:\wilson-3947-a\nxrefine\LaCd3P3\MLA1\LaCd3P3_300.nxs')
271
+
272
+ # print(data.tree)
273
+ fig = plt.figure(figsize=(5,3))
274
+ ax = fig.add_axes([0,0,1,1])
275
+ plot_slice(data[-1.0:1.0, -0.5:0.5, 0.0].counts.nxdata, vmin=0, vmax=100, cbar=False, ax=ax)
276
+ fig.tight_layout()
277
+ plt.show()