nxs-analysis-tools 0.1.10__py3-none-any.whl → 0.1.11__py3-none-any.whl

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.

_meta/__init__.py CHANGED
@@ -6,5 +6,5 @@ __author__ = 'Steven J. Gomez Alvarado'
6
6
  __email__ = 'stevenjgomez@ucsb.edu'
7
7
  __copyright__ = f"2023-2025, {__author__}"
8
8
  __license__ = 'MIT'
9
- __version__ = '0.1.10'
9
+ __version__ = '0.1.11'
10
10
  __repo_url__ = 'https://github.com/stevenjgomez/nxs_analysis_tools'
@@ -9,7 +9,7 @@ from .chess import TempDependence
9
9
 
10
10
  # What to import when running "from nxs_analysis_tools import *"
11
11
  __all__ = ['load_data', 'load_transform', 'plot_slice', 'Scissors',
12
- 'reciprocal_lattice_params', 'rotate_data', 'rotate_data_2D',
12
+ 'reciprocal_lattice_params', 'rotate_data',
13
13
  'convert_to_inverse_angstroms', 'array_to_nxdata', 'Padder',
14
14
  'rebin_nxdata', 'rebin_3d', 'rebin_1d', 'TempDependence',
15
15
  'animate_slice_temp', 'animate_slice_axis']
@@ -15,12 +15,14 @@ from matplotlib import colors
15
15
  from matplotlib import patches
16
16
  from IPython.display import display, Markdown, HTML, Image
17
17
  from nexusformat.nexus import NXfield, NXdata, nxload, NeXusError, NXroot, NXentry, nxsave
18
- from scipy import ndimage
18
+ from scipy.ndimage import rotate
19
+
20
+ from .lineartransformations import ShearTransformer
19
21
 
20
22
 
21
23
  # Specify items on which users are allowed to perform standalone imports
22
24
  __all__ = ['load_data', 'load_transform', 'plot_slice', 'Scissors',
23
- 'reciprocal_lattice_params', 'rotate_data', 'rotate_data_2D',
25
+ 'reciprocal_lattice_params', 'rotate_data',
24
26
  'convert_to_inverse_angstroms', 'array_to_nxdata', 'Padder',
25
27
  'rebin_nxdata', 'rebin_3d', 'rebin_1d', 'animate_slice_temp',
26
28
  'animate_slice_axis']
@@ -503,17 +505,8 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
503
505
  p = ax.pcolormesh(X.nxdata, Y.nxdata, data_arr, shading='auto', norm=norm, cmap=cmap, **kwargs)
504
506
 
505
507
  ## Transform data to new coordinate system if necessary
506
- # Correct skew angle
507
- skew_angle_adj = 90 - skew_angle
508
- # Create blank 2D affine transformation
509
- t = Affine2D()
510
- # Scale y-axis to preserve norm while shearing
511
- t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
512
- # Shear along x-axis
513
- t += Affine2D().skew_deg(skew_angle_adj, 0)
514
- # Return to original y-axis scaling
515
- t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
516
- ## Correct for x-displacement after shearing
508
+ t = ShearTransformer(skew_angle)
509
+
517
510
  # If ylims provided, use those
518
511
  if ylim is not None:
519
512
  # Set ylims
@@ -523,8 +516,8 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
523
516
  else:
524
517
  ymin, ymax = ax.get_ylim()
525
518
  # Use ylims to calculate translation (necessary to display axes in correct position)
526
- p.set_transform(t
527
- + Affine2D().translate(-ymin * np.sin(skew_angle_adj * np.pi / 180), 0)
519
+ p.set_transform(t.t
520
+ + Affine2D().translate(-ymin * np.sin(t.shear_angle * np.pi / 180), 0)
528
521
  + ax.transData)
529
522
 
530
523
  # Set x limits
@@ -533,12 +526,12 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
533
526
  else:
534
527
  xmin, xmax = ax.get_xlim()
535
528
  if skew_angle <= 90:
536
- ax.set(xlim=(xmin, xmax + (ymax - ymin) / np.tan((90 - skew_angle_adj) * np.pi / 180)))
529
+ ax.set(xlim=(xmin, xmax + (ymax - ymin) / np.tan((90 - t.shear_angle) * np.pi / 180)))
537
530
  else:
538
- ax.set(xlim=(xmin - (ymax - ymin) / np.tan((skew_angle_adj - 90) * np.pi / 180), xmax))
531
+ ax.set(xlim=(xmin - (ymax - ymin) / np.tan((t.shear_angle - 90) * np.pi / 180), xmax))
539
532
 
540
533
  # Correct aspect ratio for the x/y axes after transformation
541
- ax.set(aspect=np.cos(skew_angle_adj * np.pi / 180))
534
+ ax.set(aspect=np.cos(t.shear_angle * np.pi / 180))
542
535
 
543
536
 
544
537
  # Automatically set tick locations, only if NXdata or if X,Y axes are provided for an array
@@ -570,7 +563,7 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
570
563
  line = ax.xaxis.get_majorticklines()[i]
571
564
  if i % 2:
572
565
  # Top ticks (translation here makes their direction="in")
573
- m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle_adj, 0))
566
+ m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(t.shear_angle, 0))
574
567
  # This first method shifts the top ticks horizontally to match the skew angle.
