Skip to content

Commit 7af5c33

Browse files
committed
Abscissa WIP
1 parent 7d1cbd6 commit 7af5c33

4 files changed

Lines changed: 153 additions & 8 deletions

File tree

sasdata/abscissa.py

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from numpy._typing import ArrayLike
55

66
from quantities.quantity import Quantity
7+
from exceptions import InterpretationError
8+
from util import is_increasing
79

810

911
class Abscissa(ABC):
@@ -45,31 +47,78 @@ def axes(self) -> list[Quantity]:
4547
return self._axes
4648

4749
@staticmethod
48-
def determine(axes_data: list[Quantity[ArrayLike]], ordinate_data: Quantity[ArrayLike]) -> "Abscissa":
50+
def _determine_error_message(axis_arrays: list[np.ndarray], ordinate_shape: tuple):
51+
""" Error message for the `.determine` function"""
52+
53+
shape_string = ", ".join([str(axis.shape) for axis in axis_arrays])
54+
55+
return f"Cannot interpret array shapes axis: [{shape_string}], ordinate: {ordinate_shape}"
56+
57+
@staticmethod
58+
def determine(axis_data: list[Quantity[ArrayLike]], ordinate_data: Quantity[ArrayLike]) -> "Abscissa":
4959
""" Get an Abscissa object that fits the combination of axes and data"""
5060

51-
# Different possibilites:
61+
# Different posibilites:
5262
# 1: axes_data[i].shape == axes_data[j].shape == ordinate_data.shape
5363
# 1a: axis_data[i] is 1D =>
54-
# 1a-i: len(axes_data) == 1 => Grid type (trivially)
55-
# 1a-ii: len(axes_data) != 1 => Mesh type
56-
# 1b: axis_data[i] dimensionality matches len(axis_data) => Grid type
64+
# 1a-i: len(axes_data) == 1 => Grid type or Scatter type depending on sortedness
65+
# 1a-ii: len(axes_data) != 1 => Scatter type
66+
# 1b: axis_data[i] dimensionality matches len(axis_data) => Meshgrid type
5767
# 1c: other => Error
5868
# 2: (len(axes_data[0]), len(axes_data[1])... ) == ordinate_data.shape => Grid type
5969
# 3: None of the above => Error
6070

6171
ordinate_shape = np.array(ordinate_data.value).shape
72+
axis_arrays = [np.array(axis.value) for axis in axis_data]
73+
74+
# 1:
75+
if all([axis.shape == ordinate_shape for axis in axis_arrays]):
76+
# 1a:
77+
if all([len(axis.shape)== 1 for axis in axis_arrays]):
78+
# 1a-i:
79+
if len(axis_arrays) == 1:
80+
# Is it sorted
81+
if is_increasing(axis_arrays[0]):
82+
return GridAbscissa(axis_data)
83+
else:
84+
return ScatterAbscissa(axis_data)
85+
# 1a-ii
86+
else:
87+
return ScatterAbscissa(axis_data)
88+
# 1b
89+
elif [len(axis.shape) == len(axis_arrays) for axis in axis_arrays] :
90+
91+
return MeshgridAbscissa(axis_data)
92+
93+
else:
94+
raise InterpretationError(Abscissa._determine_error_message(axis_arrays, ordinate_shape))
95+
96+
elif all([len(axis.shape)== 1 for axis in axis_arrays]) and \
97+
tuple([axis.shape[0] for axis in axis_arrays]) == ordinate_shape:
6298

63-
if len(ordinate_shape) == 1 and all([np.array(axis.value).shape == ordinate_shape for axis in axes_data]):
64-
return ScatterAbscissa(axes_data)
99+
# Require that they are sorted
100+
if all([is_increasing(axis) for axis in axis_arrays]):
101+
102+
return GridAbscissa(axis_data)
103+
104+
else:
105+
raise InterpretationError("Grid axes are not sorted")
106+
107+
else:
108+
raise InterpretationError(Abscissa._determine_error_message(axis_arrays, ordinate_shape))
65109

66-
elif
67110
class GridAbscissa(Abscissa):
68111

69112
@property
70113
def is_grid(self):
71114
return True
72115

116+
class MeshgridAbscissa(Abscissa):
117+
118+
@property
119+
def is_grid(self):
120+
return True
121+
73122
class ScatterAbscissa(Abscissa):
74123

75124
@property

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

0 commit comments

Comments
 (0)