Skip to content

Commit

Permalink
Merge pull request #147 from bbean23/146-add-new-constructors-and-as_…
Browse files Browse the repository at this point in the history
…index-methods-to-vxy-classes

add copy constructor support to Vxy and Vxyz, add from_list() method …
  • Loading branch information
e10harvey authored Aug 7, 2024
2 parents beae328 + 412cc5a commit a6f42bc
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 6 deletions.
43 changes: 41 additions & 2 deletions opencsp/common/lib/geometry/Vxy.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numbers
from typing import Union

import matplotlib.pyplot as plt
import numpy as np
Expand All @@ -8,15 +9,15 @@


class Vxy:
def __init__(self, data, dtype=float):
def __init__(self, data: Union[np.ndarray, tuple[float, float], tuple[list, list], "Vxy"], dtype=float):
"""
2D vector class to represent 2D points/vectors.
To represent a single vector::
x = 1
y = 2
vec = Vxy(np.array([[x], [y])) # same as vec = Vxy([x, y])
vec = Vxy(np.array([[x], [y]]) # same as vec = Vxy([x, y])
print(vec.x) # [1.]
print(vec.y) # [2.]
Expand Down Expand Up @@ -44,6 +45,8 @@ def __init__(self, data, dtype=float):
"""
# Check input shape
if isinstance(data, Vxy):
data = data.data
if type(data) is np.ndarray:
data = data.squeeze()
if np.ndim(data) not in [1, 2]:
Expand Down Expand Up @@ -79,6 +82,15 @@ def y(self):
def _from_data(cls, data, dtype=float):
return cls(data, dtype)

@classmethod
def from_list(cls, vals: list["Vxy"]):
"""Builds a single Vxy instance from a list of Vxy instances."""
xs, ys = [], []
for val in vals:
xs += val.x.tolist()
ys += val.y.tolist()
return cls((xs, ys))

def _check_is_Vxy(self, v_in):
"""
Checks if input data is instance of Vxy for methods that require this
Expand Down Expand Up @@ -368,3 +380,30 @@ def astuple(self) -> tuple[numbers.Number, numbers.Number]:
"Error in Vxy.astuple(): " + f"can't convert a Vxy with {len(self)} sets of values to a single tuple",
)
return self.x[0], self.y[0]

def asindex(self, axis_order='xy') -> tuple[npt.NDArray[np.int64], npt.NDArray[np.int64]]:
"""
Returns the x and y values as integer arrays. This allows for indexing
of a numpy array as follows::
arr_val[vxy_val.asindex()]
For example, to get an array of 1s at each of the vxy_val coordinates::
binary_image = np.zeros((np.max(vxy_val.y), np.max(vxy_val.x)), dtype=np.uint8)
binary_image[vxy_val.asindex('yx')] = 1
In this type of usage, the 'x' values will be used for the first index,
and the 'y' values will be used for the second index. For example:
arr_val = np.array([ [0, 1, 2], [3, 4, 5], [6, 7, 8] ])
vxy_val = Vxy(([0, 1, 2], [0, 1, 0]))
print(arr_val[desired_indexes.asindex()])
# [0, 4, 6]
"""
indexes = {'x': self.x.astype(np.int64), 'y': self.y.astype(np.int64)}

ret = []
for axis in axis_order:
ret.append(indexes[axis])
return tuple(ret)
18 changes: 16 additions & 2 deletions opencsp/common/lib/geometry/Vxyz.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Three dimensional vector representation
"""

from typing import Callable
from typing import Callable, Union

import numpy as np
import numpy.typing as npt
Expand All @@ -11,7 +11,9 @@


class Vxyz:
def __init__(self, data, dtype=float):
def __init__(
self, data: Union[np.ndarray, tuple[float, float, float], tuple[list, list, list], Vxy, "Vxyz"], dtype=float
):
"""
3D vector class to represent 3D points/vectors.
Expand Down Expand Up @@ -60,6 +62,8 @@ def __init__(self, data, dtype=float):
raise ValueError('First dimension of 2-dimensional data must be length 3 if ndarray.')
elif isinstance(data, Vxy):
data = np.pad(data.data, ((0, 1), (0, 0)))
elif isinstance(data, Vxyz):
data = data.data
elif len(data) != 3:
raise ValueError('Input data must have length 3.')

Expand Down Expand Up @@ -96,6 +100,16 @@ def as_Vxyz(self):
def _from_data(cls, data, dtype=float):
return cls(data, dtype)

@classmethod
def from_list(cls, vals: list["Vxyz"]):
"""Builds a single Vxyz instance from a list of Vxyz instances."""
xs, ys, zs = [], [], []
for val in vals:
xs += val.x.tolist()
ys += val.y.tolist()
zs += val.z.tolist()
return cls((xs, ys, zs))

def _check_is_Vxyz(self, v_in):
"""
Checks if input data is instance of Vxyz for methods that require this
Expand Down
36 changes: 36 additions & 0 deletions opencsp/common/lib/geometry/test/test_Vxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,30 @@ def test_Vxy_array_shape(self):
with np.testing.assert_raises(ValueError):
Vxy(np.zeros((4, 4)))

