Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ written.
`GeometricIntegratorsBase` do not join `[deps]`: an integrator is the caller's, and the test
dependencies supply one.

- **`get_DEIM_indices(Ψ)` is exported.** It returns the `k` rows that the greedy DEIM algorithm
selects on the basis `Ψ`, and is the index form of `get_DEIM_interpolation_matrix`:
`Π' * A == A[p, :]` for `p = get_DEIM_indices(Ψ)`, so a caller samples the rows without forming
the `N × k` matrix `Π`. It was internal under another name; its selection and its
`ArgumentError`s are unchanged.

## [0.2.0] — 2026-10-12

### New Features
Expand Down
2 changes: 1 addition & 1 deletion src/ReducedBasisMethods.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export lift, projection_error, reduction_error

include("algorithms/deim.jl")

export get_DEIM_interpolation_matrix
export get_DEIM_interpolation_matrix, get_DEIM_indices

include("algorithms/symplectic_deim.jl")

Expand Down
20 changes: 12 additions & 8 deletions src/algorithms/deim.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"""
_deim_indices(Ψ)
get_DEIM_indices(Ψ)

The rows selected by the greedy DEIM algorithm (Algorithm 1 of
[doi:10.1137/090766498](https://doi.org/10.1137/090766498)) on the basis `Ψ`, whose `N × k`
columns are the basis vectors.
Returns the `k` rows that the greedy DEIM algorithm (Algorithm 1 of
[doi:10.1137/090766498](https://doi.org/10.1137/090766498)) selects on the basis `Ψ`, whose
`N × k` columns are the basis vectors.

This is the index form of [`get_DEIM_interpolation_matrix`](@ref): for
`p = get_DEIM_indices(Ψ)` and `Π = get_DEIM_interpolation_matrix(Ψ)`, `Π' * A == A[p, :]` for
every finite `N`-row matrix `A`, so `A[p, :]` evaluates `Π' * A` without forming `Π`.

The index of column `i` is the row of the largest magnitude of the residual
`r = Ψ[:, i] - Ψ[:, 1:i-1] * c`, where `c` solves the interpolation problem of the columns
Expand All @@ -15,7 +19,7 @@ is not both positive and finite — `ρ == 0` for a zero column or one exactly d
ones before it, and `NaN` or `Inf` for a `NaN` or an `Inf` in `Ψ`. Every entry of a column
enters that column's residual, so the test of each column covers the whole matrix.
"""
function _deim_indices(Ψ::AbstractMatrix)
function get_DEIM_indices(Ψ::AbstractMatrix)
N, k = size(Ψ)
k ≤ N || throw(ArgumentError(
"the DEIM basis has $k columns but only $N rows, so k ≤ N is required"))
Expand Down Expand Up @@ -57,15 +61,15 @@ Calculates the DEIM interpolation matrix as described in Algorithm 1 in

The input matrix Ψ holds the eigenvectors of the snapshot matrix fᵢ(xⱼ), 1 ≤ i ≤ N, 1 ≤ j ≤ k.
Returns the `N × k` matrix `Π` whose column `i` is the unit vector of the row that
[`_deim_indices`](@ref) selects for that column, so that `f(x) ≈ Ψ * inv(Π' * Ψ) * Π' * f(x)`.
[`get_DEIM_indices`](@ref) selects for that column, so that `f(x) ≈ Ψ * inv(Π' * Ψ) * Π' * f(x)`.

Its element type is `float(eltype(Ψ))`, so a `Float32` basis gives a `Float32` interpolation
matrix. The selection, and with it the `ArgumentError` for `k > N` and for a column whose
largest residual magnitude is not finite and positive, is [`_deim_indices`](@ref)'s.
largest residual magnitude is not finite and positive, is [`get_DEIM_indices`](@ref)'s.
"""
function get_DEIM_interpolation_matrix(Ψ::AbstractMatrix)
N, k = size(Ψ)
p = _deim_indices(Ψ)
p = get_DEIM_indices(Ψ)
Π = zeros(float(eltype(Ψ)), N, k)
for i in 1:k
Π[p[i], i] = one(eltype(Π))
Expand Down
6 changes: 3 additions & 3 deletions src/algorithms/symplectic_deim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ supported.
Throws an `ArgumentError` where `m > M`, and for a column of the density basis `U` whose largest
DEIM residual magnitude `ρ` is not both positive and finite: `ρ == 0` where the column is zero
or exactly dependent on the ones before it, and `ρ` is `NaN` or `Inf` where the column holds a
`NaN` or an `Inf`. [`_deim_indices`](@ref) is the check.
`NaN` or an `Inf`. [`get_DEIM_indices`](@ref) is the check.
"""
struct SymplecticDEIM{T, AT <: AbstractMatrix, ST, HT, GT}
A::AT
Expand All @@ -49,7 +49,7 @@ function SymplecticDEIM(

# The weights express the sum of the DEIM-interpolated densities using sampled values:
# 𝟙ᵀU(ΠᵀU)⁻¹Πᵀh = wᵀh[p].
p = _deim_indices(U)
p = get_DEIM_indices(U)
w = convert(Vector{T}, (U[p, :])' \ (U' * ones(T, size(U, 1))))

