Skip to content

♻️ refactor(transforms): declare groups() on AbstractTransform - #730

Open
nstarman wants to merge 3 commits into
GalacticDynamics:mainfrom
nstarman:claude/declare-groups
Open

♻️ refactor(transforms): declare groups() on AbstractTransform#730
nstarman wants to merge 3 commits into
GalacticDynamics:mainfrom
nstarman:claude/declare-groups

Conversation

@nstarman

Copy link
Copy Markdown
Contributor

Stacks on #728 — review the top commit only, or merge #728 first. It removes the ty: ignore that PR had to add.

The gap

Every concrete transform defines groups(), and callers depend on it — Composed.groups reads it off each component, _merge off both operands — but AbstractTransform declared nothing. It was a protocol by convention, with nothing on the base to point at.

A type checker could therefore only see the attribute where the caller happened to hold a concrete type. ty passes Composed.groups merely because its transforms field is loosely typed, and fails the moment a parameter is annotated AbstractLinearTransform — which is exactly what #728 hit, and what its ty: ignore was covering.

The fix

One abstract method on the base. Spelled as an instance method, so both existing forms satisfy it:

  • most operators answer from their type alone and implement it as a classmethod, which stays reachable from an instance
  • Composed and Linear compute it from what they hold

Abstract rather than a default. All eleven concrete transforms already implement it:

HAVE   : Boost, Composed, Identity, Linear, LorentzBoost, Reflect,
         Rotate, Scale, Shear, TimeDep, Translate
MISSING: AbstractAdd, AbstractCompositeTransform, AbstractLinearTransform

The three that don't are the abstract intermediates, which is what abstract means. A default returning some fallback group would be a wrong answer waiting for a subclass that forgot to override — and since groups() feeds least_common_supergroup, a wrong group silently widens every fusion that touches it.

Verification

🤖 Generated with Claude Code

nstarman and others added 3 commits August 17, 2026 09:38
`_merge` had rules for `Rotate` and `AbstractAdd` only, so
`simplify(S1 | S2)` returned the two-element `Composed` unchanged and a chain
of scalings paid one full `act` dispatch per element. A three-scaling chain
cost 8822 us eagerly against 3953 us fused, 2.2x.

The merge is a matrix product, so it is exact -- the fused and sequential
results agree to 0.0, not merely to a tolerance -- and it runs regardless of
`approx`, like the `Rotate` rule it mirrors.

Mixed linear chains (`Rotate | Scale`) still do not fuse; that is 4.9x and
needs a return type this library does not yet have. Filed separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`simplify` could merge two rotations or two scalings, but not a rotation with
a scaling: the product is neither, and there was no type to put it in. A
five-operator linear chain therefore stayed five operators, paying a full
`act` dispatch each -- 11481 us eagerly against 3415 us fused, 3.4x, with the
two agreeing to 8.9e-16.

`Linear` is a sibling `@final` under `AbstractLinearTransform`, which already
owns the matrix, its validation and every `act` path, so the class is a field
and three members. The generic `_merge` rule is the fallback; the same-type
rules are more specific and still win, so `Rotate | Rotate` keeps returning a
`Rotate`. Nothing that fused before fuses differently.

The one new idea is that the group travels with the matrix rather than with the
type: `Rotate | Reflect` is still orthogonal, and reporting it as merely affine
would lose that. `Composed.groups` already computes exactly this least common
supergroup, and group classes are registered static, so it is a field.

Mismatched dimensions -- a 3x3 `Rotate` beside a 4x4 `LorentzBoost` -- decline
to merge. That guard is tested through `_merge` rather than `simplify`, which
has no rule registered for `LorentzBoost` and raises first; pre-existing, fixed
separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every concrete transform defines `groups()`, and callers depend on it --
`Composed.groups` reads it off each component, `_merge` off both operands --
but the base declared nothing, so it was a protocol by convention. A type
checker could only see the attribute where the caller happened to hold a
concrete type: `ty` passes `Composed.groups` because its `transforms` field is
loosely typed, and fails the moment a parameter is annotated
`AbstractLinearTransform`. That is what the `ty: ignore` in `general_linear`
was covering; it is removed here.

Declared as an instance method so both existing forms satisfy it. Most
operators answer from their type alone and implement it as a `classmethod`,
which stays reachable from an instance; `Composed` and `Linear` compute it from
what they hold.

Abstract rather than a default: all eleven concrete transforms already
implement it, and only the abstract intermediates (`AbstractAdd`,
`AbstractLinearTransform`, `AbstractCompositeTransform`) do not, which is what
abstract means. A default returning some fallback group would be a wrong answer
waiting for a subclass that forgot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 17, 2026 16:42
@github-actions github-actions Bot added ✅ Add / update / pass tests Add, update, or pass tests. ♻️ Refactor code Refactor code. ⚡️ Improve performance Improve performance. ✨ Introduce new features Introduce new features. labels Aug 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR strengthens the transforms API by formally declaring groups() on AbstractTransform, making group membership an explicit part of the base interface while supporting both class-derived and instance-computed implementations. It also expands simplification/fusion capabilities for linear transforms by introducing a general Linear operator and adding additional _merge rules with accompanying tests.

Changes:

  • Add an abstract groups() method to AbstractTransform to formalize group membership as part of the core transform interface.
  • Introduce Linear (a general linear transform that carries its group as data) and a generic _merge(AbstractLinearTransform, AbstractLinearTransform) fallback for mixed linear fusion.
  • Add a _merge(Scale, Scale) rule and extend simplify tests to cover new scale merging and mixed linear fusion behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/unit/transforms/test_simplify.py Adds tests for Scale merging and mixed linear fusion into Linear, including group propagation assertions.
src/coordinax/transforms/_src/actions/scale.py Adds _merge(Scale, Scale) to fuse adjacent scales via matrix multiplication.
src/coordinax/transforms/_src/actions/general_linear.py Adds the new Linear transform, its group-carrying behavior, simplify rule, and generic linear _merge fallback.
src/coordinax/transforms/_src/actions/base.py Declares AbstractTransform.groups() as an abstract method with documentation and example.
src/coordinax/transforms/_src/actions/init.py Exports the new general_linear action module.
src/coordinax/transforms/init.py Re-exports Linear as part of the public coordinax.transforms API.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +90 to +94
def __init__(
self, M: Any, group: type[groups.AbstractTransformGroup] = groups.AffineGroup
) -> None:
object.__setattr__(self, "M", jnp.asarray(M))
object.__setattr__(self, "group", group)
Comment on lines 38 to 42
"Translate",
"LorentzBoost",
"Linear",
"Rotate",
"Reflect",
@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.83333% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.55%. Comparing base (cd59977) to head (f1db6a2).

Files with missing lines Patch % Lines
...oordinax/transforms/_src/actions/general_linear.py 95.45% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #730   +/-   ##
=======================================
  Coverage   96.54%   96.55%           
=======================================
  Files         265      266    +1     
  Lines        8780     8828   +48     
=======================================
+ Hits         8477     8524   +47     
- Misses        303      304    +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Labels

✅ Add / update / pass tests Add, update, or pass tests. ⚡️ Improve performance Improve performance. ✨ Introduce new features Introduce new features. ♻️ Refactor code Refactor code.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants