From e377594de1f0426eb86f00ce9f5fa699a3ade4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Wed, 28 Oct 2026 00:01:31 +0100 Subject: [PATCH 1/3] Handle Symmetric/Hermitian tangents with other uplo in ProjectTo Projecting e.g. `Symmetric(B, :L)` with the projector of a `Symmetric(A, :U)` threw `ArgumentError: Cannot construct Symmetric; uplo doesn't match`, since the already-symmetric input was rewrapped with the primal's uplo. Such inputs now go through the symmetrizing fallback, so the result is stored with the primal's uplo. Co-Authored-By: Claude Opus 5.5 --- Project.toml | 2 +- src/projection.jl | 8 +++++++- test/projection.jl | 12 ++++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 403da1e65..a56827cad 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ChainRulesCore" uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" -version = "1.26.5" +version = "1.26.6" [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" diff --git a/src/projection.jl b/src/projection.jl index ca577aef2..78612696f 100644 --- a/src/projection.jl +++ b/src/projection.jl @@ -452,6 +452,8 @@ end (project::ProjectTo{Diagonal})(dx::Diagonal) = Diagonal(project.diag(dx.diag)) # Symmetric +_same_uplo(dx::LinearAlgebra.HermOrSym, uplo::Symbol) = LinearAlgebra.sym_uplo(dx.uplo) === uplo +_same_uplo(dx, uplo::Symbol) = true for (SymHerm, chk, fun) in ((:Symmetric, :issymmetric, :transpose), (:Hermitian, :ishermitian, :adjoint)) @eval begin @@ -465,7 +467,11 @@ for (SymHerm, chk, fun) in dy = project.parent(dx) # Here $chk means this is efficient on same-type. # If we could mutate dx, then that could speed up action on dx::Matrix. - dz = $chk(dy) ? dy : (dy .+ $fun(dy)) ./ 2 + # Separate returns avoid a type-unstable `dz`, which allocates on Julia 1.10. + if $chk(dy) && _same_uplo(dy, project.uplo) + return $SymHerm(project.parent(dy), project.uplo) + end + dz = (dy .+ $fun(dy)) ./ 2 return $SymHerm(project.parent(dz), project.uplo) end # This is an example of a subspace which is not a subtype, diff --git a/test/projection.jl b/test/projection.jl index 95046147a..baeaa59c3 100644 --- a/test/projection.jl +++ b/test/projection.jl @@ -284,6 +284,18 @@ struct NoSuperType end @test pherm(pherm(reshape(1:9, 3, 3))) == pherm(reshape(1:9, 3, 3)) @test pherm(rand(ComplexF32, 3, 3, 1)) isa Hermitian{ComplexF64} + @testset "input with other uplo" begin + for dx in (Symmetric(rand(3, 3), :L), Hermitian(rand(3, 3), :L)) + @test @inferred(psymm(dx)) == dx + @test psymm(dx).uplo == 'U' + end + dx = Hermitian(rand(ComplexF64, 3, 3), :U) + @test @inferred(pherm(dx)) == dx + @test pherm(dx).uplo == 'L' + dx = Symmetric(rand(ComplexF64, 3, 3), :L) + @test @inferred(ProjectTo(Symmetric(rand(ComplexF64, 3, 3)))(dx)) == dx + end + pupp = ProjectTo(UpperTriangular(rand(3, 3))) @test pupp(reshape(1:9, 3, 3)) == [1.0 4.0 7.0; 0.0 5.0 8.0; 0.0 0.0 9.0] @test pupp(pupp(reshape(1:9, 3, 3))) == pupp(reshape(1:9, 3, 3)) From 6a739595ae5089625abc7be4fd88be9e74831885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Wed, 28 Oct 2026 00:04:40 +0100 Subject: [PATCH 2/3] Fix format Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/projection.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/projection.jl b/src/projection.jl index 78612696f..7ddadf11a 100644 --- a/src/projection.jl +++ b/src/projection.jl @@ -452,7 +452,9 @@ end (project::ProjectTo{Diagonal})(dx::Diagonal) = Diagonal(project.diag(dx.diag)) # Symmetric -_same_uplo(dx::LinearAlgebra.HermOrSym, uplo::Symbol) = LinearAlgebra.sym_uplo(dx.uplo) === uplo +function _same_uplo(dx::LinearAlgebra.HermOrSym, uplo::Symbol) + return LinearAlgebra.sym_uplo(dx.uplo) === uplo +end _same_uplo(dx, uplo::Symbol) = true for (SymHerm, chk, fun) in ((:Symmetric, :issymmetric, :transpose), (:Hermitian, :ishermitian, :adjoint)) From ab9f10b018f3f31a5eb1bca871521f3c6ca36a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Wed, 28 Oct 2026 09:27:08 +0100 Subject: [PATCH 3/3] Test ProjectTo{Symmetric/Hermitian} on already-symmetric plain matrices Covers the generic `_same_uplo` fallback, which is only reached when the input is a plain array that already passes issymmetric/ishermitian. Co-Authored-By: Claude Opus 5.5 --- test/projection.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/projection.jl b/test/projection.jl index baeaa59c3..d77788b71 100644 --- a/test/projection.jl +++ b/test/projection.jl @@ -296,6 +296,15 @@ struct NoSuperType end @test @inferred(ProjectTo(Symmetric(rand(ComplexF64, 3, 3)))(dx)) == dx end + @testset "plain matrix input that is already symmetric/hermitian" begin + dx = [1.0 2.0 3.0; 2.0 4.0 5.0; 3.0 5.0 6.0] + @test @inferred(psymm(dx)) == dx + @test psymm(dx) isa Symmetric{Float64} + dx = [1.0 2.0+im 3.0; 2.0-im 4.0 5.0im; 3.0 -5.0im 6.0] + @test @inferred(pherm(dx)) == dx + @test pherm(dx) isa Hermitian{ComplexF64} + end + pupp = ProjectTo(UpperTriangular(rand(3, 3))) @test pupp(reshape(1:9, 3, 3)) == [1.0 4.0 7.0; 0.0 5.0 8.0; 0.0 0.0 9.0] @test pupp(pupp(reshape(1:9, 3, 3))) == pupp(reshape(1:9, 3, 3))