nxs-analysis-tools 0.0.23__tar.gz → 0.0.25__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 (25) hide show
  1. {nxs-analysis-tools-0.0.23/src/nxs_analysis_tools.egg-info → nxs-analysis-tools-0.0.25}/PKG-INFO +22 -2
  2. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/pyproject.toml +1 -1
  3. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/src/_meta/__init__.py +1 -1
  4. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/src/nxs_analysis_tools/__init__.py +2 -2
  5. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/src/nxs_analysis_tools/datareduction.py +271 -18
  6. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/src/nxs_analysis_tools/pairdistribution.py +99 -179
  7. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25/src/nxs_analysis_tools.egg-info}/PKG-INFO +22 -2
  8. nxs-analysis-tools-0.0.25/tests/test_datareduction.py +19 -0
  9. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/tests/test_pairdistribution.py +20 -7
  10. nxs-analysis-tools-0.0.23/tests/test_datareduction.py +0 -10
  11. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/LICENSE +0 -0
  12. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/MANIFEST.in +0 -0
  13. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/README.md +0 -0
  14. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/setup.cfg +0 -0
  15. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/setup.py +0 -0
  16. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/src/nxs_analysis_tools/chess.py +0 -0
  17. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/src/nxs_analysis_tools/fitting.py +0 -0
  18. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/src/nxs_analysis_tools.egg-info/SOURCES.txt +0 -0
  19. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/src/nxs_analysis_tools.egg-info/dependency_links.txt +0 -0
  20. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/src/nxs_analysis_tools.egg-info/requires.txt +0 -0
  21. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/src/nxs_analysis_tools.egg-info/top_level.txt +0 -0
  22. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/tests/test_chess.py +0 -0
  23. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/tests/test_chess_fitting.py +0 -0
  24. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/tests/test_fitting.py +0 -0
  25. {nxs-analysis-tools-0.0.23 → nxs-analysis-tools-0.0.25}/tests/test_lmfit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nxs-analysis-tools
3
- Version: 0.0.23
3
+ Version: 0.0.25
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
@@ -41,8 +41,28 @@ Classifier: Topic :: Scientific/Engineering :: Image Processing
41
41
  Classifier: Topic :: Scientific/Engineering
42
42
  Requires-Python: >=3.7
43
43
  Description-Content-Type: text/markdown
44
- Provides-Extra: dev
45
44
  License-File: LICENSE
45
+ Requires-Dist: matplotlib>=3.7.1
46
+ Requires-Dist: numpy>=1.24.3
47
+ Requires-Dist: ipython>=1.0.0
48
+ Requires-Dist: pandas>=2.0.2
49
+ Requires-Dist: nexusformat>=1.0.1
50
+ Requires-Dist: lmfit>=1.2.1
51
+ Provides-Extra: dev
52
+ Requires-Dist: build>=0.8.0; extra == "dev"
53
+ Requires-Dist: furo>=2022.6.21; extra == "dev"
54
+ Requires-Dist: ipykernel>=6.9.1; extra == "dev"
55
+ Requires-Dist: myst-nb>=0.16.0; extra == "dev"
56
+ Requires-Dist: myst-parser>=0.18.0; extra == "dev"
57
+ Requires-Dist: numpydoc>=1.4.0; extra == "dev"
58
+ Requires-Dist: pandoc>=2.2; extra == "dev"
59
+ Requires-Dist: pylint>=2.12.2; extra == "dev"
60
+ Requires-Dist: pytest>=7.1.2; extra == "dev"
61
+ Requires-Dist: sphinx>=5.0.2; extra == "dev"
62
+ Requires-Dist: sphinx-autobuild>=2021.3.14; extra == "dev"
63
+ Requires-Dist: sphinx-copybutton>=0.5.0; extra == "dev"
64
+ Requires-Dist: sphinxext-opengraph>=0.6.3; extra == "dev"
65
+ Requires-Dist: twine>=4.0.1; extra == "dev"
46
66
 
47
67
  # nxs-analysis-tools
48
68
 
@@ -6,7 +6,7 @@ build-backend = 'setuptools.build_meta'
6
6
 
7
7
  [project]
8
8
  name = 'nxs-analysis-tools'
9
- version = '0.0.23'
9
+ version = '0.0.25'
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.23'
9
+ __version__= '0.0.25'
10
10
  __repo_url__ = 'https://github.com/stevenjgomez/nxs_analysis_tools'
@@ -4,9 +4,9 @@ Reduce and transform nexus format (.nxs) scattering data.
4
4
 
5
5
  import numpy as np
6
6
  from _meta import __author__, __copyright__, __license__, __version__
7
- from .datareduction import load_data, plot_slice, Scissors, reciprocal_lattice_params
7
+ from .datareduction import load_data, plot_slice, reciprocal_lattice_params, Scissors, rotate_data
8
8
  from .chess import TempDependence
9
9
 
10
10
  # What to import when running "from nxs_analysis_tools import *"
