Skip to content

Commit 2829ced

Browse files
petercorkeclaude
andauthored
perf(base): speed up isR/ishom/ishom2 orthogonality checks (#213)
## Summary - `isR` (SO(2)/SO(3) orthogonality test used by `SE3`/`SO3`/`SE2`/`SO2` construction with `check=True`) previously went through `np.linalg.det`/`np.linalg.norm`/`np.eye`, whose generic dispatch overhead dominates cost for such small (2x2/3x3) matrices. Added explicit fast paths for the 3x3 and 2x2 cases using a hand-written cofactor-expansion determinant and a squared-residual orthogonality check, falling back to the original generic implementation for any other size. - `ishom`/`ishom2` bottom-row validity checks replaced `all(T[-1,:] == np.array([...]))` (array allocation + Python-level `all()` over a NumPy bool array) with direct scalar comparisons. - Net effect: `isR` is roughly 2x faster standalone, and `SE3(T, check=True)` construction is roughly 30-40% faster end to end. ## Test plan - [x] `pytest tests/` — 343 passed - [x] Verified pre-existing behaviour for symbolic (SymPy `dtype=object`) matrices with `check=True` is unchanged (was already unsupported before this change, for unrelated reasons - `np.linalg.det`/`norm` don't support `dtype=object` either) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 8455bd9 commit 2829ced

3 files changed

Lines changed: 41 additions & 6 deletions

File tree

‎spatialmath/base/transforms2d.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,12 @@ def ishom2(T: Any, check: bool = False, tol: float = 20) -> bool: # TypeGuard(S
367367
and T.shape == (3, 3)
368368
and (
369369
not check
370-
or (smb.isR(T[:2, :2], tol=tol) and all(T[2, :] == np.array([0, 0, 1])))
370+
or (
371+
smb.isR(T[:2, :2], tol=tol)
372+
and T[2, 0] == 0
373+
and T[2, 1] == 0
374+
and T[2, 2] == 1
375+
)
371376
)
372377
)
373378

‎spatialmath/base/transforms3d.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,13 @@ def ishom(T: Any, check: bool = False, tol: float = 20) -> bool:
391391
and T.shape == (4, 4)
392392
and (
393393
not check
394-
or (isR(T[:3, :3], tol=tol) and all(T[3, :] == np.array([0, 0, 0, 1])))
394+
or (
395+
isR(T[:3, :3], tol=tol)
396+
and T[3, 0] == 0
397+
and T[3, 1] == 0
398+
and T[3, 2] == 0
399+
and T[3, 3] == 1
400+
)
395401
)
396402
)
397403

‎spatialmath/base/transformsNd.py‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -378,10 +378,34 @@ def isR(R: NDArray, tol: float = 20) -> bool: # -> TypeGuard[SOnArray]:
378378
379379
:seealso: isrot2, isrot
380380
"""
381-
return bool(
382-
np.linalg.norm(R @ R.T - np.eye(R.shape[0])) < tol * _eps
383-
and np.linalg.det(R) > 0
384-
)
381+
n = R.shape[0]
382+
if n == 3:
383+
# explicit cofactor expansion avoids the dispatch overhead of
384+
# np.linalg.det/norm, which dominates cost for such a small matrix
385+
det = (
386+
R[0, 0] * (R[1, 1] * R[2, 2] - R[1, 2] * R[2, 1])
387+
- R[0, 1] * (R[1, 0] * R[2, 2] - R[1, 2] * R[2, 0])
388+
+ R[0, 2] * (R[1, 0] * R[2, 1] - R[1, 1] * R[2, 0])
389+
)
390+
if det <= 0:
391+
return False
392+
D = R @ R.T
393+
D[0, 0] -= 1.0
394+
D[1, 1] -= 1.0
395+
D[2, 2] -= 1.0
396+
return bool(np.sum(D * D) < (tol * _eps) ** 2)
397+
elif n == 2:
398+
det = R[0, 0] * R[1, 1] - R[0, 1] * R[1, 0]
399+
if det <= 0:
400+
return False
401+
D = R @ R.T
402+
D[0, 0] -= 1.0
403+
D[1, 1] -= 1.0
404+
return bool(np.sum(D * D) < (tol * _eps) ** 2)
405+
else:
406+
return bool(
407+
np.linalg.norm(R @ R.T - np.eye(n)) < tol * _eps and np.linalg.det(R) > 0
408+
)
385409

386410

387411
def isskew(S: NDArray, tol: float = 20) -> bool: # -> TypeGuard[sonArray]:

0 commit comments

Comments
 (0)