Skip to content

Commit f4ebcb6

Browse files
committed
Added orthogonalization to TwoVectors w unit tests
1 parent 07c6f62 commit f4ebcb6

File tree

3 files changed

+181
-121
lines changed

3 files changed

+181
-121
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 - np.dot(v1, v2) * 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: 28 additions & 13 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

@@ -337,7 +338,7 @@ def eulervec(self) -> R3:
337338
"""
338339
theta, v = smb.tr2angvec(self.R)
339340
return theta * v
340-
341+
341342
# ------------------------------------------------------------------------ #
342343

343344
@staticmethod
@@ -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:
@@ -1190,11 +1209,11 @@ def yaw_SE2(self, order: str = "zyx") -> SE2:
11901209
"""
11911210
if len(self) == 1:
11921211
if order == "zyx":
1193-
return SE2(self.x, self.y, self.rpy(order = order)[2])
1212+
return SE2(self.x, self.y, self.rpy(order=order)[2])
11941213
elif order == "xyz":
1195-
return SE2(self.z, self.y, self.rpy(order = order)[2])
1214+
return SE2(self.z, self.y, self.rpy(order=order)[2])
11961215
elif order == "yxz":
1197-
return SE2(self.z, self.x, self.rpy(order = order)[2])
1216+
return SE2(self.z, self.x, self.rpy(order=order)[2])
11981217
else:
11991218
return SE2([e.yaw_SE2() for e in self])
12001219

@@ -1938,11 +1957,7 @@ def Rt(
19381957
return cls(smb.rt2tr(R, t, check=check), check=check)
19391958

19401959
@classmethod
1941-
def CopyFrom(
1942-
cls,
1943-
T: SE3Array,
1944-
check: bool = True
1945-
) -> SE3:
1960+
def CopyFrom(cls, T: SE3Array, check: bool = True) -> SE3:
19461961
"""
19471962
Create an SE(3) from a 4x4 numpy array that is passed by value.
19481963

0 commit comments

Comments
 (0)