Skip to content

Commit 469b5fa

Browse files
Merge pull request #85 from bdaiinstitute/bokorn-bdaii/orthoginalize_TwoVector_axes
Added orthogonalization to TwoVectors w unit tests
2 parents 07c6f62 + bf890b2 commit 469b5fa

File tree

3 files changed

+82
-4
lines changed

3 files changed

+82
-4
lines changed

spatialmath/base/vectors.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,40 @@ def removesmall(v: ArrayLike, tol: float = 100) -> NDArray:
789789
return np.where(np.abs(v) < tol * _eps, 0, v)
790790

791791

792+
def project(v1: ArrayLike3, v2: ArrayLike3) -> ArrayLike3:
793+
"""
794+
Projects vector v1 onto v2. Returns a vector parallel to v2.
795+
796+
:param v1: vector to be projected
797+
:type v1: array_like(n)
798+
:param v2: vector to be projected onto
799+
:type v2: array_like(n)
800+
:return: vector projection of v1 onto v2 (parrallel to v2)
801+
:rtype: ndarray(n)
802+
"""
803+
return np.dot(v1, v2) * v2
804+
805+
806+
def orthogonalize(v1: ArrayLike3, v2: ArrayLike3, normalize: bool = True) -> ArrayLike3:
807+
"""
808+
Orthoginalizes vector v1 with respect to v2 with minimum rotation.
809+
Returns a the nearest vector to v1 that is orthoginal to v2.
810+
811+
:param v1: vector to be orthoginalized
812+
:type v1: array_like(n)
813+
:param v2: vector that returned vector will be orthoginal to
814+
:type v2: array_like(n)
815+
:param normalize: whether to normalize the output vector
816+
:type normalize: bool
817+
:return: nearest vector to v1 that is orthoginal to v2
818+
:rtype: ndarray(n)
819+
"""
820+
v_orth = v1 - project(v1, v2)
821+
if normalize:
822+
v_orth = v_orth / np.linalg.norm(v_orth)
823+
return v_orth
824+
825+
792826
if __name__ == "__main__": # pragma: no cover
793827
import pathlib
794828

spatialmath/pose3d.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
import spatialmath.base as smb
3030
from spatialmath.base.types import *
31+
from spatialmath.base.vectors import orthogonalize
3132
from spatialmath.baseposematrix import BasePoseMatrix
3233
from spatialmath.pose2d import SE2
3334

@@ -651,8 +652,10 @@ def TwoVectors(
651652
axes in terms of the old axes. Axes are denoted by strings ``"x"``,
652653
``"y"``, ``"z"``, ``"-x"``, ``"-y"``, ``"-z"``.
653654
654-
The directions can also be specified by 3-element vectors, but these
655-
must be orthogonal.
655+
The directions can also be specified by 3-element vectors. If the vectors are not orthogonal,
656+
they will orthogonalized w.r.t. the first available dimension. I.e. if x is available, it will be
657+
normalized and the remaining vector will be orthogonalized w.r.t. x, else, y will be normalized
658+
and z will be orthogonalized w.r.t. y.
656659
657660
To create a rotation where the new frame has its x-axis in -z-direction
658661
of the previous frame, and its z-axis in the x-direction of the previous
@@ -679,25 +682,41 @@ def vval(v):
679682
else:
680683
return smb.unitvec(smb.getvector(v, 3))
681684

682-
if x is not None and y is not None and z is None:
685+
if x is not None and y is not None and z is not None:
686+
raise ValueError(
687+
"Only two vectors should be provided. Please set one to None."
688+
)
689+
690+
elif x is not None and y is not None and z is None:
683691
# z = x x y
684692
x = vval(x)
685693
y = vval(y)
694+
# Orthogonalizes y w.r.t. x
695+
y = orthogonalize(y, x, normalize=True)
686696
z = np.cross(x, y)
687697

688698
elif x is None and y is not None and z is not None:
689699
# x = y x z
690700
y = vval(y)
691701
z = vval(z)
702+
# Orthogonalizes z w.r.t. y
703+
z = orthogonalize(z, y, normalize=True)
692704
x = np.cross(y, z)
693705

694706
elif x is not None and y is None and z is not None:
695707
# y = z x x
696708
z = vval(z)
697709
x = vval(x)
710+
# Orthogonalizes z w.r.t. x
711+
z = orthogonalize(z, x, normalize=True)
698712
y = np.cross(z, x)
699713

700-
return cls(np.c_[x, y, z], check=False)
714+
else:
715+
raise ValueError(
716+
"Insufficient number of vectors. Please provide exactly two vectors."
717+
)
718+
719+
return cls(np.c_[x, y, z], check=True)
701720

702721
@classmethod
703722
def AngleAxis(cls, theta: float, v: ArrayLike3, *, unit: str = "rad") -> Self:

tests/test_pose3d.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,32 @@ def test_constructor_AngVec(self):
216216
array_compare(R, roty(0.3))
217217
self.assertIsInstance(R, SO3)
218218

219+
def test_constructor_TwoVec(self):
220+
# Randomly selected vectors
221+
v1 = [1, 73, -42]
222+
v2 = [0, 0.02, 57]
223+
v3 = [-2, 3, 9]
224+
225+
# x and y given
226+
R = SO3.TwoVectors(x=v1, y=v2)
227+
self.assertIsInstance(R, SO3)
228+
nt.assert_almost_equal(np.linalg.det(R), 1, 5)
229+
# x axis should equal normalized x vector
230+
nt.assert_almost_equal(R.R[:, 0], v1 / np.linalg.norm(v1), 5)
231+
232+
# y and z given
233+
R = SO3.TwoVectors(y=v2, z=v3)
234+
self.assertIsInstance(R, SO3)
235+
nt.assert_almost_equal(np.linalg.det(R), 1, 5)
236+
# y axis should equal normalized y vector
237+
nt.assert_almost_equal(R.R[:, 1], v2 / np.linalg.norm(v2), 5)
219238

239+
# x and z given
240+
R = SO3.TwoVectors(x=v3, z=v1)
241+
self.assertIsInstance(R, SO3)
242+
nt.assert_almost_equal(np.linalg.det(R), 1, 5)
243+
# x axis should equal normalized x vector
244+
nt.assert_almost_equal(R.R[:, 0], v3 / np.linalg.norm(v3), 5)
220245

221246
def test_shape(self):
222247
a = SO3()

0 commit comments

Comments
 (0)