Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ jobs:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: julia-actions/cache@v1
# `[sources]` in Project.toml is Pkg 1.11+, so the 1.10 jobs ignore it and fail to
# resolve the unregistered NeuralNetworkParameters. Adding it explicitly works on every
# version. Remove this step, and the `[sources]` entry, once it is registered.
- name: Add unregistered NeuralNetworkParameters
run: julia --project=. -e 'using Pkg; Pkg.add(url="https://github.com/JuliaGNI/NeuralNetworkParameters.jl", rev="main")'
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
Expand Down
63 changes: 63 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,69 @@ breaking release).
> alongside the work. Where a release removed exported names the list is given; where it is a
> reconstruction of intent, it says so.

## [Unreleased]

### Added

**Parametric generalized Hamiltonian neural networks (PGHNNs)**
([#207](https://github.com/JuliaGNI/GeometricMachineLearning.jl/pull/207)). A family of
architectures whose forward pass takes the parameters of the *system* alongside the state, so one
network covers a whole parameter range rather than a single problem instance.

- **`GeneralizedHamiltonianArchitecture`** is implemented. It used to be a stub whose constructor
threw `error("GHNN still has to be implemented!")`. It composes `n_integrators` symplectic Euler
steps, each of which differentiates a learned kinetic or potential energy —
`SymbolicKineticEnergy` and `SymbolicPotentialEnergy`, built into an executable gradient by
`build_gradient`.
- **`ForcedGeneralizedHamiltonianArchitecture`** and **`ForcedSympNet`**, which add `ForcingLayer`s
for forcing and dissipation in the `q`, `p` or both coordinates, following the
Lagrange–d'Alembert integrator of [marsden2001discrete](@cite).
- **`ParametricDataLoader`**, which carries one set of system parameters per trajectory and hands
the matching parameters to each sample of a batch. Built from an `EnsembleSolution` whose members
were integrated at different parameters.
- **`ParametricLoss`**, `FeedForwardLoss` with the system parameters threaded through, and a
`SymbolicPullback(nn, ::ParametricLoss, system_params)` that differentiates it symbolically.
- **`ParametricResNet`** and a widened **`ResNet`**, which now takes a `width` separate from the
system dimension and uses `WideResNetLayer` when the two differ. This is the non-structure-preserving
baseline the PGHNNs are compared against.
- `QPT2` and `QPTOAT2`: `QPT`/`QPTOAT` with the array rank fixed but the two array *types* allowed to
differ, which is what splitting an input array into `q` and `p` produces.

**New dependency: [NeuralNetworkParameters][nnp].** The system parameters are flattened into the
network input, and `flatten`/`unflatten` do that. `ParameterHandling` cannot: `GeometricOptimizers`
defines `ParameterHandling.flatten(x)` with an unbound type parameter, and that method wins.

Until NeuralNetworkParameters is registered it is pinned to its `main` branch: a `[sources]` entry
in `Project.toml` and `docs/Project.toml`, plus an explicit `Pkg.add(url = …, rev = "main")` step
in `.github/workflows/CI.yml`, because `[sources]` is Pkg 1.11+ and the test matrix includes
Julia 1.10. All three go away on registration.

### Changed

- **`SymplecticEuler`, `SymplecticEulerA` and `SymplecticEulerB` are no longer exported.** The names
now belong to the layer type of the generalized architectures; the *training methods* they used to
name are `SymplecticEulerIntegrator`, `SymplecticEulerIntegratorA` and
`SymplecticEulerIntegratorB`. `SEuler`, `SEulerA` and `SEulerB`, which is how they are constructed,
are unchanged.
- `src/architectures/hamiltonian_neural_network.jl` is split: it keeps the abstract
`HamiltonianArchitecture`, and `StandardHamiltonianArchitecture` moves to
`standard_hamiltonian_neural_network.jl`. `hamiltonian_vector_field` is narrowed from
`::HamiltonianArchitecture` to `::StandardHamiltonianArchitecture` accordingly.

### Fixed

- `concatenate_array_with_parameters(::AbstractMatrix, ::AbstractVector)` concatenated a batch with
`vcat` rather than `hcat`, collapsing it into a single long vector.
- **`ForcedGeneralizedHamiltonianArchitecture` could not be evaluated at all.** The
parameter-dependent `NeuralNetwork` functor and the `Optimizer` entry point were defined for
`GeneralizedHamiltonianArchitecture` only, and the two are siblings under `HamiltonianArchitecture`
rather than sub- and supertype, so `nn(x, μ)` fell through to the generic functor and read the
*system* parameters as the *network* parameters.
- `ParametricResNet(::DataLoader, n_blocks, width; parameters = …)` accepted `parameters` and then
dropped it, silently building a network with no parameter dependence.

[nnp]: https://github.com/JuliaGNI/NeuralNetworkParameters.jl

## [0.5.0] — 2026-08-19

**The optimizer machinery moves to [GeometricOptimizers][go].** GML no longer implements its own
Expand Down
8 changes: 8 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c"
LazyArrays = "5078a376-72f3-5289-bfd5-ec5146d43c02"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
NNlib = "872c559c-99b0-510c-b3b7-b6c96a88d5cd"
NeuralNetworkParameters = "67f4d93a-60e9-472b-8cdd-1ccf6005724a"
ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SymbolicNeuralNetworks = "aed23131-dcd0-47ca-8090-d21e605652e3"
Expand All @@ -31,6 +32,12 @@ HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
[extensions]
HDF5Ext = "HDF5"

# NeuralNetworkParameters is not in the General registry yet. `[sources]` is Pkg 1.11+, so the
# Julia 1.10 CI jobs add it explicitly in `.github/workflows/CI.yml` instead. Both go away, along
# with this comment, once it is registered.
[sources]
NeuralNetworkParameters = {url = "https://github.com/JuliaGNI/NeuralNetworkParameters.jl", rev = "main"}

[compat]
AbstractNeuralNetworks = "0.6.4"
ChainRules = "1"
Expand All @@ -47,6 +54,7 @@ HDF5 = "0.16, 0.17"
KernelAbstractions = "0.9"
LazyArrays = "=2.3.2"
NNlib = "0.8, 0.9"
NeuralNetworkParameters = "0.1"
ProgressMeter = "1"
SafeTestsets = "0.1"
SymbolicNeuralNetworks = "0.5"
Expand Down
2 changes: 2 additions & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ GeometricOptimizers = "fc236c15-5557-4942-aa65-b650f329279e"
GeometricProblems = "18cb22b4-ad41-5c80-9c5f-710df63fbdc9"
HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
LaTeXStrings = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
NeuralNetworkParameters = "67f4d93a-60e9-472b-8cdd-1ccf6005724a"
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"

[sources]
BrenierTwoFluid = {rev = "main", url = "https://github.com/ToBlick/BrenierTwoFluids.git"}
GeometricMachineLearning = {path = ".."}
NeuralNetworkParameters = {url = "https://github.com/JuliaGNI/NeuralNetworkParameters.jl", rev = "main"}
15 changes: 13 additions & 2 deletions docs/src/GeometricMachineLearning.bib
Original file line number Diff line number Diff line change
Expand Up @@ -746,9 +746,10 @@ @article{bon2024optimal

@article{kingma2014adam,
title={Adam: a method for stochastic optimization},
author={Kingma, DP},
author={Kingma, Diederik P. and Ba, Jimmy Lei},
journal={arXiv preprint arXiv:1412.6980},
year={2014}
year={2014},
note={Published as a conference paper at ICLR 2015}
}

@article{toda1967vibration,
Expand Down Expand Up @@ -923,6 +924,16 @@ @article{ge1988lie
publisher={Elsevier}
}

@article{marsden2001discrete,
title={Discrete mechanics and variational integrators},
author={Marsden, Jerrold E and West, Matthew},
journal={Acta numerica},
volume={10},
pages={357--514},
year={2001},
publisher={Cambridge University Press}
}

@article{otto2023learning,
title={Learning nonlinear projections for reduced-order modeling of dynamical systems using constrained autoencoders},
author={Otto, Samuel E and Macchio, Gregory R and Rowley, Clarence W},
Expand Down
17 changes: 16 additions & 1 deletion docs/src/architectures/hamiltonian_neural_network.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,28 @@ Here the derivatives (i.e. vector field data) ``\dot{q}_i^{(t)}`` and ``\dot{p}_
## Library Functions

```@docs
GeometricMachineLearning.hamiltonian_vector_field(::HamiltonianArchitecture)
GeometricMachineLearning.hamiltonian_vector_field(::StandardHamiltonianArchitecture)
GeometricMachineLearning.HamiltonianArchitecture
GeometricMachineLearning.StandardHamiltonianArchitecture
GeometricMachineLearning.HNNLoss
GeometricMachineLearning.symbolic_hamiltonian_vector_field(::GeometricMachineLearning.SymbolicNeuralNetwork)
GeometricMachineLearning.SymbolicPullback(::HamiltonianArchitecture)
GeometricMachineLearning.SymbolicEnergy
GeometricMachineLearning.SymbolicPotentialEnergy
GeometricMachineLearning.SymbolicKineticEnergy
GeometricMachineLearning.build_gradient
GeometricMachineLearning.SymplecticEulerA
GeometricMachineLearning.SymplecticEulerB
GeometricMachineLearning.GeneralizedHamiltonianArchitecture
GeometricMachineLearning.ForcedGeneralizedHamiltonianArchitecture
GeometricMachineLearning.ForcingLayer
GeometricMachineLearning.ForcingLayerQ
GeometricMachineLearning.ForcingLayerP
GeometricMachineLearning.ForcingLayerQP
GeometricMachineLearning.ParametricDataLoader
GeometricMachineLearning.SymbolicPullback(::GeometricMachineLearning.NeuralNetwork, ::GeometricMachineLearning.ParametricLoss, ::GeometricMachineLearning.GeometricBase.OptionalParameters)
GeometricMachineLearning._flatten_system_parameters
GeometricMachineLearning._unwrap_gradient
GeometricMachineLearning._processing
GeometricMachineLearning._get_contents
GeometricMachineLearning._get_params
Expand Down
1 change: 1 addition & 0 deletions docs/src/architectures/sympnet.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ is the predicted state. In the [example section](@ref "SympNets with `GeometricM
SympNet
LASympNet
GSympNet
ForcedSympNet
```

```@raw latex
Expand Down
1 change: 1 addition & 0 deletions docs/src/reduced_order_modeling/losses.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ where ``\mathbf{x}^{(t)}`` is the solution of the FOM at point ``t`` and ``\math
TransformerLoss
AutoEncoderLoss
ReducedLoss
ParametricLoss
projection_error
reduction_error
```
Expand Down
2 changes: 2 additions & 0 deletions docs/src/structure_preservation/symplecticity.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ It is important to note that symplecticity is a very strong property[^2] that ma
```@docs
PoissonTensor
GeometricMachineLearning.QPT
GeometricMachineLearning.QPT2
GeometricMachineLearning.QPTOAT
GeometricMachineLearning.QPTOAT2
```

```@raw latex
Expand Down
10 changes: 10 additions & 0 deletions ext/HDF5Ext.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ function h5save(h5::HDF5.H5DataStore, A::UpperTriangular, path::AbstractString)
group["n"] = A.n
end

# A `NeuralNetworkParameters` nested inside a parameter tree -- the parameter-dependent
# architectures put one per sub-network. AbstractNeuralNetworks has `save(::H5DataStore,
# ::NeuralNetworkParameters)` for the top level only.
#
# TODO: type piracy -- `h5save` and `NeuralNetworkParameters` are both AbstractNeuralNetworks'.
# This belongs in ANN's own `ext/HDF5Ext.jl`, next to `h5save(::H5DataStore, ::NamedTuple, …)`.
function h5save(h5::HDF5.Group, p::NeuralNetworkParameters, path::AbstractString)
h5save(h5, params(p), path)
end

# ---------------------------------------------------------------------------
# changebackend — new methods for GML special array types
#
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using HDF5
using GeometricMachineLearning
using GeometricMachineLearning: QPT, QPT2, Activation, ParametricLoss, SymbolicNeuralNetwork, SymbolicPullback
using CairoMakie
using NNlib: relu

# PARAMETERS
omega = 1.0 # natural frequency of the harmonic Oscillator
Omega = 3.5 # frequency of the external sinusoidal forcing
F = .9 # amplitude of the external sinusoidal forcing
ni_dim = 10 # number of initial conditions per dimension (so ni_dim^2 total)
T = 2π * 20
nt = 1000 # number of time steps
dt = T/nt # time step

# Generating the initial condition array
IC = vec( [(q=q0, p=p0) for q0 in range(-1, 1, ni_dim), p0 in range(-1, 1, ni_dim)] )

# Generating the solution array
ni = ni_dim^2
q = zeros(Float64, ni, nt+1)
p = zeros(Float64, ni, nt+1)
t = collect(dt * range(0, nt, step=1))

"""
Turn a vector of numbers into a vector of `NamedTuple`s to be used by `ParametricDataLoader`.
"""
function turn_parameters_into_correct_format(t::AbstractVector, IC::AbstractVector{<:NamedTuple})

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This functionality should most likely be in GeometricProblems or GMLDataSets.

vec_of_params = NamedTuple[]
for time_step ∈ t
time_step == t[end] || push!(vec_of_params, (t = time_step, ))
end
vcat((vec_of_params for _ in axes(IC, 1))...)
end

for i in 1:nt+1

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The data set should be generated with GeometricProblems (I would think). The problem may be that there is no good way at the moment to obtain analytic solutions (which is what happens here).

for j=1:ni
q[j,i] = ( IC[j].p - Omega*F/(omega^2-Omega^2) )/ omega *sin(omega*t[i]) + IC[j].q*cos(omega*t[i]) + F/(omega^2-Omega^2)*sin(Omega*t[i])
p[j,i] = -omega^2*IC[j].q*sin(omega*t[i]) + ( IC[j].p - Omega*F/(omega^2-Omega^2) )*cos(omega*t[i]) + Omega*F/(omega^2-Omega^2)*cos(Omega*t[i])
# q[j,i] = ( IC[j].p - Omega*F/(omega^2-Omega^2) )/ omega *exp(-omega*t[i]) - IC[j].q*exp(-omega*t[i]) + F/(omega^2-Omega^2)*exp(-Omega*t[i])
# p[j,i] = -omega^2*IC[j].q*exp(-omega*t[i]) + ( IC[j].p + Omega*F/(omega^2-Omega^2) )*exp(-omega*t[i]) - Omega*F/(omega^2-Omega^2)*exp(-Omega*t[i])
end

end

@doc raw"""
Turn a `NamedTuple` of ``(q,p)`` data into two tensors of the correct format.

This is the tricky part as the structure of the input array(s) needs to conform with the structure of the parameters.

Here the data are rearranged in an array of size ``(n, 2, t_f - 1)`` where ``[t_0, t_1, \ldots, t_f]`` is the vector storing the time steps.

If we deal with different initial conditions as well, we still put everything into the third (parameter) axis.

# Example

```jldoctest
using GeometricMachineLearning

q = [1. 2. 3.; 4. 5. 6.]
p = [1.5 2.5 3.5; 4.5 5.5 6.5]
qp = (q = q, p = p)
turn_q_p_data_into_correct_format(qp)

# output

(q = [1.0 2.0; 4.0 5.0;;; 2.0 3.0; 5.0 6.0], p = [1.5 2.5; 4.5 5.5;;; 2.5 3.5; 5.5 6.5])
```
"""
function turn_q_p_data_into_correct_format(qp::QPT2{T, 2}) where {T}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This should be in GMLDataSets (I think).

number_of_time_steps = size(qp.q, 2) - 1 # not counting t₀
number_of_initial_conditions = size(qp.q, 1)
q_array = zeros(T, 1, 2, number_of_time_steps * number_of_initial_conditions)
p_array = zeros(T, 1, 2, number_of_time_steps * number_of_initial_conditions)
for initial_condition_index ∈ 0:(number_of_initial_conditions - 1)
for time_index ∈ 1:number_of_time_steps
q_array[:, 1, initial_condition_index * number_of_time_steps + time_index] .= qp.q[initial_condition_index + 1, time_index]
q_array[:, 2, initial_condition_index * number_of_time_steps + time_index] .= qp.q[initial_condition_index + 1, time_index + 1]
p_array[:, 1, initial_condition_index * number_of_time_steps + time_index] .= qp.p[initial_condition_index + 1, time_index]
p_array[:, 2, initial_condition_index * number_of_time_steps + time_index] .= qp.p[initial_condition_index + 1, time_index + 1]
end
end
(q = q_array, p = p_array)
end

# SAVING TO FILE

# h5 = h5open(path, "w")
# write(h5, "q", q)
# write(h5, "p", p)
# write(h5, "t", t)
#
# attrs(h5)["ni"] = ni
# attrs(h5)["nt"] = nt
# attrs(h5)["dt"] = dt
#
# close(h5)

"""
This takes time as a single additional parameter (third axis).
"""
function load_time_dependent_harmonic_oscillator_with_parametric_data_loader(qp::QPT{T}, t::AbstractVector{T}, IC::AbstractVector) where {T}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Again, probably better in GMLDataSets.

qp_reformatted = turn_q_p_data_into_correct_format(qp)
t_reformatted = turn_parameters_into_correct_format(t, IC)
ParametricDataLoader(qp_reformatted, t_reformatted)
end

# This sets up the data loader
dl = load_time_dependent_harmonic_oscillator_with_parametric_data_loader((q = q, p = p), t, IC)

# This sets up the neural network
width::Int = 1
nhidden::Int = 1
n_integrators::Int = 2
# sigmoid_linear_unit(x::T) where {T<:Number} = x / (T(1) + exp(-x))
arch1 = ForcedGeneralizedHamiltonianArchitecture(2; activation = tanh, width = width, nhidden = nhidden, n_integrators = n_integrators, parameters = turn_parameters_into_correct_format(t, IC)[1], forcing_type = :P)
arch2 = ForcedGeneralizedHamiltonianArchitecture(2; activation = tanh, width = width, nhidden = nhidden, n_integrators = n_integrators, parameters = turn_parameters_into_correct_format(t, IC)[1], forcing_type = :Q)
arch3 = ForcedGeneralizedHamiltonianArchitecture(2; activation = tanh, width = 2width, nhidden = nhidden, n_integrators = n_integrators, parameters = turn_parameters_into_correct_format(t, IC)[1], forcing_type = :QP)
nn1 = NeuralNetwork(arch1)
nn2 = NeuralNetwork(arch2)
nn3 = NeuralNetwork(arch3)

# This is where training starts
batch_size = 128
n_epochs = 200
batch = Batch(batch_size)
o1 = Optimizer(AdamOptimizer(), nn1)
o2 = Optimizer(AdamOptimizer(), nn2)
o3 = Optimizer(AdamOptimizer(), nn3)
loss = ParametricLoss()
_pb = SymbolicPullback(nn1, loss, turn_parameters_into_correct_format(t, IC)[1]);
_pb = SymbolicPullback(nn2, loss, turn_parameters_into_correct_format(t, IC)[1]);
_pb = SymbolicPullback(nn3, loss, turn_parameters_into_correct_format(t, IC)[1]);

function train_network()
o1(nn1, dl, batch, n_epochs, loss, _pb)
o2(nn2, dl, batch, n_epochs, loss, _pb)
o3(nn3, dl, batch, n_epochs, loss, _pb)
end

loss_array = train_network()

trajectory_number = 20

# Testing the network
initial_conditions = (q = q[trajectory_number, 1], p = p[trajectory_number, 1])
n_steps = nt
trajectory = (q = zeros(1, n_steps), p = zeros(1, n_steps))
trajectory.q[:, 1] .= initial_conditions.q
trajectory.p[:, 1] .= initial_conditions.p
# note that we have to supply the parameters as a named tuple as well here:
for t_step ∈ 0:(n_steps-2)
qp_temporary = nn3.model((q = [trajectory.q[1, t_step+1]], p = [trajectory.p[1, t_step+1]]), (t = t[t_step+1],), nn3.params)
trajectory.q[:, t_step+2] .= qp_temporary.q
trajectory.p[:, t_step+2] .= qp_temporary.p
end

fig = Figure()
ax = Axis(fig[1,1])
lines!(ax, trajectory.q[1,:]; label="nn")
lines!(ax, q[trajectory_number,:]; label="analytic")
Loading
Loading