From 44c72191ff695d06f9a25e02b90f31885f05ba3d Mon Sep 17 00:00:00 2001 From: Aurora Rossi Date: Fri, 24 Jul 2026 13:22:41 +0200 Subject: [PATCH 1/7] Learnable epsilon --- GNNlib/src/layers/conv.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GNNlib/src/layers/conv.jl b/GNNlib/src/layers/conv.jl index ab40f13f2..6b7907f1d 100644 --- a/GNNlib/src/layers/conv.jl +++ b/GNNlib/src/layers/conv.jl @@ -252,7 +252,8 @@ function gin_conv(l, g::AbstractGNNGraph, x) xj, xi = expand_srcdst(g, x) m = propagate(copy_xj, g, l.aggr, xj = xj) - return l.nn((1 .+ ofeltype(xi, l.ϵ)) .* xi .+ m) + eps = convert.(float(eltype(xi)), l.eps) + return l.nn((1 .+ eps) .* xi .+ m) end ####################### NNConv ###################################### From f86744fba9d714def02b28ad85a6515f242f1d77 Mon Sep 17 00:00:00 2001 From: Aurora Rossi Date: Fri, 24 Jul 2026 13:23:09 +0200 Subject: [PATCH 2/7] Learnable epsilon and flag train --- GNNLux/src/layers/conv.jl | 36 ++++++++++++++++++++++++++++-------- GNNLux/test/layers/conv.jl | 11 ++++++++++- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/GNNLux/src/layers/conv.jl b/GNNLux/src/layers/conv.jl index f67e54381..199494926 100644 --- a/GNNLux/src/layers/conv.jl +++ b/GNNLux/src/layers/conv.jl @@ -1287,7 +1287,7 @@ function Base.show(io::IO, l::GatedGraphConv) end @doc raw""" - GINConv(f, ϵ; aggr=+) + GINConv(f, [eps]; aggr=+, train_eps=false) Graph Isomorphism convolutional layer from paper [How Powerful are Graph Neural Networks?](https://arxiv.org/pdf/1810.00826.pdf). @@ -1300,7 +1300,9 @@ where ``f_\Theta`` typically denotes a learnable function, e.g. a linear layer o # Arguments - `f`: A (possibly learnable) function acting on node features. -- `ϵ`: Weighting factor. +- `eps`: Initial value of the weighting factor ``\epsilon``. Default `0.0f0`. +- `train_eps`: If `true`, ``\epsilon`` is trainable. Default `false`. +- `aggr`: Neighborhood aggregation function. Default `+`. # Examples: @@ -1322,7 +1324,7 @@ x = randn(rng, Float32, in_channel, g.num_nodes) nn = Dense(in_channel, out_channel) # create layer -l = GINConv(nn, 0.01f0, aggr = mean) +l = GINConv(nn; eps = 0.01f0, train_eps = true, aggr = mean) # setup layer ps, st = LuxCore.setup(rng, l) @@ -1333,15 +1335,31 @@ y, st = l(g, x, ps, st) # size: out_channel × num_nodes """ @concrete struct GINConv <: GNNContainerLayer{(:nn,)} nn <: AbstractLuxLayer - ϵ <: Real + eps aggr + train_eps::Bool end -GINConv(nn, ϵ; aggr = +) = GINConv(nn, ϵ, aggr) +GINConv(nn, eps::Real; aggr = +, train_eps::Bool = false) = + GINConv(nn, train_eps ? [eps] : eps, aggr, train_eps) + +GINConv(nn, eps::Real, aggr) = GINConv(nn, eps; aggr) + +GINConv(nn; eps::Real = 0.0f0, aggr = +, train_eps::Bool = false) = + GINConv(nn, eps; aggr, train_eps) + +function LuxCore.initialparameters(rng::AbstractRNG, l::GINConv) + nn = LuxCore.initialparameters(rng, l.nn) + return l.train_eps ? (; nn, eps = l.eps) : (; nn) +end + +LuxCore.parameterlength(l::GINConv) = parameterlength(l.nn) + l.train_eps +LuxCore.statelength(l::GINConv) = statelength(l.nn) function (l::GINConv)(g, x, ps, st) nn = StatefulLuxLayer{true}(l.nn, ps.nn, st.nn) - m = (; nn, l.ϵ, l.aggr) + eps = l.train_eps ? ps.eps : l.eps + m = (; nn, eps, l.aggr) y = GNNlib.gin_conv(m, g, x) stnew = (; nn = _getstate(nn)) return y, stnew @@ -1349,7 +1367,9 @@ end function Base.show(io::IO, l::GINConv) print(io, "GINConv($(l.nn)") - print(io, ", $(l.ϵ)") + print(io, ", eps=$(l.eps)") + l.train_eps && print(io, ", train_eps=true") + l.aggr == (+) || print(io, ", aggr=$(l.aggr)") print(io, ")") end @@ -1879,4 +1899,4 @@ function (l::SAGEConv)(g, x, ps, st) m = (; ps.weight, bias = _getbias(ps), l.σ, l.aggr) return GNNlib.sage_conv(m, g, x), st -end \ No newline at end of file +end diff --git a/GNNLux/test/layers/conv.jl b/GNNLux/test/layers/conv.jl index 5bfb01825..959a0f79a 100644 --- a/GNNLux/test/layers/conv.jl +++ b/GNNLux/test/layers/conv.jl @@ -92,8 +92,17 @@ @testset "GINConv" begin nn = Chain(Dense(in_dims => out_dims, tanh), Dense(out_dims => out_dims)) - l = GINConv(nn, 0.5) + l = GINConv(nn; eps = 0.5) test_lux_layer(rng, l, g, x, sizey=(out_dims,g.num_nodes), container=true) + ps = LuxCore.initialparameters(rng, l) + @test !hasproperty(ps, :eps) + + l = GINConv(nn; train_eps = true) + test_lux_layer(rng, l, g, x, sizey=(out_dims,g.num_nodes), container=true) + ps = LuxCore.initialparameters(rng, l) + @test ps.eps == [0.0f0] + + @test GINConv(nn, 0.5).eps == 0.5 end @testset "MEGNetConv" begin From 1a3e313001ec32ada5a6ef3b25ac23bb23786df8 Mon Sep 17 00:00:00 2001 From: Aurora Rossi Date: Fri, 24 Jul 2026 13:23:28 +0200 Subject: [PATCH 3/7] Learnable epsilon and flag --- GraphNeuralNetworks/src/layers/conv.jl | 27 +++++++++++++++++-------- GraphNeuralNetworks/test/layers/conv.jl | 18 ++++++++++++++--- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/GraphNeuralNetworks/src/layers/conv.jl b/GraphNeuralNetworks/src/layers/conv.jl index 10a340e90..4bd6cde76 100644 --- a/GraphNeuralNetworks/src/layers/conv.jl +++ b/GraphNeuralNetworks/src/layers/conv.jl @@ -590,7 +590,7 @@ function Base.show(io::IO, l::EdgeConv) end @doc raw""" - GINConv(f, ϵ; aggr=+) + GINConv(f, [eps]; aggr=+, train_eps=false) Graph Isomorphism convolutional layer from paper [How Powerful are Graph Neural Networks?](https://arxiv.org/pdf/1810.00826.pdf). @@ -603,7 +603,9 @@ where ``f_\Theta`` typically denotes a learnable function, e.g. a linear layer o # Arguments - `f`: A (possibly learnable) function acting on node features. -- `ϵ`: Weighting factor. +- `eps`: Initial value of the weighting factor ``\epsilon``. Default `0.0f0`. +- `train_eps`: If `true`, ``\epsilon`` is trainable. Default `false`. +- `aggr`: Neighborhood aggregation function. Default `+`. # Examples: @@ -619,28 +621,37 @@ g = GNNGraph(s, t) nn = Dense(in_channel, out_channel) # create layer -l = GINConv(nn, 0.01f0, aggr = mean) +l = GINConv(nn; eps = 0.01f0, train_eps = true, aggr = mean) # forward pass y = l(g, x) ``` """ -struct GINConv{R <: Real, NN, A} <: GNNLayer +struct GINConv{E, NN, A} <: GNNLayer nn::NN - ϵ::R + eps::E aggr::A + train_eps::Bool end Flux.@layer :expand GINConv -Flux.trainable(l::GINConv) = (nn = l.nn,) +Flux.trainable(l::GINConv) = l.train_eps ? (; l.nn, l.eps) : (; l.nn) -GINConv(nn, ϵ; aggr = +) = GINConv(nn, ϵ, aggr) +GINConv(nn, eps::Real; aggr = +, train_eps::Bool = false) = + GINConv(nn, train_eps ? [eps] : eps, aggr, train_eps) + +GINConv(nn, eps::Real, aggr) = GINConv(nn, eps; aggr) + +GINConv(nn; eps::Real = 0.0f0, aggr = +, train_eps::Bool = false) = + GINConv(nn, eps; aggr, train_eps) (l::GINConv)(g, x) = GNNlib.gin_conv(l, g, x) function Base.show(io::IO, l::GINConv) print(io, "GINConv($(l.nn)") - print(io, ", $(l.ϵ)") + print(io, ", eps=$(l.eps)") + l.train_eps && print(io, ", train_eps=true") + l.aggr == (+) || print(io, ", aggr=$(l.aggr)") print(io, ")") end diff --git a/GraphNeuralNetworks/test/layers/conv.jl b/GraphNeuralNetworks/test/layers/conv.jl index 7585b817d..fafd68fcd 100644 --- a/GraphNeuralNetworks/test/layers/conv.jl +++ b/GraphNeuralNetworks/test/layers/conv.jl @@ -278,19 +278,31 @@ end using .TestModule nn = Dense(D_IN, D_OUT) - l = GINConv(nn, 0.01, aggr = mean) + l = GINConv(nn; eps = 0.01, aggr = mean) for g in TEST_GRAPHS @test size(l(g, g.x)) == (D_OUT, g.num_nodes) test_gradients(l, g, g.x, rtol = RTOL_HIGH) end + @test keys(Flux.trainable(l)) == (:nn,) - @test !in(:eps, Flux.trainable(l)) + l = GINConv(Dense(D_IN, D_OUT); train_eps = true, aggr = mean) + @test keys(Flux.trainable(l)) == (:nn, :eps) + @test l.eps == [0.0f0] + for g in TEST_GRAPHS + @test size(l(g, g.x)) == (D_OUT, g.num_nodes) + end + g = first(TEST_GRAPHS) + grad = Flux.gradient(l -> mean(l(g, g.x)), l)[1] + @test grad.eps isa AbstractVector + @test length(grad.eps) == 1 + + @test GINConv(nn, 0.5).eps == 0.5 end @testitem "GINConv GPU" setup=[TolSnippet, TestModule] tags=[:gpu] begin using .TestModule nn = Dense(D_IN, D_OUT) - l = GINConv(nn, 0.01, aggr = mean) + l = GINConv(nn; train_eps = true, aggr = mean) for g in TEST_GRAPHS g.graph isa AbstractSparseMatrix && continue @test size(l(g, g.x)) == (D_OUT, g.num_nodes) From c7b2b15edc286760995491b1e44b9b3591f4b318 Mon Sep 17 00:00:00 2001 From: Aurora Rossi Date: Fri, 24 Jul 2026 14:01:15 +0200 Subject: [PATCH 4/7] Change eps to unicode char --- GNNLux/src/layers/conv.jl | 26 ++++++++++++------------- GNNLux/test/layers/conv.jl | 8 ++++---- GNNlib/src/layers/conv.jl | 4 ++-- GraphNeuralNetworks/src/layers/conv.jl | 22 ++++++++++----------- GraphNeuralNetworks/test/layers/conv.jl | 12 ++++++------ 5 files changed, 36 insertions(+), 36 deletions(-) diff --git a/GNNLux/src/layers/conv.jl b/GNNLux/src/layers/conv.jl index 199494926..73a78f223 100644 --- a/GNNLux/src/layers/conv.jl +++ b/GNNLux/src/layers/conv.jl @@ -1287,7 +1287,7 @@ function Base.show(io::IO, l::GatedGraphConv) end @doc raw""" - GINConv(f, [eps]; aggr=+, train_eps=false) + GINConv(f, [ϵ]; aggr=+, train_eps=false) Graph Isomorphism convolutional layer from paper [How Powerful are Graph Neural Networks?](https://arxiv.org/pdf/1810.00826.pdf). @@ -1300,7 +1300,7 @@ where ``f_\Theta`` typically denotes a learnable function, e.g. a linear layer o # Arguments - `f`: A (possibly learnable) function acting on node features. -- `eps`: Initial value of the weighting factor ``\epsilon``. Default `0.0f0`. +- `ϵ`: Initial value of the weighting factor ``\epsilon``. Default `0.0f0`. - `train_eps`: If `true`, ``\epsilon`` is trainable. Default `false`. - `aggr`: Neighborhood aggregation function. Default `+`. @@ -1324,7 +1324,7 @@ x = randn(rng, Float32, in_channel, g.num_nodes) nn = Dense(in_channel, out_channel) # create layer -l = GINConv(nn; eps = 0.01f0, train_eps = true, aggr = mean) +l = GINConv(nn; ϵ = 0.01f0, train_eps = true, aggr = mean) # setup layer ps, st = LuxCore.setup(rng, l) @@ -1335,22 +1335,22 @@ y, st = l(g, x, ps, st) # size: out_channel × num_nodes """ @concrete struct GINConv <: GNNContainerLayer{(:nn,)} nn <: AbstractLuxLayer - eps + ϵ aggr train_eps::Bool end -GINConv(nn, eps::Real; aggr = +, train_eps::Bool = false) = - GINConv(nn, train_eps ? [eps] : eps, aggr, train_eps) +GINConv(nn, ϵ::Real; aggr = +, train_eps::Bool = false) = + GINConv(nn, train_eps ? [ϵ] : ϵ, aggr, train_eps) -GINConv(nn, eps::Real, aggr) = GINConv(nn, eps; aggr) +GINConv(nn, ϵ::Real, aggr) = GINConv(nn, ϵ; aggr) -GINConv(nn; eps::Real = 0.0f0, aggr = +, train_eps::Bool = false) = - GINConv(nn, eps; aggr, train_eps) +GINConv(nn; ϵ::Real = 0.0f0, aggr = +, train_eps::Bool = false) = + GINConv(nn, ϵ; aggr, train_eps) function LuxCore.initialparameters(rng::AbstractRNG, l::GINConv) nn = LuxCore.initialparameters(rng, l.nn) - return l.train_eps ? (; nn, eps = l.eps) : (; nn) + return l.train_eps ? (; nn, ϵ = l.ϵ) : (; nn) end LuxCore.parameterlength(l::GINConv) = parameterlength(l.nn) + l.train_eps @@ -1358,8 +1358,8 @@ LuxCore.statelength(l::GINConv) = statelength(l.nn) function (l::GINConv)(g, x, ps, st) nn = StatefulLuxLayer{true}(l.nn, ps.nn, st.nn) - eps = l.train_eps ? ps.eps : l.eps - m = (; nn, eps, l.aggr) + ϵ = l.train_eps ? ps.ϵ : l.ϵ + m = (; nn, ϵ, l.aggr) y = GNNlib.gin_conv(m, g, x) stnew = (; nn = _getstate(nn)) return y, stnew @@ -1367,7 +1367,7 @@ end function Base.show(io::IO, l::GINConv) print(io, "GINConv($(l.nn)") - print(io, ", eps=$(l.eps)") + print(io, ", ϵ=$(l.ϵ)") l.train_eps && print(io, ", train_eps=true") l.aggr == (+) || print(io, ", aggr=$(l.aggr)") print(io, ")") diff --git a/GNNLux/test/layers/conv.jl b/GNNLux/test/layers/conv.jl index 959a0f79a..d32c2772e 100644 --- a/GNNLux/test/layers/conv.jl +++ b/GNNLux/test/layers/conv.jl @@ -92,17 +92,17 @@ @testset "GINConv" begin nn = Chain(Dense(in_dims => out_dims, tanh), Dense(out_dims => out_dims)) - l = GINConv(nn; eps = 0.5) + l = GINConv(nn; ϵ = 0.5) test_lux_layer(rng, l, g, x, sizey=(out_dims,g.num_nodes), container=true) ps = LuxCore.initialparameters(rng, l) - @test !hasproperty(ps, :eps) + @test !hasproperty(ps, :ϵ) l = GINConv(nn; train_eps = true) test_lux_layer(rng, l, g, x, sizey=(out_dims,g.num_nodes), container=true) ps = LuxCore.initialparameters(rng, l) - @test ps.eps == [0.0f0] + @test ps.ϵ == [0.0f0] - @test GINConv(nn, 0.5).eps == 0.5 + @test GINConv(nn, 0.5).ϵ == 0.5 end @testset "MEGNetConv" begin diff --git a/GNNlib/src/layers/conv.jl b/GNNlib/src/layers/conv.jl index 6b7907f1d..685f3c6e4 100644 --- a/GNNlib/src/layers/conv.jl +++ b/GNNlib/src/layers/conv.jl @@ -252,8 +252,8 @@ function gin_conv(l, g::AbstractGNNGraph, x) xj, xi = expand_srcdst(g, x) m = propagate(copy_xj, g, l.aggr, xj = xj) - eps = convert.(float(eltype(xi)), l.eps) - return l.nn((1 .+ eps) .* xi .+ m) + ϵ = convert.(float(eltype(xi)), l.ϵ) + return l.nn((1 .+ ϵ) .* xi .+ m) end ####################### NNConv ###################################### diff --git a/GraphNeuralNetworks/src/layers/conv.jl b/GraphNeuralNetworks/src/layers/conv.jl index 4bd6cde76..b105aa93b 100644 --- a/GraphNeuralNetworks/src/layers/conv.jl +++ b/GraphNeuralNetworks/src/layers/conv.jl @@ -590,7 +590,7 @@ function Base.show(io::IO, l::EdgeConv) end @doc raw""" - GINConv(f, [eps]; aggr=+, train_eps=false) + GINConv(f, [ϵ]; aggr=+, train_eps=false) Graph Isomorphism convolutional layer from paper [How Powerful are Graph Neural Networks?](https://arxiv.org/pdf/1810.00826.pdf). @@ -603,7 +603,7 @@ where ``f_\Theta`` typically denotes a learnable function, e.g. a linear layer o # Arguments - `f`: A (possibly learnable) function acting on node features. -- `eps`: Initial value of the weighting factor ``\epsilon``. Default `0.0f0`. +- `ϵ`: Initial value of the weighting factor ``\epsilon``. Default `0.0f0`. - `train_eps`: If `true`, ``\epsilon`` is trainable. Default `false`. - `aggr`: Neighborhood aggregation function. Default `+`. @@ -621,7 +621,7 @@ g = GNNGraph(s, t) nn = Dense(in_channel, out_channel) # create layer -l = GINConv(nn; eps = 0.01f0, train_eps = true, aggr = mean) +l = GINConv(nn; ϵ = 0.01f0, train_eps = true, aggr = mean) # forward pass y = l(g, x) @@ -629,27 +629,27 @@ y = l(g, x) """ struct GINConv{E, NN, A} <: GNNLayer nn::NN - eps::E + ϵ::E aggr::A train_eps::Bool end Flux.@layer :expand GINConv -Flux.trainable(l::GINConv) = l.train_eps ? (; l.nn, l.eps) : (; l.nn) +Flux.trainable(l::GINConv) = l.train_eps ? (; l.nn, l.ϵ) : (; l.nn) -GINConv(nn, eps::Real; aggr = +, train_eps::Bool = false) = - GINConv(nn, train_eps ? [eps] : eps, aggr, train_eps) +GINConv(nn, ϵ::Real; aggr = +, train_eps::Bool = false) = + GINConv(nn, train_eps ? [ϵ] : ϵ, aggr, train_eps) -GINConv(nn, eps::Real, aggr) = GINConv(nn, eps; aggr) +GINConv(nn, ϵ::Real, aggr) = GINConv(nn, ϵ; aggr) -GINConv(nn; eps::Real = 0.0f0, aggr = +, train_eps::Bool = false) = - GINConv(nn, eps; aggr, train_eps) +GINConv(nn; ϵ::Real = 0.0f0, aggr = +, train_eps::Bool = false) = + GINConv(nn, ϵ; aggr, train_eps) (l::GINConv)(g, x) = GNNlib.gin_conv(l, g, x) function Base.show(io::IO, l::GINConv) print(io, "GINConv($(l.nn)") - print(io, ", eps=$(l.eps)") + print(io, ", ϵ=$(l.ϵ)") l.train_eps && print(io, ", train_eps=true") l.aggr == (+) || print(io, ", aggr=$(l.aggr)") print(io, ")") diff --git a/GraphNeuralNetworks/test/layers/conv.jl b/GraphNeuralNetworks/test/layers/conv.jl index fafd68fcd..7cecaf143 100644 --- a/GraphNeuralNetworks/test/layers/conv.jl +++ b/GraphNeuralNetworks/test/layers/conv.jl @@ -278,7 +278,7 @@ end using .TestModule nn = Dense(D_IN, D_OUT) - l = GINConv(nn; eps = 0.01, aggr = mean) + l = GINConv(nn; ϵ = 0.01, aggr = mean) for g in TEST_GRAPHS @test size(l(g, g.x)) == (D_OUT, g.num_nodes) test_gradients(l, g, g.x, rtol = RTOL_HIGH) @@ -286,17 +286,17 @@ end @test keys(Flux.trainable(l)) == (:nn,) l = GINConv(Dense(D_IN, D_OUT); train_eps = true, aggr = mean) - @test keys(Flux.trainable(l)) == (:nn, :eps) - @test l.eps == [0.0f0] + @test keys(Flux.trainable(l)) == (:nn, :ϵ) + @test l.ϵ == [0.0f0] for g in TEST_GRAPHS @test size(l(g, g.x)) == (D_OUT, g.num_nodes) end g = first(TEST_GRAPHS) grad = Flux.gradient(l -> mean(l(g, g.x)), l)[1] - @test grad.eps isa AbstractVector - @test length(grad.eps) == 1 + @test grad.ϵ isa AbstractVector + @test length(grad.ϵ) == 1 - @test GINConv(nn, 0.5).eps == 0.5 + @test GINConv(nn, 0.5).ϵ == 0.5 end @testitem "GINConv GPU" setup=[TolSnippet, TestModule] tags=[:gpu] begin From 65f5d5c7b09e87ad0820f9454b8207cd1816ea29 Mon Sep 17 00:00:00 2001 From: Aurora Rossi Date: Fri, 24 Jul 2026 19:40:50 +0200 Subject: [PATCH 5/7] Fix gpu problem --- GNNlib/src/layers/conv.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GNNlib/src/layers/conv.jl b/GNNlib/src/layers/conv.jl index 685f3c6e4..14171e69d 100644 --- a/GNNlib/src/layers/conv.jl +++ b/GNNlib/src/layers/conv.jl @@ -252,7 +252,7 @@ function gin_conv(l, g::AbstractGNNGraph, x) xj, xi = expand_srcdst(g, x) m = propagate(copy_xj, g, l.aggr, xj = xj) - ϵ = convert.(float(eltype(xi)), l.ϵ) + ϵ = l.ϵ isa Real ? ofeltype(xi, l.ϵ) : l.ϵ return l.nn((1 .+ ϵ) .* xi .+ m) end From 1d99de0f78d49282610b596bde0932e897aecfa0 Mon Sep 17 00:00:00 2001 From: Aurora Rossi Date: Wed, 30 Sep 2026 15:09:19 +0200 Subject: [PATCH 6/7] Address GIN epsilon review feedback --- CHANGELOG.md | 18 +++++++++++++++--- GNNLux/Project.toml | 4 ++-- GNNLux/src/layers/conv.jl | 2 +- GNNLux/test/layers/conv.jl | 4 ++-- GNNlib/Project.toml | 2 +- GraphNeuralNetworks/Project.toml | 4 ++-- GraphNeuralNetworks/src/layers/conv.jl | 2 +- GraphNeuralNetworks/test/layers/conv.jl | 4 ++-- 8 files changed, 26 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ae90818b..6dcc6b4e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the packages adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Entries link to the pull request that introduced them. +## GNNLux.jl — Unreleased (towards 0.3.0) + +**Added** +- `GINConv` now supports a trainable `ϵ` parameter through the `train_eps` keyword ([#697]). + ## GNNGraphs.jl — Unreleased (towards 1.6.0) **Added** @@ -19,7 +24,7 @@ Entries link to the pull request that introduced them. - Added an `EnzymeCore` package extension marking `scaled_laplacian` inactive for Enzyme, matching its existing `@non_differentiable` ChainRules declaration. Enzyme previously differentiated the Krylov eigensolve inside it and failed; this unblocks `ChebConv`, `GConvGRUCell` and `GConvLSTMCell` ([#706]). - Enzyme can now differentiate through the adjacency-matrix → COO graph conversion: a new internal keyword-free helper `_to_coo_graph` avoids the union-typed keyword handling of the `GNNGraph(g; graph_type)` constructor and of `to_coo`, which Enzyme's type analysis cannot compile. Together with upstream fixes in Enzyme ≥ 0.13.197 this removes the `EnzymeInternalError` crash on `:dense`/`:sparse` graphs ([#703]). -## GNNLux.jl — Unreleased (towards 0.2.0) +## GNNLux.jl 0.2.0 — 2026-07-22 **Added** - Added pooling layers (`GlobalPool`, `GlobalAttentionPool`, `TopKPool`) ([#576]). @@ -33,12 +38,18 @@ Entries link to the pull request that introduced them. **Removed** - Removed the `A3TGCN` layer, mirroring its removal from the Flux frontend ([#696]). -## GNNlib.jl — Unreleased (towards 1.4.1) +## GNNlib.jl — Unreleased (towards 1.5.0) + +**Added** +- Extended `gin_conv` to accept a scalar-shaped array for `ϵ`, enabling frontends to expose it as a trainable parameter ([#697]). **Fixed** - Fixed `Enzyme.gradient` failing when differentiating `GCNConv`, `SGConv` and `TAGConv` on `:dense`/`:sparse` adjacency graphs: their adjacency-matrix fallbacks now convert via the Enzyme-differentiable `GNNGraphs._to_coo_graph` instead of the keyword `GNNGraph` constructor. Requires GNNGraphs ≥ 1.5.2 and, for Enzyme, Enzyme ≥ 0.13.197 ([#703]). -## GraphNeuralNetworks.jl — Unreleased (towards 1.1.1) +## GraphNeuralNetworks.jl — Unreleased (towards 1.2.0) + +**Added** +- `GINConv` now supports a trainable `ϵ` parameter through the `train_eps` keyword ([#697]). **Changed** - `GNNRecurrence` now hands each cell an indexed time slice instead of an `eachslice` view, matching the Lux frontend. Enzyme's type analysis fails on `SubArray` cell inputs, so this unblocks `DCGRU`, `EvolveGCNO`, `GConvGRU` and `GConvLSTM` under Enzyme ([#707]). @@ -234,6 +245,7 @@ Lux implementations of the graph convolutional, pooling, and temporal layers [#623]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/issues/623 [#695]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/pull/695 [#696]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/pull/696 +[#697]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/pull/697 [#703]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/pull/703 [#704]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/pull/704 [#707]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/pull/707 diff --git a/GNNLux/Project.toml b/GNNLux/Project.toml index 0fb3b2e05..86b566822 100644 --- a/GNNLux/Project.toml +++ b/GNNLux/Project.toml @@ -1,6 +1,6 @@ name = "GNNLux" uuid = "e8545f4d-a905-48ac-a8c4-ca114b98986d" -version = "0.2.0" +version = "0.3.0-DEV" authors = ["Carlo Lucibello and contributors"] [workspace] @@ -26,7 +26,7 @@ GNNlib = {path = "../GNNlib"} [compat] ConcreteStructs = "0.2.3" GNNGraphs = "1.4" -GNNlib = "1.4" +GNNlib = "1.5" Lux = "1" LuxCore = "1" MLUtils = "0.4.4" diff --git a/GNNLux/src/layers/conv.jl b/GNNLux/src/layers/conv.jl index 73a78f223..387a57caf 100644 --- a/GNNLux/src/layers/conv.jl +++ b/GNNLux/src/layers/conv.jl @@ -1341,7 +1341,7 @@ y, st = l(g, x, ps, st) # size: out_channel × num_nodes end GINConv(nn, ϵ::Real; aggr = +, train_eps::Bool = false) = - GINConv(nn, train_eps ? [ϵ] : ϵ, aggr, train_eps) + GINConv(nn, train_eps ? Float32[ϵ] : ϵ, aggr, train_eps) GINConv(nn, ϵ::Real, aggr) = GINConv(nn, ϵ; aggr) diff --git a/GNNLux/test/layers/conv.jl b/GNNLux/test/layers/conv.jl index d32c2772e..f472b95b2 100644 --- a/GNNLux/test/layers/conv.jl +++ b/GNNLux/test/layers/conv.jl @@ -97,10 +97,10 @@ ps = LuxCore.initialparameters(rng, l) @test !hasproperty(ps, :ϵ) - l = GINConv(nn; train_eps = true) + l = GINConv(nn; ϵ = 0.5, train_eps = true) test_lux_layer(rng, l, g, x, sizey=(out_dims,g.num_nodes), container=true) ps = LuxCore.initialparameters(rng, l) - @test ps.ϵ == [0.0f0] + @test ps.ϵ == Float32[0.5] @test GINConv(nn, 0.5).ϵ == 0.5 end diff --git a/GNNlib/Project.toml b/GNNlib/Project.toml index 4c2e00a75..26f51d6fa 100644 --- a/GNNlib/Project.toml +++ b/GNNlib/Project.toml @@ -1,7 +1,7 @@ name = "GNNlib" uuid = "a6a84749-d869-43f8-aacc-be26a1996e48" authors = ["Carlo Lucibello and contributors"] -version = "1.4.1-DEV" +version = "1.5.0-DEV" [workspace] projects = ["test", "docs", "../GNNGraphs"] diff --git a/GraphNeuralNetworks/Project.toml b/GraphNeuralNetworks/Project.toml index ce79f146f..eff1c2b24 100644 --- a/GraphNeuralNetworks/Project.toml +++ b/GraphNeuralNetworks/Project.toml @@ -1,7 +1,7 @@ name = "GraphNeuralNetworks" uuid = "cffab07f-9bc2-4db1-8861-388f63bf7694" authors = ["Carlo Lucibello and contributors"] -version = "1.1.1-DEV" +version = "1.2.0-DEV" [workspace] projects = ["test", "docs"] @@ -25,7 +25,7 @@ ChainRulesCore = "1" ConcreteStructs = "0.2.3" Flux = "0.16.0" GNNGraphs = "1.4" -GNNlib = "1.4" +GNNlib = "1.5" LinearAlgebra = "1" MLUtils = "0.4" MacroTools = "0.5" diff --git a/GraphNeuralNetworks/src/layers/conv.jl b/GraphNeuralNetworks/src/layers/conv.jl index b105aa93b..569ba3559 100644 --- a/GraphNeuralNetworks/src/layers/conv.jl +++ b/GraphNeuralNetworks/src/layers/conv.jl @@ -638,7 +638,7 @@ Flux.@layer :expand GINConv Flux.trainable(l::GINConv) = l.train_eps ? (; l.nn, l.ϵ) : (; l.nn) GINConv(nn, ϵ::Real; aggr = +, train_eps::Bool = false) = - GINConv(nn, train_eps ? [ϵ] : ϵ, aggr, train_eps) + GINConv(nn, train_eps ? Float32[ϵ] : ϵ, aggr, train_eps) GINConv(nn, ϵ::Real, aggr) = GINConv(nn, ϵ; aggr) diff --git a/GraphNeuralNetworks/test/layers/conv.jl b/GraphNeuralNetworks/test/layers/conv.jl index 7cecaf143..bc0525e2a 100644 --- a/GraphNeuralNetworks/test/layers/conv.jl +++ b/GraphNeuralNetworks/test/layers/conv.jl @@ -285,9 +285,9 @@ end end @test keys(Flux.trainable(l)) == (:nn,) - l = GINConv(Dense(D_IN, D_OUT); train_eps = true, aggr = mean) + l = GINConv(Dense(D_IN, D_OUT); ϵ = 0.5, train_eps = true, aggr = mean) @test keys(Flux.trainable(l)) == (:nn, :ϵ) - @test l.ϵ == [0.0f0] + @test l.ϵ == Float32[0.5] for g in TEST_GRAPHS @test size(l(g, g.x)) == (D_OUT, g.num_nodes) end From 1b51c53d48dcd675fd25b6856b2e3a8facab3480 Mon Sep 17 00:00:00 2001 From: Aurora Rossi Date: Wed, 30 Sep 2026 15:16:52 +0200 Subject: [PATCH 7/7] Fix --- CHANGELOG.md | 18 +++--------------- GNNLux/Project.toml | 4 ++-- GNNlib/Project.toml | 2 +- GraphNeuralNetworks/Project.toml | 4 ++-- 4 files changed, 8 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dcc6b4e9..2ae90818b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the packages adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Entries link to the pull request that introduced them. -## GNNLux.jl — Unreleased (towards 0.3.0) - -**Added** -- `GINConv` now supports a trainable `ϵ` parameter through the `train_eps` keyword ([#697]). - ## GNNGraphs.jl — Unreleased (towards 1.6.0) **Added** @@ -24,7 +19,7 @@ Entries link to the pull request that introduced them. - Added an `EnzymeCore` package extension marking `scaled_laplacian` inactive for Enzyme, matching its existing `@non_differentiable` ChainRules declaration. Enzyme previously differentiated the Krylov eigensolve inside it and failed; this unblocks `ChebConv`, `GConvGRUCell` and `GConvLSTMCell` ([#706]). - Enzyme can now differentiate through the adjacency-matrix → COO graph conversion: a new internal keyword-free helper `_to_coo_graph` avoids the union-typed keyword handling of the `GNNGraph(g; graph_type)` constructor and of `to_coo`, which Enzyme's type analysis cannot compile. Together with upstream fixes in Enzyme ≥ 0.13.197 this removes the `EnzymeInternalError` crash on `:dense`/`:sparse` graphs ([#703]). -## GNNLux.jl 0.2.0 — 2026-07-22 +## GNNLux.jl — Unreleased (towards 0.2.0) **Added** - Added pooling layers (`GlobalPool`, `GlobalAttentionPool`, `TopKPool`) ([#576]). @@ -38,18 +33,12 @@ Entries link to the pull request that introduced them. **Removed** - Removed the `A3TGCN` layer, mirroring its removal from the Flux frontend ([#696]). -## GNNlib.jl — Unreleased (towards 1.5.0) - -**Added** -- Extended `gin_conv` to accept a scalar-shaped array for `ϵ`, enabling frontends to expose it as a trainable parameter ([#697]). +## GNNlib.jl — Unreleased (towards 1.4.1) **Fixed** - Fixed `Enzyme.gradient` failing when differentiating `GCNConv`, `SGConv` and `TAGConv` on `:dense`/`:sparse` adjacency graphs: their adjacency-matrix fallbacks now convert via the Enzyme-differentiable `GNNGraphs._to_coo_graph` instead of the keyword `GNNGraph` constructor. Requires GNNGraphs ≥ 1.5.2 and, for Enzyme, Enzyme ≥ 0.13.197 ([#703]). -## GraphNeuralNetworks.jl — Unreleased (towards 1.2.0) - -**Added** -- `GINConv` now supports a trainable `ϵ` parameter through the `train_eps` keyword ([#697]). +## GraphNeuralNetworks.jl — Unreleased (towards 1.1.1) **Changed** - `GNNRecurrence` now hands each cell an indexed time slice instead of an `eachslice` view, matching the Lux frontend. Enzyme's type analysis fails on `SubArray` cell inputs, so this unblocks `DCGRU`, `EvolveGCNO`, `GConvGRU` and `GConvLSTM` under Enzyme ([#707]). @@ -245,7 +234,6 @@ Lux implementations of the graph convolutional, pooling, and temporal layers [#623]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/issues/623 [#695]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/pull/695 [#696]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/pull/696 -[#697]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/pull/697 [#703]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/pull/703 [#704]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/pull/704 [#707]: https://github.com/JuliaGraphs/GraphNeuralNetworks.jl/pull/707 diff --git a/GNNLux/Project.toml b/GNNLux/Project.toml index 86b566822..0fb3b2e05 100644 --- a/GNNLux/Project.toml +++ b/GNNLux/Project.toml @@ -1,6 +1,6 @@ name = "GNNLux" uuid = "e8545f4d-a905-48ac-a8c4-ca114b98986d" -version = "0.3.0-DEV" +version = "0.2.0" authors = ["Carlo Lucibello and contributors"] [workspace] @@ -26,7 +26,7 @@ GNNlib = {path = "../GNNlib"} [compat] ConcreteStructs = "0.2.3" GNNGraphs = "1.4" -GNNlib = "1.5" +GNNlib = "1.4" Lux = "1" LuxCore = "1" MLUtils = "0.4.4" diff --git a/GNNlib/Project.toml b/GNNlib/Project.toml index 26f51d6fa..4c2e00a75 100644 --- a/GNNlib/Project.toml +++ b/GNNlib/Project.toml @@ -1,7 +1,7 @@ name = "GNNlib" uuid = "a6a84749-d869-43f8-aacc-be26a1996e48" authors = ["Carlo Lucibello and contributors"] -version = "1.5.0-DEV" +version = "1.4.1-DEV" [workspace] projects = ["test", "docs", "../GNNGraphs"] diff --git a/GraphNeuralNetworks/Project.toml b/GraphNeuralNetworks/Project.toml index eff1c2b24..ce79f146f 100644 --- a/GraphNeuralNetworks/Project.toml +++ b/GraphNeuralNetworks/Project.toml @@ -1,7 +1,7 @@ name = "GraphNeuralNetworks" uuid = "cffab07f-9bc2-4db1-8861-388f63bf7694" authors = ["Carlo Lucibello and contributors"] -version = "1.2.0-DEV" +version = "1.1.1-DEV" [workspace] projects = ["test", "docs"] @@ -25,7 +25,7 @@ ChainRulesCore = "1" ConcreteStructs = "0.2.3" Flux = "0.16.0" GNNGraphs = "1.4" -GNNlib = "1.5" +GNNlib = "1.4" LinearAlgebra = "1" MLUtils = "0.4" MacroTools = "0.5"