Skip to content

fix(space_ops): flip the axis when angle_axis_from_quaternion folds the angle - #4980

Open
dylanpulver wants to merge 1 commit into
ManimCommunity:mainfrom
dylanpulver:fix/angle-axis-from-quaternion-axis-sign
Open

fix(space_ops): flip the axis when angle_axis_from_quaternion folds the angle#4980
dylanpulver wants to merge 1 commit into
ManimCommunity:mainfrom
dylanpulver:fix/angle-axis-from-quaternion-axis-sign

Conversation

@dylanpulver

@dylanpulver dylanpulver commented Sep 1, 2026

Copy link
Copy Markdown

Overview: What does this pull request change?

angle_axis_from_quaternion (manim/utils/space_ops.py:143) folds an angle above PI with

if angle > TAU / 2:
    angle = TAU - angle

but leaves the axis untouched. A rotation by angle about axis is a rotation by TAU - angle
about -axis, so for every quaternion whose angle exceeds PI the returned pair describes the
inverse rotation. This adds axis = -axis to the fold, plus a test.

Motivation and Explanation: Why and how do your changes improve the library?

On main, feeding quaternion_from_angle_axis's own output back in does not round trip:

axis = np.array([0.0, 0.0, 1.0])
q = quaternion_from_angle_axis(4.0, axis)
angle, ax = angle_axis_from_quaternion(q)      # -> 2.2832, [0. 0. 1.]
np.allclose(rotation_matrix(4.0, axis), rotation_matrix(angle, ax))   # False

5000 random (angle, axis) pairs, uniform angle over [0, TAU): 2556 fail the round trip on
main, 0 after. The split is clean — all 2556 have angle > PI, none have angle <= PI. The
neighbouring rotation_matrix_from_quaternion and rotation_matrix_transpose_from_quaternion were
run over the same 5000 inputs as a control: 0 failures each, before and after.

Which of the two possible remedies to take was settled by scipy, already a hard dependency and what
rotation_matrix in this same module builds on: Rotation.as_rotvec returns exactly the
angle in [0, PI] + signed-axis form, and the patched function agrees with it on all 5000 cases.
Simply deleting the fold also repairs the round trip, but changes the returned angle range — happy
to go that way instead if you prefer it. No test caught this because the function has zero call
sites inside manim/ and had no test.

Matched pair, same command and env both times (uv run pytest tests/module -q -p no:randomly):
492 passed / 90 failed before → 499 passed / 90 failed after. The 90 are pre-existing here
because this machine has no LaTeX (FileNotFoundError: 'latex'); they are identical on an
unmodified checkout. pre-commit run --files ... passes all hooks at the pinned versions
(ruff 0.16.2 lint + format, mypy, codespell).

Two mutants, both with the source marker asserted before measuring:

  • revert the source, keep the test → the 4 cases above PI fail, the 3 at or below PI pass;
  • drop the fold entirely instead → the rotation-matrix assertion passes and the [0, PI]
    assertion fails, which is what pins the remedy rather than just the round trip.

Links to added or changed documentation pages

angle_axis_from_quaternion in the space_ops reference; its Returns text now names the angle
range.

Further Information and Comments

Not tested: the LaTeX-dependent tests did not run here. I have not touched the -> Sequence[float]
return annotation, which does not describe the (float, ndarray) tuple, to keep the diff minimal.

Written with AI assistance (Claude); the measurements above were run locally on this branch.

Reviewer Checklist

  • The PR title is descriptive enough for the changelog, and the PR is labeled correctly
  • If applicable: newly added non-private functions and classes have a docstring including a short summary and a PARAMETERS section
  • If applicable: newly added functions and classes are tested

…he angle

A rotation by `angle` about `axis` equals a rotation by `TAU - angle`
about `-axis`. The fold at the end of `angle_axis_from_quaternion`
replaced the angle without negating the axis, so for any quaternion whose
angle exceeds PI the returned pair described the inverse rotation.

Adds a round-trip test that checks the returned pair against the input
rotation matrix and against scipy's canonical `Rotation.as_rotvec` form.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant