Skip to content

Commit ebb93a5

Browse files
committed
Refactors sasmanipulations to use new data objects for input
1 parent dd691e0 commit ebb93a5

11 files changed

Lines changed: 449 additions & 454 deletions

sasdata/data.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,38 @@ def default(self, obj):
148148
}
149149
case _:
150150
return super().default(obj)
151+
152+
153+
def sasdata_reader2D_converter(data2d: SasData | None = None) -> SasData:
154+
"""
155+
convert old 2d format opened by IhorReader or danse_reader
156+
to new 2D SasData format
157+
This is mainly used by the Readers
158+
159+
:param data2d: SasData object with 2D arrays
160+
:return: SasData object with 1D arrays
161+
162+
"""
163+
if data2d._data_contents["I"] is None or data2d.x_bins is None or data2d.y_bins is None:
164+
raise ValueError("Can't convert this data: data=None...")
165+
new_x = np.tile(data2d.x_bins, (len(data2d.y_bins), 1))
166+
new_y = np.tile(data2d.y_bins, (len(data2d.x_bins), 1))
167+
new_y = new_y.swapaxes(0, 1)
168+
169+
new_data = data2d._data_contents["I"].value.flatten()
170+
qx_data = new_x.flatten()
171+
qy_data = new_y.flatten()
172+
err_data = np.sqrt(data2d._data_contents["I"].variance.value)
173+
if not data2d._data_contents["I"].has_variance or np.any(err_data <= 0):
174+
new_err_data = np.sqrt(np.abs(new_data))
175+
else:
176+
new_err_data = err_data.flatten()
177+
mask = np.ones(len(new_data), dtype=bool)
178+
179+
data2d._data_contents["I"].value = new_data
180+
data2d._data_contents["I"].variance.value = new_err_data ** 2
181+
data2d._data_contents["Qx"].value = qx_data
182+
data2d._data_contents["Qy"].value = qy_data
183+
data2d.mask = mask
184+
185+
return data2d

sasdata/data_util/averaging.py

Lines changed: 115 additions & 179 deletions
Large diffs are not rendered by default.

sasdata/data_util/binning.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class DirectionalAverage:
1111
Average along one coordinate axis of 2D data and return data for a 1D plot.
1212
This can also be thought of as a projection onto the major axis: 2D -> 1D.
1313
14-
This class operates on a decomposed Data2D object, and returns data needed
14+
This class operates on a decomposed SasData object, and returns data needed
1515
to construct a Data1D object. The class is instantiated with two arrays of
1616
orthogonal coordinate data (depending on the coordinate system, these may
1717
have undergone some pre-processing) and two corresponding two-element
@@ -20,7 +20,7 @@ class DirectionalAverage:
2020
the other is divided into bins and becomes the dependent variable of the
2121
eventual 1D plot. These are called the minor and major axes respectively.
2222
When a class instance is called, it is passed the intensity and error data
23-
from the original Data2D object. These should not have undergone any
23+
from the original SasData object. These should not have undergone any
2424
coordinate system dependent pre-processing.
2525
2626
Note that the old version of manipulations.py had an option for logarithmic
@@ -203,7 +203,7 @@ def __call__(self, data, err_data):
203203
"""
204204
Compute the directional average of the supplied intensity & error data.
205205
206-
:param data: intensity data from the origninal Data2D object.
206+
:param data: intensity data from the original SasData object.
207207
:param err_data: the corresponding errors for the intensity data.
208208
"""
209209
weights = self.compute_weights()

sasdata/data_util/manipulations.py

Lines changed: 64 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
"""
22
Data manipulations for 2D data sets.
3-
Using the meta data information, various types of averaging are performed in Q-space
3+
Using the meta data information, various types of averaging are performed in Q-space.
44
55
To test this module use:
66
```
77
cd test
8-
PYTHONPATH=../src/ python2 -m sasmanipulations.test.utest_averaging DataInfoTests.test_sectorphi_quarter
8+
PYTHONPATH=../src/ python2 -m sasmanipulations.test.utest_averaging DataInfoTests.test_sectorphi_quarter
99
```
1010
"""
1111
#####################################################################
@@ -42,6 +42,7 @@
4242
SlabY,
4343
WedgePhi,
4444
WedgeQ,
45+
get_dq_data,
4546
)
4647
from sasdata.dataloader.data_info import Data1D, Data2D
4748
from sasdata.dataloader.data_info import reader2D_converter as _di_reader2D_converter
@@ -70,10 +71,10 @@ def deduce_qz(qx: float, qy: float, wavelength: float) -> float:
7071

