Skip to content

Let SymplecticEuler say how it meets the layerwise seam - #251

Open
michakraus wants to merge 1 commit into
pghnn-rebasefrom
pghnn-layerwise-seam
Open

Let SymplecticEuler say how it meets the layerwise seam#251
michakraus wants to merge 1 commit into
pghnn-rebasefrom
pghnn-layerwise-seam

Conversation

@michakraus

Copy link
Copy Markdown
Member

Resolves #245, given SymbolicNeuralNetworks #56.

Base branch. This targets pghnn-rebase, the rebased form of #207's work, because that is where SymplecticEuler lives — on main the GeneralizedHamiltonianArchitecture constructor is still error("GHNN still has to be implemented!"). #207 and its branch are untouched.

What this does

SymbolicNeuralNetworks composes the pullback of a chain layer by layer, putting fresh symbolic variables between two layers instead of inlining one layer's expression into the next — which is what keeps the symbolic material a sum over layers rather than a product. Its seam was a plain vector, so it assumed every layer maps an array to an array.

A SymplecticEuler built with return_parameters = true threads the parameters of the system on to the next layer and therefore returns a Tuple, which could not be seeded at all. Worse, composes_layerwise said the chain decomposes, so layerwise = :auto committed to that path and then raised — SNN #54.

SNN 0.7 widens the seam. SymplecticEuler now carries SymbolicEnergy's parameter_length and parameter_layout — as a trailing type parameter, so every existing partial signature goes on matching with the layout left free — and implements the four methods that say how it meets the seam: carried_variables, seam_value, state_expressions and seam_arguments. What it carries is data and never a differentiation target: the sensitivity pairs with the state, and the flattened system parameters become one more argument of each generated kernel.

ParametricLoss declares its loss_expression, so the sweep can seed it. The construction guesses one by applying the loss in its four-argument form, and this loss takes five — without the declaration loss_seed would decline and fall back to the construction that cannot build. The declaration restates _compute_loss, which is what the functor itself uses.

Results

SymbolicNeuralNetworks.SymbolicPullback(SymbolicNeuralNetwork(arch), FeedForwardLoss())
case before now
n_integrators = 1, no system parameters MethodError under :auto builds layerwise; 1.4e-16 vs the monolithic path, 8.3e-17 vs Zygote
n_integrators = 2 expression of 10⁹ terms, never finished 4 steps in ~1.5 s; 7.6e-17 vs Zygote
parametrized, ParametricLoss, 2 integrators no construction builds 2.2e-16 vs Zygote

The parametrized row is the one worth dwelling on. The monolithic construction traces the chain from a plain vector, so every layer defaults its system parameters to NullParameters and the energy network's first Dense is then short of the components it reads; the build dies inside Symbolics. The layerwise construction is handed the pair (state, system parameters) and differentiates the map that was actually asked for. It is not the faster construction there — it is the only one.

The tractability limit stays, and now says where to go

_check_symbolic_pullback_is_tractable keeps its n_integrators > 1 refusal, because the two constructors in src/pullbacks/symbolic_hnn_pullback.jl still build one expression for the whole network by hand. It also guards SymbolicPullback(arch), which had no guard at all and failed with an obscure Symbolics error.

That one cannot move onto the layerwise construction: HNNLoss evaluates the pre-built Hamiltonian vector field rather than the chain, so there is no prediction for a layerwise seed to start from. SymbolicPullback(nn, ::ParametricLoss, system_params) can, and doing so would also retire its concatenation of the system parameters onto the network input — that is #245's remaining half, left for a follow-up.

Known limitation

The sweep runs the forward pass by calling each layer, so it inherits SymplecticEuler's own single-sample restriction — the @view(…)[:, 1] in the layer and the @assert size(qp, 2) == 1 in concatenate_array_with_parameters. A batch has to be taken sample by sample, as apply_parametric already does. The monolithic construction never calls the layer at run time and so does not have this restriction. Lifting it is layer work, independent of this PR.

Testing

New test/generalized_hamiltonian_neural_networks/layerwise_symbolic_pullback_test.jl, registered in runtests.jl. All five GHNN test files pass, including the four that existed before — the struct gained fields and a type parameter, so those are the regression check.

Blocked on

SymbolicNeuralNetworks 0.7 is not yet registered. Verified locally with that tree Pkg.developed in.

🤖 Generated with Claude Code

`SymbolicNeuralNetworks` composes the pullback of a chain layer by layer, putting fresh symbolic
variables between two layers instead of inlining one layer's expression into the next. Its seam was a
plain vector, so it assumed every layer maps an array to an array. A `SymplecticEuler` built with
`return_parameters = true` threads the parameters of the system on to the next layer and therefore
returns a `Tuple`, which could not be seeded at all -- and because `composes_layerwise` said the chain
decomposes, `layerwise = :auto` committed to that path and then raised (SNN #54).

SNN 0.7 widened the seam. `SymplecticEuler` now carries `SymbolicEnergy`'s `parameter_length` and
`parameter_layout` -- as a trailing type parameter, so every existing partial signature goes on
matching with the layout left free -- and implements the four methods that say how it meets the seam:
`carried_variables`, `seam_value`, `state_expressions` and `seam_arguments`. What it carries is data and
never a differentiation target: the sensitivity pairs with the state, and the flattened system
parameters become one more argument of each generated kernel.

So

    SymbolicNeuralNetworks.SymbolicPullback(SymbolicNeuralNetwork(arch), FeedForwardLoss())

builds for any `n_integrators` -- four steps for two integrators in about 1.5 s, against a monolithic
expression of 10⁹ terms that never finished -- and agrees with `Zygote` to machine precision.

For an architecture built with `parameters` it is the *only* construction that builds. The monolithic
one traces the chain from a plain vector, so every layer defaults its system parameters to
`NullParameters` and the energy network's first `Dense` is then short of the components it reads; the
build dies inside `Symbolics`. The layerwise construction is handed the pair and differentiates the map
that was asked for. `ParametricLoss` declares its `loss_expression` so that the sweep can seed it --
the guess applies a loss in its four-argument form and this loss takes five, so without the
declaration `loss_seed` would decline and fall back to the construction that cannot build.

`_check_symbolic_pullback_is_tractable` keeps its `n_integrators > 1` refusal, because the two
constructors in that file still build one expression for the whole network by hand -- and it now says
where to go instead. It also guards `SymbolicPullback(arch)`, which had no guard and failed with an
obscure `Symbolics` error. That one cannot move onto the layerwise construction at all: `HNNLoss`
evaluates the pre-built Hamiltonian vector field rather than the chain, so there is no prediction for a
layerwise seed to start from. `SymbolicPullback(nn, ::ParametricLoss, system_params)` can, and doing so
would retire its concatenation of the system parameters onto the network input; that is #245's
remaining half.

One limitation stays, and it is the layer's own: the sweep runs the forward pass by *calling* each
layer, so it inherits `SymplecticEuler`'s single-sample restriction -- the `@view(…)[:, 1]` here and
the `@assert size(qp, 2) == 1` in `concatenate_array_with_parameters`. A batch has to be taken sample
by sample, as `apply_parametric` already does. The monolithic construction never calls the layer at run
time and so does not have this restriction.

Needs SymbolicNeuralNetworks 0.7, which is not yet registered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 24, 2026 13:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

2 participants