575
568
  # This does not look good in all cases.
576
569
  # line.set_transform(Affine2D().translate((ymax-ymin)*np.sin(skew_angle*np.pi/180),0) +
@@ -580,7 +573,7 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
580
573
  line.set_transform(line.get_transform()) # This does nothing
581
574
  else:
582
575
  # Bottom ticks
583
- m._transform.set(Affine2D().skew_deg(skew_angle_adj, 0))
576
+ m._transform.set(Affine2D().skew_deg(t.shear_angle, 0))
584
577
 
585
578
  line.set_marker(m)
586
579
 
@@ -588,9 +581,9 @@ def plot_slice(data, X=None, Y=None, sum_axis=None, transpose=False, vmin=None,
588
581
  m = MarkerStyle(2)
589
582
  line = ax.xaxis.get_minorticklines()[i]
590
583
  if i % 2:
591
- m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(skew_angle_adj, 0))
584
+ m._transform.set(Affine2D().translate(0, -1) + Affine2D().skew_deg(t.shear_angle, 0))
592
585
  else:
593
- m._transform.set(Affine2D().skew_deg(skew_angle_adj, 0))
586
+ m._transform.set(Affine2D().skew_deg(t.shear_angle, 0))
594
587
 
595
588
  line.set_marker(m)
596
589
 
@@ -1234,9 +1227,9 @@ def convert_to_inverse_angstroms(data, lattice_params):
1234
1227
  return NXdata(new_data, (a_star, b_star, c_star))
1235
1228
 
1236
1229
 
1237
- def rotate_data(data, lattice_angle, rotation_angle, rotation_axis, printout=False):
1230
+ def rotate_data(data, lattice_angle, rotation_angle, rotation_axis=None, printout=False):
1238
1231
  """
1239
- Rotates 3D data around a specified axis.
1232
+ Rotates slices of data around the normal axis.
1240
1233
 
1241
1234
  Parameters
1242
1235
  ----------
@@ -1246,13 +1239,12 @@ def rotate_data(data, lattice_angle, rotation_angle, rotation_axis, printout=Fal
1246
1239
  Angle between the two in-plane lattice axes in degrees.
1247
1240
  rotation_angle : float
1248
1241
  Angle of rotation in degrees.
1249
- rotation_axis : int
1250
- Axis of rotation (0, 1, or 2).
1242
+ rotation_axis : int, optional
1243
+ Axis of rotation (0, 1, or 2). Only necessary when data is three-dimensional.
1251
1244
  printout : bool, optional
1252
- Enables printout of rotation progress. If set to True, information
1253
- about each rotation slice will be printed to the console, indicating
1254
- the axis being rotated and the corresponding coordinate value.
1255
- Defaults to False.
1245
+ Enables printout of rotation progress for three-dimensional data. If set to True,
1246
+ information about each rotation slice will be printed to the console, indicating
1247
+ the axis being rotated and the corresponding coordinate value. Defaults to False.
1256
1248
 
1257
1249
 
1258
1250
  Returns
@@ -1260,36 +1252,36 @@ def rotate_data(data, lattice_angle, rotation_angle, rotation_axis, printout=Fal
1260
1252
  rotated_data : :class:`nexusformat.nexus.NXdata`
1261
1253
  Rotated data as an NXdata object.
1262
1254
  """
1255
+
1256
+ if data.ndim == 3 and rotation_axis is None:
1257
+ raise ValueError('rotation_axis must be specified for three-dimensional datasets.')
1258
+
1259
+ if not((data.ndim == 2) or (data.ndim == 3)):
1260
+ raise ValueError('Data must be 2 or 3 dimensional.')
1261
+
1263
1262
  # Define output array
1264
1263
  output_array = np.zeros(data.nxsignal.shape)
1265
1264
 