11
11
  __all__ = ['load_data', 'plot_slice', 'Scissors', 'TempDependence',
12
- 'reciprocal_lattice_params']
12
+ 'reciprocal_lattice_params', 'rotate_data']
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Reduces scattering data into 2D and 1D datasets.
3
3
  """
4
-
4
+ import os
5
5
  import numpy as np
6
6
  import matplotlib.pyplot as plt
7
7
  from matplotlib.transforms import Affine2D
@@ -10,9 +10,11 @@ from matplotlib.ticker import MultipleLocator
10
10
  from matplotlib import colors
11
11
  from matplotlib import patches
12
12
  from IPython.display import display, Markdown
13
- from nexusformat.nexus import NXfield, NXdata, nxload, NeXusError
13
+ from nexusformat.nexus import NXfield, NXdata, nxload, NeXusError, NXroot, NXentry, nxsave
14
+ from scipy import ndimage
14
15
 
15
- __all__ = ['load_data', 'plot_slice', 'Scissors']
16
+ # Specify items on which users are allowed to perform standalone imports
17
+ __all__ = ['load_data', 'plot_slice', 'Scissors', 'reciprocal_lattice_params', 'rotate_data', 'array_to_nxdata', 'Padder']
16
18
 
17
19
 
18
20
  def load_data(path):
@@ -39,6 +41,31 @@ def load_data(path):
39
41
  return g.entry.data
40
42
 
41
43
 
44
+ def array_to_nxdata(array, data_template, signal_name='counts'):
45
+ """
46
+ Create an NXdata object from an input array and an NXdata template, with an optional signal name.
47
+
48
+ Parameters
49
+ ----------
50
+ array : array-like
51
+ The data array to be included in the NXdata object.
52
+
53
+ data_template : NXdata
54
+ An NXdata object serving as a template, which provides information about axes and other metadata.
55
+
56
+ signal_name : str, optional
57
+ The name of the signal within the NXdata object. If not provided,
58
+ the default signal name 'counts' is used.
59
+
60
+ Returns
61
+ -------
62
+ NXdata
63
+ An NXdata object containing the input data array and associated axes based on the template.
64
+ """
65
+ d = data_template
66
+ return NXdata(NXfield(array, name=signal_name), tuple([d[d.axes[i]] for i in range(len(d.axes))]))
67
+
68
+
42
69
  def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew_angle=90,
43
70
  ax=None, xlim=None, ylim=None, xticks=None, yticks=None, cbar=True, logscale=False,
44
71
  symlogscale=False, cmap='viridis', linthresh=1, title=None, mdheading=None, cbartitle=None,
@@ -113,7 +140,6 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
113
140
 
114
141
  """
115
142
  if type(data) == np.ndarray:
116
- data = data.transpose()
117
143
  if X is None:
118
144
  X = NXfield(np.linspace(0, data.shape[1], data.shape[1]), name='x')
119
145
  if Y is None:
@@ -121,7 +147,8 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
121
147
  if transpose:
122
148
  X, Y = Y, X
123
149
  data = data.transpose()
124
- data = NXdata(NXfield(data, name='value'), (X,Y))
150
+ data = NXdata(NXfield(data, name='value'), (X, Y))
151
+ data_arr = data
125
152
  elif type(data) == NXdata or type(data) == NXfield:
126
153
  if X is None:
127
154
  X = data[data.axes[0]]
@@ -130,11 +157,10 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
130
157
  if transpose:
131
158
  X, Y = Y, X
132
159
  data = data.transpose()
160
+ data_arr = data[data.signal].nxdata.transpose()
133
161
  else:
134
162
  raise TypeError(f"Unexpected data type: {type(data)}. Supported types are np.ndarray and NXdata.")
135
163
 
136
- data_arr = data[data.signal].nxdata.transpose()
137
-
138
164
  # Display Markdown heading
139
165
  if mdheading is None:
140
166
  pass
@@ -171,15 +197,15 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
171
197
 
172
198
  ## Transform data to new coordinate system if necessary
173
199
  # Correct skew angle
174
- skew_angle = 90 - skew_angle
200
+ skew_angle_adj = 90 - skew_angle
175
201
  # Create blank 2D affine transformation
176
202
  t = Affine2D()
177
203
  # Scale y-axis to preserve norm while shearing
178
- t += Affine2D().scale(1, np.cos(skew_angle * np.pi / 180))
204
+ t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
179
205
  # Shear along x-axis
180
- t += Affine2D().skew_deg(skew_angle, 0)
206
+ t += Affine2D().skew_deg(skew_angle_adj, 0)
181
207
  # Return to original y-axis scaling
182
- t += Affine2D().scale(1, np.cos(skew_angle * np.pi / 180)).inverted()
208
+ t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
183
209
  ## Correct for x-displacement after shearing
184
210
  # If ylims provided, use those
185
211
  if ylim is not None:
@@ -190,17 +216,20 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
190
216
  else:
191
217
  ymin, ymax = ax.get_ylim()
192
218
  # Use ylims to calculate translation (necessary to display axes in correct position)
193
- p.set_transform(t + Affine2D().translate(-ymin * np.sin(skew_angle * np.pi / 180), 0) + ax.transData)
219
+ p.set_transform(t + Affine2D().translate(-ymin * np.sin(skew_angle_adj * np.pi / 180), 0) + ax.transData)
194
220
 
195
221
  # Set x limits
196
222
  if xlim is not None:
197
223
  xmin, xmax = xlim
198
224
  else:
199
225
  xmin, xmax = ax.get_xlim()
200
- ax.set(xlim=(xmin, xmax + (ymax - ymin) / np.tan((90 - skew_angle) * np.pi / 180)))
226
+ if skew_angle <= 90:
227
+ ax.set(xlim=(xmin, xmax + (ymax - ymin) / np.tan((90 - skew_angle_adj) * np.pi / 180)))
228
+ else:
229
+ ax.set(xlim=(xmin - (ymax - ymin) / np.tan((skew_angle_adj - 90) * np.pi / 180), xmax))
201
230
 
202
231
  # Correct aspect ratio for the x/y axes after transformation
