|
85 | 85 | end
|
86 | 86 | end
|
87 | 87 |
|
| 88 | + @testset "eigendecomposition" begin |
| 89 | + @testset "eigen/eigen!" begin |
| 90 | + # NOTE: eigen!/eigen are not type-stable, so neither are their frule/rrule |
| 91 | + |
| 92 | + # avoid implementing to_vec(::Eigen) |
| 93 | + f(E::Eigen) = (values=E.values, vectors=E.vectors) |
| 94 | + |
| 95 | + # NOTE: for unstructured matrices, low enough n, and this specific seed, finite |
| 96 | + # differences of eigen seems to be stable enough for direct comparison. |
| 97 | + # This allows us to directly check differential of normalization/phase |
| 98 | + # convention |
| 99 | + n = 10 |
| 100 | + |
| 101 | + @testset "eigen!(::Matrix{$T}) frule" for T in (Float64,ComplexF64) |
| 102 | + X = randn(T, n, n) |
| 103 | + Ẋ = rand_tangent(X) |
| 104 | + F = eigen!(copy(X)) |
| 105 | + F_fwd, Ḟ_ad = frule((Zero(), copy(Ẋ)), eigen!, copy(X)) |
| 106 | + @test F_fwd == F |
| 107 | + @test Ḟ_ad isa Composite{typeof(F)} |
| 108 | + Ḟ_fd = jvp(_fdm, f ∘ eigen! ∘ copy, (X, Ẋ)) |
| 109 | + @test Ḟ_ad.values ≈ Ḟ_fd.values |
| 110 | + @test Ḟ_ad.vectors ≈ Ḟ_fd.vectors |
| 111 | + @test frule((Zero(), Zero()), eigen!, copy(X)) == (F, Zero()) |
| 112 | + |
| 113 | + @testset "tangents are real when outputs are" begin |
| 114 | + # hermitian matrices have real eigenvalues and, when real, real eigenvectors |
| 115 | + X = Matrix(Hermitian(randn(T, n, n))) |
| 116 | + Ẋ = Matrix(Hermitian(rand_tangent(X))) |
| 117 | + _, Ḟ = frule((Zero(), Ẋ), eigen!, X) |
| 118 | + @test eltype(Ḟ.values) <: Real |
| 119 | + T <: Real && @test eltype(Ḟ.vectors) <: Real |
| 120 | + end |
| 121 | + end |
| 122 | + |
| 123 | + @testset "eigen(::Matrix{$T}) rrule" for T in (Float64,ComplexF64) |
| 124 | + # NOTE: eigen is not type-stable, so neither are is its rrule |
| 125 | + X = randn(T, n, n) |
| 126 | + F = eigen(X) |
| 127 | + V̄ = rand_tangent(F.vectors) |
| 128 | + λ̄ = rand_tangent(F.values) |
| 129 | + CT = Composite{typeof(F)} |
| 130 | + F_rev, back = rrule(eigen, X) |
| 131 | + @test F_rev == F |
| 132 | + _, X̄_values_ad = @inferred back(CT(values = λ̄)) |
| 133 | + @test X̄_values_ad ≈ j′vp(_fdm, x -> eigen(x).values, λ̄, X)[1] |
| 134 | + _, X̄_vectors_ad = @inferred back(CT(vectors = V̄)) |
| 135 | + @test X̄_vectors_ad ≈ j′vp(_fdm, x -> eigen(x).vectors, V̄, X)[1] |
| 136 | + F̄ = CT(values = λ̄, vectors = V̄) |
| 137 | + s̄elf, X̄_ad = @inferred back(F̄) |
| 138 | + @test s̄elf === NO_FIELDS |
| 139 | + X̄_fd = j′vp(_fdm, f ∘ eigen, F̄, X)[1] |
| 140 | + @test X̄_ad ≈ X̄_fd |
| 141 | + @test @inferred(back(Zero())) === (NO_FIELDS, Zero()) |
| 142 | + F̄zero = CT(values = Zero(), vectors = Zero()) |
| 143 | + @test @inferred(back(F̄zero)) === (NO_FIELDS, Zero()) |
| 144 | + |
| 145 | + T <: Real && @testset "cotangent is real when input is" begin |
| 146 | + X = randn(T, n, n) |
| 147 | + Ẋ = rand_tangent(X) |
| 148 | + |
| 149 | + F = eigen(X) |
| 150 | + V̄ = rand_tangent(F.vectors) |
| 151 | + λ̄ = rand_tangent(F.values) |
| 152 | + F̄ = Composite{typeof(F)}(values = λ̄, vectors = V̄) |
| 153 | + X̄ = rrule(eigen, X)[2](F̄)[2] |
| 154 | + @test eltype(X̄) <: Real |
| 155 | + end |
| 156 | + end |
| 157 | + |
| 158 | + @testset "normalization/phase functions are idempotent" for T in (Float64,ComplexF64) |
| 159 | + # this is as much a math check as a code check. because normalization when |
| 160 | + # applied repeatedly is idempotent, repeated pushforward/pullback should |
| 161 | + # leave the (co)tangent unchanged |
| 162 | + X = randn(T, n, n) |
| 163 | + Ẋ = rand_tangent(X) |
| 164 | + F = eigen(X) |
| 165 | + |
| 166 | + V̇ = rand_tangent(F.vectors) |
| 167 | + V̇proj = ChainRules._eigen_norm_phase_fwd!(copy(V̇), X, F.vectors) |
| 168 | + @test !isapprox(V̇, V̇proj) |
| 169 | + V̇proj2 = ChainRules._eigen_norm_phase_fwd!(copy(V̇proj), X, F.vectors) |
| 170 | + @test V̇proj2 ≈ V̇proj |
| 171 | + |
| 172 | + V̄ = rand_tangent(F.vectors) |
| 173 | + V̄proj = ChainRules._eigen_norm_phase_rev!(copy(V̄), X, F.vectors) |
| 174 | + @test !isapprox(V̄, V̄proj) |
| 175 | + V̄proj2 = ChainRules._eigen_norm_phase_rev!(copy(V̄proj), X, F.vectors) |
| 176 | + @test V̄proj2 ≈ V̄proj |
| 177 | + end |
| 178 | + end |
| 179 | + |
| 180 | + @testset "eigvals/eigvals!" begin |
| 181 | + # NOTE: eigvals!/eigvals are not type-stable, so neither are their frule/rrule |
| 182 | + @testset "eigvals!(::Matrix{$T}) frule" for T in (Float64,ComplexF64) |
| 183 | + n = 10 |
| 184 | + X = randn(T, n, n) |
| 185 | + λ = eigvals!(copy(X)) |
| 186 | + Ẋ = rand_tangent(X) |
| 187 | + frule_test(eigvals!, (X, Ẋ)) |
| 188 | + @test frule((Zero(), Zero()), eigvals!, copy(X)) == (λ, Zero()) |
| 189 | + |
| 190 | + @testset "tangents are real when outputs are" begin |
| 191 | + # hermitian matrices have real eigenvalues |
| 192 | + X = Matrix(Hermitian(randn(T, n, n))) |
| 193 | + Ẋ = Matrix(Hermitian(rand_tangent(X))) |
| 194 | + _, λ̇ = frule((Zero(), Ẋ), eigvals!, X) |
| 195 | + @test eltype(λ̇) <: Real |
| 196 | + end |
| 197 | + end |
| 198 | + |
| 199 | + @testset "eigvals(::Matrix{$T}) rrule" for T in (Float64,ComplexF64) |
| 200 | + n = 10 |
| 201 | + X = randn(T, n, n) |
| 202 | + X̄ = rand_tangent(X) |
| 203 | + λ̄ = rand_tangent(eigvals(X)) |
| 204 | + rrule_test(eigvals, λ̄, (X, X̄)) |
| 205 | + back = rrule(eigvals, X)[2] |
| 206 | + @inferred back(λ̄) |
| 207 | + @test @inferred(back(Zero())) === (NO_FIELDS, Zero()) |
| 208 | + |
| 209 | + T <: Real && @testset "cotangent is real when input is" begin |
| 210 | + X = randn(T, n, n) |
| 211 | + λ = eigvals(X) |
| 212 | + λ̄ = rand_tangent(λ) |
| 213 | + X̄ = rrule(eigvals, X)[2](λ̄)[2] |
| 214 | + @test eltype(X̄) <: Real |
| 215 | + end |
| 216 | + end |
| 217 | + end |
| 218 | + end |
| 219 | + |
88 | 220 | # These tests are generally a bit tricky to write because FiniteDifferences doesn't
|
89 | 221 | # have fantastic support for this stuff at the minute.
|
90 | 222 | @testset "cholesky" begin
|
|
0 commit comments