Skip to content

Commit 15d1c5a

Browse files
authored
Make gaugefix! optional (#95)
* move gaugefix code to gaugefix section * add gaugefix keyword argument for enabling/disabling `svd` gauge fixing * add gaugefix keyword argument for enabling/disabling `eig` gauge fixing * add gaugefix kewyord argument for enabling/disabling `eigh` gauge fixing * add gaugefix keyword argument for enabling/disabling `gen_eig` gauge fixing * update docstrings * centralize gaugefix function * remove unnecessary size arguments * bring eig gaugefix in the same shape * move include order to ensure things are defined * update error messages in tests * update docs * add gaugefix keyword to GenericLinearAlgebra extension * rename `dogaugefix` to `do_gauge_fix` * add global gaugefix toggle * implement review comments * more careful about gaugefixing svd_trunc * rename `lapack_kwargs` to `alg_kwargs`
1 parent a1ffedc commit 15d1c5a

File tree

13 files changed

+357
-165
lines changed

13 files changed

+357
-165
lines changed

docs/src/user_interface/decompositions.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ eigh_trunc
7272
eigh_vals
7373
```
7474

75+
!!! note "Gauge Degrees of Freedom"
76+
The eigenvectors returned by these functions have residual phase degrees of freedom.
77+
By default, MatrixAlgebraKit applies a gauge fixing convention to ensure reproducible results.
78+
See [Gauge choices](@ref sec_gaugefix) for more details.
79+
7580
Alongside these functions, we provide a LAPACK-based implementation for dense arrays, as provided by the following algorithms:
7681

7782
```@autodocs; canonical=false
@@ -89,6 +94,11 @@ eig_trunc
8994
eig_vals
9095
```
9196

97+
!!! note "Gauge Degrees of Freedom"
98+
The eigenvectors returned by these functions have residual phase degrees of freedom.
99+
By default, MatrixAlgebraKit applies a gauge fixing convention to ensure reproducible results.
100+
See [Gauge choices](@ref sec_gaugefix) for more details.
101+
92102
Alongside these functions, we provide a LAPACK-based implementation for dense arrays, as provided by the following algorithms:
93103

94104
```@autodocs; canonical=false
@@ -137,6 +147,11 @@ svd_vals
137147
svd_trunc
138148
```
139149

150+
!!! note "Gauge Degrees of Freedom"
151+
The singular vectors returned by these functions have residual phase degrees of freedom.
152+
By default, MatrixAlgebraKit applies a gauge fixing convention to ensure reproducible results.
153+
See [Gauge choices](@ref sec_gaugefix) for more details.
154+
140155
MatrixAlgebraKit again ships with LAPACK-based implementations for dense arrays:
141156

142157
```@autodocs; canonical=false
@@ -371,3 +386,48 @@ norm(A * N1') < 1e-14 && norm(A * N2') < 1e-14 &&
371386
# output
372387
true
373388
```
389+
390+
## [Gauge choices](@id sec_gaugefix)
391+
392+
Both eigenvalue and singular value decompositions have residual gauge degrees of freedom even when the eigenvalues or singular values are unique.
393+
These arise from the fact that even after normalization, the eigenvectors and singular vectors are only determined up to a phase factor.
394+
395+
### Phase Ambiguity in Decompositions
396+
397+
For the eigenvalue decomposition `A * V = V * D`, if `v` is an eigenvector with eigenvalue `λ` and `|v| = 1`, then so is `e^(iθ) * v` for any real phase `θ`.
398+
When `λ` is non-degenerate (i.e., has multiplicity 1), the eigenvector is unique up to this phase.
399+
400+
Similarly, for the singular value decomposition `A = U * Σ * Vᴴ`, the singular vectors `u` and `v` corresponding to a non-degenerate singular value `σ` are unique only up to a common phase.
401+
We can replace `u → e^(iθ) * u` and `vᴴ → e^(-iθ) * vᴴ` simultaneously.
402+
403+
### Gauge Fixing Convention
404+
405+
To remove this phase ambiguity and ensure reproducible results, MatrixAlgebraKit implements a gauge fixing convention by default.
406+
The convention ensures that **the entry with the largest magnitude in each eigenvector or left singular vector is real and positive**.
407+
408+
For eigenvectors, this means that for each column `v` of `V`, we multiply by `conj(sign(v[i]))` where `i` is the index of the entry with largest absolute value.
409+
410+
For singular vectors, we apply the phase factor to both `u` and `v` based on the entry with largest magnitude in `u`.
411+
This preserves the decomposition `A = U * Σ * Vᴴ` while fixing the gauge.
412+
413+
### Disabling Gauge Fixing
414+
415+
Gauge fixing is enabled by default for all eigenvalue and singular value decompositions.
416+
If you prefer to obtain the raw results from the underlying computational routines without gauge fixing, you can disable it using the `gaugefix` keyword argument:
417+
418+
```julia
419+
# With gauge fixing (default)
420+
D, V = eigh_full(A)
421+
422+
# Without gauge fixing
423+
D, V = eigh_full(A; gaugefix = false)
424+
```
425+
426+
The same keyword is available for `eig_full`, `eig_trunc`, `svd_full`, `svd_compact`, and `svd_trunc` functions.
427+
Additionally, the default value can also be controlled with a global toggle using [`MatrixAlgebraKit.default_gaugefix`](@ref).
428+
429+
```@docs; canonical=false
430+
MatrixAlgebraKit.gaugefix!
431+
MatrixAlgebraKit.default_gaugefix
432+
```
433+

ext/MatrixAlgebraKitGenericLinearAlgebraExt.jl

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module MatrixAlgebraKitGenericLinearAlgebraExt
22

33
using MatrixAlgebraKit
4-
using MatrixAlgebraKit: sign_safe, check_input, diagview
4+
using MatrixAlgebraKit: sign_safe, check_input, diagview, gaugefix!, default_gaugefix
55
using GenericLinearAlgebra: svd!, svdvals!, eigen!, eigvals!, Hermitian, qr!
66
using LinearAlgebra: I, Diagonal, lmul!
77

@@ -13,18 +13,26 @@ for f! in (:svd_compact!, :svd_full!, :svd_vals!)
1313
@eval MatrixAlgebraKit.initialize_output(::typeof($f!), A::AbstractMatrix, ::GLA_QRIteration) = nothing
1414
end
1515

16-
function MatrixAlgebraKit.svd_compact!(A::AbstractMatrix, USVᴴ, ::GLA_QRIteration)
16+
function MatrixAlgebraKit.svd_compact!(A::AbstractMatrix, USVᴴ, alg::GLA_QRIteration)
1717
F = svd!(A)
1818
U, S, Vᴴ = F.U, Diagonal(F.S), F.Vt
19-
return MatrixAlgebraKit.gaugefix!(svd_compact!, U, S, Vᴴ, size(A)...)
19+
20+
do_gauge_fix = get(alg.kwargs, :gaugefix, default_gaugefix())::Bool
21+
do_gauge_fix && gaugefix!(svd_compact!, U, Vᴴ)
22+
23+
return U, S, Vᴴ
2024
end
2125

22-
function MatrixAlgebraKit.svd_full!(A::AbstractMatrix, USVᴴ, ::GLA_QRIteration)
26+
function MatrixAlgebraKit.svd_full!(A::AbstractMatrix, USVᴴ, alg::GLA_QRIteration)
2327
F = svd!(A; full = true)
2428
U, Vᴴ = F.U, F.Vt
2529
S = MatrixAlgebraKit.zero!(similar(F.S, (size(U, 2), size(Vᴴ, 1))))
2630
diagview(S) .= F.S
27-
return MatrixAlgebraKit.gaugefix!(svd_full!, U, S, Vᴴ, size(A)...)
31+
32+
do_gauge_fix = get(alg.kwargs, :gaugefix, default_gaugefix())::Bool
33+
do_gauge_fix && gaugefix!(svd_full!, U, Vᴴ)
34+
35+
return U, S, Vᴴ
2836
end
2937

3038
function MatrixAlgebraKit.svd_vals!(A::AbstractMatrix, S, ::GLA_QRIteration)

src/MatrixAlgebraKit.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ include("common/safemethods.jl")
7676
include("common/view.jl")
7777
include("common/regularinv.jl")
7878
include("common/matrixproperties.jl")
79-
include("common/gauge.jl")
8079

8180
include("yalapack.jl")
8281
include("algorithms.jl")
@@ -93,6 +92,8 @@ include("interface/schur.jl")
9392
include("interface/polar.jl")
9493
include("interface/orthnull.jl")
9594

95+
include("common/gauge.jl") # needs to be defined after the functions are
96+
9697
include("implementations/projections.jl")
9798
include("implementations/truncation.jl")
9899
include("implementations/qr.jl")

src/common/defaults.jl

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,20 @@ default_pullback_rank_atol(A) = eps(norm(A, Inf))^(3 / 4)
4141
Default tolerance for deciding to warn if the provided `A` is not hermitian.
4242
"""
4343
default_hermitian_tol(A) = eps(norm(A, Inf))^(3 / 4)
44+
45+
46+
const DEFAULT_GAUGEFIX = Ref(true)
47+
48+
@doc """
49+
default_gaugefix() -> current_value
50+
default_gaugefix(new_value::Bool) -> previous_value
51+
52+
Global toggle for enabling or disabling the default behavior of gauge fixing the output of the eigen- and singular value decompositions.
53+
""" default_gaugefix
54+
55+
default_gaugefix() = DEFAULT_GAUGEFIX[]
56+
function default_gaugefix(new_value::Bool)
57+
previous_value = DEFAULT_GAUGEFIX[]
58+
DEFAULT_GAUGEFIX[] = new_value
59+
return previous_value
60+
end

src/common/gauge.jl

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,53 @@
1-
function gaugefix!(V::AbstractMatrix)
1+
"""
2+
gaugefix!(f_eig, V)
3+
gaugefix!(f_svd, U, Vᴴ)
4+
5+
Fix the residual gauge degrees of freedom in the eigen or singular vectors, that are
6+
obtained from the decomposition performed by `f_eig` or `f_svd`.
7+
This is achieved by ensuring that the entry with the largest magnitude in `V` or `U`
8+
is real and positive.
9+
""" gaugefix!
10+
11+
12+
function gaugefix!(::Union{typeof(eig_full!), typeof(eigh_full!), typeof(gen_eig_full!)}, V::AbstractMatrix)
213
for j in axes(V, 2)
314
v = view(V, :, j)
4-
s = conj(sign(_argmaxabs(v)))
5-
@inbounds v .*= s
15+
s = sign(_argmaxabs(v))
16+
@inbounds v .*= conj(s)
617
end
718
return V
819
end
20+
21+
function gaugefix!(::typeof(svd_full!), U, Vᴴ)
22+
m, n = size(U, 2), size(Vᴴ, 1)
23+
for j in 1:max(m, n)
24+
if j <= min(m, n)
25+
u = view(U, :, j)
26+
v = view(Vᴴ, j, :)
27+
s = sign(_argmaxabs(u))
28+
u .*= conj(s)
29+
v .*= s
30+
elseif j <= m
31+
u = view(U, :, j)
32+
s = sign(_argmaxabs(u))
33+
u .*= conj(s)
34+
else
35+
v = view(Vᴴ, j, :)
36+
s = sign(_argmaxabs(v))
37+
v .*= conj(s)
38+
end
39+
end
40+
return (U, Vᴴ)
41+
end
42+
43+
function gaugefix!(::Union{typeof(svd_compact!), typeof(svd_trunc!)}, U, Vᴴ)
44+
@assert axes(U, 2) == axes(Vᴴ, 1)
45+
for j in axes(U, 2)
46+
u = view(U, :, j)
47+
v = view(Vᴴ, j, :)
48+
s = sign(_argmaxabs(u))
49+
u .*= conj(s)
50+
v .*= s
51+
end
52+
return (U, Vᴴ)
53+
end

src/implementations/eig.jl

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,28 +81,37 @@ end
8181
function eig_full!(A::AbstractMatrix, DV, alg::LAPACK_EigAlgorithm)
8282
check_input(eig_full!, A, DV, alg)
8383
D, V = DV
84+
85+
do_gauge_fix = get(alg.kwargs, :gaugefix, default_gaugefix())::Bool
86+
alg_kwargs = Base.structdiff(alg.kwargs, NamedTuple{(:gaugefix,)})
87+
8488
if alg isa LAPACK_Simple
85-
isempty(alg.kwargs) ||
86-
throw(ArgumentError("LAPACK_Simple (geev) does not accept any keyword arguments"))
89+
isempty(alg_kwargs) ||
90+
throw(ArgumentError("invalid keyword arguments for LAPACK_Simple"))
8791
YALAPACK.geev!(A, D.diag, V)
8892
else # alg isa LAPACK_Expert
89-
YALAPACK.geevx!(A, D.diag, V; alg.kwargs...)
93+
YALAPACK.geevx!(A, D.diag, V; alg_kwargs...)
9094
end
91-
# TODO: make this controllable using a `gaugefix` keyword argument
92-
V = gaugefix!(V)
95+
96+
do_gauge_fix && (V = gaugefix!(eig_full!, V))
97+
9398
return D, V
9499
end
95100

96101
function eig_vals!(A::AbstractMatrix, D, alg::LAPACK_EigAlgorithm)
97102
check_input(eig_vals!, A, D, alg)
98103
V = similar(A, complex(eltype(A)), (size(A, 1), 0))
104+
105+
alg_kwargs = Base.structdiff(alg.kwargs, NamedTuple{(:gaugefix,)})
106+
99107
if alg isa LAPACK_Simple
100-
isempty(alg.kwargs) ||
101-
throw(ArgumentError("LAPACK_Simple (geev) does not accept any keyword arguments"))
108+
isempty(alg_kwargs) ||
109+
throw(ArgumentError("invalid keyword arguments for LAPACK_Simple"))
102110
YALAPACK.geev!(A, D, V)
103111
else # alg isa LAPACK_Expert
104-
YALAPACK.geevx!(A, D, V; alg.kwargs...)
112+
YALAPACK.geevx!(A, D, V; alg_kwargs...)
105113
end
114+
106115
return D
107116
end
108117

@@ -135,23 +144,30 @@ _gpu_geev!(A, D, V) = throw(MethodError(_gpu_geev!, (A, D, V)))
135144
function eig_full!(A::AbstractMatrix, DV, alg::GPU_EigAlgorithm)
136145
check_input(eig_full!, A, DV, alg)
137146
D, V = DV
147+
148+
do_gauge_fix = get(alg.kwargs, :gaugefix, default_gaugefix())::Bool
149+
alg_kwargs = Base.structdiff(alg.kwargs, NamedTuple{(:gaugefix,)})
150+
138151
if alg isa GPU_Simple
139-
isempty(alg.kwargs) ||
140-
@warn "GPU_Simple (geev) does not accept any keyword arguments"
152+
isempty(alg_kwargs) || @warn "invalid keyword arguments for GPU_Simple"
141153
_gpu_geev!(A, D.diag, V)
142154
end
143-
# TODO: make this controllable using a `gaugefix` keyword argument
144-
V = gaugefix!(V)
155+
156+
do_gauge_fix && (V = gaugefix!(eig_full!, V))
157+
145158
return D, V
146159
end
147160

148161
function eig_vals!(A::AbstractMatrix, D, alg::GPU_EigAlgorithm)
149162
check_input(eig_vals!, A, D, alg)
150163
V = similar(A, complex(eltype(A)), (size(A, 1), 0))
164+
165+
alg_kwargs = Base.structdiff(alg.kwargs, NamedTuple{(:gaugefix,)})
166+
151167
if alg isa GPU_Simple
152-
isempty(alg.kwargs) ||
153-
@warn "GPU_Simple (geev) does not accept any keyword arguments"
168+
isempty(alg_kwargs) || @warn "invalid keyword arguments for GPU_Simple"
154169
_gpu_geev!(A, D, V)
155170
end
171+
156172
return D
157173
end

0 commit comments

Comments
 (0)