203
- ax.set(aspect=np.cos(skew_angle * np.pi / 180))
232
+ ax.set(aspect=np.cos(skew_angle_adj * np.pi / 180))
204
233
 
205
234
  # Add tick marks all around
206
235
  ax.tick_params(direction='in', top=True, right=True, which='both')
@@ -228,7 +257,7 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
228
257
  line = ax.xaxis.get_majorticklines()[i]
229
258
  if i % 2:
230
259
  # Top ticks (translation here makes their direction="in")
231
- m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle, 0))
260
+ m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle_adj, 0))
232
261
  # This first method shifts the top ticks horizontally to match the skew angle.
233
262
  # This does not look good in all cases.
234
263
  # line.set_transform(Affine2D().translate((ymax-ymin)*np.sin(skew_angle*np.pi/180),0) +
@@ -238,7 +267,7 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
238
267
  line.set_transform(line.get_transform()) # This does nothing
239
268
  else:
240
269
  # Bottom ticks
241
- m._transform.set(Affine2D().skew_deg(skew_angle, 0))
270
+ m._transform.set(Affine2D().skew_deg(skew_angle_adj, 0))
242
271
 
243
272
  line.set_marker(m)
244
273
 
@@ -246,9 +275,9 @@ def plot_slice(data, X=None, Y=None, transpose=False, vmin=None, vmax=None, skew
246
275
  m = MarkerStyle(2)
247
276
  line = ax.xaxis.get_minorticklines()[i]
248
277
  if i % 2:
249
- m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle, 0))
278
+ m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle_adj, 0))
250
279
  else:
251
- m._transform.set(Affine2D().skew_deg(skew_angle, 0))
280
+ m._transform.set(Affine2D().skew_deg(skew_angle_adj, 0))
252
281
 
253
282
  line.set_marker(m)
254
283
 
@@ -624,3 +653,227 @@ def reciprocal_lattice_params(lattice_params):
624
653
  gamma_star = np.rad2deg(np.arccos((np.cos(alpha) * np.cos(beta) - np.cos(gamma)) / (np.sin(alpha) * np.sin(beta))))
625
654
 
626
655
  return a_star, b_star, c_star, alpha_star, beta_star, gamma_star