def test_Vxy_copy_constructor(self):
original = Vxy([[0, 1, 2], [3, 4, 5]])
copy = Vxy(original)
self.assertEqual(copy.x.tolist(), [0, 1, 2])
self.assertEqual(copy.y.tolist(), [3, 4, 5])

def test_from_list(self):
# test single-valued Vxy instances
a1 = Vxy([0, 1])
b1 = Vxy([2, 3])
c2 = Vxy.from_list([a1, b1])
self.assertEqual(len(c2), 2)
self.assertEqual(c2.x.tolist(), [0, 2])
self.assertEqual(c2.y.tolist(), [1, 3])

# test multi-valued Vxy instances
a2 = Vxy([[0, 1], [2, 3]])
b3 = Vxy([[4, 5, 6], [7, 8, 9]])
c4 = Vxy([[10, 11, 12, 13], [14, 15, 16, 17]])
d9 = Vxy.from_list([a2, b3, c4])
self.assertEqual(len(d9), 9)
self.assertEqual(d9.x.tolist(), [0, 1, 4, 5, 6, 10, 11, 12, 13])
self.assertEqual(d9.y.tolist(), [2, 3, 7, 8, 9, 14, 15, 16, 17])

def test_xy(self):
assert self.V1.x[0] == self.V1_array[0]
assert self.V1.y[0] == self.V1_array[1]
Expand Down Expand Up @@ -291,6 +315,18 @@ def test_astuple(self):
with self.assertRaises(Exception):
b.astuple()

def test_asindex(self):
# test that indexing into the following array:
# 0 1 2
# 3 4 5
# 6 7 8
# at rows/cols (0, 0), (1, 1), and (2, 0), returns the following values:
# 0 4 6
arr_val = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
desired_indexes = Vxy(([0, 1, 2], [0, 1, 0]))
actual_values = arr_val[desired_indexes.asindex()]
np.testing.assert_array_equal(actual_values, np.array([0, 4, 6]))


if __name__ == '__main__':
unittest.main()
36 changes: 34 additions & 2 deletions opencsp/common/lib/geometry/test/test_Vxyz.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import unittest
import numpy as np
from scipy.spatial.transform import Rotation

from opencsp.common.lib.geometry.Vxyz import Vxyz


class TestVxyz:
class TestVxyz(unittest.TestCase):
@classmethod
def setup_class(cls):
def setUpClass(cls):
cls.V1 = Vxyz((2, 2, 2))
cls.V1_array = np.array([[2], [2], [2]])

Expand Down Expand Up @@ -46,6 +47,33 @@ def test_Vxyz_array_shape(self):
with np.testing.assert_raises(ValueError):
Vxyz(np.zeros((4, 4)))

def test_Vxyz_copy_constructor(self):
original = Vxyz([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
copy = Vxyz(original)
self.assertEqual(copy.x.tolist(), [0, 1, 2])
self.assertEqual(copy.y.tolist(), [3, 4, 5])
self.assertEqual(copy.z.tolist(), [6, 7, 8])

def test_from_list(self):
# test single-valued Vxy instances
a1 = Vxyz([0, 1, 2])
b1 = Vxyz([3, 4, 5])
c2 = Vxyz.from_list([a1, b1])
self.assertEqual(len(c2), 2)
self.assertEqual(c2.x.tolist(), [0, 3])
self.assertEqual(c2.y.tolist(), [1, 4])
self.assertEqual(c2.z.tolist(), [2, 5])

# test multi-valued Vxyz instances
a2 = Vxyz(list(zip([0, 1, 2], [3, 4, 5])))
b3 = Vxyz(list(zip([6, 7, 8], [9, 10, 11], [12, 13, 14])))
c4 = Vxyz(list(zip([15, 16, 17], [18, 19, 20], [21, 22, 23], [24, 25, 26])))
d9 = Vxyz.from_list([a2, b3, c4])
self.assertEqual(len(d9), 9)
self.assertEqual(d9.x.tolist(), [0, 3, 6, 9, 12, 15, 18, 21, 24])
self.assertEqual(d9.y.tolist(), [1, 4, 7, 10, 13, 16, 19, 22, 25])
self.assertEqual(d9.z.tolist(), [2, 5, 8, 11, 14, 17, 20, 23, 26])

def test_xyz(self):
assert self.V1.x[0] == self.V1_array[0]
assert self.V1.y[0] == self.V1_array[1]
Expand Down Expand Up @@ -299,3 +327,7 @@ def test_align_to(self):
Vy = Vxyz((0, 3, 0))
r_out = Vx.align_to(Vy)
np.testing.assert_almost_equal(r_out.as_rotvec(), np.array([0, 0, np.pi / 2]))


if __name__ == "__main__":
unittest.main()

0 comments on commit a6f42bc

Please sign in to comment.