1266
- # Define shear transformation
1267
- skew_angle_adj = 90 - lattice_angle
1268
- t = Affine2D()
1269
- # Scale y-axis to preserve norm while shearing
1270
- t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
1271
- # Shear along x-axis
1272
- t += Affine2D().skew_deg(skew_angle_adj, 0)
1273
- # Return to original y-axis scaling
1274
- t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
1275
-
1276
1265
  # Iterate over all layers perpendicular to the rotation axis
1277
- for i in range(len(data[data.axes[rotation_axis]])):
1278
- # Print progress
1279
- if printout:
1280
- print(f'\rRotating {data.axes[rotation_axis]}'
1281
- f'={data[data.axes[rotation_axis]][i]}... ',
1282
- end='', flush=True)
1283
-
1284
- # Identify current slice
1285
- if rotation_axis == 0:
1286
- sliced_data = data[i, :, :]
1287
- elif rotation_axis == 1:
1288
- sliced_data = data[:, i, :]
1289
- elif rotation_axis == 2:
1290
- sliced_data = data[:, :, i]
1291
- else:
1292
- sliced_data = None
1266
+ if data.ndim == 3:
1267
+ num_slices = len(data.nxaxes[rotation_axis])
1268
+ elif data.ndim == 2:
1269
+ num_slices = 1
1270
+
1271
+ for i in range(num_slices):
1272
+
1273
+ if data.ndim == 3:
1274
+ # Print progress
1275
+ if printout:
1276
+ print(f'\rRotating {data.axes[rotation_axis]}'
1277
+ f'={data.nxaxes[rotation_axis][i]}... ',
1278
+ end='', flush=True)
1279
+ index = [slice(None)] * 3
1280
+ index[rotation_axis] = i
1281
+ sliced_data = data[tuple(index)]
1282
+
1283
+ elif data.ndim == 2:
1284
+ sliced_data = data
1293
1285
 
1294
1286
  # Add padding to avoid data cutoff during rotation
1295
1287
  p = Padder(sliced_data)
@@ -1297,76 +1289,38 @@ def rotate_data(data, lattice_angle, rotation_angle, rotation_axis, printout=Fal
1297
1289
  counts = p.pad(padding)
1298
1290
  counts = p.padded[p.padded.signal]
1299
1291
 
1300
- # Perform shear operation
1301
- counts_skewed = ndimage.affine_transform(counts,
1302
- t.inverted().get_matrix()[:2, :2],
1303
- offset=[counts.shape[0] / 2
1304
- * np.sin(skew_angle_adj * np.pi / 180),
1305
- 0],
1306
- order=0,
1307
- )
1308
- # Scale data based on skew angle
1309
- scale1 = np.cos(skew_angle_adj * np.pi / 180)
1310
- counts_scaled1 = ndimage.affine_transform(counts_skewed,
1311
- Affine2D().scale(scale1, 1).get_matrix()[:2, :2],
1312
- offset=[(1 - scale1) * counts.shape[0] / 2, 0],
1313
- order=0,
1314
- )
1315
- # Scale data based on ratio of array dimensions
1316
- scale2 = 1 # counts.shape[0] / counts.shape[1]
1317
- counts_scaled2 = ndimage.affine_transform(counts_scaled1,
1318
- Affine2D().scale(scale2, 1).get_matrix()[:2, :2],
1319
- offset=[(1 - scale2) * counts.shape[0] / 2, 0],
1320
- order=0,
1321
- )
1292
+ # Skew data to match lattice angle
1293
+ t = ShearTransformer(lattice_angle)
1294
+ counts = t.apply(counts)
1322
1295
 
1323
1296
  # Perform rotation
1324
- counts_rotated = ndimage.rotate(counts_scaled2, rotation_angle, reshape=False, order=0)
1325
-
1326
- # Undo scaling 2
1327
- counts_unscaled2 = ndimage.affine_transform(counts_rotated,
1328
- Affine2D().scale(
1329
- scale2, 1
1330
- ).inverted().get_matrix()[:2, :2],
1331
- offset=[-(1 - scale2) * counts.shape[
1332
- 0] / 2 / scale2, 0],
1333
- order=0,
1334
- )
1335
- # Undo scaling 1
1336
- counts_unscaled1 = ndimage.affine_transform(counts_unscaled2,
1337
- Affine2D().scale(
1338
- scale1, 1
1339
- ).inverted().get_matrix()[:2, :2],
1340
- offset=[-(1 - scale1) * counts.shape[
1341
- 0] / 2 / scale1, 0],
1342
- order=0,
1343
- )
1344
- # Undo shear operation
1345
- counts_unskewed = ndimage.affine_transform(counts_unscaled1,
1346
- t.get_matrix()[:2, :2],
1347
- offset=[
1348
- (-counts.shape[0] / 2
1349
- * np.sin(skew_angle_adj * np.pi / 180)),
1350
- 0],
1351
- order=0,
1352
- )
1297
+ counts = rotate(counts, rotation_angle, reshape=False, order=0)
1298
+
1299
+ # Undo skew transformation
1300
+ counts = t.invert(counts)
1301
+
1353
1302
  # Remove padding
1354
- counts_unpadded = p.unpad(counts_unskewed)
1355
-
1356
- # Write current slice
1357
- if rotation_axis == 0:
1358
- output_array[i, :, :] = counts_unpadded
1359
- elif rotation_axis == 1:
1360
- output_array[:, i, :] = counts_unpadded
1361
- elif rotation_axis == 2:
1362
- output_array[:, :, i] = counts_unpadded
1363
- print('\nDone.')
1303
+ counts = p.unpad(counts)
1304
+
1305
+ # Write slice
1306
+ if data.ndim == 3:
1307
+ index = [slice(None)] * 3
1308
+ index[rotation_axis] = i
1309
+ output_array[tuple(index)] = counts
1310
+ elif data.ndim == 2:
1311
+ output_array = counts
1312
+
1313
+ print('\nRotation completed.')
1314
+
1364
1315
  return NXdata(NXfield(output_array, name=p.padded.signal),
1365
- (data.nxaxes[0], data.nxaxes[1], data.nxaxes[2]))
1316
+ ([axis for axis in data.nxaxes]))
1317
+
1366
1318
 