656
+
657
+
658
+ def rotate_data(data, lattice_angle, rotation_angle, rotation_axis):
659
+ """
660
+ Rotates 3D data around a specified axis.
661
+
662
+ Parameters
663
+ ----------
664
+ data : :class:`nexusformat.nexus.NXdata`
665
+ Input data.
666
+ lattice_angle : float
667
+ Angle between the two in-plane lattice axes in degrees.
668
+ rotation_angle : float
669
+ Angle of rotation in degrees.
670
+ rotation_axis : int
671
+ Axis of rotation (0, 1, or 2).
672
+
673
+ Returns
674
+ -------
675
+ rotated_data : :class:`nexusformat.nexus.NXdata`
676
+ Rotated data as an NXdata object.
677
+ """
678
+ # Define output array
679
+ output_array = np.zeros(data[data.signal].shape)
680
+
681
+ # Define transformation
682
+ skew_angle_adj = 90 - lattice_angle
683
+ t = Affine2D()
684
+ # Scale y-axis to preserve norm while shearing
685
+ t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
686
+ # Shear along x-axis
687
+ t += Affine2D().skew_deg(skew_angle_adj, 0)
688
+ # Return to original y-axis scaling
689
+ t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
690
+
691
+ for i in range(len(data[data.axes[rotation_axis]])):
692
+ # Identify current slice
693
+ if rotation_axis == 0:
694
+ sliced_data = data[i, :, :]
695
+ elif rotation_axis == 1:
696
+ sliced_data = data[:, i, :]
697
+ elif rotation_axis == 2:
698
+ sliced_data = data[:, :, i]
699
+ else:
700
+ sliced_data = None
701
+
702
+ p = Padder(sliced_data)
703
+ padding = tuple([len(sliced_data[axis]) for axis in sliced_data.axes])
704
+ counts = p.pad(padding)
705
+
706
+ counts_skewed = ndimage.affine_transform(counts,
707
+ t.inverted().get_matrix()[:2, :2],
708
+ offset=[counts.shape[0] / 2 * np.sin(skew_angle_adj * np.pi / 180), 0],
709
+ order=0,
710
+ )
711
+ scale1 = np.cos(skew_angle_adj * np.pi / 180)
712
+ counts_scaled1 = ndimage.affine_transform(counts_skewed,
713
+ Affine2D().scale(scale1, 1).get_matrix()[:2, :2],
714
+ offset=[(1 - scale1) * counts.shape[0] / 2, 0],
715
+ order=0,
716
+ )
717
+ scale2 = counts.shape[0] / counts.shape[1]
718
+ counts_scaled2 = ndimage.affine_transform(counts_scaled1,
719
+ Affine2D().scale(scale2, 1).get_matrix()[:2, :2],
720
+ offset=[(1 - scale2) * counts.shape[0] / 2, 0],
721
+ order=0,
722
+ )
723
+
724
+ counts_rotated = ndimage.rotate(counts_scaled2, rotation_angle, reshape=False, order=0)
725
+
726
+ counts_unscaled2 = ndimage.affine_transform(counts_rotated,
727
+ Affine2D().scale(scale2, 1).inverted().get_matrix()[:2, :2],
728
+ offset=[-(1 - scale2) * counts.shape[
729
+ 0] / 2 / scale2, 0],
730
+ order=0,
731
+ )
732
+
733
+ counts_unscaled1 = ndimage.affine_transform(counts_unscaled2,
734
+ Affine2D().scale(scale1,
735
+ 1).inverted().get_matrix()[:2, :2],
736
+ offset=[-(1 - scale1) * counts.shape[
737
+ 0] / 2 / scale1, 0],
738
+ order=0,
739
+ )
740
+
741
+ counts_unskewed = ndimage.affine_transform(counts_unscaled1,
742
+ t.get_matrix()[:2, :2],
743
+ offset=[
744
+ (-counts.shape[0] / 2 * np.sin(skew_angle_adj * np.pi / 180)),
745
+ 0],
746
+ order=0,
747
+ )
748
+
749
+ counts_unpadded = p.unpad(counts_unskewed)
750
+
751
+ # Write current slice
752
+ if rotation_axis == 0:
753
+ output_array[i, :, :] = counts_unpadded
754
+ elif rotation_axis == 1:
755
+ output_array[:, i, :] = counts_unpadded
756
+ elif rotation_axis == 2:
757
+ output_array[:, :, i] = counts_unpadded
758
+
759
+ return NXdata(NXfield(output_array, name='counts'),
760
+ (data[data.axes[0]], data[data.axes[1]], data[data.axes[2]]))
761
+
762
+
763
+ class Padder():
764
+ """
765
+ A class to pad and unpad datasets with a symmetric region of zeros.
766
+ """
767
+
768
+ def __init__(self, data=None):
769
+ """
770
+ Initialize the Symmetrizer3D object.
771
+
772
+ Parameters
773
+ ----------
774
+ data : NXdata, optional
775
+ The input data to be symmetrized. If provided, the `set_data` method is called to set the data.
776
+
777
+ """
778
+ self.padded = None
779
+ self.padding = None
780
+ if data is not None:
781
+ self.set_data(data)
782
+
783
+ def set_data(self, data):
784
+ """
785
+ Set the input data for symmetrization.
786
+
787
+ Parameters
788
+ ----------
789
+ data : NXdata
790
+ The input data to be symmetrized.
791
+
792
+ """
793
+ self.data = data
794
+
795
+ self.steps = tuple([(data[axis].nxdata[1] - data[axis].nxdata[0]) for axis in data.axes])
796
+
797
+ # Absolute value of the maximum value; assumes the domain of the input is symmetric (eg, -H_min = H_max)
798
+ self.maxes = tuple([data[axis].nxdata.max() for axis in data.axes])
799
+
800
+ def pad(self, padding):
801
+ """
802
+ Symmetrically pads the data with zero values.
803
+
804
+ Parameters
805
+ ----------
806
+ padding : tuple
807
+ The number of zero-value pixels to add along each edge of the array.
808
+ """
809
+ data = self.data
810
+ self.padding = padding
811
+
812
+ padded_shape = tuple([data[data.signal].nxdata.shape[i] + self.padding[i] * 2 for i in range(data.ndim)])
813
+
814
+ # Create padded dataset
815
+ padded = np.zeros(padded_shape)
816
+
817
+ slice_obj = [slice(None)] * data.ndim
818
+ for i, _ in enumerate(slice_obj):
819
+ slice_obj[i] = slice(self.padding[i], -self.padding[i], None)
820
+ slice_obj = tuple(slice_obj)
821
+ padded[slice_obj] = data[data.signal].nxdata
822
+
823
+ padmaxes = tuple([self.maxes[i] + self.padding[i] * self.steps[i] for i in range(data.ndim)])
824
+
825
+ padded = NXdata(NXfield(padded, name=data.signal),
826
+ tuple([NXfield(np.linspace(-padmaxes[i], padmaxes[i], padded_shape[i]),
827
+ name=data.axes[i])
828
+ for i in range(data.ndim)]))
829
+
830
+ self.padded = padded
831
+ return padded
832
+
833
+ def save(self, fout_name=None):
834
+ """
835
+ Saves the padded dataset to a .nxs file.
836
+
837
+ Parameters
838
+ ----------
839
+ fout_name : str, optional
840
+ The output file name. Default is padded_(Hpadding)_(Kpadding)_(Lpadding).nxs
841
+ """
842
+ padH, padK, padL = self.padding
843
+
844
+ # Save padded dataset
845
+ print("Saving padded dataset...")
846
+ f = NXroot()
847
+ f['entry'] = NXentry()
848
+ f['entry']['data'] = self.padded
849
+ if fout_name is None:
850
+ fout_name = 'padded_' + str(padH) + '_' + str(padK) + '_' + str(padL) + '.nxs'
851
+ nxsave(fout_name, f)
852
+ print("Output file saved to: " + os.path.join(os.getcwd(), fout_name))
853
+
854
+ def unpad(self, data):
855
+ """
856
+ Removes the padded region from the data.
857
+
858
+ Parameters
859
+ ----------
860
+ data : ndarray or NXdata
861
+ The padded data from which to remove the padding.
862
+
863
+ Returns
864
+ -------
865
+ ndarray or NXdata
866
+ The unpadded data, with the symmetric padding region removed.
867
+
868
+ Notes
869
+ -----
870
+ This method removes the symmetric padding region that was added using the `pad` method. It returns the data
871
+ without the padded region.
872
+
873
+
874
+ """
875
+ slice_obj = [slice(None)] * data.ndim
876
+ for i in range(data.ndim):
877
+ slice_obj[i] = slice(self.padding[i], -self.padding[i], None)
878
+ slice_obj = tuple(slice_obj)
879
+ return data[slice_obj]
@@ -8,128 +8,7 @@ import matplotlib.pyplot as plt
8
8
  from matplotlib.transforms import Affine2D
