Skip to content

Commit e9d2cbd

Browse files
lucas-wilkinsDrPaulSharp
authored andcommitted
Adds the "Abscissa" class
1 parent 0d5dde8 commit e9d2cbd

5 files changed

Lines changed: 262 additions & 2 deletions

File tree

sasdata/abscissa.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
from abc import ABC, abstractmethod
2+
3+
import numpy as np
4+
from exceptions import InterpretationError
5+
from numpy._typing import ArrayLike
6+
from quantities.quantity import Quantity
7+
from util import is_increasing
8+
9+
10+
class Abscissa(ABC):
11+
12+
def __init__(self, axes: list[Quantity]):
13+
self._axes = axes
14+
self._dimensionality = len(axes)
15+
16+
@property
17+
def dimensionality(self) -> int:
18+
""" Dimensionality of this data """
19+
return self._dimensionality
20+
21+
@property
22+
@abstractmethod
23+
def is_grid(self) -> bool:
24+
""" Are these coordinates using a grid representation
25+
(as opposed to a general list representation)
26+
27+
is_grid = True: implies that the corresponding ordinate is n-dimensional tensor
28+
is_grid = False: implies that the corresponding ordinate is a 1D list
29+
30+
If the data is one dimensional, is_grid=True
31+
"""
32+
33+
@property
34+
def axes(self) -> list[Quantity]:
35+
""" Axes of the data:
36+
37+
If it's an (n1-by-n2-by-n3...) grid (is_grid=True): give the values for each axis, returning a list like
38+
[Quantity(length n1), Quantity(length n2), Quantity(length n3) ... ]
39+
40+
If it is not grid data (is_grid=False), but n points on a general mesh, give one array for each dimension
41+
[Quantity(length n), Quantity(length n), Quantity(length n) ... ]
42+
"""
43+
44+
return self._axes
45+
46+
@staticmethod
47+
def _determine_error_message(axis_arrays: list[np.ndarray], ordinate_shape: tuple):
48+
""" Error message for the `.determine` function"""
49+
50+
shape_string = ", ".join([str(axis.shape) for axis in axis_arrays])
51+
52+
return f"Cannot interpret array shapes axis: [{shape_string}], ordinate: {ordinate_shape}"
53+
54+
@staticmethod
55+
def determine(axis_data: list[Quantity[ArrayLike]], ordinate_data: Quantity[ArrayLike]) -> "Abscissa":
56+
""" Get an Abscissa object that fits the combination of axes and data"""
57+
58+
# Different posibilites:
59+
# 1: axes_data[i].shape == axes_data[j].shape == ordinate_data.shape
60+
# 1a: axis_data[i] is 1D =>
61+
# 1a-i: len(axes_data) == 1 => Grid type or Scatter type depending on sortedness
62+
# 1a-ii: len(axes_data) != 1 => Scatter type
63+
# 1b: axis_data[i] dimensionality matches len(axis_data) => Meshgrid type
64+
# 1c: other => Error
65+
# 2: (len(axes_data[0]), len(axes_data[1])... ) == ordinate_data.shape => Grid type
66+
# 3: None of the above => Error
67+
68+
ordinate_shape = np.array(ordinate_data.value).shape
69+
axis_arrays = [np.array(axis.value) for axis in axis_data]
70+
71+
# 1:
72+
if all([axis.shape == ordinate_shape for axis in axis_arrays]):
73+
# 1a:
74+
if all([len(axis.shape)== 1 for axis in axis_arrays]):
75+
# 1a-i:
76+
if len(axis_arrays) == 1:
77+
# Is it sorted
78+
if is_increasing(axis_arrays[0]):
79+
return GridAbscissa(axis_data)
80+
else:
81+
return ScatterAbscissa(axis_data)
82+
# 1a-ii
83+
else:
84+
return ScatterAbscissa(axis_data)
85+
# 1b
86+
elif all([len(axis.shape) == len(axis_arrays) for axis in axis_arrays]):
87+
88+
return MeshgridAbscissa(axis_data)
89+
90+
else:
91+
raise InterpretationError(Abscissa._determine_error_message(axis_arrays, ordinate_shape))
92+
93+
elif all([len(axis.shape)== 1 for axis in axis_arrays]) and \
94+
tuple([axis.shape[0] for axis in axis_arrays]) == ordinate_shape:
95+
96+
# Require that they are sorted
97+
if all([is_increasing(axis) for axis in axis_arrays]):
98+
99+
return GridAbscissa(axis_data)
100+
101+
else:
102+
raise InterpretationError("Grid axes are not sorted")
103+
104+
else:
105+
raise InterpretationError(Abscissa._determine_error_message(axis_arrays, ordinate_shape))
106+
107+
class GridAbscissa(Abscissa):
108+
109+
@property
110+
def is_grid(self):
111+
return True
112+
113+
class MeshgridAbscissa(Abscissa):
114+
115+
@property
116+
def is_grid(self):
117+
return True
118+
119+
class ScatterAbscissa(Abscissa):
120+
121+
@property
122+
def is_grid(self):
123+
return False

sasdata/data.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,58 @@
1111

1212

