Skip to content

Commit 9da39a2

Browse files
kshyattKatharine Hyatt
andauthored
Add eig_trunc_no_error and eigh_trunc_no_error (#117)
Co-authored-by: Katharine Hyatt <katharine.s.hyatt@gmail.com>
1 parent c591e2e commit 9da39a2

File tree

9 files changed

+211
-32
lines changed

9 files changed

+211
-32
lines changed

ext/MatrixAlgebraKitMooncakeExt/MatrixAlgebraKitMooncakeExt.jl

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,9 +163,9 @@ for (f!, f, f_full, pb, adj) in (
163163
end
164164
end
165165

166-
for (f, pb, adj) in (
167-
(:eig_trunc, :eig_trunc_pullback!, :eig_trunc_adjoint),
168-
(:eigh_trunc, :eigh_trunc_pullback!, :eigh_trunc_adjoint),
166+
for (f, f_ne, pb, adj) in (
167+
(:eig_trunc, :eig_trunc_no_error, :eig_trunc_pullback!, :eig_trunc_adjoint),
168+
(:eigh_trunc, :eigh_trunc_no_error, :eigh_trunc_pullback!, :eigh_trunc_adjoint),
169169
)
170170
@eval begin
171171
@is_primitive Mooncake.DefaultCtx Mooncake.ReverseMode Tuple{typeof($f), Any, MatrixAlgebraKit.AbstractAlgorithm}
@@ -192,6 +192,29 @@ for (f, pb, adj) in (
192192
end
193193
return output_codual, $adj
194194
end
195+
@is_primitive Mooncake.DefaultCtx Mooncake.ReverseMode Tuple{typeof($f_ne), Any, MatrixAlgebraKit.AbstractAlgorithm}
196+
function Mooncake.rrule!!(::CoDual{typeof($f_ne)}, A_dA::CoDual, alg_dalg::CoDual)
197+
# compute primal
198+
A, dA = arrayify(A_dA)
199+
alg = Mooncake.primal(alg_dalg)
200+
output = $f_ne(A, alg)
201+
# fdata call here is necessary to convert complicated Tangent type (e.g. of a Diagonal
202+
# of ComplexF32) into the correct **forwards** data type (since we are now in the forward
203+
# pass). For many types this is done automatically when the forward step returns, but
204+
# not for nested structs with various fields (like Diagonal{Complex})
205+
output_codual = CoDual(output, Mooncake.fdata(Mooncake.zero_tangent(output)))
206+
function $adj(::NoRData)
207+
Dtrunc, Vtrunc = Mooncake.primal(output_codual)
208+
dDtrunc_, dVtrunc_ = Mooncake.tangent(output_codual)
209+
D, dD = arrayify(Dtrunc, dDtrunc_)
210+
V, dV = arrayify(Vtrunc, dVtrunc_)
211+
$pb(dA, A, (D, V), (dD, dV))
212+
MatrixAlgebraKit.zero!(dD)
213+
MatrixAlgebraKit.zero!(dV)
214+
return NoRData(), NoRData(), NoRData()
215+
end
216+
return output_codual, $adj
217+
end
195218
end
196219
end
197220

src/MatrixAlgebraKit.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ export qr_compact, qr_full, qr_null, lq_compact, lq_full, lq_null
1818
export qr_compact!, qr_full!, qr_null!, lq_compact!, lq_full!, lq_null!
1919
export svd_compact, svd_full, svd_vals, svd_trunc, svd_trunc_no_error
2020
export svd_compact!, svd_full!, svd_vals!, svd_trunc!, svd_trunc_no_error!
21-
export eigh_full, eigh_vals, eigh_trunc
22-
export eigh_full!, eigh_vals!, eigh_trunc!
23-
export eig_full, eig_vals, eig_trunc
24-
export eig_full!, eig_vals!, eig_trunc!
21+
export eigh_full, eigh_vals, eigh_trunc, eigh_trunc_no_error
22+
export eigh_full!, eigh_vals!, eigh_trunc!, eigh_trunc_no_error!
23+
export eig_full, eig_vals, eig_trunc, eig_trunc_no_error
24+
export eig_full!, eig_vals!, eig_trunc!, eig_trunc_no_error!
2525
export gen_eig_full, gen_eig_vals
2626
export gen_eig_full!, gen_eig_vals!
2727
export schur_full, schur_vals

src/implementations/eig.jl

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ function copy_input(::typeof(eig_full), A::AbstractMatrix)
44
return copy!(similar(A, float(eltype(A))), A)
55
end
66
copy_input(::typeof(eig_vals), A) = copy_input(eig_full, A)
7-
copy_input(::typeof(eig_trunc), A) = copy_input(eig_full, A)
7+
copy_input(::Union{typeof(eig_trunc), typeof(eig_trunc_no_error)}, A) = copy_input(eig_full, A)
88

99
copy_input(::typeof(eig_full), A::Diagonal) = copy(A)
1010

@@ -65,7 +65,7 @@ function initialize_output(::typeof(eig_vals!), A::AbstractMatrix, ::AbstractAlg
6565
D = similar(A, Tc, n)
6666
return D
6767
end
68-
function initialize_output(::typeof(eig_trunc!), A, alg::TruncatedAlgorithm)
68+
function initialize_output(::Union{typeof(eig_trunc!), typeof(eig_trunc_no_error!)}, A, alg::TruncatedAlgorithm)
6969
return initialize_output(eig_full!, A, alg.alg)
7070
end
7171

@@ -121,6 +121,12 @@ function eig_trunc!(A, DV, alg::TruncatedAlgorithm)
121121
return DVtrunc..., truncation_error!(diagview(D), ind)
122122
end
123123

124+
function eig_trunc_no_error!(A, DV, alg::TruncatedAlgorithm)
125+
D, V = eig_full!(A, DV, alg.alg)
126+
DVtrunc, ind = truncate(eig_trunc!, (D, V), alg.trunc)
127+
return DVtrunc
128+
end
129+
124130
# Diagonal logic
125131
# --------------
126132
function eig_full!(A::Diagonal, (D, V)::Tuple{Diagonal, Diagonal}, alg::DiagonalAlgorithm)

src/implementations/eigh.jl

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ function copy_input(::typeof(eigh_full), A::AbstractMatrix)
44
return copy!(similar(A, float(eltype(A))), A)
55
end
66
copy_input(::typeof(eigh_vals), A) = copy_input(eigh_full, A)
7-
copy_input(::typeof(eigh_trunc), A) = copy_input(eigh_full, A)
7+
copy_input(::Union{typeof(eigh_trunc), typeof(eigh_trunc_no_error)}, A) = copy_input(eigh_full, A)
88

99
copy_input(::typeof(eigh_full), A::Diagonal) = copy(A)
1010

@@ -74,7 +74,7 @@ function initialize_output(::typeof(eigh_vals!), A::AbstractMatrix, ::AbstractAl
7474
D = similar(A, real(eltype(A)), n)
7575
return D
7676
end
77-
function initialize_output(::typeof(eigh_trunc!), A, alg::TruncatedAlgorithm)
77+
function initialize_output(::Union{typeof(eigh_trunc!), typeof(eigh_trunc_no_error!)}, A, alg::TruncatedAlgorithm)
7878
return initialize_output(eigh_full!, A, alg.alg)
7979
end
8080

@@ -135,6 +135,12 @@ function eigh_trunc!(A, DV, alg::TruncatedAlgorithm)
135135
return DVtrunc..., truncation_error!(diagview(D), ind)
136136
end
137137

138+
function eigh_trunc_no_error!(A, DV, alg::TruncatedAlgorithm)
139+
D, V = eigh_full!(A, DV, alg.alg)
140+
DVtrunc, ind = truncate(eigh_trunc!, (D, V), alg.trunc)
141+
return DVtrunc
142+
end
143+
138144
# Diagonal logic
139145
# --------------
140146
function eigh_full!(A::Diagonal, DV, alg::DiagonalAlgorithm)

src/interface/eig.jl

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ and the diagonal matrix `D` contains the associated eigenvalues.
2727
!!! note
2828
$(docs_eig_note)
2929
30-
See also [`eig_vals(!)`](@ref eig_vals) and [`eig_trunc(!)`](@ref eig_trunc).
30+
See also [`eig_vals(!)`](@ref eig_vals), [`eig_trunc_no_error`](@ref eig_trunc_no_error)
31+
and [`eig_trunc(!)`](@ref eig_trunc).
3132
"""
3233
@functiondef eig_full
3334

@@ -79,11 +80,63 @@ truncation strategy is already embedded in the algorithm.
7980
!!! note
8081
$docs_eig_note
8182
82-
See also [`eig_full(!)`](@ref eig_full), [`eig_vals(!)`](@ref eig_vals), and
83-
[Truncations](@ref) for more information on truncation strategies.
83+
See also [`eig_full(!)`](@ref eig_full), [`eig_vals(!)`](@ref eig_vals),
84+
[`eig_trunc_no_error!`](@ref eig_trunc_no_error) and [Truncations](@ref)
85+
for more information on truncation strategies.
8486
"""
8587
@functiondef eig_trunc
8688

89+
"""
90+
eig_trunc_no_error(A; [trunc], kwargs...) -> D, V
91+
eig_trunc_no_error(A, alg::AbstractAlgorithm) -> D, V
92+
eig_trunc_no_error!(A, [DV]; [trunc], kwargs...) -> D, V
93+
eig_trunc_no_error!(A, [DV], alg::AbstractAlgorithm) -> D, V
94+
95+
Compute a partial or truncated eigenvalue decomposition of the matrix `A`,
96+
such that `A * V ≈ V * D`, where the (possibly rectangular) matrix `V` contains
97+
a subset of eigenvectors and the diagonal matrix `D` contains the associated eigenvalues,
98+
selected according to a truncation strategy. The truncation error is *not* returned.
99+
100+
## Truncation
101+
The truncation strategy can be controlled via the `trunc` keyword argument. This can be
102+
either a `NamedTuple` or a [`TruncationStrategy`](@ref). If `trunc` is not provided or
103+
nothing, all values will be kept.
104+
105+
### `trunc::NamedTuple`
106+
The supported truncation keyword arguments are:
107+
108+
$docs_truncation_kwargs
109+
110+
### `trunc::TruncationStrategy`
111+
For more control, a truncation strategy can be supplied directly.
112+
By default, MatrixAlgebraKit supplies the following:
113+
114+
$docs_truncation_strategies
115+
116+
## Keyword Arguments
117+
Other keyword arguments are passed to the algorithm selection procedure. If no explicit
118+
`alg` is provided, these keywords are used to select and configure the algorithm through
119+
[`MatrixAlgebraKit.select_algorithm`](@ref). The remaining keywords after algorithm
120+
selection are passed to the algorithm constructor. See [`MatrixAlgebraKit.default_algorithm`](@ref)
121+
for the default algorithm selection behavior.
122+
123+
When `alg` is a [`TruncatedAlgorithm`](@ref), the `trunc` keyword cannot be specified as the
124+
truncation strategy is already embedded in the algorithm.
125+
126+
!!! note
127+
The bang method `eig_trunc!` optionally accepts the output structure and
128+
possibly destroys the input matrix `A`. Always use the return value of the function
129+
as it may not always be possible to use the provided `DV` as output.
130+
131+
!!! note
132+
$docs_eig_note
133+
134+
See also [`eig_full(!)`](@ref eig_full), [`eig_vals(!)`](@ref eig_vals),
135+
[`eig_trunc(!)`](@ref eig_trunc) and [Truncations](@ref) for more
136+
information on truncation strategies.
137+
"""
138+
@functiondef eig_trunc_no_error
139+
87140
"""
88141
eig_vals(A; kwargs...) -> D
89142
eig_vals(A, alg::AbstractAlgorithm) -> D
@@ -121,13 +174,15 @@ for f in (:eig_full!, :eig_vals!)
121174
end
122175
end
123176

124-
function select_algorithm(::typeof(eig_trunc!), A, alg; trunc = nothing, kwargs...)
125-
if alg isa TruncatedAlgorithm
126-
isnothing(trunc) ||
127-
throw(ArgumentError("`trunc` can't be specified when `alg` is a `TruncatedAlgorithm`"))
128-
return alg
129-
else
130-
alg_eig = select_algorithm(eig_full!, A, alg; kwargs...)
131-
return TruncatedAlgorithm(alg_eig, select_truncation(trunc))
177+
for f in (:eig_trunc!, :eig_trunc_no_error!)
178+
@eval function select_algorithm(::typeof($f), A, alg; trunc = nothing, kwargs...)
179+
if alg isa TruncatedAlgorithm
180+
isnothing(trunc) ||
181+
throw(ArgumentError("`trunc` can't be specified when `alg` is a `TruncatedAlgorithm`"))
182+
return alg
183+
else
184+
alg_eig = select_algorithm(eig_full!, A, alg; kwargs...)
185+
return TruncatedAlgorithm(alg_eig, select_truncation(trunc))
186+
end
132187
end
133188
end

src/interface/eigh.jl

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,63 @@ truncation strategy is already embedded in the algorithm.
8484
!!! note
8585
$(docs_eigh_note)
8686
87-
See also [`eigh_full(!)`](@ref eigh_full), [`eigh_vals(!)`](@ref eigh_vals), and
88-
[Truncations](@ref) for more information on truncation strategies.
87+
See also [`eigh_full(!)`](@ref eigh_full), [`eigh_vals(!)`](@ref eigh_vals),
88+
[`eigh_trunc_no_error(!)`](@ref eigh_trunc_no_error) and [Truncations](@ref)
89+
for more information on truncation strategies.
8990
"""
9091
@functiondef eigh_trunc
9192

93+
"""
94+
eigh_trunc_no_error(A; [trunc], kwargs...) -> D, V
95+
eigh_trunc_no_error(A, alg::AbstractAlgorithm) -> D, V
96+
eigh_trunc_no_error!(A, [DV]; [trunc], kwargs...) -> D, V
97+
eigh_trunc_no_error!(A, [DV], alg::AbstractAlgorithm) -> D, V
98+
99+
Compute a partial or truncated eigenvalue decomposition of the symmetric or hermitian matrix
100+
`A`, such that `A * V ≈ V * D`, where the isometric matrix `V` contains a subset of the
101+
orthogonal eigenvectors and the real diagonal matrix `D` contains the associated eigenvalues,
102+
selected according to a truncation strategy. The function does *not* returns the truncation error.
103+
104+
## Truncation
105+
The truncation strategy can be controlled via the `trunc` keyword argument. This can be
106+
either a `NamedTuple` or a [`TruncationStrategy`](@ref). If `trunc` is not provided or
107+
nothing, all values will be kept.
108+
109+
### `trunc::NamedTuple`
110+
The supported truncation keyword arguments are:
111+
112+
$docs_truncation_kwargs
113+
114+
### `trunc::TruncationStrategy`
115+
For more control, a truncation strategy can be supplied directly.
116+
By default, MatrixAlgebraKit supplies the following:
117+
118+
$docs_truncation_strategies
119+
120+
## Keyword arguments
121+
Other keyword arguments are passed to the algorithm selection procedure. If no explicit
122+
`alg` is provided, these keywords are used to select and configure the algorithm through
123+
[`MatrixAlgebraKit.select_algorithm`](@ref). The remaining keywords after algorithm
124+
selection are passed to the algorithm constructor. See [`MatrixAlgebraKit.default_algorithm`](@ref)
125+
for the default algorithm selection behavior.
126+
127+
When `alg` is a [`TruncatedAlgorithm`](@ref), the `trunc` keyword cannot be specified as the
128+
truncation strategy is already embedded in the algorithm.
129+
130+
!!! note
131+
The bang method `eigh_trunc!` optionally accepts the output structure and
132+
possibly destroys the input matrix `A`. Always use the return value of the function
133+
as it may not always be possible to use the provided `DV` as output.
134+
135+
!!! note
136+
$(docs_eigh_note)
137+
138+
See also [`eigh_full(!)`](@ref eigh_full), [`eigh_vals(!)`](@ref eigh_vals),
139+
[`eigh_trunc(!)`](@ref eig_trunc), and [Truncations](@ref) for more information
140+
on truncation strategies.
141+
"""
142+
@functiondef eigh_trunc_no_error
143+
92144
"""
93145
eigh_vals(A; kwargs...) -> D
94146
eigh_vals(A, alg::AbstractAlgorithm) -> D
@@ -128,13 +180,15 @@ for f in (:eigh_full!, :eigh_vals!)
128180
end
129181
end
130182

131-
function select_algorithm(::typeof(eigh_trunc!), A, alg; trunc = nothing, kwargs...)
132-
if alg isa TruncatedAlgorithm
133-
isnothing(trunc) ||
134-
throw(ArgumentError("`trunc` can't be specified when `alg` is a `TruncatedAlgorithm`"))
135-
return alg
136-
else
137-
alg_eig = select_algorithm(eigh_full!, A, alg; kwargs...)
138-
return TruncatedAlgorithm(alg_eig, select_truncation(trunc))
183+
for f in (:eigh_trunc!, :eigh_trunc_no_error!)
184+
@eval function select_algorithm(::typeof($f), A, alg; trunc = nothing, kwargs...)
185+
if alg isa TruncatedAlgorithm
186+
isnothing(trunc) ||
187+
throw(ArgumentError("`trunc` can't be specified when `alg` is a `TruncatedAlgorithm`"))
188+
return alg
189+
else
190+
alg_eig = select_algorithm(eigh_full!, A, alg; kwargs...)
191+
return TruncatedAlgorithm(alg_eig, select_truncation(trunc))
192+
end
139193
end
140194
end

test/eig.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ end
6464
@test A * V3 V3 * D3
6565
@test ϵ3 norm(view(D₀, (r + 1):m)) atol = atol
6666

67+
s = 1 - sqrt(eps(real(T)))
68+
trunc = truncerror(; atol = s * norm(@view(D₀[r:end]), 1), p = 1)
69+
D4, V4 = @constinferred eig_trunc_no_error(A; alg, trunc)
70+
@test length(diagview(D4)) == r
71+
@test A * V4 V4 * D4
6772
# trunctol keeps order, truncrank might not
6873
# test for same subspace
6974
@test V1 * ((V1' * V1) \ (V1' * V2)) V2
@@ -90,6 +95,10 @@ end
9095
D3, V3, ϵ3 = @constinferred eig_trunc(A; alg)
9196
@test diagview(D3) diagview(D)[1:2]
9297
@test ϵ3 norm(diagview(D)[3:4]) atol = atol
98+
99+
alg = TruncatedAlgorithm(LAPACK_Simple(), truncerror(; atol = 0.2, p = 1))
100+
D4, V4 = @constinferred eig_trunc_no_error(A; alg)
101+
@test diagview(D4) diagview(D)[1:2]
93102
end
94103

95104
@testset "eig for Diagonal{$T}" for T in (BLASFloats..., GenericFloats...)
@@ -113,4 +122,9 @@ end
113122
D2, V2, ϵ2 = @constinferred eig_trunc(A2; alg)
114123
@test diagview(D2) diagview(A2)[1:2]
115124
@test ϵ2 norm(diagview(A2)[3:4]) atol = atol
125+
126+
A3 = Diagonal(T[0.9, 0.3, 0.1, 0.01])
127+
alg = TruncatedAlgorithm(DiagonalAlgorithm(), truncrank(2))
128+
D3, V3 = @constinferred eig_trunc_no_error(A3; alg)
129+
@test diagview(D3) diagview(A3)[1:2]
116130
end

test/eigh.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ end
7373
@test A * V3 V3 * D3
7474
@test ϵ3 norm(view(D₀, (r + 1):m)) atol = atol
7575

76+
s = 1 - sqrt(eps(real(T)))
77+
trunc = truncerror(; atol = s * norm(@view(D₀[r:end]), 1), p = 1)
78+
D4, V4 = @constinferred eigh_trunc_no_error(A; alg, trunc)
79+
@test length(diagview(D4)) == r
80+
@test A * V4 V4 * D4
81+
7682
# test for same subspace
7783
@test V1 * (V1' * V2) V2
7884
@test V2 * (V2' * V1) V1
@@ -99,6 +105,10 @@ end
99105
D3, V3, ϵ3 = @constinferred eigh_trunc(A; alg)
100106
@test diagview(D3) diagview(D)[1:2]
101107
@test ϵ3 norm(diagview(D)[3:4]) atol = atol
108+
109+
alg = TruncatedAlgorithm(LAPACK_QRIteration(), truncerror(; atol = 0.2))
110+
D4, V4 = @constinferred eigh_trunc_no_error(A; alg)
111+
@test diagview(D4) diagview(D)[1:2]
102112
end
103113

104114
@testset "eigh for Diagonal{$T}" for T in (BLASFloats..., GenericFloats...)
@@ -123,4 +133,9 @@ end
123133
D2, V2, ϵ2 = @constinferred eigh_trunc(A2; alg)
124134
@test diagview(D2) diagview(A2)[1:2]
125135
@test ϵ2 norm(diagview(A2)[3:4]) atol = atol
136+
137+
A3 = Diagonal(T[0.9, 0.3, 0.1, 0.01])
138+
alg = TruncatedAlgorithm(DiagonalAlgorithm(), truncrank(3))
139+
D3, V3 = @constinferred eigh_trunc_no_error(A3; alg)
140+
@test diagview(D3) diagview(A3)[1:3]
126141
end

test/mooncake.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ end
276276
dDVtrunc = Mooncake.build_tangent(typeof((ΔDtrunc, ΔVtrunc, zero(real(T)))), dDtrunc, dVtrunc, zero(real(T)))
277277
Mooncake.TestUtils.test_rule(rng, eig_trunc, A, truncalg; mode = Mooncake.ReverseMode, output_tangent = dDVtrunc, atol = atol, rtol = rtol, is_primitive = false)
278278
test_pullbacks_match(rng, eig_trunc!, eig_trunc, A, (D, V), (ΔD2, ΔV), truncalg; rdata = (Mooncake.NoRData(), Mooncake.NoRData(), zero(real(T))))
279+
dDVtrunc = Mooncake.build_tangent(typeof((ΔDtrunc, ΔVtrunc)), dDtrunc, dVtrunc)
280+
Mooncake.TestUtils.test_rule(rng, eig_trunc_no_error, A, truncalg; mode = Mooncake.ReverseMode, output_tangent = dDVtrunc, atol = atol, rtol = rtol, is_primitive = false)
281+
test_pullbacks_match(rng, eig_trunc_no_error!, eig_trunc_no_error, A, (D, V), (ΔD2, ΔV), truncalg)
279282
end
280283
truncalg = TruncatedAlgorithm(alg, truncrank(5; by = real))
281284
ind = MatrixAlgebraKit.findtruncated(Ddiag, truncalg.trunc)
@@ -288,6 +291,9 @@ end
288291
dDVtrunc = Mooncake.build_tangent(typeof((ΔDtrunc, ΔVtrunc, zero(real(T)))), dDtrunc, dVtrunc, zero(real(T)))
289292
Mooncake.TestUtils.test_rule(rng, eig_trunc, A, truncalg; mode = Mooncake.ReverseMode, output_tangent = dDVtrunc, atol = atol, rtol = rtol, is_primitive = false)
290293
test_pullbacks_match(rng, eig_trunc!, eig_trunc, A, (D, V), (ΔD2, ΔV), truncalg; rdata = (Mooncake.NoRData(), Mooncake.NoRData(), zero(real(T))))
294+
dDVtrunc = Mooncake.build_tangent(typeof((ΔDtrunc, ΔVtrunc)), dDtrunc, dVtrunc)
295+
Mooncake.TestUtils.test_rule(rng, eig_trunc_no_error, A, truncalg; mode = Mooncake.ReverseMode, output_tangent = dDVtrunc, atol = atol, rtol = rtol, is_primitive = false)
296+
test_pullbacks_match(rng, eig_trunc_no_error!, eig_trunc_no_error, A, (D, V), (ΔD2, ΔV), truncalg)
291297
end
292298
end
293299
end

0 commit comments

Comments
 (0)