9
9
  from nexusformat.nexus import nxsave, NXroot, NXentry, NXdata, NXfield
10
10
  import numpy as np
11
- from nxs_analysis_tools import plot_slice, reciprocal_lattice_params
12
-
13
-
14
-
15
- class Padder():
16
- """
17
- A class to pad and unpad datasets with a symmetric region of zeros.
18
- """
19
-
20
- def __init__(self, data=None):
21
- """
22
- Initialize the Symmetrizer3D object.
23
-
24
- Parameters
25
- ----------
26
- data : NXdata, optional
27
- The input data to be symmetrized. If provided, the `set_data` method is called to set the data.
28
-
29
- """
30
- self.padded = None
31
- self.padding = None
32
- if data is not None:
33
- self.set_data(data)
34
-
35
- def set_data(self, data):
36
- """
37
- Set the input data for symmetrization.
38
-
39
- Parameters
40
- ----------
41
- data : NXdata
42
- The input data to be symmetrized.
43
-
44
- """
45
- self.data = data
46
-
47
- self.steps = tuple([(data[axis].nxdata[1] - data[axis].nxdata[0]) for axis in data.axes])
48
-
49
- # Absolute value of the maximum value; assumes the domain of the input is symmetric (eg, -H_min = H_max)
50
- self.maxes = tuple([data[axis].nxdata.max() for axis in data.axes])
51
-
52
- def pad(self, padding):
53
- """
54
- Symmetrically pads the data with zero values.
55
-
56
- Parameters
57
- ----------
58
- padding : tuple
59
- The number of zero-value pixels to add along each edge of the array.
60
- """
61
- data = self.data
62
- self.padding = padding
63
-
64
- padded_shape = tuple([data[data.signal].nxdata.shape[i] + self.padding[i] * 2 for i in range(data.ndim)])
65
-
66
- # Create padded dataset
67
- padded = np.zeros(padded_shape)
68
-
69
- slice_obj = [slice(None)] * data.ndim
70
- for i, _ in enumerate(slice_obj):
71
- slice_obj[i] = slice(self.padding[i], -self.padding[i], None)
72
- slice_obj = tuple(slice_obj)
73
- padded[slice_obj] = data[data.signal].nxdata
74
-
75
- padmaxes = tuple([self.maxes[i] + self.padding[i] * self.steps[i] for i in range(data.ndim)])
76
-
77
- padded = NXdata(NXfield(padded, name=data.signal),
78
- tuple([NXfield(np.linspace(-padmaxes[i], padmaxes[i], padded_shape[i]),
79
- name=data.axes[i])
80
- for i in range(data.ndim)]))
81
-
82
- self.padded = padded
83
- return padded
84
-
85
- def save(self, fout_name=None):
86
- """
87
- Saves the padded dataset to a .nxs file.
88
-
89
- Parameters
90
- ----------
91
- fout_name : str, optional
92
- The output file name. Default is padded_(Hpadding)_(Kpadding)_(Lpadding).nxs
93
- """
94
- padH, padK, padL = self.padding
95
-
96
- # Save padded dataset
97
- print("Saving padded dataset...")
98
- f = NXroot()
99
- f['entry'] = NXentry()
100
- f['entry']['data'] = self.padded
101
- if fout_name is None:
102
- fout_name = 'padded_' + str(padH) + '_' + str(padK) + '_' + str(padL) + '.nxs'
103
- nxsave(fout_name, f)
104
- print("Output file saved to: " + os.path.join(os.getcwd(), fout_name))
105
-
106
- def unpad(self, data):
107
- """
108
- Removes the padded region from the data.
109
-
110
- Parameters
111
- ----------
112
- data : ndarray or NXdata
113
- The padded data from which to remove the padding.
114
-
115
- Returns
116
- -------
117
- ndarray or NXdata
118
- The unpadded data, with the symmetric padding region removed.
119
-
120
- Notes
121
- -----
122
- This method removes the symmetric padding region that was added using the `pad` method. It returns the data
123
- without the padded region.
124
-
125
-
126
- """
127
- slice_obj = [slice(None)] * data.ndim
128
- for i in range(data.ndim):
129
- slice_obj[i] = slice(self.padding[i], -self.padding[i], None)
130
- slice_obj = tuple(slice_obj)
131
- return data[slice_obj]
132
-
11
+ from .datareduction import plot_slice, reciprocal_lattice_params, Padder
133
12
 
134
13
  class Symmetrizer2D:
135
14
  """
@@ -185,7 +64,7 @@ class Symmetrizer2D:
185
64
  t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
186
65
  self.transform = t
187
66
 
188
- # Calculate number of rotations needed to reconstructed the dataset
67
+ # Calculate number of rotations needed to reconstruct the dataset
189
68
  if mirror:
190
69
  rotations = abs(int(360 / (theta_max - theta_min) / 2))
191
70
  else:
@@ -368,24 +247,53 @@ class Symmetrizer3D:
368
247
  A class to symmetrize 3D datasets.
369
248
  """
370
249
 
371
- def __init__(self, data):
250
+ def __init__(self, data=None):
372
251
  """
373
252
  Initialize the Symmetrizer3D object.
374
253
 
375
254
  Parameters
376
255
  ----------
377
- data : NXdata
256
+ data : NXdata, optional
378
257
  The input 3D dataset to be symmetrized.
379
258
 
380
259
  """
