Skip to content

tech-debt: vision_collision_damper breaks when SMTB angvec2r rejects a zero axis (parallel/antiparallel vectors) #843

Closed

Description

@petercorke

Summary

Robot.vision_collision_damper builds the line-of-sight cylinder orientation with a local helper, rotation_between_vectors, that calls SE3.AngleAxis(angle, axis) with axis = cross(a, b). When a and b are parallel or antiparallel the cross product is the zero vector. This code already misbehaves today, and an upcoming SMTB change will turn the silent misbehaviour into a ValueError, so it should be fixed in RTB before that SMTB release ships.

This is not yet a live crash. It becomes one when a SMTB release containing the change described below is published and RTB picks it up (RTB pins spatialmath-python>=1.1.18, so the newest SMTB is installed for new users automatically).

Where

src/roboticstoolbox/robot/Robot.py, Robot.vision_collision_damper (def at line ~1595 on feat/symbolic-ik; the helper is at ~1638-1645, the use at ~1658):

def rotation_between_vectors(a: Any, b: Any) -> Any:
    a = a / np.linalg.norm(a)
    b = b / np.linalg.norm(b)

    angle = np.arccos(np.dot(a, b))
    axis = np.cross(a, b)

    return SE3.AngleAxis(angle, axis)
...
los_orientation = rotation_between_vectors(
    np.array([0.0, 0.0, 1.0]),
    wTcp - wTtp,          # camera position minus target position
)

It orients a thin Cylinder (the "line of sight" between camera and target) along wTcp - wTtp. Degenerate whenever the camera-to-target direction is exactly (anti)parallel to world +z, e.g. a camera directly above or below the target, which is a natural configuration for a visual-servoing / collision-avoidance example (see examples/fetch_vision.py).

Background: the SMTB change

SMTB PR (pending; branch fix/angvec2r-zero-axis in spatialmath-python, will be linked here when opened), following SMTB PR #266 (UnitQuaternion.AngVec did not normalize its axis).

Before: smb.angvec2r(theta, v) returned the identity whenever |v| was ~0, regardless of theta. This is what SO3/SE3.AngleAxis, SO3/SE3.AngVec and angvec2tr all rely on.

After: angvec2r returns the identity only when theta is ~0 (within tol eps, default 20 eps ~ 4.4e-15). For any other theta the axis is normalized with unitvec, which raises ValueError("zero norm vector") for a zero-length axis. A non-zero rotation about no axis is meaningless, and the docstring already said "if θ ~= 0 then V must have a finite length"; the code simply did not enforce it.

Behaviour of rotation_between_vectors (verified, 2026-10-19)

Run against SMTB 1.1.18 (today) vs the branch above. a = [0,0,1] in each case.

case angle axis = cross(a,b) SMTB 1.1.18 (today) after SMTB change
b exactly parallel ([0,0,2]) 0 [0,0,0] identity (correct) identity (correct, angle is exactly 0)
b parallel up to rounding (e.g. [.3,.4,.5], a = 2x same) ~2.1e-8 [0,0,0] identity (correct) ValueError (angle 2e-8 is well above 20 eps, axis is zero)
b antiparallel ([0,0,-3]) pi [-0,0,0] identity (wrong, should be a half-turn) ValueError
generic ([1,2,3]) 0.64 non-zero correct correct

Two additional pre-existing problems in the same helper, independent of the SMTB change:

  1. np.arccos domain error. After normalizing, np.dot(a, b) can be 1.0000000000000002, giving arccos(...) = nan plus a RuntimeWarning. Needs np.clip(..., -1, 1) if the arccos approach is kept.
  2. Antiparallel is silently wrong today (identity instead of 180 degrees), so the line-of-sight cylinder is drawn pointing the wrong way when the camera is directly below the target.

Other RTB call sites (audited, feat/symbolic-ik @ 61d1ddc)

