Skip to content

Commit a016ffb

Browse files
authored
Make vector projections independent of direction scale (#242)
`project(v1, v2)` currently multiplies by `dot(v1, v2)` without dividing by `dot(v2, v2)`. As a result, projecting `[3,1,4]` onto `[2,2,0]` returns `[16,16,0]` instead of `[2,2,0]`; `orthogonalize()` then produces a vector that is not perpendicular to its reference direction. List/tuple direction inputs also raise TypeError. Convert the documented array-like arguments to vectors and compute the full projection formula. Rescale the direction before dot products so very small/large directions do not underflow/overflow. Keep the historical zero-direction result. Validation: two regressions fail before the fix; the zero-direction control passes. Tests cover signed/scaled directions, list/tuple inputs, scales1e-200 and1e200, and normalized/unnormalized perpendicular results. All184 base+pose3d tests pass with1 skip and8 existing warnings; repository-pinned Black23.10.0 and `git diff --check` pass. Prepared with AI assistance.
1 parent 6ed7eff commit a016ffb

2 files changed

Lines changed: 25 additions & 1 deletion

File tree

‎spatialmath/base/vectors.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -829,7 +829,14 @@ def project(v1: ArrayLike3, v2: ArrayLike3) -> ArrayLike3:
829829
:return: vector projection of v1 onto v2 (parrallel to v2)
830830
:rtype: ndarray(n)
831831
"""
832-
return np.dot(v1, v2) * v2
832+
v1 = getvector(v1)
833+
v2 = getvector(v2)
834+
scale = np.max(np.abs(v2))
835+
if scale == 0:
836+
return np.zeros_like(v2)
837+
# Normalize the scale before dot products to avoid over/underflow.
838+
v2 = v2 / scale
839+
return (np.dot(v1, v2) / np.dot(v2, v2)) * v2
833840

834841

835842
def orthogonalize(v1: ArrayLike3, v2: ArrayLike3, normalize: bool = True) -> ArrayLike3:

‎tests/base/test_vectors.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ class TestVector(unittest.TestCase):
3333
def tearDownClass(cls):
3434
plt.close("all")
3535

36+
def test_project_nonunit_direction(self):
37+
for scale in (1, 2, -3, 1e-200, 1e200):
38+
direction = np.array([scale, scale, 0])
39+
nt.assert_allclose(project([3, 1, 4], direction), [2, 2, 0])
40+
nt.assert_allclose(project((3, 1, 4), (2, 2, 0)), [2, 2, 0])
41+
42+
def test_orthogonalize_nonunit_direction(self):
43+
result = orthogonalize([3, 1, 4], [2, 2, 0], normalize=False)
44+
nt.assert_allclose(result, [1, -1, 4])
45+
self.assertAlmostEqual(np.dot(result, [2, 2, 0]), 0)
46+
normalized = orthogonalize([3, 1, 4], [2, 2, 0])
47+
self.assertAlmostEqual(np.linalg.norm(normalized), 1)
48+
self.assertAlmostEqual(np.dot(normalized, [2, 2, 0]), 0)
49+
50+
def test_project_zero_direction(self):
51+
nt.assert_array_equal(project([1, 2, 3], [0, 0, 0]), [0, 0, 0])
52+
3653
def test_unit(self):
3754
nt.assert_array_almost_equal(unitvec([1, 0, 0]), np.r_[1, 0, 0])
3855
nt.assert_array_almost_equal(unitvec([0, 1, 0]), np.r_[0, 1, 0])

0 commit comments

Comments
 (0)