1367
1319
 
1368
1320
  def rotate_data_2D(data, lattice_angle, rotation_angle):
1369
1321
  """
1322
+ DEPRECATED: Use `rotate_data` instead.
1323
+
1370
1324
  Rotates 2D data.
1371
1325
 
1372
1326
  Parameters
@@ -1378,86 +1332,20 @@ def rotate_data_2D(data, lattice_angle, rotation_angle):
1378
1332
  rotation_angle : float
1379
1333
  Angle of rotation in degrees.
1380
1334
 
1381
-
1382
1335
  Returns
1383
1336
  -------
1384
1337
  rotated_data : :class:`nexusformat.nexus.NXdata`
1385
1338
  Rotated data as an NXdata object.
1386
1339
  """
1340
+ warnings.warn(
1341
+ "rotate_data_2D is deprecated and will be removed in a future release. "
1342
+ "Use rotate_data instead.",
1343
+ DeprecationWarning,
1344
+ stacklevel=2,
1345
+ )
1387
1346
 
1388
- # Define transformation
1389
- skew_angle_adj = 90 - lattice_angle
1390
- t = Affine2D()
1391
- # Scale y-axis to preserve norm while shearing
1392
- t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
1393
- # Shear along x-axis
1394
- t += Affine2D().skew_deg(skew_angle_adj, 0)
1395
- # Return to original y-axis scaling
1396
- t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
1397
-
1398
- # Add padding to avoid data cutoff during rotation
1399
- p = Padder(data)
1400
- padding = tuple(len(data[axis]) for axis in data.axes)
1401
- counts = p.pad(padding)
1402
- counts = p.padded[p.padded.signal]
1403
-
1404
- # Perform shear operation
1405
- counts_skewed = ndimage.affine_transform(counts,
1406
- t.inverted().get_matrix()[:2, :2],
1407
- offset=[counts.shape[0] / 2
1408
- * np.sin(skew_angle_adj * np.pi / 180), 0],
1409
- order=0,
1410
- )
1411
- # Scale data based on skew angle
1412
- scale1 = np.cos(skew_angle_adj * np.pi / 180)
1413
- counts_scaled1 = ndimage.affine_transform(counts_skewed,
1414
- Affine2D().scale(scale1, 1).get_matrix()[:2, :2],
1415
- offset=[(1 - scale1) * counts.shape[0] / 2, 0],
1416
- order=0,
1417
- )
1418
- # Scale data based on ratio of array dimensions
1419
- scale2 = 1 # counts.shape[0] / counts.shape[1]
1420
- counts_scaled2 = ndimage.affine_transform(counts_scaled1,
1421
- Affine2D().scale(scale2, 1).get_matrix()[:2, :2],
1422
- offset=[(1 - scale2) * counts.shape[0] / 2, 0],
1423
- order=0,
1424
- )
1425
- # Perform rotation
1426
- counts_rotated = ndimage.rotate(counts_scaled2, rotation_angle, reshape=False, order=0)
1427
-
1428
- # Undo scaling 2
1429
- counts_unscaled2 = ndimage.affine_transform(counts_rotated,
1430
- Affine2D().scale(
1431
- scale2, 1
1432
- ).inverted().get_matrix()[:2, :2],
1433
- offset=[-(1 - scale2) * counts.shape[
1434
- 0] / 2 / scale2, 0],
1435
- order=0,
1436
- )
1437
- # Undo scaling 1
1438
- counts_unscaled1 = ndimage.affine_transform(counts_unscaled2,
1439
- Affine2D().scale(
1440
- scale1, 1
1441
- ).inverted().get_matrix()[:2, :2],
1442
- offset=[-(1 - scale1) * counts.shape[
1443
- 0] / 2 / scale1, 0],
1444
- order=0,
1445
- )
1446
- # Undo shear operation
1447
- counts_unskewed = ndimage.affine_transform(counts_unscaled1,
1448
- t.get_matrix()[:2, :2],
1449
- offset=[
1450
- (-counts.shape[0] / 2
1451
- * np.sin(skew_angle_adj * np.pi / 180)),
1452
- 0],
1453
- order=0,
1454
- )
1455
- # Remove padding
1456
- counts_unpadded = p.unpad(counts_unskewed)
1457
-
1458
- print('\nDone.')
1459
- return NXdata(NXfield(counts_unpadded, name=p.padded.signal),
1460
- (data.nxaxes[0], data.nxaxes[1]))
1347
+ # Call the new general function
1348
+ return rotate_data(data, lattice_angle=lattice_angle, rotation_angle=rotation_angle)
1461
1349
 