260
+
261
+ self.a, self.b, self.c, self.al, self.be, self.ga = [None] * 6
262
+ self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = [None] * 6
263
+ self.lattice_params = None
264
+ self.reciprocal_lattice_params = None
381
265
  self.symmetrized = None
382
266
  self.data = data
383
- self.q1 = data[data.axes[0]]
384
- self.q2 = data[data.axes[1]]
385
- self.q3 = data[data.axes[2]]
386
267
  self.plane1symmetrizer = Symmetrizer2D()
387
268
  self.plane2symmetrizer = Symmetrizer2D()
388
269
  self.plane3symmetrizer = Symmetrizer2D()
270
+
271
+ if data is not None:
272
+ self.q1 = data[data.axes[0]]
273
+ self.q2 = data[data.axes[1]]
274
+ self.q3 = data[data.axes[2]]
275
+ self.plane1 = self.q1.nxname + self.q2.nxname
276
+ self.plane2 = self.q1.nxname + self.q3.nxname
277
+ self.plane3 = self.q2.nxname + self.q3.nxname
278
+
279
+ print("Plane 1: " + self.plane1)
280
+ print("Plane 2: " + self.plane2)
281
+ print("Plane 3: " + self.plane3)
282
+
283
+ def set_data(self, data):
284
+ """
285
+ Sets the data to be symmetrized.
286
+
287
+ Parameters
288
+ ----------
289
+ data : NXdata
290
+ The input 3D dataset to be symmetrized.
291
+
292
+ """
293
+ self.data = data
294
+ self.q1 = data[data.axes[0]]
295
+ self.q2 = data[data.axes[1]]
296
+ self.q3 = data[data.axes[2]]
389
297
  self.plane1 = self.q1.nxname + self.q2.nxname
390
298
  self.plane2 = self.q1.nxname + self.q3.nxname
391
299
  self.plane3 = self.q2.nxname + self.q3.nxname
@@ -394,6 +302,12 @@ class Symmetrizer3D:
394
302
  print("Plane 2: " + self.plane2)
395
303
  print("Plane 3: " + self.plane3)
396
304
 
305
+ def set_lattice_params(self, lattice_params):
306
+ self.a, self.b, self.c, self.al, self.be, self.ga = lattice_params
307
+ self.lattice_params = lattice_params
308
+ self.reciprocal_lattice_params = reciprocal_lattice_params(lattice_params)
309
+ self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = self.reciprocal_lattice_params
310
+
397
311
  def symmetrize(self):