7172
def position_and_wavelength_to_q(dx: float, dy: float, detector_distance: float, wavelength: float) -> float:
7273
"""
73-
:param dx: x-distance from beam center [mm]
74-
:param dy: y-distance from beam center [mm]
75-
:param detector_distance: sample to detector distance [mm]
76-
:param wavelength: neutron wavelength [nm]
74+
:param dx: x-distance from beam center
75+
:param dy: y-distance from beam center
76+
:param detector_distance: sample to detector distance
77+
:param wavelength: neutron wavelength
7778
:return: q-value at the given position
7879
"""
7980
# Distance from beam center in the plane of detector
@@ -233,55 +234,13 @@ def get_pixel_fraction(q_max: float, q_00: float, q_01: float, q_10: float, q_11
233234
return frac_max
234235

235236

236-
def get_dq_data(data2d: Data2D) -> np.array:
237-
'''
238-
Get the dq for resolution averaging
239-
The pinholes and det. pix contribution present
240-
in both direction of the 2D which must be subtracted when
241-
converting to 1D: dq_overlap should be calculated ideally at
242-
q = 0. Note This method works on only pinhole geometry.
243-
Extrapolate dqx(r) and dqy(phi) at q = 0, and take an average.
244-
'''
245-
z_max = max(data2d.q_data)
246-
z_min = min(data2d.q_data)
247-
dqx_at_z_max = data2d.dqx_data[np.argmax(data2d.q_data)]
248-
dqx_at_z_min = data2d.dqx_data[np.argmin(data2d.q_data)]
249-
dqy_at_z_max = data2d.dqy_data[np.argmax(data2d.q_data)]
250-
dqy_at_z_min = data2d.dqy_data[np.argmin(data2d.q_data)]
251-
# Find qdx at q = 0
252-
dq_overlap_x = (dqx_at_z_min * z_max - dqx_at_z_max * z_min) / (z_max - z_min)
253-
# when extrapolation goes wrong
254-
if dq_overlap_x > min(data2d.dqx_data):
255-
dq_overlap_x = min(data2d.dqx_data)
256-
dq_overlap_x *= dq_overlap_x
257-
# Find qdx at q = 0
258-
dq_overlap_y = (dqy_at_z_min * z_max - dqy_at_z_max * z_min) / (z_max - z_min)
259-
# when extrapolation goes wrong
260-
if dq_overlap_y > min(data2d.dqy_data):
261-
dq_overlap_y = min(data2d.dqy_data)
262-
# get dq at q=0.
263-
dq_overlap_y *= dq_overlap_y
264-
265-
dq_overlap = np.sqrt((dq_overlap_x + dq_overlap_y) / 2.0)
266-
# Final protection of dq
267-
if dq_overlap < 0:
268-
dq_overlap = dqy_at_z_min
269-
dqx_data = data2d.dqx_data[np.isfinite(data2d.data)]
270-
dqy_data = data2d.dqy_data[np.isfinite(
271-
data2d.data)] - dq_overlap
272-
# def; dqx_data = dq_r dqy_data = dq_phi
273-
# Convert dq 2D to 1D here
274-
dq_data = np.sqrt(dqx_data**2 + dqy_data**2)
275-
return dq_data
276-
277237
################################################################################
278238

279239

280240
def reader2D_converter(data2d: Data2D | None = None) -> Data2D:
281241
"""
282-
convert old 2d format opened by IhorReader or danse_reader
283-
to new Data2D format
284-
This is mainly used by the Readers
242+
Convert old 2d format opened by IhorReader or danse_reader to new Data2D format.
243+
This is mainly used by the Readers.
285244
286245
:param data2d: 2d array of Data2D object
287246
:return: 1d arrays of Data2D object
@@ -358,8 +317,10 @@ def __init__(self, x_min=0.0, x_max=0.0, y_min=0.0,
358317
width = max(abs(x_max - x_min), 1e-12)
359318
nbins = int(math.ceil(width / abs(bin_width))) if bin_width != 0 else 1
360319
self.nbins=nbins
361-
super().__init__(qx_range=(x_min, x_max), qy_range=(y_min, y_max),
362-
nbins=nbins, fold=fold)
320+
super().__init__(qx_range=(x_min, x_max),
321+
qy_range=(y_min, y_max),
322+
nbins=nbins,
323+
fold=fold)
363324

364325

365326
class SlabY(SlabY):
@@ -382,22 +343,22 @@ def __init__(self, x_min=0.0, x_max=0.0, y_min=0.0,
382343
height = max(abs(y_max - y_min), 1e-12)
383344
nbins = int(math.ceil(height / abs(bin_width))) if bin_width != 0 else 1
384345
self.nbins=nbins
385-
super().__init__(qx_range=(x_min, x_max), qy_range=(y_min, y_max),
386-
nbins=nbins, fold=fold)
346+
super().__init__(qx_range=(x_min, x_max),
347+
qy_range=(y_min, y_max),
348+
nbins=nbins,
349+
fold=fold)
387350

388351

389352
################################################################################
390353

391354

392355
class Boxsum(Boxsum):
393356
def __init__(self, x_min=0.0, x_max=0.0, y_min=0.0, y_max=0.0):
394-
395357
super().__init__(qx_range=(x_min, x_max), qy_range=(y_min, y_max))
396358

397359

398360
class Boxavg(Boxavg):
399361
def __init__(self, x_min=0.0, x_max=0.0, y_min=0.0, y_max=0.0):
400-
401362
super().__init__(qx_range=(x_min, x_max), qy_range=(y_min, y_max))
402363

403364
################################################################################
@@ -422,23 +383,22 @@ def __call__(self, data2D, ismask=False):
422383
"""
423384
Perform circular averaging on the data
424385
425-
:param data2D: Data2D object
386+
:param data2D: SasData object
426387
:return: Data1D object
427388
"""
428389
# Get data W/ finite values
429-
data = data2D.data[np.isfinite(data2D.data)]
430-
q_data = data2D.q_data[np.isfinite(data2D.data)]
431-
err_data = None
432-
if data2D.err_data is not None:
433-
err_data = data2D.err_data[np.isfinite(data2D.data)]
434-
mask_data = data2D.mask[np.isfinite(data2D.data)]
390+
finite_mask = np.isfinite(data2D._data_contents["I"].value)
391+
data = data2D._data_contents["I"].value[finite_mask]
392+
q_data = np.sqrt(data2D._data_contents["Qx"].value**2 + data2D._data_contents["Qy"].value**2)[finite_mask]
393+
err_data = np.sqrt(data2D._data_contents["I"].variance.value)[finite_mask]
394+
mask_data = (data2D.mask if data2D.mask is not None else np.ones_like(data2D._data_contents["I"].value, dtype=bool))[finite_mask]
435395

436396
dq_data = None
437-
if data2D.dqx_data is not None and data2D.dqy_data is not None:
397+
if data2D._data_contents["Qx"].has_variance and data2D._data_contents["Qy"].has_variance:
438398
dq_data = get_dq_data(data2D)
439399

440400
if len(q_data) == 0:
441-
msg = "Circular averaging: invalid q_data: %g" % data2D.q_data
401+
msg = "Circular averaging: invalid q_data: %g" % q_data
442402
raise RuntimeError(msg)
443403

444404
# Build array of Q intervals
@@ -523,10 +483,7 @@ def __call__(self, data2D, ismask=False):
523483

524484

525485
class Ring(Ring):
526-
"""
527-
Wrapper for new Ring.
528-
"""
529-
486+
"""Wrapper for new Ring."""
530487

531488
@property
532489
def nbins_phi(self):
@@ -537,12 +494,9 @@ def nbins_phi(self, value):
537494
self.nbins = value
538495

539496
def __init__(self, r_min=0.0, r_max=0.0, center_x=0.0, center_y=0.0, nbins=36):
540-
541-
super().__init__(
542-
r_range=(r_min, r_max),
543-
center=(center_x, center_y),
544-
nbins=nbins
545-
)
497+
super().__init__(r_range=(r_min, r_max),
498+
center=(center_x, center_y),
499+
nbins=nbins)
546500

547501
class _Sector:
548502
"""
@@ -556,12 +510,10 @@ class _Sector:
556510
starting from the negative x-axis.
557511
"""
558512

559-
def __init__(self, r_min, r_max, phi_min=0, phi_max=2 * math.pi, nbins=20,
560-
base=None):
561-
'''
562-
:param base: must be a valid base for an algorithm, i.e.,
563-
a positive number
564-
'''
513+
def __init__(self, r_min, r_max, phi_min=0, phi_max=2*math.pi, nbins=20, base=None):
514+
"""
515+
:param base: must be a valid base for an algorithm, i.e., a positive number
516+
"""
565517
self.r_min = r_min
566518
self.r_max = r_max
567519
self.phi_min = phi_min
@@ -576,26 +528,29 @@ def _agv(self, data2D, run='phi'):
576528
"""
577529
Perform sector averaging.
578530
579-
:param data2D: Data2D object
580-
:param run: define the varying parameter ('phi' , or 'sector')
531+
:param data2D: SasData object
532+
:param run: define the varying parameter ('phi' or 'sector')
581533
582534
:return: Data1D object
583535
"""
584-
if data2D.__class__.__name__ not in ["Data2D", "plottable_2D"]:
585-
raise RuntimeError("Ring averaging only take plottable_2D objects")
536+
if not ("Qx" in data2D._data_contents and
537+
"Qy" in data2D._data_contents and
538+
"I" in data2D._data_contents):
539+
raise RuntimeError("For ring averaging the SasData object must contain 'Qx', 'Qy', and 'I' data.")
586540

587541
# Get all the data & info
588-
data = data2D.data[np.isfinite(data2D.data)]
589-
q_data = data2D.q_data[np.isfinite(data2D.data)]
590-
err_data=None
591-
if data2D.err_data is not None:
592-
err_data = data2D.err_data[np.isfinite(data2D.data)]
593-
qx_data = data2D.qx_data[np.isfinite(data2D.data)]
594-
qy_data = data2D.qy_data[np.isfinite(data2D.data)]
595-
mask_data = data2D.mask[np.isfinite(data2D.data)]
542+
finite_mask = np.isfinite(data2D._data_contents["I"].value)
543+
data = data2D._data_contents["I"].value[finite_mask]
544+
err_data = np.sqrt(data2D._data_contents["I"].variance.value)[finite_mask]
545+
qx_data = data2D._data_contents["Qx"].value[finite_mask]
546+
qy_data = data2D._data_contents["Qy"].value[finite_mask]
547+
q_data = np.sqrt(data2D._data_contents["Qx"].value ** 2 +
548+
data2D._data_contents["Qy"].value ** 2
549+
)[finite_mask]
550+
mask_data = (data2D.mask if data2D.mask is not None else np.ones_like(data2D._data_contents["I"].value, dtype=bool))[finite_mask]
596551

597552
dq_data = None
598-
if data2D.dqx_data is not None and data2D.dqy_data is not None:
553+
if data2D._data_contents["Qx"].has_variance and data2D._data_contents["Qy"].has_variance:
599554
dq_data = get_dq_data(data2D)
600555

601556
# set space for 1d outputs
@@ -775,7 +730,7 @@ def __call__(self, data2D):
775730
"""
776731
Perform sector average and return I(phi).
777732
778-
:param data2D: Data2D object
733+
:param data2D: SasData object
779734
:return: Data1D object
780735
"""
781736
return self._agv(data2D, 'phi')
@@ -798,51 +753,38 @@ def __call__(self, data2D):
798753
"""
799754
Perform sector average and return I(Q).
800755
801-
:param data2D: Data2D object
756+
:param data2D: SasData object
802757
803758
:return: Data1D object
804759
"""
805760
return self._agv(data2D, 'sector')
806761

807762

808763
class WedgePhi(WedgePhi):
809-
"""
810-
Wrapper for new WedgePhi (behaviour matches legacy WedgePhi expectations).
811-
"""
764+
"""Wrapper for new WedgePhi (behaviour matches legacy WedgePhi expectations)."""
812765
def __init__(self, r_min, r_max, phi_min=0, phi_max=TwoPi, center_x=0.0, center_y=0.0, nbins=10):
813-
814-
super().__init__(
815-
r_range=(r_min, r_max),
816-
phi_range=(phi_min, phi_max),
817-
center=(center_x, center_y),
818-
nbins=nbins
819-
)
766+
super().__init__(r_range=(r_min, r_max),
767+
phi_range=(phi_min, phi_max),
768+
center=(center_x, center_y),
769+
nbins=nbins)
820770

821771
class WedgeQ(WedgeQ):
822-
"""
823-
Wrapper for new WedgeQ (behaviour matches legacy WedgeQ expectations).
824-
"""
772+
"""Wrapper for new WedgeQ (behaviour matches legacy WedgeQ expectations)."""
825773
def __init__(self, r_min, r_max, phi_min=0, phi_max=TwoPi, center_x=0.0, center_y=0.0, nbins=10):
826-
827-
super().__init__(
828-
r_range=(r_min, r_max),
829-
phi_range=(phi_min, phi_max),
830-
center=(center_x, center_y),
831-
nbins=nbins
832-
)
774+
super().__init__(r_range=(r_min, r_max),
775+
phi_range=(phi_min, phi_max),
776+
center=(center_x, center_y),
777+
nbins=nbins)
833778

834779
################################################################################
835780

836781

837782
class Ringcut(Ringcut):
838783
def __init__(self, r_min=0.0, r_max=0.0, center_x=0.0, center_y=0.0):
839784
# center_x, center_y ignored for compatibility
840-
841-
super().__init__(
842-
r_range=(r_min, r_max),
843-
phi_range=(0.0, TwoPi),
844-
center=(center_x, center_y)
845-
)
785+
super().__init__(r_range=(r_min, r_max),
786+
phi_range=(0.0, TwoPi),
787+
center=(center_x, center_y))
846788

847789
################################################################################
848790

0 commit comments

Comments
 (0)