1462
1350
 
1463
1351
  class Padder:
@@ -0,0 +1,51 @@
1
+ import numpy as np
2
+ from scipy.ndimage import affine_transform
3
+ from matplotlib.transforms import Affine2D
4
+
5
+ def shear_transformation(angle):
6
+ # Define shear transformation
7
+ t = Affine2D()
8
+
9
+ # Scale y-axis to preserve norm while shearing
10
+ t += Affine2D().scale(1, np.cos(angle * np.pi / 180))
11
+
12
+ # Shear along x-axis
13
+ t += Affine2D().skew_deg(angle, 0)
14
+
15
+ # Return to original y-axis scaling
16
+ t += Affine2D().scale(1, np.cos(angle * np.pi / 180)).inverted()
17
+
18
+ return t
19
+
20
+ class ShearTransformer():
21
+ def __init__(self, angle):
22
+ self.shear_angle = 90 - angle
23
+ self.t = shear_transformation(self.shear_angle)
24
+ self.scale = np.cos(self.shear_angle * np.pi / 180)
25
+
26
+ def apply(self, image):
27
+ # Perform shear operation
28
+ image_skewed = affine_transform(image, self.t.inverted().get_matrix()[:2, :2],
29
+ offset=[image.shape[0] / 2 * np.sin(self.shear_angle * np.pi / 180), 0],
30
+ order=0
31
+ )
32
+ # Scale data based on skew angle
33
+ image_scaled = affine_transform(image_skewed, Affine2D().scale(self.scale, 1).get_matrix()[:2, :2],
34
+ offset=[(1 - self.scale) * image.shape[0] / 2, 0],
35
+ order=0
36
+ )
37
+ return image_scaled
38
+
39
+ def invert(self, image):
40
+
41
+ # Undo scaling
42
+ image_unscaled = affine_transform(image, Affine2D().scale(self.scale, 1).inverted().get_matrix()[:2, :2],
43
+ offset=[-(1 - self.scale) * image.shape[0] / 2 / self.scale, 0],
44
+ order=0
45
+ )
46
+ # Undo shear operation
47
+ image_unskewed = affine_transform(image_unscaled, self.t.get_matrix()[:2, :2],
48
+ offset=[(-image.shape[0] / 2 * np.sin(self.shear_angle * np.pi / 180)), 0],
49
+ order=0
50
+ )
51
+ return image_unskewed
@@ -5,7 +5,7 @@ import time
5
5
  import os
6
6
  import gc
7
7
  import math
8
- from scipy import ndimage
8
+ from scipy.ndimage import rotate, affine_transform
9
9
  import scipy
10
10
  import matplotlib.pyplot as plt
11
11
  from matplotlib.transforms import Affine2D
@@ -15,6 +15,7 @@ from astropy.convolution import Kernel, convolve_fft
15
15
  import pyfftw
