Skip to content

Forward-mode AD via the shared scalar rule table#84

Open
dpsanders wants to merge 1 commit into
derivative-hardeningfrom
forward-mode-ad
Open

Forward-mode AD via the shared scalar rule table#84
dpsanders wants to merge 1 commit into
derivative-hardeningfrom
forward-mode-ad

Conversation

@dpsanders

Copy link
Copy Markdown
Member

Summary

Stacked on #83. Adds forward-mode AD, reusing the unary_rule / binary_rule dispatch table that PR #83 introduced for reverse-mode. One rule table, two AD passes — no second source of truth to drift.

API

  • dotted(v) — tangent variable for v, written with a combining overdot (parallel to the internal bar(v) for reverse mode).
  • tangent_code(ex_or_ssa, vars) — appends one tangent assignment per forward assignment, surfacing input_tangents and output_tangent in SSAFunction.variables.
  • tangent(ex, vars) — exported. Compiles a callable
    (__inputs, __tangents) -> (value, directional_derivative)

Behaviour

@variables x y
t = tangent(x^2 + y^2, [x, y])
t((3.0, 4.0), (1.0, 0.0))   # (25.0, 6.0) — ∂f/∂x = 2x
t((3.0, 4.0), (0.0, 1.0))   # (25.0, 8.0) — ∂f/∂y = 2y
t((3.0, 4.0), (0.7, -0.4))  # full Gateaux derivative along (0.7, -0.4)

Verified against reverse-mode gradient in tests.

Note on RGF naming

The compiled function's first argument is __inputs, not __args. The latter collides with RuntimeGeneratedFunctions' own internal binding and triggers a spurious BoundsError at call time. Documented inline where the Expr is built.

Test plan

  • 68/68 tests pass (59 inherited from Derivative hardening: own the scalar rule table #83, +9 new for tangent).
  • Directional derivative matches reverse-mode gradient component-by-component.
  • Full Gateaux derivative along arbitrary direction matches ⟨∇f, v⟩.

Follow-ups

Hessian via forward-over-reverse (applying tangent_code to the SSA produced by gradient_code) will land in a separate PR on top of this one.

🤖 Generated with Claude Code

Introduces `tangent_code` and `tangent` as the forward-mode counterparts
to `gradient_code` and `gradient`. Both pull partials from the same
`unary_rule` / `binary_rule` dispatch table, so there is only one source
of truth for scalar derivatives.

- `dotted(v)` — tangent variable for `v`, written with a combining
  overdot (parallel to `bar(v)` for reverse mode).
- `tangent_eq(eq)` — `_ȧ = Σᵢ (∂f/∂xᵢ) · ẋᵢ` for one forward assignment.
- `tangent_code(ex_or_ssa, vars)` — appends the tangent pass and
  surfaces `input_tangents` and `output_tangent` in `variables`.
- `tangent(ex, vars)` — compiles to
  `(__inputs, __tangents) -> (value, directional_derivative)`. The
  argument name is deliberately `__inputs`, not `__args`: the latter
  collides with RuntimeGeneratedFunctions' own internal binding and
  causes a spurious `BoundsError` at call time.

Verified: `tangent((x,y), (1,0))` reproduces `gradient`'s first
component, `tangent((x,y), dir)` gives the full Gateaux derivative.
Bumps to v0.7.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dpsanders dpsanders mentioned this pull request Apr 20, 2026
6 tasks
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