398
312
  """
399
313
  Perform the symmetrization of the 3D dataset.
@@ -491,7 +405,7 @@ def generate_gaussian(H, K, L, amp, stddev, lattice_params, coeffs=None):
491
405
  coeffs = [1, 0, 1, 0, 1, 0]
492
406
  a, b, c, al, be, ga = lattice_params
493
407
  a_, b_, c_, al_, be_, ga_ = reciprocal_lattice_params((a, b, c, al, be, ga))
494
- H, K, L = np.meshgrid(H, K, L)
408
+ H, K, L = np.meshgrid(H, K, L, indexing='ij')
495
409
  gaussian = amp * np.exp(-(coeffs[0] * H ** 2 +
496
410
  coeffs[1] * (b_ * a_ / (a_ ** 2)) * H * K +
497
411
  coeffs[2] * (b_ / a_) ** 2 * K ** 2 +
@@ -502,16 +416,25 @@ def generate_gaussian(H, K, L, amp, stddev, lattice_params, coeffs=None):
502
416
  gaussian = gaussian.transpose(1, 0, 2)
503
417
  elif gaussian.ndim == 2:
504
418
  gaussian = gaussian.transpose()
505
- return gaussian.transpose(1,0,2)
419
+ return gaussian.transpose(1, 0, 2)
506
420
 
507
421
 
508
422
  class Puncher:
509
423
  def __init__(self):
424
+ self.punched = None
425
+ self.data = None
426
+ self.H, self.K, self.L = [None] * 3
510
427
  self.mask = None
428
+ self.reciprocal_lattice_params = None
429
+ self.lattice_params = None
511
430
  self.a, self.b, self.c, self.al, self.be, self.ga = [None] * 6
431
+ self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = [None] * 6
512
432
 
513
433
  def set_data(self, data):
514
434
  self.data = data
435
+ if self.mask is None:
436
+ self.mask = np.zeros(data[data.signal].nxdata.shape)
437
+ self.H, self.K, self.L = np.meshgrid(data[data.axes[0]], data[data.axes[1]], data[data.axes[2]], indexing='ij')
515
438
 
516
439
  def set_lattice_params(self, lattice_params):
517
440
  self.a, self.b, self.c, self.al, self.be, self.ga = lattice_params
@@ -519,53 +442,18 @@ class Puncher:
519
442
  self.reciprocal_lattice_params = reciprocal_lattice_params(lattice_params)
520
443
  self.a_star, self.b_star, self.c_star, self.al_star, self.be_star, self.ga_star = self.reciprocal_lattice_params
521
444
 
445
+ def add_mask(self, maskaddition):
446
+ self.mask = np.logical_or(self.mask, maskaddition)
522
447
 
523
- def set_gaussian_background(self, amp, stddev, coeffs=None):
524
- if coeffs is None:
525
- coeffs = [1, 0, 1, 0, 1, 0]
526
- data = self.data
527
- self.background = generate_gaussian(data[data.axes[0]], data[data.axes[1]], data[data.axes[2]],
528
- amp=amp, stddev=stddev, lattice_params=self.lattice_params,
529
- coeffs=coeffs)
530
-
531
- def plot_gaussian_background(self):
532
- data = self.data
533
- fig, axes = plt.subplots(1, 3)
534
- # Plot the background and subtracted
535
- plot_slice(self.background[:, :, len(data[data.axes[2]]) // 2], data[data.axes[0]], data[data.axes[1]],
536
- ax=axes[0], skew_angle=self.ga_star)
537
- plot_slice(self.background[:, len(data[data.axes[1]]) // 2, :],data[data.axes[0]], data[data.axes[2]],
538
- ax=axes[1], skew_angle=self.be_star)
539
- plot_slice(self.background[len(data[data.axes[0]]) // 2, :, :],data[data.axes[1]], data[data.axes[2]],
540
- ax=axes[2], skew_angle=self.al_star)
541
- plt.show()
542
- return fig, axes
448
+ def subtract_mask(self, masksubtraction):
449
+ self.mask = np.logical_and(self.mask, np.logical_not(masksubtraction))
543
450
 
544
- def plot_background_subtraction(self, **kwargs):
545
- data = self.data
546
- fig, axes = plt.subplots(1, 3, figsize=(10,3))
547
- # Plot the background and subtracted
548
- plot_slice(data[data.signal][:, :, 0.0] - self.background[:, :, len(data[data.axes[2]]) // 2],
549
- data[data.axes[0]], data[data.axes[1]],
550
- ax=axes[0], skew_angle=self.ga_star, **kwargs)
551
- plot_slice(data[data.signal][:, 0.0, :] - self.background[:, len(data[data.axes[1]]) // 2, :],
552
- data[data.axes[0]], data[data.axes[2]],
553
- ax=axes[1], skew_angle=self.be_star, **kwargs)
554
- plot_slice(data[data.signal][0.0, :, :] - self.background[len(data[data.axes[0]]) // 2, :, :],
555
- data[data.axes[1]], data[data.axes[2]],
556
- ax=axes[2], skew_angle=self.al_star, **kwargs)
557
- plt.show()
558
- return fig, axes
559
-
560
- def generate_mask(self, punch_radius, coeffs=None, thresh=None):
451
+ def generate_bragg_mask(self, punch_radius, coeffs=None, thresh=None):
561
452
  if coeffs is None:
562
453
  coeffs = [1, 0, 1, 0, 1, 0]
563
454
  data = self.data
564
- lattice_params = self.lattice_params
565
- a, b, c, al, be, ga = lattice_params
566
- a_, b_, c_, al_, be_, ga_ = reciprocal_lattice_params((a, b, c, al, be, ga))
567
-
568
- H, K, L = np.meshgrid(data[data.axes[0]], data[data.axes[1]], data[data.axes[2]])
455
+ H, K, L = self.H, self.K, self.L
456
+ a_, b_, c_, al_, be_, ga_ = self.reciprocal_lattice_params
569
457
 
570
458
  mask = (coeffs[0] * (H - np.rint(H)) ** 2 +
571
459
  coeffs[1] * (b_ * a_ / (a_ ** 2)) * (H - np.rint(H)) * (K - np.rint(K)) +
@@ -577,19 +465,51 @@ class Puncher:
577
465
  if thresh:
578
466
  mask = np.logical_and(mask, data[data.signal] > thresh)
579
467
 
580
- self.mask = mask
468
+ return mask
469
+
470
+ def generate_mask_at_coord(self, coordinate, punch_radius, coeffs=None, thresh=None):
471
+ if coeffs is None:
472
+ coeffs = [1, 0, 1, 0, 1, 0]
473
+ data = self.data
474
+ H, K, L = self.H, self.K, self.L
475
+ a_, b_, c_, al_, be_, ga_ = self.reciprocal_lattice_params
476
+ centerH, centerK, centerL = coordinate
477
+ mask = (coeffs[0] * (H - centerH) ** 2 +
478
+ coeffs[1] * (b_ * a_ / (a_ ** 2)) * (H - centerH) * (K - centerK) +
479
+ coeffs[2] * (b_ / a_) ** 2 * (K - centerK) ** 2 +
480
+ coeffs[3] * (b_ * c_ / (a_ ** 2)) * (K - centerK) * (L - centerL) +
481
+ coeffs[4] * (c_ / a_) ** 2 * (L - centerL) ** 2 +
482
+ coeffs[5] * (c_ * a_ / (a_ ** 2)) * (L - centerL) * (H - centerH)) < punch_radius ** 2
483
+
484
+ if thresh:
485
+ mask = np.logical_and(mask, data[data.signal] > thresh)
581
486
 
582
487
  return mask
583
488
 
489
+ def punch(self):
490
+ data= self.data
491
+ self.punched = NXdata(NXfield(np.where(self.mask, np.nan, data[data.signal].nxdata), name=data.signal),
492
+ (data[data.axes[0]],data[data.axes[1]],data[data.axes[2]]))
493
+ return self.punched
584
494
 
585
495
 
586
496
  # class Reducer():
587
497
  # pass
588
498
  #
589
499
  #
590
- # class Interpolator():
591
- # pass
592
- #
593
- #
594
- # class FourierTransformer():
595
- # pass
500
+
501
+ class Interpolator():
502
+ def __init__(self):
503
+ self.data = None
504
+
505
+ def set_data(self, data):
506
+ self.data = data
507
+
508
+ def set_kernel(self, kernel):
509
+ self.kernel = kernel
510
+
511
+ def generate_gaussian_kernel(self, amp, stddev, coeffs=None):
512
+ pass
513
+
514
+ def interpolate(self):
515
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nxs-analysis-tools
3
- Version: 0.0.23
3
+ Version: 0.0.25
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
@@ -41,8 +41,28 @@ Classifier: Topic :: Scientific/Engineering :: Image Processing
41
41
  Classifier: Topic :: Scientific/Engineering
42
42
  Requires-Python: >=3.7
43
43
  Description-Content-Type: text/markdown
44
- Provides-Extra: dev
45
44
  License-File: LICENSE
45
+ Requires-Dist: matplotlib>=3.7.1
46
+ Requires-Dist: numpy>=1.24.3
47
+ Requires-Dist: ipython>=1.0.0
48
+ Requires-Dist: pandas>=2.0.2
49
+ Requires-Dist: nexusformat>=1.0.1
50
+ Requires-Dist: lmfit>=1.2.1
51
+ Provides-Extra: dev
52
+ Requires-Dist: build>=0.8.0; extra == "dev"
53
+ Requires-Dist: furo>=2022.6.21; extra == "dev"
54
+ Requires-Dist: ipykernel>=6.9.1; extra == "dev"
55
+ Requires-Dist: myst-nb>=0.16.0; extra == "dev"
56
+ Requires-Dist: myst-parser>=0.18.0; extra == "dev"
57
+ Requires-Dist: numpydoc>=1.4.0; extra == "dev"
58
+ Requires-Dist: pandoc>=2.2; extra == "dev"
59
+ Requires-Dist: pylint>=2.12.2; extra == "dev"
60
+ Requires-Dist: pytest>=7.1.2; extra == "dev"
61
+ Requires-Dist: sphinx>=5.0.2; extra == "dev"
62
+ Requires-Dist: sphinx-autobuild>=2021.3.14; extra == "dev"
63
+ Requires-Dist: sphinx-copybutton>=0.5.0; extra == "dev"
64
+ Requires-Dist: sphinxext-opengraph>=0.6.3; extra == "dev"
65
+ Requires-Dist: twine>=4.0.1; extra == "dev"
46
66
 
47
67
  # nxs-analysis-tools
48
68
 
@@ -0,0 +1,19 @@
1
+ # import sys
2
+ # sys.path.append('../src/nxs_analysis_tools/')
3
+ from nxs_analysis_tools.datareduction import load_data, Scissors, rotate_data, plot_slice
4
+
5
+ data = load_data('../docs/source/examples/example_data/sample_name/15/example_hkli.nxs')
6
+ # scissors = Scissors(data, center=(0,0,0), window=(0.1,2,0.3))
7
+ # scissors.cut_data()
8
+ # print(scissors.integration_window)
9
+ # scissors.plot_integration_window()
10
+ # scissors.linecut.plot()
11
+ # scissors.highlight_integration_window()
12
+
13
+ import matplotlib.pyplot as plt
14
+
15
+ plot_slice(data[:, :, 0.0])
16
+ plt.show()
17
+ rotated_data = rotate_data(data=data, lattice_angle=90, rotation_angle=45, rotation_axis=2)
18
+ plot_slice(rotated_data[:, :, 0.0])
19
+ plt.show()
@@ -1,7 +1,5 @@
1
- import sys
2
- sys.path.append('../src/nxs_analysis_tools/')
3
- from datareduction import load_data
4
- from pairdistribution import *
1
+ from nxs_analysis_tools.datareduction import load_data
2
+ from nxs_analysis_tools.pairdistribution import *
5
3
  import matplotlib.pyplot as plt
6
4
 
7
5
  data = load_data('../docs/source/examples/example_data/pairdistribution_data/test_hkli.nxs')
@@ -21,7 +19,22 @@ data = load_data('../docs/source/examples/example_data/pairdistribution_data/tes
21
19
  p = Puncher()
22
20
  p.set_data(data)
23
21
  p.set_lattice_params((1,1,1,90,90,90))
24
- p.set_gaussian_background(amp=1, stddev=0.2)
25
- p.plot_gaussian_background()
26
- p.plot_background_subtraction()
22
+ bm = p.generate_bragg_mask(punch_radius=0.25)
23
+ p.add_mask(bm)
24
+ p.punch()
25
+ plot_slice(p.punched[:,:,0.0])
26
+ plt.show()
27
27
 
28
+ m = p.generate_mask_at_coord(coordinate=(0.33, 0.33, 0.0), punch_radius=0.25)
29
+ p.add_mask(m)
30
+ p.punch()
31
+ plot_slice(p.punched[:,:,0.0])
32
+ plt.show()
33
+
34
+ m = p.generate_mask_at_coord(coordinate=(-0.1, -0.1, 0.0), punch_radius=0.2)
35
+ p.subtract_mask(m)
36
+ p.punch()
37
+ plot_slice(p.punched[:,:,0.0])
38
+ plt.show()
39
+
40
+ # SUCCESS
@@ -1,10 +0,0 @@
1
- import sys
2
- sys.path.append('../src/nxs_analysis_tools/')
3
- from datareduction import load_data, Scissors
4
- data = load_data('../docs/source/examples/example_data/sample_name/15/example_hkli.nxs')
5
- scissors = Scissors(data, center=(0,0,0), window=(0.1,2,0.3))
6
- scissors.cut_data()
7
- # print(scissors.integration_window)
8
- # scissors.plot_integration_window()
9
- # scissors.linecut.plot()
10
- scissors.highlight_integration_window()