16
16
  from .datareduction import plot_slice, reciprocal_lattice_params, Padder, \
17
17
  array_to_nxdata
18
+ from .lineartransformations import ShearTransformer
18
19
 
19
20
  __all__ = ['Symmetrizer2D', 'Symmetrizer3D', 'Puncher', 'Interpolator',
20
21
  'fourier_transform_nxdata', 'Gaussian3DKernel', 'DeltaPDF',
@@ -37,9 +38,6 @@ class Symmetrizer2D:
37
38
  symmetrized : NXdata or None
38
39
  The symmetrized dataset after applying the symmetrization operations.
39
40
  Default is None until symmetrization is performed.
40
- wedges : NXdata or None
41
- The wedges extracted from the dataset based on the angular limits.
42
- Default is None until symmetrization is performed.
43
41
  rotations : int or None
44
42
  The number of rotations needed to reconstruct the full dataset from
45
43
  a single wedge. Default is None until parameters are set.
@@ -93,7 +91,6 @@ class Symmetrizer2D:
93
91
  """
94
92
  self.mirror_axis = None
95
93
  self.symmetrized = None
96
- self.wedges = None
97
94
  self.rotations = None
98
95
  self.transform = None
99
96
  self.mirror = None
@@ -129,16 +126,8 @@ class Symmetrizer2D:
129
126
  self.mirror = mirror
130
127
  self.mirror_axis = mirror_axis
131
128
 
132
- # Define Transformation
133
- skew_angle_adj = 90 - lattice_angle
134
- t = Affine2D()
135
- # Scale y-axis to preserve norm while shearing
136
- t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180))
137
- # Shear along x-axis
138
- t += Affine2D().skew_deg(skew_angle_adj, 0)
139
- # Return to original y-axis scaling
140
- t += Affine2D().scale(1, np.cos(skew_angle_adj * np.pi / 180)).inverted()
141
- self.transform = t
129
+ self.transformer = ShearTransformer(lattice_angle)
130
+ self.transform = self.transformer.t
142
131
 
143
132
  # Calculate number of rotations needed to reconstruct the dataset
144
133
  if mirror:
@@ -172,7 +161,6 @@ class Symmetrizer2D:
172
161
  theta_max = self.theta_max
173
162
  mirror = self.mirror
174
163
  mirror_axis = self.mirror_axis
175
- t = self.transform
176
164
  rotations = self.rotations
177
165
 
178
166
  # Pad the dataset so that rotations don't get cutoff if they extend
@@ -191,37 +179,9 @@ class Symmetrizer2D:
191
179
  symmetrization_mask = np.logical_and(theta >= theta_min * np.pi / 180,
192
180
  theta <= theta_max * np.pi / 180)
193
181
 
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 = 1 # q1.max()/q2.max() # TODO: Need to double check this
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)
182
+ # Bring mask from skewed basis to data array basis
183
+ mask = array_to_nxdata(self.transformer.invert(symmetrization_mask), data_padded)
184
+
225
185
 
226
186
  # Save mask for user interaction
227
187
  self.symmetrization_mask = p.unpad(mask)
@@ -235,84 +195,49 @@ class Symmetrizer2D:
235
195
  # Convert wedge back to array for further transformations
236
196
  wedge = wedge[data.signal].nxdata
237
197
 
238
- # Define signal to be transformed
239
- counts = wedge
198
+ # Bring wedge from data array basis to skewed basis for reconstruction
199
+ wedge = self.transformer.apply(wedge)
240
200
 
241
- # Scale and skew counts
242
- skew_angle_adj = 90 - self.skew_angle
243
- counts_skew = ndimage.affine_transform(counts,
244
- t.inverted().get_matrix()[:2, :2],
245
- offset=[counts.shape[0] / 2
246
- * np.sin(skew_angle_adj * np.pi / 180), 0],
247
- order=0,
248
- )
249
- scale1 = np.cos(skew_angle_adj * np.pi / 180)
250
- wedge = ndimage.affine_transform(counts_skew,
251
- Affine2D().scale(scale1, 1).get_matrix()[:2, :2],
252
- offset=[(1 - scale1) * counts.shape[0] / 2, 0],
253
- order=0,
254
- )
255
-
256
- scale2 = counts.shape[0]/counts.shape[1]
257
- wedge = ndimage.affine_transform(wedge,
258
- Affine2D().scale(scale2, 1).get_matrix()[:2, :2],
259
- offset=[(1 - scale2) * counts.shape[0] / 2, 0],
201
+ # Apply additional scaling before rotations
202
+ scale = wedge.shape[0]/wedge.shape[1]
203
+ wedge = affine_transform(wedge,
204
+ Affine2D().scale(scale, 1).get_matrix()[:2, :2],
205
+ offset=[(1 - scale) * wedge.shape[0] / 2, 0],
260
206
  order=0,
261
207
  )
262
208
 
263
209
  # Reconstruct full dataset from wedge
264
- reconstructed = np.zeros(counts.shape)
210
+ reconstructed = np.zeros(wedge.shape)
211
+
265
212
  for _ in range(0, rotations):
266
- # The following are attempts to combine images with minimal overlapping pixels
267
213
  reconstructed += wedge
268
- # reconstructed = np.where(reconstructed == 0, reconstructed + wedge, reconstructed)
269
-
270
- wedge = ndimage.rotate(wedge, 360 / rotations, reshape=False, order=0)
271
-
272
- # self.rotated_only = NXdata(NXfield(reconstructed, name=data.signal),
273
- # (q1, q2))
214
+ wedge = rotate(wedge, 360 / rotations, reshape=False, order=0)
274
215
 
275
216
  if mirror:
276
- # The following are attempts to combine images with minimal overlapping pixels
277
217
  reconstructed = np.where(reconstructed == 0,
278
218
  reconstructed + np.flip(reconstructed, axis=mirror_axis),
279
219
  reconstructed)
280
- # reconstructed += np.flip(reconstructed, axis=0)
281
-
282
- # self.rotated_and_mirrored = NXdata(NXfield(reconstructed, name=data.signal),
283
- # (q1, q2))
220
+
284
221
 
285
- reconstructed = ndimage.affine_transform(reconstructed,
286
- Affine2D().scale(
287
- scale2, 1
288
- ).inverted().get_matrix()[:2, :2],
289
- offset=[-(1 - scale2) * counts.shape[
290
- 0] / 2 / scale2, 0],
291
- order=0,
292
- )
293
- reconstructed = ndimage.affine_transform(reconstructed,
222
+ # Undo scaling transformation
223
+ reconstructed = affine_transform(reconstructed,
294
224
  Affine2D().scale(
295
- scale1, 1
225
+ scale, 1
296
226
  ).inverted().get_matrix()[:2, :2],
297
- offset=[-(1 - scale1) * counts.shape[
298
- 0] / 2 / scale1, 0],
299
- order=0,
300
- )
301
- reconstructed = ndimage.affine_transform(reconstructed,
302
- t.get_matrix()[:2, :2],
303
- offset=[(-counts.shape[0] / 2
304
- * np.sin(skew_angle_adj * np.pi / 180)),
305
- 0],
227
+ offset=[-(1 - scale) * wedge.shape[
228
+ 0] / 2 / scale, 0],
306
229
  order=0,
307
230
  )
231
+
232
+ reconstructed = self.transformer.invert(reconstructed)
308
233
 
309
- reconstructed_unpadded = p.unpad(reconstructed)
234
+ reconstructed = p.unpad(reconstructed)
310
235
 
311
236
  # Fix any overlapping pixels by truncating counts to max
312
- reconstructed_unpadded[reconstructed_unpadded > data[data.signal].nxdata.max()] \
237
+ reconstructed[reconstructed > data[data.signal].nxdata.max()] \
313
238
  = data[data.signal].nxdata.max()
314
239
 
315
- symmetrized = NXdata(NXfield(reconstructed_unpadded, name=data.signal),
240
+ symmetrized = NXdata(NXfield(reconstructed, name=data.signal),
316
241
  (data[data.axes[0]],
317
242
  data[data.axes[1]]))
318
243
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nxs-analysis-tools
3
- Version: 0.1.10
3
+ Version: 0.1.11
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-Expression: MIT
@@ -14,11 +14,10 @@ Classifier: Development Status :: 5 - Production/Stable
14
14
  Classifier: Intended Audience :: Science/Research
15
15
  Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.9
18
17
  Classifier: Programming Language :: Python :: 3.10
19
18
  Classifier: Topic :: Scientific/Engineering :: Image Processing
20
19
  Classifier: Topic :: Scientific/Engineering
21
- Requires-Python: >=3.7
20
+ Requires-Python: >=3.10
22
21
  Description-Content-Type: text/markdown
23
22
  License-File: LICENSE
24
23
  Requires-Dist: matplotlib>=3.10.0
@@ -57,7 +56,7 @@ Dynamic: license-file
57
56
 
58
57
  ## Overview
59
58
 
60
- nxs-analysis-tools provides a suite of tools for slicing (2D), cutting (1D), and transforming (_e.g._, symmetrization, interpolation, delta-PDF) nexus format (.nxs) scattering data.
59
+ nxs-analysis-tools provides a suite of tools for performing slices (2D), cuts (1D), and transformations (_e.g._, symmetrization, interpolation, delta-PDF) on nexus format (.nxs) scattering data.
61
60
 
62
61
  View the documentation [here](https://nxs-analysis-tools.readthedocs.io/en/stable/).
63
62
 
@@ -0,0 +1,13 @@
1
+ _meta/__init__.py,sha256=O-Csyugy_6ZYl7nR_6FgU1-D1-64dSBtRSRzDF1aiq4,347
2
+ nxs_analysis_tools/__init__.py,sha256=Gs61l3FrgmhVRv77oqz58W6vmxFhNARfcioYA0FZbqU,604
3
+ nxs_analysis_tools/chess.py,sha256=_gFkLZnoYSCOKPZHm0b1CileCveHJpOszLufeg4QKFg,34086
4
+ nxs_analysis_tools/datareduction.py,sha256=Ky8Q5izEXmOUj3OKOlvZ8A-YeKRdAOD22luggiZtLoY,52181
5
+ nxs_analysis_tools/datasets.py,sha256=KnJBdxuCV7n2Q6MOM0Cv5-Dq6x1hvrrEXIc3Jn7Xsos,3497
6
+ nxs_analysis_tools/fitting.py,sha256=kRMhjObetGqmZ5-Jk1OHKGrXW4qI4D37s8VeC2ygJV8,10275
7
+ nxs_analysis_tools/lineartransformations.py,sha256=-Ce2RzcRcUgg8_kM1o0kO9lOpS_nC2AxRyZTUA1tAe8,2144
8
+ nxs_analysis_tools/pairdistribution.py,sha256=u4WyOfK_nBX78QJr7QO8QWGKV9bH_sBWtdzVaqs2wWo,61238
9
+ nxs_analysis_tools-0.1.11.dist-info/licenses/LICENSE,sha256=bE6FnYixueAGAnEfUuumbkSeMgdBguAAkheVgjv47Jo,1086
10
+ nxs_analysis_tools-0.1.11.dist-info/METADATA,sha256=sdphx8gVih0ZeQOPEHw4_R-sS6D6XlQNZWc6JOxAB5g,3493
11
+ nxs_analysis_tools-0.1.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ nxs_analysis_tools-0.1.11.dist-info/top_level.txt,sha256=8U000GNPzo6T6pOMjRdgOSO5heMzLMGjkxa1CDtyMHM,25
13
+ nxs_analysis_tools-0.1.11.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- _meta/__init__.py,sha256=PpRK3SY3jA8q8ioGFF4DEjAaUUb35J7AB3hwAZ2iiMw,347
2
- nxs_analysis_tools/__init__.py,sha256=lutfLk7oBaMpKq2G2hf6V59SNqAhzSUyKLXGwTI_iDg,622
3
- nxs_analysis_tools/chess.py,sha256=_gFkLZnoYSCOKPZHm0b1CileCveHJpOszLufeg4QKFg,34086
4
- nxs_analysis_tools/datareduction.py,sha256=BexXztaNQ4yNJko_MBQy-eVF2J5C7BTANehvqkwRu_E,59314
5
- nxs_analysis_tools/datasets.py,sha256=KnJBdxuCV7n2Q6MOM0Cv5-Dq6x1hvrrEXIc3Jn7Xsos,3497
6
- nxs_analysis_tools/fitting.py,sha256=kRMhjObetGqmZ5-Jk1OHKGrXW4qI4D37s8VeC2ygJV8,10275
7
- nxs_analysis_tools/pairdistribution.py,sha256=bY80N9y50yPjVKPzWfcPSAtiA8UDPDNCblVXGOw2Lzo,65605
8
- nxs_analysis_tools-0.1.10.dist-info/licenses/LICENSE,sha256=bE6FnYixueAGAnEfUuumbkSeMgdBguAAkheVgjv47Jo,1086
9
- nxs_analysis_tools-0.1.10.dist-info/METADATA,sha256=f9qlf_zWAWqRPXqw6D-pJRiFcWV7xiS5vgrGXGJLE98,3529
10
- nxs_analysis_tools-0.1.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- nxs_analysis_tools-0.1.10.dist-info/top_level.txt,sha256=8U000GNPzo6T6pOMjRdgOSO5heMzLMGjkxa1CDtyMHM,25
12
- nxs_analysis_tools-0.1.10.dist-info/RECORD,,