1313
class SasData:
14+
""" General object containing data in the SasView ecosystem"""
15+
16+
def __init__(self,
17+
name: str,
18+
ordinate: Quantity,
19+
mask: Quantity,
20+
abscissae: list[Quantity],
21+
dependents: list["SasData"]):
22+
23+
self._ordinate = ordinate
24+
self._abscissae = abscissae
25+
self._mask = mask
26+
27+
@property
28+
def ordinate(self) -> Quantity:
29+
return self._ordinate
30+
31+
@property
32+
def abscissae(self) -> list[Quantity]:
33+
return self._abscissae
34+
35+
@property
36+
def mask(self) -> Quantity:
37+
return self._mask
38+
39+
def scatter_data(self):
40+
""" Return data in the coordinate/value form [(x1, x2, x3, y)...]"""
41+
42+
43+
class SasDerivedMeasurement(SasData):
44+
""" General object sas measurement that has not come directly from a file,
45+
for example, the difference between two datasets"""
46+
47+
48+
def __init__(self,
49+
name: str,
50+
ordinate: Quantity,
51+
abscissae: list[Quantity],
52+
dependents: list["SasData"],
53+
metadata: DerivedMetadata):
54+
55+
super().__init__(
56+
name=name,
57+
ordinate=ordinate,
58+
abscissae=abscissae,
59+
dependents=dependents)
60+
61+
self.metadata = metadata
62+
63+
64+
class SasMeasurement(SasData):
65+
1466
def __init__(
1567
self,
1668
name: str,
@@ -119,7 +171,6 @@ def _save_h5(self, sasentry: HDF5Group):
119171
for idx, (key, sasdata) in enumerate(self._data_contents.items()):
120172
sasdata.as_h5(group, key)
121173

122-
123174
@staticmethod
124175
def save_h5(data: dict[str, typing.Self], path: str | typing.BinaryIO):
125176
with h5py.File(path, "w") as f:
@@ -130,7 +181,6 @@ def save_h5(data: dict[str, typing.Self], path: str | typing.BinaryIO):
130181
data._save_h5(sasentry)
131182

132183

133-
134184
class SasDataEncoder(MetadataEncoder):
135185
def default(self, obj):
136186
match obj:

sasdata/exceptions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class InterpretationError(Exception):
2+
""" Error interpreting data """

sasdata/util.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from collections.abc import Callable
22
from typing import TypeVar
33

4+
import numpy as np
5+
46
T = TypeVar("T")
57

68
def cache[T](fun: Callable[[], T]):
@@ -16,3 +18,15 @@ def wrapper() -> T:
1618
return cache_state[1]
1719

1820
return wrapper
21+
22+
def is_increasing(data: np.ndarray):
23+
""" Check if a 1D array is sorted in strictly increasing order"""
24+
return np.all(data[1:] > data[:-1])
25+
26+
def is_decreasing(data: np.ndarray):
27+
""" Check if a 1D array is sorted in strictly decreasing order"""
28+
return np.all(data[1:] < data[:-1])
29+
30+
def is_sorted(data: np.ndarray):
31+
""" Check if a 1D array is strictly sorted """
32+
return is_increasing(data) or is_decreasing(data)

test/utest_asbscissa.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import numpy as np
2+
import pytest
3+
from abscissa import Abscissa, GridAbscissa, MeshgridAbscissa, ScatterAbscissa
4+
from exceptions import InterpretationError
5+
from quantities.quantity import Quantity
6+
from quantities.units import none
7+
8+
9+
def test_deterimine_1d_grid():
10+
""" Test that 1D ordered data is grid type"""
11+
q = Quantity(np.arange(10), units=none)
12+
13+
determined = Abscissa.determine([q], q)
14+
15+
assert isinstance(determined, GridAbscissa)
16+
17+
def test_deterimine_1d_scatter():
18+
""" Test that 1D unordered data is scatter type """
19+
a = Quantity(np.array([1, 2, 3, 4, 5, 0, 9, 8, 7, 6]), units=none)
20+
d = Quantity(np.arange(10), units=none)
21+
22+
determined = Abscissa.determine([a], d)
23+
24+
assert isinstance(determined, ScatterAbscissa)
25+
26+
def test_2D_scatter():
27+
""" Test the nD scatter case with 2D data """
28+
q = Quantity(np.arange(10), units=none)
29+
30+
determined = Abscissa.determine([q, q], q)
31+
32+
assert isinstance(determined, ScatterAbscissa)
33+
34+
def test_2D_meshgrid():
35+
""" Test the meshgrid case with 2x5"""
36+
q = Quantity(np.arange(10).reshape(2, 5), units=none)
37+
38+
determined = Abscissa.determine([q, q], q)
39+
40+
assert isinstance(determined, MeshgridAbscissa)
41+
42+
43+
def test_2D_grid():
44+
""" Test the nD grid case with 2x5 """
45+
a1 = Quantity(np.arange(2), units=none)
46+
a2 = Quantity(np.arange(5), units=none)
47+
48+
d = Quantity(np.arange(10).reshape(2, 5), units=none)
49+
50+
determined = Abscissa.determine([a1, a2], d)
51+
52+
assert isinstance(determined, GridAbscissa)
53+
54+
def test_2D_grid_axis_error():
55+
""" Test the nD grid case with bad axes """
56+
57+
a1 = Quantity(np.arange(2), units=none)
58+
a2 = Quantity(np.arange(5), units=none)
59+
60+
d = Quantity(np.arange(10).reshape(5, 2, 1), units=none)
61+
62+
with pytest.raises(InterpretationError):
63+
Abscissa.determine([a1, a2], d)
64+
65+
def test_2D_meshgrid_error_mismatched_dimensionality():
66+
""" Test the nD meshgrid case with bad axes """
67+
68+
q = Quantity(np.arange(10).reshape(5, 2), units=none)
69+
70+
with pytest.raises(InterpretationError):
71+
Abscissa.determine([q, q, q], q) # three axes, each 2D

0 commit comments

Comments
 (0)