From dcb2ee0dc0aa157ed6d06570f89f85da688bb738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Wed, 28 Oct 2026 15:47:06 +0100 Subject: [PATCH 1/3] Make the diagonal of Diagonal tangents projected onto Hermitian real ProjectTo{Hermitian} kept the imaginary part of a Diagonal tangent, so its result was not Hermitian and disagreed with the dense path. Take the real part with oftype, like LinearAlgebra.hermitian(::Number), so the element type is kept also for e.g. quaternions and Unitful quantities. Fixes #722. Co-Authored-By: Claude Opus 5.5 --- Project.toml | 2 +- src/projection.jl | 13 ++++++++++++- test/projection.jl | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index a56827cad..30a618092 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "ChainRulesCore" uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" -version = "1.26.6" +version = "1.26.7" [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" diff --git a/src/projection.jl b/src/projection.jl index 7ddadf11a..d746e9ebe 100644 --- a/src/projection.jl +++ b/src/projection.jl @@ -483,10 +483,21 @@ for (SymHerm, chk, fun) in sub_one = ProjectTo{project_type(sub)}(; element=sub.element, axes=(sub.axes[1],) ) - return Diagonal(sub_one(dx.diag)) + return Diagonal(_project_diag(project, sub_one, dx.diag)) end end end +_project_diag(::ProjectTo{Symmetric}, sub_one, diag) = sub_one(diag) +function _project_diag(::ProjectTo{Hermitian}, sub_one, diag) + dy = sub_one(diag) + # The diagonal of a Hermitian matrix is real. Like `LinearAlgebra.hermitian(::Number)`, + # `oftype` keeps the element type, e.g. for quaternions + if eltype(dy) <: Real + return dy + else + return map(y -> oftype(y, real(y)), dy) + end +end # Triangular for UL in (:UpperTriangular, :LowerTriangular, :UnitUpperTriangular, :UnitLowerTriangular) # UpperHessenberg diff --git a/test/projection.jl b/test/projection.jl index d77788b71..6952b8361 100644 --- a/test/projection.jl +++ b/test/projection.jl @@ -314,6 +314,19 @@ struct NoSuperType end # an experiment with allowing subspaces which aren't subtypes @test psymm(Diagonal([1, 2, 3])) isa Diagonal{Float64} @test pupp(Diagonal([1, 2, 3 + 4im])) isa Diagonal{Float64} + + @testset "Diagonal input to Hermitian has real diagonal" begin + dx = Diagonal([1 + 2im, 3im, 4.0]) + @test @inferred(pherm(dx)) == Diagonal([1.0, 0.0, 4.0]) + @test pherm(dx) isa Diagonal{ComplexF64} + @test pherm(dx) == pherm(Matrix(dx)) + @test pherm(Diagonal([1.0, 2.0, 3.0])) isa Diagonal{ComplexF64} + pherm32 = ProjectTo(Hermitian(rand(ComplexF32, 3, 3))) + @test @inferred(pherm32(dx)) isa Diagonal{ComplexF32} + @test @inferred(ProjectTo(Hermitian(rand(3, 3)))(dx)) == + Diagonal([1.0, 0.0, 4.0]) + @test_throws DimensionMismatch pherm(Diagonal(rand(ComplexF64, 2))) + end end @testset "LinearAlgebra: sparse structured matrices" begin @@ -499,6 +512,9 @@ struct NoSuperType end psymm = ProjectTo(Symmetric(rand(10^3, 10^3))) @test 0 == @ballocated $psymm(dx) setup = (dx = Symmetric(rand(10^3, 10^3))) + + pherm = ProjectTo(Hermitian(rand(10^3, 10^3))) + @test 0 == @ballocated $pherm(dx) setup = (dx = Diagonal(rand(10^3))) end @testset "#407" begin From f2448a2648a37594508ee42e16585824e5abe9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Wed, 28 Oct 2026 16:21:31 +0100 Subject: [PATCH 2/3] Skip the real part for real Diagonal tangents onto Hermitian A real input diagonal is already real after projection, so this avoids a second copy. Also check the value for a real tangent, that Symmetric keeps a complex diagonal, and the allocations for a real tangent onto a complex Hermitian. Co-Authored-By: Claude Opus 5.5 --- src/projection.jl | 5 ++--- test/projection.jl | 9 +++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/projection.jl b/src/projection.jl index d746e9ebe..ddc797bd5 100644 --- a/src/projection.jl +++ b/src/projection.jl @@ -490,9 +490,8 @@ end _project_diag(::ProjectTo{Symmetric}, sub_one, diag) = sub_one(diag) function _project_diag(::ProjectTo{Hermitian}, sub_one, diag) dy = sub_one(diag) - # The diagonal of a Hermitian matrix is real. Like `LinearAlgebra.hermitian(::Number)`, - # `oftype` keeps the element type, e.g. for quaternions - if eltype(dy) <: Real + # The diagonal of a Hermitian matrix is real + if eltype(diag) <: Real || eltype(dy) <: Real return dy else return map(y -> oftype(y, real(y)), dy) diff --git a/test/projection.jl b/test/projection.jl index 6952b8361..d042b4aaa 100644 --- a/test/projection.jl +++ b/test/projection.jl @@ -320,12 +320,16 @@ struct NoSuperType end @test @inferred(pherm(dx)) == Diagonal([1.0, 0.0, 4.0]) @test pherm(dx) isa Diagonal{ComplexF64} @test pherm(dx) == pherm(Matrix(dx)) + @test @inferred(pherm(Diagonal([1.0, 2.0, 3.0]))) == Diagonal([1.0, 2.0, 3.0]) @test pherm(Diagonal([1.0, 2.0, 3.0])) isa Diagonal{ComplexF64} pherm32 = ProjectTo(Hermitian(rand(ComplexF32, 3, 3))) @test @inferred(pherm32(dx)) isa Diagonal{ComplexF32} @test @inferred(ProjectTo(Hermitian(rand(3, 3)))(dx)) == Diagonal([1.0, 0.0, 4.0]) @test_throws DimensionMismatch pherm(Diagonal(rand(ComplexF64, 2))) + + # Symmetric keeps a complex diagonal + @test @inferred(ProjectTo(Symmetric(rand(ComplexF64, 3, 3)))(dx)) == dx end end @@ -515,6 +519,11 @@ struct NoSuperType end pherm = ProjectTo(Hermitian(rand(10^3, 10^3))) @test 0 == @ballocated $pherm(dx) setup = (dx = Diagonal(rand(10^3))) + + # a real tangent onto complex Hermitian is copied once, when converted to complex + pherm = ProjectTo(Hermitian(rand(ComplexF64, 10^3, 10^3))) + @test 2 * sizeof(ComplexF64) * 10^3 > + @ballocated $pherm(dx) setup = (dx = Diagonal(rand(10^3))) end @testset "#407" begin From 1a2a5593b01c8e11a1cf76f8f87b4266c76c60cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller-Widmann?= Date: Wed, 28 Oct 2026 17:38:31 +0100 Subject: [PATCH 3/3] Rename complex Hermitian projector in allocation test and trim comment Co-Authored-By: Claude Opus 5.5 --- test/projection.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/projection.jl b/test/projection.jl index d042b4aaa..dee6ef146 100644 --- a/test/projection.jl +++ b/test/projection.jl @@ -520,10 +520,10 @@ struct NoSuperType end pherm = ProjectTo(Hermitian(rand(10^3, 10^3))) @test 0 == @ballocated $pherm(dx) setup = (dx = Diagonal(rand(10^3))) - # a real tangent onto complex Hermitian is copied once, when converted to complex - pherm = ProjectTo(Hermitian(rand(ComplexF64, 10^3, 10^3))) + # a real tangent onto complex Hermitian is copied only once + pherm_c = ProjectTo(Hermitian(rand(ComplexF64, 10^3, 10^3))) @test 2 * sizeof(ComplexF64) * 10^3 > - @ballocated $pherm(dx) setup = (dx = Diagonal(rand(10^3))) + @ballocated $pherm_c(dx) setup = (dx = Diagonal(rand(10^3))) end @testset "#407" begin