# `L̃ = Aᵀ·sym(L)·A`, symmetrised once more after the product, so that the Jacobian of the
Expand Down Expand Up @@ -123,7 +123,7 @@ the write reads 0 B, and anything a callback allocates is the caller's.
The write gives no guarantee for an aliased array: `g` and `z` must be distinct, because the
quadratic part is written with `mul!`, which requires its output and its input to differ.

The constructor validates the density basis through [`_deim_indices`](@ref): `m > M`, and a
The constructor validates the density basis through [`get_DEIM_indices`](@ref): `m > M`, and a
column whose largest DEIM residual magnitude is zero or non-finite, raise `ArgumentError`.
This write propagates errors from array operations and callbacks. A `NaN` or an `Inf` in `z`,
or from `∇h`, enters `g` with no check.
Expand Down
45 changes: 29 additions & 16 deletions test/algorithms/deim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ using LinearAlgebra
using Random
using ReducedBasisMethods
using ReducedComplexityModeling: Parameter, ParameterSpace
using ReducedBasisMethods: _deim_indices
using Test

"""
Expand Down Expand Up @@ -93,21 +92,21 @@ end
# selects row 3, while the largest magnitude selects row 2.
# The first pick is row 1, so neither selection repeats it.
Ψ₁ = T[1 0; 0 -2; 0 1] ./ T[1 sqrt(5)]
@test _deim_indices(Ψ₁) == [1, 2]
@test get_DEIM_indices(Ψ₁) == [1, 2]
@test get_DEIM_interpolation_matrix(Ψ₁) == T[1 0; 0 1; 0 0]

# The first pick: the largest magnitude is in row 2, the largest value in row 3.
Ψ₂ = T[0.1 0; -1 -2; 0.2 1] ./ T[norm([0.1, -1, 0.2]) sqrt(5)]
@test _deim_indices(Ψ₂) == [2, 3]
@test get_DEIM_indices(Ψ₂) == [2, 3]
# At k = 1 the first pick is the whole selection.
@test _deim_indices(Ψ₂[:, 1:1]) == [2]
@test get_DEIM_indices(Ψ₂[:, 1:1]) == [2]

# The residual is zero at the selected row and has equal largest magnitudes in
# rows 2 and 3, so the tie goes to row 2.
Ψ₃ = T[1 0; 0 -1; 0 -1] ./ T[1 sqrt(2)]
@test _deim_indices(Ψ₃) == [1, 2]
@test get_DEIM_indices(Ψ₃) == [1, 2]

p₃ = _deim_indices(Ψ₃)
p₃ = get_DEIM_indices(Ψ₃)
Π₃ = get_DEIM_interpolation_matrix(Ψ₃)
@test allunique(p₃)
@test rank(Π₃' * Ψ₃) == 2
Expand All @@ -118,15 +117,15 @@ end
# Π follows the element type of Ψ.
Ψ₃₂ = Float32[1 0; 0 -1; 0 -1] ./ Float32[1 sqrt(2)]
@test eltype(get_DEIM_interpolation_matrix(Ψ₃₂)) === Float32
@test _deim_indices(Ψ₃₂) == [1, 2]
@test get_DEIM_indices(Ψ₃₂) == [1, 2]

# An integer Ψ gives a Float64 Π, never an integer one.
Ψᵢ = Int32[1 0; 0 -2; 0 1]
@test _deim_indices(Ψᵢ) == [1, 2]
@test get_DEIM_indices(Ψᵢ) == [1, 2]
@test eltype(get_DEIM_interpolation_matrix(Ψᵢ)) === Float64

# k = 0: the empty selection, and the N × 0 matrix built from it.
@test _deim_indices(zeros(3, 0)) == Int[]
@test get_DEIM_indices(zeros(3, 0)) == Int[]
@test size(get_DEIM_interpolation_matrix(zeros(3, 0))) == (3, 0)

# k > N leaves too few distinct rows for the interpolation.
Expand All @@ -142,33 +141,47 @@ end
for T in (Float32, Float64), bad in (zero(T), T(NaN), T(Inf))

@test_throws ArgumentError get_DEIM_interpolation_matrix(reshape(T[bad, 0, 0], 3, 1))
@test_throws ArgumentError _deim_indices(reshape(T[bad, 0, 0], 3, 1))
@test_throws ArgumentError get_DEIM_indices(reshape(T[bad, 0, 0], 3, 1))

# A bad first column of a two-column basis is reported as column 1, not as column 2, and
# a zero first column is rejected before it reaches the solve, which as a singular
# `PᵀΨ` would raise `SingularException` where the `ArgumentError` is due.
err = try
_deim_indices(T[bad 1; 0 0; 0 1])
get_DEIM_indices(T[bad 1; 0 0; 0 1])
nothing
catch e
e
end
@test err isa ArgumentError
@test occursin("column 1", sprint(showerror, err))
end
@test_throws ArgumentError _deim_indices([0.0 1; 0 0; 0 0])
@test_throws ArgumentError get_DEIM_indices([0.0 1; 0 0; 0 0])

# The selection is scale-invariant: the residual is compared with zero, never with a
# cutoff. At 3.7e-8 in Float32 the largest residual is below `eps(Float32)`.
for T in (Float32, Float64)
Ψ, _ = pod_basis(example1_training(T); k = 20)
p = _deim_indices(Ψ)
p = get_DEIM_indices(Ψ)

@test _deim_indices(T(3.7e8) .* Ψ) == p
@test _deim_indices(T(3.7e-8) .* Ψ) == p
@test get_DEIM_indices(T(3.7e8) .* Ψ) == p
@test get_DEIM_indices(T(3.7e-8) .* Ψ) == p

# A subnormal first column is positive, so it is selected rather than refused: column 1's
# test is the same `0 < ρ`, with no threshold.
@test _deim_indices(reshape(T[nextfloat(zero(T)), zero(T)], 2, 1)) == [1]
@test get_DEIM_indices(reshape(T[nextfloat(zero(T)), zero(T)], 2, 1)) == [1]
end
end

@testset "indices are the index form of the interpolation matrix" begin
# Π' * A samples the rows of A that get_DEIM_indices selects, for any basis and any finite A.
for T in (Float32, Float64)
rng = Xoshiro(7)
Ψ = Matrix(qr(randn(rng, T, 50, 8)).Q)
A = randn(rng, T, 50, 5)
p = get_DEIM_indices(Ψ)

@test length(p) == 8
@test allunique(p)
@test get_DEIM_interpolation_matrix(Ψ)' * A == A[p, :]
end
end
5 changes: 2 additions & 3 deletions test/algorithms/symplectic_deim.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ using GeometricIntegrators
using LinearAlgebra
using Random
using ReducedBasisMethods
using ReducedBasisMethods: _deim_indices
using ReducedComplexityModeling: Parameter, ParameterSpace
using Test

Expand Down Expand Up @@ -178,7 +177,7 @@ end
# At m = M the interpolation is exact for the densities: w = 𝟙 and H̃(z) = H(Az).
sd = SymplecticDEIM(b.A, b.L, U, stencil_test, h_test, ∇h_test)

@test _deim_indices(U) != 1:NQ
@test get_DEIM_indices(U) != 1:NQ
@test norm(sd.w .- one(T), Inf) ≤ sqrt(eps(T))

for z in eachslice(randn(Xoshiro(4), T, 2KT, 20); dims = 2)
Expand Down Expand Up @@ -213,7 +212,7 @@ end
∇hᵢ(xs, i) = [(1 + i / NQ) * sin(xs[1])]
sdᵢ = SymplecticDEIM(b.A, b.L, U, stencil_test, hᵢ, ∇hᵢ)

@test _deim_indices(U) != 1:NQ
@test get_DEIM_indices(U) != 1:NQ

for z in eachslice(randn(Xoshiro(5), T, 2KT, 20); dims = 2)
Az = b.A * z
Expand Down
Loading