From 7f35e01680cff3675ab3411dd69c56731e9018d6 Mon Sep 17 00:00:00 2001 From: benedict-96 Date: Tue, 6 Oct 2026 12:25:20 +0200 Subject: [PATCH 1/2] Add StandardAutoencoder, an autoencoder of Dense layers The unstructured counterpart of SymplecticAutoencoder: an encoder and a decoder of Dense layers at a hidden width, each ending in an affine layer. It is there to compare the latent space of a symplectic autoencoder with that of an autoencoder whose decoder is not symplectic, at a similar number of parameters. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + .../pod_autoencoders.md | 3 + src/GeometricMachineLearning.jl | 3 +- src/architectures/standard_autoencoder.jl | 84 +++++++++++++++++++ .../standard_autoencoder_tests.jl | 60 +++++++++++++ test/integration/hdf5_support.jl | 18 ++++ test/runtests.jl | 1 + 7 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/architectures/standard_autoencoder.jl create mode 100644 test/architectures/standard_autoencoder_tests.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a84549a8..c058d037d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ breaking release). ### Added +- **`StandardAutoencoder` is an autoencoder without structure.** Its encoder and its decoder are feedforward networks of `Dense` layers: `n_encoder_layers` (resp. `n_decoder_layers`) layers with activation at the hidden width `width`, then an affine output layer. Nothing makes the decoder symplectic, and neither dimension has to be even. It is the unstructured counterpart of `SymplecticAutoencoder`, for comparing the two at a similar number of parameters. `encoder` and `decoder` split it as they split any `AutoEncoder`, and it saves and loads with `save` and `load(NeuralNetwork, path, arch)`. `test/architectures/standard_autoencoder_tests.jl` checks the split, the parameter count, that training reduces the loss and that the decoder is not symplectic at a random point; `test/integration/hdf5_support.jl` checks the save/load round trip. - **The test suite runs on an Apple GPU in its `metal` group.** `test/devices/metal.jl` checks the two device paths that were measured by hand and not tested before: the products of a `PoissonTensor` with a wrapped device array, `𝕁 * view(A, [1, 2, 3, 4], :)` and `𝕁 * B'`, which `ext/GPUArraysCoreExt.jl` carries; and the `tensor_mat_mul!` kernel on `MetalBackend()`. Each result must stay on the device and match the host in `Float32`, with scalar indexing off. `Pkg.test(test_args = ["metal"])` runs them without the subject tests, and a missing device records a skip, which happens inside a sandbox. Metal (1.10 or later) is a test dependency on every platform; it installs and precompiles on Linux and Windows, and nothing there loads it. On an Apple M4 Max with Metal 1.11.1 all five assertions pass. - **A `Metal` workflow runs the Metal tests on GitHub's `macos-15` runner**, for the `min` and `1` Julia versions. The runner's GPU is Apple's paravirtual device, which Metal.jl supports from 1.10 and on macOS 15 or later. The workflow includes a step "Metal is functional" before the tests that fails the job where the device is unavailable, so it cannot pass without having run the tests. The job is not a required check. However, the Metal tests also run in two required checks—CI.yml's `Julia min - macOS-latest - default` and `Julia 1 - macOS-latest - default`—which execute `test/runtests.jl` with no test arguments on Apple silicon, selecting the `metal` group. - **`test/quality/jet.jl` checks the kernel launchers with JET.** It runs in `core`, directly after `quality/aqua.jl`, and takes about 14 s. Each function that launches a kernel of `src/` on the `CPU()` backend, the pullbacks among them, and the `Batch` functor that `test/data_loader/batch_index_set.jl` measures with `@allocated` get one `JET.report_opt` line per element type that the tests use, with `target_modules = (GeometricMachineLearning,)`. Each `@kernel` method also gets a line that analyses its generated `cpu_` function at the launcher's context, because JET drops the reports of a kernel body when it analyses the launcher. 142 lines pass on Julia 1.11.9 (JET 0.9.20) and 1.13.1 (JET 0.12.2). Three lines report a runtime dispatch on both versions and are `@test_broken` with issue #325: `PoissonTensor(::CPU, ::Int, ::DataType)`, and the input-and-output method of `convert_input_and_batch_indices_to_array` at `Float32` and `Float64`. Where JET does not work, the file records one `@test_skip`. JET joins `test/Project.toml` with no `[compat]` bound. `KNOWN_ISSUES.md` K2 records the Revise `EMFILE` messages that JET brings into the test log, and K3 the dispatches that the lines do not see: an unstable argument of a kernel launch, and two barriers in the Poisson-tensor kernel. diff --git a/docs/src/reduced_order_modeling/pod_autoencoders.md b/docs/src/reduced_order_modeling/pod_autoencoders.md index 835fa42ae..540afbebd 100644 --- a/docs/src/reduced_order_modeling/pod_autoencoders.md +++ b/docs/src/reduced_order_modeling/pod_autoencoders.md @@ -75,6 +75,8 @@ Main.proof(raw"If we take as test function ``\tilde{\psi}_i = (\nabla\Psi^\mathr " * Main.indentation * raw"and since this must be true for every ``i = 1, \ldots, n`` we obtain the desired expression for the reduced vector field.") ``` +A standard autoencoder of this kind, with [`Dense`](@ref) layers in the encoder and the decoder, is implemented as [`StandardAutoencoder`](@ref). + Both POD and standard autoencoders suffer from the problem that they completely neglect the structure of the differential equation and the data they are applied to. This can have grave consequences [peng2016symplectic, tyranowski2023symplectic, buchfink2023symplectic](@cite). [Hamiltonian model order reduction](@ref "Hamiltonian Model Order Reduction") can improve the approximation significantly in these situations. ## Library Functions @@ -85,6 +87,7 @@ GeometricMachineLearning.Encoder GeometricMachineLearning.Decoder GeometricMachineLearning.UnknownEncoder GeometricMachineLearning.UnknownDecoder +StandardAutoencoder encoder decoder ``` diff --git a/src/GeometricMachineLearning.jl b/src/GeometricMachineLearning.jl index bb474b69b..8ad1992a4 100644 --- a/src/GeometricMachineLearning.jl +++ b/src/GeometricMachineLearning.jl @@ -233,6 +233,7 @@ include("architectures/sympnet.jl") include("architectures/autoencoder.jl") include("architectures/symplectic_autoencoder.jl") include("architectures/psd.jl") +include("architectures/standard_autoencoder.jl") include("architectures/hamiltonian_neural_network.jl") include("architectures/lagrangian_neural_network.jl") include("architectures/transformer_neural_network.jl") @@ -244,7 +245,7 @@ export LagrangianNeuralNetwork export SympNet, LASympNet, GSympNet export ClassificationTransformer, ClassificationLayer export VolumePreservingFeedForward -export SymplecticAutoencoder, PSDArch +export SymplecticAutoencoder, PSDArch, StandardAutoencoder export HamiltonianArchitecture, StandardHamiltonianArchitecture, GeneralizedHamiltonianArchitecture diff --git a/src/architectures/standard_autoencoder.jl b/src/architectures/standard_autoencoder.jl new file mode 100644 index 000000000..1807664db --- /dev/null +++ b/src/architectures/standard_autoencoder.jl @@ -0,0 +1,84 @@ +@doc raw""" + StandardAutoencoder(full_dim, reduced_dim) + +Make an instance of `StandardAutoencoder` for dimensions `full_dim` and `reduced_dim`. + +# The architecture + +A standard autoencoder [goodfellow2016deep](@cite) has an encoder ``\Psi^\mathrm{enc}:\mathbb{R}^N\to\mathbb{R}^n`` and a decoder ``\Psi^\mathrm{dec}:\mathbb{R}^n\to\mathbb{R}^N`` that are both feedforward networks of [`Dense`](@ref) layers, with no structure imposed on either. The encoder is + +```math +\Psi^\mathrm{enc} = L^\mathrm{out}_{w\to{}n}\circ{}L_{w\to{}w}\circ\cdots\circ{}L_{w\to{}w}\circ{}L_{N\to{}w}, +``` + +with `n_encoder_layers` nonlinear layers ``L``, the first of which maps ``\mathbb{R}^N`` to the hidden width ``w`` and the others ``\mathbb{R}^w`` to itself, followed by an affine output layer ``L^\mathrm{out}`` without activation. The decoder is built the same way from ``\mathbb{R}^n`` to ``\mathbb{R}^N`` with `n_decoder_layers` nonlinear layers. + +Unlike for a [`SymplecticAutoencoder`](@ref), ``\nabla\Psi^\mathrm{dec}`` is in general not symplectic, so the reduced system is not Hamiltonian, and neither ``N`` nor ``n`` has to be even. It is the unstructured counterpart of the [`SymplecticAutoencoder`](@ref), and serves to compare the two. + +# Arguments + +Besides the required arguments `full_dim` and `reduced_dim` you can provide the following keyword arguments: +- `width::Integer = 2full_dim`: the hidden width ``w``. +- `n_encoder_layers::Integer = 2`: the number of nonlinear layers in the encoder. +- `n_decoder_layers::Integer = 2`: the number of nonlinear layers in the decoder. +- `activation = tanh`: the activation of the nonlinear layers. + +# Examples + +The two parts compose to the whole network, and the number of parameters follows from the widths: + +```jldoctest +using GeometricMachineLearning +using GeometricMachineLearning: params + +arch = StandardAutoencoder(4, 2; width = 10, n_encoder_layers = 2, n_decoder_layers = 3) +nn = NeuralNetwork(arch) +x = rand(4) + +nn(x) ≈ decoder(nn)(encoder(nn)(x)), parameterlength(nn.model) + +# output + +(true, 476) +``` + +Here the encoder has ``(4 + 1)\cdot10 + (10 + 1)\cdot10 + (10 + 1)\cdot2 = 182`` parameters and the decoder ``(2 + 1)\cdot10 + 2\cdot(10 + 1)\cdot10 + (10 + 1)\cdot4 = 294``. +""" +struct StandardAutoencoder{AT} <: AutoEncoder + full_dim::Int + reduced_dim::Int + width::Int + n_encoder_layers::Int + n_decoder_layers::Int + n_encoder_blocks::Int + n_decoder_blocks::Int + activation::AT +end + +function StandardAutoencoder(full_dim::Integer, reduced_dim::Integer; + width::Integer = 2full_dim, + n_encoder_layers::Integer = 2, + n_decoder_layers::Integer = 2, + activation = tanh) + @assert full_dim ≥ reduced_dim "The dimension of the full-order model has to be at least that of the reduced-order model!" + @assert n_encoder_layers ≥ 1 && n_decoder_layers ≥ 1 "Encoder and decoder need at least one nonlinear layer each!" + # The dimension changes once in each direction, from `full_dim` to `reduced_dim`; the hidden + # layers are not blocks in the sense of `compute_iterations`. + StandardAutoencoder{typeof(activation)}(full_dim, reduced_dim, width, n_encoder_layers, + n_decoder_layers, 2, 2, activation) +end + +function _standard_autoencoder_layers(arch::StandardAutoencoder, input_dim::Integer, + output_dim::Integer, n_layers::Integer) + (Dense(input_dim, arch.width, arch.activation), + Tuple(Dense(arch.width, arch.width, arch.activation) for _ in 2:n_layers)..., + Dense(arch.width, output_dim, identity)) +end + +function encoder_layers_from_iteration(arch::StandardAutoencoder, ::AbstractVector{<:Integer}) + _standard_autoencoder_layers(arch, arch.full_dim, arch.reduced_dim, arch.n_encoder_layers) +end + +function decoder_layers_from_iteration(arch::StandardAutoencoder, ::AbstractVector{<:Integer}) + _standard_autoencoder_layers(arch, arch.reduced_dim, arch.full_dim, arch.n_decoder_layers) +end diff --git a/test/architectures/standard_autoencoder_tests.jl b/test/architectures/standard_autoencoder_tests.jl new file mode 100644 index 000000000..d2f96a042 --- /dev/null +++ b/test/architectures/standard_autoencoder_tests.jl @@ -0,0 +1,60 @@ +using GeometricMachineLearning +using GeometricMachineLearning: params +using Test +using Zygote: jacobian +import Random + +Random.seed!(123) + +function test_encoder_and_decoder(N::Integer, n::Integer; kwargs...) + nn = NeuralNetwork(StandardAutoencoder(N, n; kwargs...)) + ae_encoder = encoder(nn) + ae_decoder = decoder(nn) + + test_vec = rand(N) + test_mat = rand(N, N) + + @test size(ae_encoder(test_vec)) == (n,) + @test size(ae_decoder(rand(n))) == (N,) + @test nn(test_vec) ≈ ae_decoder(ae_encoder(test_vec)) + @test nn(test_mat) ≈ ae_decoder(ae_encoder(test_mat)) +end + +# Each nonlinear layer is one Dense layer with bias, and each part ends in an affine layer. +function test_parameter_count(N::Integer, n::Integer, w::Integer, n_enc::Integer, n_dec::Integer) + nn = NeuralNetwork(StandardAutoencoder(N, n; width = w, + n_encoder_layers = n_enc, n_decoder_layers = n_dec)) + n_encoder = (N + 1) * w + (n_enc - 1) * (w + 1) * w + (w + 1) * n + n_decoder = (n + 1) * w + (n_dec - 1) * (w + 1) * w + (w + 1) * N + @test parameterlength(nn.model) == n_encoder + n_decoder + @test parameterlength(encoder(nn).model) == n_encoder + @test parameterlength(decoder(nn).model) == n_decoder + @test length(encoder(nn).model.layers) == n_enc + 1 + @test length(decoder(nn).model.layers) == n_dec + 1 +end + +function test_training_reduces_loss(N::Integer, n::Integer; n_epochs::Integer = 100) + dl = DataLoader(rand(N, 10 * N); autoencoder = true) + nn = NeuralNetwork(StandardAutoencoder(N, n)) + loss = AutoEncoderLoss() + loss_before = loss(nn, dl.input) + o = Optimizer(Adam(), nn) + o(nn, dl, Batch(10), n_epochs; show_progress = false) + @test loss(nn, dl.input) < loss_before +end + +# Nothing makes the decoder symplectic: ∇Ψᵀ 𝕁 ∇Ψ differs from 𝕁 at a random point. +function test_not_symplectic(N::Integer, n::Integer) + ae_decoder = decoder(NeuralNetwork(StandardAutoencoder(N, n))) + J = jacobian(ae_decoder, rand(n))[1] + @test !(J' * PoissonTensor(N) * J ≈ PoissonTensor(n)) +end + +for (N, n) in ((10, 4), (4, 2), (7, 3)) + test_encoder_and_decoder(N, n) + test_parameter_count(N, n, 2N, 2, 2) +end +test_encoder_and_decoder(4, 2; width = 16, n_encoder_layers = 1, n_decoder_layers = 4) +test_parameter_count(4, 2, 40, 3, 4) +test_training_reduces_loss(10, 4) +test_not_symplectic(10, 4) diff --git a/test/integration/hdf5_support.jl b/test/integration/hdf5_support.jl index 6e04ea5f8..0e0845392 100644 --- a/test/integration/hdf5_support.jl +++ b/test/integration/hdf5_support.jl @@ -78,6 +78,24 @@ end end end +# Plain Dense layers; the architecture rebuilds the model that the weights are read into. +@testset "save/load roundtrip: StandardAutoencoder" begin + arch = StandardAutoencoder(10, 4; width = 12, n_encoder_layers = 2, n_decoder_layers = 3) + nn = NeuralNetwork(arch) + x = rand(10) + y_before = nn(x) + + mktempdir() do dir + path = joinpath(dir, "ae.h5") + save(path, nn) + nn2 = load(NeuralNetwork, path, arch) + + @test _ps_eq(params(nn), params(nn2)) + @test nn2(x) ≈ y_before + @test encoder(nn2)(x) ≈ encoder(nn)(x) + end +end + # LASympNet contains LinearLayer → SymmetricMatrix parameters. @testset "save/load roundtrip: LASympNet (SymmetricMatrix)" begin arch = LASympNet(4) diff --git a/test/runtests.jl b/test/runtests.jl index 9d1e773bc..f6cc85156 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -46,6 +46,7 @@ if "core" in GROUPS @safetestset "SympNet integrator" include("architectures/sympnet_integrator.jl") @safetestset "PSD tests" include("architectures/psd_architecture_tests.jl") @safetestset "SymplecticAutoencoder tests" include("architectures/symplectic_autoencoder_tests.jl") + @safetestset "StandardAutoencoder tests" include("architectures/standard_autoencoder_tests.jl") @safetestset "Check if autoencoder error is lower than PSD error" include("architectures/sae_error_lower_than_psd_error.jl") @safetestset "Check reduced model" include("reduced_system/reduced_system.jl") @safetestset "_norm keeps the element type of its argument" include("norm_eltype.jl") From 2816d0258d44825233e43a737668e26db8badb7e Mon Sep 17 00:00:00 2001 From: benedict-96 Date: Tue, 6 Oct 2026 13:14:00 +0200 Subject: [PATCH 2/2] Fix the StandardAutoencoder docs references and equal dimensions `Dense` has no docstring in this manual, so [`Dense`](@ref) left two unresolved references; docs/make.jl stops on them, and so does test/quality/docs_make_checks_references.jl in every test job. It is now plain code. With full_dim == reduced_dim, which the constructor allows, the generic compute_iterations built a range of step zero and NeuralNetwork failed. The layers depend only on the two end dimensions, so StandardAutoencoder now returns just those (from a CodeRabbit review comment). Tested at (4, 4). The SymplecticAutoencoder docstring points to StandardAutoencoder. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 2 +- docs/src/reduced_order_modeling/pod_autoencoders.md | 2 +- src/architectures/standard_autoencoder.jl | 7 ++++++- src/architectures/symplectic_autoencoder.jl | 2 ++ test/architectures/standard_autoencoder_tests.jl | 2 +- 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c058d037d..5230a36fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ breaking release). ### Added -- **`StandardAutoencoder` is an autoencoder without structure.** Its encoder and its decoder are feedforward networks of `Dense` layers: `n_encoder_layers` (resp. `n_decoder_layers`) layers with activation at the hidden width `width`, then an affine output layer. Nothing makes the decoder symplectic, and neither dimension has to be even. It is the unstructured counterpart of `SymplecticAutoencoder`, for comparing the two at a similar number of parameters. `encoder` and `decoder` split it as they split any `AutoEncoder`, and it saves and loads with `save` and `load(NeuralNetwork, path, arch)`. `test/architectures/standard_autoencoder_tests.jl` checks the split, the parameter count, that training reduces the loss and that the decoder is not symplectic at a random point; `test/integration/hdf5_support.jl` checks the save/load round trip. +- **`StandardAutoencoder` is an autoencoder without structure.** Its encoder and its decoder are feedforward networks of `Dense` layers: `n_encoder_layers` (resp. `n_decoder_layers`) layers with activation at the hidden width `width`, then an affine output layer. Nothing makes the decoder symplectic, and neither dimension has to be even. It is the unstructured counterpart of `SymplecticAutoencoder`, for comparing the two at a similar number of parameters. `encoder` and `decoder` split it as they split any `AutoEncoder`, and it saves and loads with `save` and `load(NeuralNetwork, path, arch)`. `test/architectures/standard_autoencoder_tests.jl` checks the split (also for `full_dim == reduced_dim`), the parameter count, that training reduces the loss and that the decoder is not symplectic at a random point; `test/integration/hdf5_support.jl` checks the save/load round trip. - **The test suite runs on an Apple GPU in its `metal` group.** `test/devices/metal.jl` checks the two device paths that were measured by hand and not tested before: the products of a `PoissonTensor` with a wrapped device array, `𝕁 * view(A, [1, 2, 3, 4], :)` and `𝕁 * B'`, which `ext/GPUArraysCoreExt.jl` carries; and the `tensor_mat_mul!` kernel on `MetalBackend()`. Each result must stay on the device and match the host in `Float32`, with scalar indexing off. `Pkg.test(test_args = ["metal"])` runs them without the subject tests, and a missing device records a skip, which happens inside a sandbox. Metal (1.10 or later) is a test dependency on every platform; it installs and precompiles on Linux and Windows, and nothing there loads it. On an Apple M4 Max with Metal 1.11.1 all five assertions pass. - **A `Metal` workflow runs the Metal tests on GitHub's `macos-15` runner**, for the `min` and `1` Julia versions. The runner's GPU is Apple's paravirtual device, which Metal.jl supports from 1.10 and on macOS 15 or later. The workflow includes a step "Metal is functional" before the tests that fails the job where the device is unavailable, so it cannot pass without having run the tests. The job is not a required check. However, the Metal tests also run in two required checks—CI.yml's `Julia min - macOS-latest - default` and `Julia 1 - macOS-latest - default`—which execute `test/runtests.jl` with no test arguments on Apple silicon, selecting the `metal` group. - **`test/quality/jet.jl` checks the kernel launchers with JET.** It runs in `core`, directly after `quality/aqua.jl`, and takes about 14 s. Each function that launches a kernel of `src/` on the `CPU()` backend, the pullbacks among them, and the `Batch` functor that `test/data_loader/batch_index_set.jl` measures with `@allocated` get one `JET.report_opt` line per element type that the tests use, with `target_modules = (GeometricMachineLearning,)`. Each `@kernel` method also gets a line that analyses its generated `cpu_` function at the launcher's context, because JET drops the reports of a kernel body when it analyses the launcher. 142 lines pass on Julia 1.11.9 (JET 0.9.20) and 1.13.1 (JET 0.12.2). Three lines report a runtime dispatch on both versions and are `@test_broken` with issue #325: `PoissonTensor(::CPU, ::Int, ::DataType)`, and the input-and-output method of `convert_input_and_batch_indices_to_array` at `Float32` and `Float64`. Where JET does not work, the file records one `@test_skip`. JET joins `test/Project.toml` with no `[compat]` bound. `KNOWN_ISSUES.md` K2 records the Revise `EMFILE` messages that JET brings into the test log, and K3 the dispatches that the lines do not see: an unstable argument of a kernel launch, and two barriers in the Poisson-tensor kernel. diff --git a/docs/src/reduced_order_modeling/pod_autoencoders.md b/docs/src/reduced_order_modeling/pod_autoencoders.md index 540afbebd..f4bb7abf5 100644 --- a/docs/src/reduced_order_modeling/pod_autoencoders.md +++ b/docs/src/reduced_order_modeling/pod_autoencoders.md @@ -75,7 +75,7 @@ Main.proof(raw"If we take as test function ``\tilde{\psi}_i = (\nabla\Psi^\mathr " * Main.indentation * raw"and since this must be true for every ``i = 1, \ldots, n`` we obtain the desired expression for the reduced vector field.") ``` -A standard autoencoder of this kind, with [`Dense`](@ref) layers in the encoder and the decoder, is implemented as [`StandardAutoencoder`](@ref). +A standard autoencoder of this kind, with `Dense` layers in the encoder and the decoder, is implemented as [`StandardAutoencoder`](@ref). Both POD and standard autoencoders suffer from the problem that they completely neglect the structure of the differential equation and the data they are applied to. This can have grave consequences [peng2016symplectic, tyranowski2023symplectic, buchfink2023symplectic](@cite). [Hamiltonian model order reduction](@ref "Hamiltonian Model Order Reduction") can improve the approximation significantly in these situations. diff --git a/src/architectures/standard_autoencoder.jl b/src/architectures/standard_autoencoder.jl index 1807664db..3c69b1c95 100644 --- a/src/architectures/standard_autoencoder.jl +++ b/src/architectures/standard_autoencoder.jl @@ -5,7 +5,7 @@ Make an instance of `StandardAutoencoder` for dimensions `full_dim` and `reduced # The architecture -A standard autoencoder [goodfellow2016deep](@cite) has an encoder ``\Psi^\mathrm{enc}:\mathbb{R}^N\to\mathbb{R}^n`` and a decoder ``\Psi^\mathrm{dec}:\mathbb{R}^n\to\mathbb{R}^N`` that are both feedforward networks of [`Dense`](@ref) layers, with no structure imposed on either. The encoder is +A standard autoencoder [goodfellow2016deep](@cite) has an encoder ``\Psi^\mathrm{enc}:\mathbb{R}^N\to\mathbb{R}^n`` and a decoder ``\Psi^\mathrm{dec}:\mathbb{R}^n\to\mathbb{R}^N`` that are both feedforward networks of `Dense` layers, with no structure imposed on either. The encoder is ```math \Psi^\mathrm{enc} = L^\mathrm{out}_{w\to{}n}\circ{}L_{w\to{}w}\circ\cdots\circ{}L_{w\to{}w}\circ{}L_{N\to{}w}, @@ -68,6 +68,11 @@ function StandardAutoencoder(full_dim::Integer, reduced_dim::Integer; n_decoder_layers, 2, 2, activation) end +# The layers do not depend on intermediate dimensions, only on the two ends. `compute_iterations` +# would build a range of step zero for `full_dim == reduced_dim`, which the constructor allows. +compute_encoder_iterations(arch::StandardAutoencoder) = [arch.reduced_dim, arch.full_dim] +compute_decoder_iterations(arch::StandardAutoencoder) = [arch.reduced_dim, arch.full_dim] + function _standard_autoencoder_layers(arch::StandardAutoencoder, input_dim::Integer, output_dim::Integer, n_layers::Integer) (Dense(input_dim, arch.width, arch.activation), diff --git a/src/architectures/symplectic_autoencoder.jl b/src/architectures/symplectic_autoencoder.jl index a89243448..a7f1cb9a2 100644 --- a/src/architectures/symplectic_autoencoder.jl +++ b/src/architectures/symplectic_autoencoder.jl @@ -22,6 +22,8 @@ Because the decoder has this particular property, the reduced system can be desc where ``(\nabla_\xi\Psi^d)^+`` is the *symplectic inverse* of ``\nabla_\xi\Psi^d`` (for more details see the docs on the [`AutoEncoder`](@ref) type). +For an autoencoder without this structure, whose decoder is not symplectic and whose reduced system is therefore not Hamiltonian, see [`StandardAutoencoder`](@ref). + # Arguments Besides the required arguments `full_dim` and `reduced_dim` you can provide the following keyword arguments: diff --git a/test/architectures/standard_autoencoder_tests.jl b/test/architectures/standard_autoencoder_tests.jl index d2f96a042..ae1b10654 100644 --- a/test/architectures/standard_autoencoder_tests.jl +++ b/test/architectures/standard_autoencoder_tests.jl @@ -50,7 +50,7 @@ function test_not_symplectic(N::Integer, n::Integer) @test !(J' * PoissonTensor(N) * J ≈ PoissonTensor(n)) end -for (N, n) in ((10, 4), (4, 2), (7, 3)) +for (N, n) in ((10, 4), (4, 2), (7, 3), (4, 4)) test_encoder_and_decoder(N, n) test_parameter_count(N, n, 2N, 2, 2) end