Skip to content

Commit dda041a

Browse files
lucas-wilkinsDrPaulSharp
authored andcommitted
Adds the "Abscissa" class
1 parent d6aeda2 commit dda041a

5 files changed

Lines changed: 274 additions & 0 deletions

File tree

sasdata/abscissa.py

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

sasdata/data.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,61 @@
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+
65+
66+
class SasMeasurement(SasData):
67+
68+
1469
def __init__(
1570
self,
1671
name: str,

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

0 commit comments

Comments
 (0)