Skip to content

functional: lazy functional algebra (mirror LinOp)#60

Merged
Pavlo3P merged 4 commits into
0.4.2from
59-functional-algebra-mirror-linop
Jul 1, 2026
Merged

functional: lazy functional algebra (mirror LinOp)#60
Pavlo3P merged 4 commits into
0.4.2from
59-functional-algebra-mirror-linop

Conversation

@Pavlo3P

@Pavlo3P Pavlo3P commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

Gives Functional the same lazy additive/scalar algebra LinOp already has (0.4.2 W4), so objectives compose naturally:

2.0 * F        # ScaledFunctional
F + G          # SumFunctional
F - G          # SumFunctional(F, -G)
-F             # ScaledFunctional(-1, F)
F + 3.0        # ShiftedFunctional (affine — the one place functionals extend beyond LinOp)

The key correctness invariant: a functional's grad is a metric (Riesz) gradient — a domain element — so gradients combine through X.add/X.scale (not raw +/*), which is correct on tree/stacked/weighted domains.

Changes (four gradual commits)

  1. Scalar algebraScaledFunctional (grad = X.scale(a, F.grad)), make_scaled_functional (unit passthrough, nested-scalar fold), __neg__/__mul__/__rmul__.
  2. Additive algebraSumFunctional (grad = term grads folded via X.add), make_functional_sum (flatten nested sums, single-unwrap, shared-domain validation), __add__/__radd__/__sub__/__rsub__ (0 + F enables builtin sum).
  3. Affine + identityShiftedFunctional (F + c, grad unchanged), ZeroFunctional (additive identity; make_scaled returns it for a zero scalar, make_functional_sum drops it). Scalar operands route to the affine shift. Registered all four in functional_cases() so the registry-completeness check + generic functional laws (value / gradient / directional-derivative / conversion) exercise them.
  4. Verify fixes (below).

Adversarial verify — two bugs caught and fixed pre-merge

A multi-agent review (gradient correctness, factory laws, overload dispatch, jit integration) found:

  • Complex-scalar conjugation: the Riesz gradient of a·F is conj(a)·grad(F) (the domain inner product conjugates its first argument), matching ScaledLinOp.rapply. ScaledFunctional.grad was scaling by a, not conj(a) — wrong for complex scalars. Fixed; value still scales by a.
  • Swallowed domain mismatch: make_functional_sum dropped ZeroFunctional terms before validating domains, so F(X) + Zero(Y) silently returned F. Now validates all flattened terms' domains (folding in backend/dtype) before dropping zeros, mirroring linop.make_sum.

Both are covered by regression tests (complex Riesz identity; mismatched-domain Zero raising). Two jit-edge findings were reviewed and dismissed as unreachable.

Testing

  • tests/functional/test_algebra.py (31 tests): scalar/additive/affine/zero algebra, factory simplifications, tree-domain grad via X.add/X.scale, complex Riesz identity, domain-mismatch guards, pytree round-trips, and an end-to-end minimize_optax(0.5F + 0.5F).
  • Full suite: 3455 passed, 170 skipped; ruff / ruff-D / jit_audit / docstring_audit all clean.

🤖 Generated with Claude Code

Pavlo3P and others added 4 commits July 1, 2026 15:27
Add the scalar half of the functional algebra (0.4.2 W4), mirroring
linop/_algebra.py: ScaledFunctional (value = a*F.value; grad = X.scale(a, F.grad)
in the domain geometry, not raw *), the make_scaled_functional factory (unit
passthrough + nested-scalar fold), and Functional.__neg__/__mul__/__rmul__
delegating to it (NotImplemented for non-scalar operands). Exported at package
and top level.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the additive half of the functional algebra (0.4.2 W4): SumFunctional
(value = sum of term values; grad = the term grads folded via X.add in the
domain geometry, correct on tree/stacked domains), make_functional_sum (flattens
nested sums, unwraps a lone term, validates a common domain), and
Functional.__add__/__radd__/__sub__/__rsub__ (0 + F enables builtin sum();
NotImplemented for non-Functional operands). Exported at package and top level.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erage

Complete the functional algebra (0.4.2 W4):

- ShiftedFunctional (F + c: value shifted, gradient unchanged) and the
  make_shifted_functional factory (zero-offset passthrough, nested-offset fold).
- ZeroFunctional (value 0, grad = X.zeros()) as the additive identity;
  make_scaled_functional now returns it for a zero scalar and make_functional_sum
  drops it (empty sum -> ZeroFunctional).
- Functional.__add__/__radd__/__sub__/__rsub__ route a scalar operand to the
  affine shift (the one place functionals extend beyond LinOp).
- Register the four algebra nodes in functional_cases() so the registry-
  completeness check and the generic functional laws (value / gradient /
  directional-derivative / conversion) exercise them.
- Tests: affine + zero behavior, factory zero-handling, and an end-to-end
  minimize_optax(0.5F + 0.5F) integration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dation

Adversarial review caught two bugs:

- ScaledFunctional.grad/value_and_grad must conjugate a complex scalar: the
  Riesz gradient of a*F is conj(a)*grad(F) (the domain inner product conjugates
  its first argument), matching ScaledLinOp.rapply. Value still scales by a;
  only the metric-gradient side conjugates. Real scalars are unaffected.
- make_functional_sum dropped ZeroFunctional terms BEFORE validating domains, so
  F(X) + Zero(Y) silently returned F. Validate all flattened terms' domains
  (which fold in the backend/dtype context) before dropping zeros, mirroring
  linop make_sum.

Regression tests: the complex Riesz identity <grad(aF), h> == a<c, h>, and a
mismatched-domain Zero term raising instead of being swallowed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Pavlo3P
Pavlo3P merged commit f48f16b into 0.4.2 Jul 1, 2026
5 checks passed
@Pavlo3P
Pavlo3P deleted the 59-functional-algebra-mirror-linop branch July 1, 2026 18:59
@Pavlo3P Pavlo3P mentioned this pull request Jul 1, 2026
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