feat(pose,quaternion): add __imatmul__ (@=) operator - #199
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
Merge-order suggestion across the currently open PRs, based on a full file-overlap check across all of them. Nothing here is a conflict with anything already merged — these are parallel PRs sharing some source files with each other, so a rebase will likely be needed wherever paths cross, regardless of order. Recommended order:
|
`X @= Y` now compounds poses/quaternions in place with automatic normalization, mirroring the existing `X @ Y` (__matmul__) behaviour but writing the normalized result back into the left operand instead of returning a new one. Useful when a pose is updated incrementally over many cycles and you don't want to pay for a fresh object each time. Added to BasePoseMatrix (covers SO2/SE2/SO3/SE3) and Quaternion (covers Quaternion/UnitQuaternion).
Quaternion.__imatmul__'s own docstring claimed `q1 @= q2` sets `q1 := qnorm(q1 * q2)`, but the implementation just delegated to __mul__ - identical to plain *=, no normalization at all, contradicting both the docstring and the entire point of adding a separate @= operator. UnitQuaternion.__matmul__ (pre-existing, unchanged) already does this correctly via smb.qunit(smb.qqmul(x, y)) - qunit being the normalizer; qnorm just returns the scalar magnitude, so it was never actually the right function despite the docstring's wording. Fixed by having __imatmul__ delegate to `left @ right` instead of left.__mul__(right). Deliberately not left.__matmul__(right): plain Quaternion has no __matmul__ (only UnitQuaternion defines one, with normalization), and calling the dunder directly as a plain attribute bypasses Python's normal operator fallback, raising a confusing AttributeError instead of the same TypeError `q1 @ q2` already raises for plain Quaternion. `left @ right` matches @'s behaviour exactly in both cases: normalizes for UnitQuaternion, raises consistently for Quaternion. Also fixed the docstring's `-> bool` return type (should be `-> Quaternion`) and its example, which used Quaternion.Eul() - a method that only exists on UnitQuaternion. Tests: added @= coverage for UnitQuaternion (must match @, not *) and for plain Quaternion (must raise TypeError, matching @, not silently degrade to *=).
There was a problem hiding this comment.
🟡 Changes recommended
The new __imatmul__ docstrings and pose @= tests need small adjustments to accurately document raised exceptions/normalization semantics and to unambiguously assert @= matches @.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds support for the augmented matrix-multiply operator (@=) for pose matrices and quaternions, aligning augmented assignment behavior with existing @ composition semantics (including normalization where applicable) and extending test coverage to exercise the new operator.
Changes:
- Add
__imatmul__toBasePoseMatrixto supportSO2/SE2/SO3/SE3 @=via normalized pose composition. - Add
__imatmul__toQuaternionto supportUnitQuaternion @=(normalized) and ensure plainQuaternion @=fails consistently like@. - Extend unit tests to cover
@=forUnitQuaternion,SO3, andSE3.
File summaries
| File | Description |
|---|---|
spatialmath/quaternion.py |
Adds Quaternion.__imatmul__ delegating to @ and updates related doc text nearby. |
spatialmath/baseposematrix.py |
Adds BasePoseMatrix.__imatmul__ delegating to __matmul__ for normalized composition. |
tests/test_quaternion.py |
Adds assertions that @= matches @ for UnitQuaternion and raises TypeError for plain Quaternion. |
tests/test_pose3d.py |
Adds basic @= coverage for SO3 and SE3. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| :return: Product of two operands with normalization | ||
| :rtype: Pose instance or NumPy array | ||
| :raises ValueError: for incompatible arguments |
| :raises: ValueError | ||
|
|
||
| ``q1 @= q2`` sets ``q1 := qnorm(q1 * q2)``. Only meaningful for | ||
| ``UnitQuaternion``, which is the only subclass defining ``__matmul__`` | ||
| (with normalization) that this delegates to; on a plain ``Quaternion`` | ||
| this raises the same ``TypeError`` that ``q1 @ q2`` would. |
| R = SO3() | ||
| R @= SO3.Rx(pi / 2) | ||
| self.assertIsInstance(R, SO3) | ||
| array_compare(R, rotx(pi / 2)) | ||
|
|
| T = SE3(1, 2, 3) | ||
| T @= SE3.Ry(pi / 2) | ||
| self.assertIsInstance(T, SE3) | ||
| array_compare( | ||
| T, np.array([[0, 0, 1, 1], [0, 1, 0, 2], [-1, 0, 0, 3], [0, 0, 0, 1]]) | ||
| ) | ||
|
|
What
X @= Ynow works as the augmented-assignment form of the existingX @ Y(
__matmul__, which composes with normalization) — previously onlyX *= Yexisted, which composes without normalization. Useful when a pose or unit
quaternion is updated incrementally over many cycles and you want the
normalized form without writing
X = X @ Yby hand.Added to
BasePoseMatrix(coversSO2/SE2/SO3/SE3) andQuaternion(covers
Quaternion/UnitQuaternion).Note on semantics: like the existing
__imul__, this doesn't mutate theobject in place —
__imatmul__returns a new (normalized) object and Pythonrebinds the name, same as
X = X @ Y. That matches the existing*=pattern in this codebase exactly, just saves writing
X = X @ Y.A bug found while adding test coverage
This started as a cherry-pick of old, never-merged WIP work. While writing
tests I found
Quaternion.__imatmul__'s docstring claimedq1 @= q2setsq1 := qnorm(q1 * q2), but the implementation just delegated to__mul__— identical to plain
*=, no normalization at all, contradicting both thedocstring and the entire point of adding
@=.UnitQuaternion.__matmul__(pre-existing, unchanged) already normalizescorrectly via
smb.qunit(smb.qqmul(x, y))—qunitbeing the actualnormalizer;
qnormjust returns the scalar magnitude, so it was neverreally the right function despite the docstring's wording.
Fixed by having
__imatmul__delegate toleft @ rightinstead ofleft.__mul__(right). Deliberately notleft.__matmul__(right)either:plain
Quaternionhas no__matmul__(onlyUnitQuaterniondefines one),and calling the dunder directly as a plain attribute bypasses Python's
normal operator fallback, raising a confusing
AttributeErrorinstead ofthe
TypeErrorthatq1 @ q2already raises consistently for plainQuaternion.left @ rightmatches@'s behaviour exactly in both cases.Also fixed the docstring's
-> boolreturn type annotation (should be-> Quaternion) and its example, which calledQuaternion.Eul()— amethod that only exists on
UnitQuaternion.Testing
@=coverage forSO3/SE3(must match@) alongside theexisting
*=tests intest_pose3d.py.@=coverage forUnitQuaternion(must match@, not*) and forplain
Quaternion(must raiseTypeError, matching@, not silentlydegrade to
*=) intest_quaternion.py.black --checkclean at the pinned 23.10.0.