Searched src, tests, examples, docs for angvec2r, angvec2tr, .AngVec(, .AngleAxis(. Only these use the affected functions:

site verdict
src/roboticstoolbox/robot/Robot.py ~1645 (rotation_between_vectors) affected, this issue
src/roboticstoolbox/models/URDF/URDFRobot.py ~410: u, n = unitvec_norm(v); R = angvec2r(n, u) safe: unitvec_norm already raises on a zero axis, and angvec2r is then given a unit axis and angle n > 0. (The n used as an angle is odd, but not related to this issue.)
tests/test_servo_half_turn.py (SE3.AngVec(np.linalg.norm(w), w) and SE3.AngVec(np.pi, axis) with a non-zero axis) safe: angle is the norm of the axis, so a zero axis implies a zero angle, which is still the identity
examples/icra2021.py, docs/notebooks/icra2021.ipynb: UnitQuaternion.AngVec(0.3, [1, 0, 0]) safe (unit, non-zero axis)

No other sibling toolbox is affected: machinevision-toolbox-python, bdsim, swift and spatialgeometry have no non-test uses of these functions beyond a scratch notebook script in MVTB (docs/notebooks/test_angvec2r.py, unit axis).

vision_collision_damper has no test in tests/, which is how this has gone unnoticed.

Suggested fix

Replace the helper with SE3(SO3.RotatedVector(a, b)). SO3.RotatedVector(v1, v2) is SMTB's purpose-built "rotation taking vector v1 to v2". It accepts non-unit vectors, returns the identity for parallel vectors, and for antiparallel vectors uses a closed-form half-turn (2uu^T - I) about a perpendicular axis. It does not use arccos, so there is no nan problem either.

It is already in the released SMTB 1.1.18 (SMTB #201, "SO3.RotatedVector returned identity for antipodal vectors", is an ancestor of the 1.1.18 tag), so no pin change is needed: RTB already requires spatialmath-python>=1.1.18. I checked the released 1.1.18 wheel from PyPI directly (not the dev checkout): SE3(SO3.RotatedVector(a, b)) maps a to b with a proper orthonormal R for parallel, near-parallel (angle ~1e-8), antiparallel and 500 random pairs, 0 failures of 507.

los_orientation = SE3(SO3.RotatedVector(np.array([0.0, 0.0, 1.0]), wTcp - wTtp))

and delete rotation_between_vectors. (Check that SO3 is imported in Robot.py.)

Because the fix is valid on today's released SMTB, it can, and should, land before the SMTB release containing the angvec2r change.

Acceptance criteria

  • The line-of-sight orientation returns the identity for parallel a, b (including parallel up to rounding, e.g. angle ~1e-8), a proper 180-degree rotation for antiparallel a, b, and the usual rotation otherwise. Never nan, never ValueError.
  • New unit tests for vision_collision_damper's orientation helper covering parallel, near-parallel (rounding), antiparallel and generic vectors. Factor the orientation computation out into a small module-level private function so it can be tested without building a robot and a Cylinder.
  • Tests pass against both SMTB 1.1.18 (released) and SMTB master (with the angvec2r change), e.g. pip install git+https://github.com/rai-opensource/spatialmath-python@master in a throwaway venv (confirm with sys.prefix / the venv's own bin/pip that it is isolated before installing).
  • examples/fetch_vision.py still runs for a camera directly above the target.

Notes for whoever picks this up

  • Do this before the SMTB release containing the angvec2r change; it is safe to land first because the fix is valid on the already-released 1.1.18.
  • Branch per the usual one-change-set rule; this is a separate concern from feat/symbolic-ik.
  • RTB conventions: NDArray/ArrayLike from roboticstoolbox.tools.types, modern type hints, reST docstrings, Australian/"color" spelling.
  • Context: SMTB is RAI-owned (rai-opensource/spatialmath-python), merges are RAI's; Peter reviews/approves PRs there.

Activity

  1. added
    tech-debtKnown technical debt / deferred cleanup, not a live bug
    on Oct 19, 2026
  2. petercorke commented on Oct 19, 2026

    @petercorke
    OwnerAuthor

    SMTB PR is now open: rai-opensource/spatialmath-python#267 (follow-up to rai-opensource/spatialmath-python#266). Fix here should land before that is released.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    SMTBtech-debtKnown technical debt / deferred cleanup, not a live bug

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions