Skip to content

Commit ef9cf54

Browse files
Add WyNDA online estimator (#631)
Co-authored-by: ChrisRackauckas-Claude <accounts@chrisrackauckas.com>
1 parent b9fd6fb commit ef9cf54

4 files changed

Lines changed: 134 additions & 0 deletions

File tree

docs/src/libs/datadrivensparse/sparse_regression.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Where the matrix of evaluated basis elements $\varPhi_y \in \mathbb R^{\lvert \v
5252
STLSQ
5353
ADMM
5454
SR3
55+
WyNDA
5556
ImplicitOptimizer
5657
```
5758

lib/DataDrivenSparse/src/DataDrivenSparse.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ export ADMM
123123
include("algorithms/SR3.jl")
124124
export SR3
125125

126+
include("algorithms/WyNDA.jl")
127+
export WyNDA
128+
126129
include("algorithms/Implicit.jl")
127130
export ImplicitOptimizer
128131

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
$(TYPEDEF)
3+
4+
`WyNDA` implements the Wide-Array of Nonlinear Dynamics Approximation update as an
5+
online recursive least-squares estimator with exponential forgetting. Given a
6+
matrix of evaluated basis functions `X` and targets `Y`, it updates the
7+
coefficient matrix sample-by-sample so that `Y ≈ coefficients * X`.
8+
9+
The forgetting factor `λ` controls how quickly older samples are discounted.
10+
Values close to one recover a batch least-squares-like fit, while smaller values
11+
adapt faster to parameter drift.
12+
13+
# Fields
14+
15+
$(FIELDS)
16+
17+
# Example
18+
19+
```julia
20+
opt = WyNDA()
21+
opt = WyNDA(0.998)
22+
opt = WyNDA(0.998; initial_covariance = 1.0e4)
23+
```
24+
"""
25+
struct WyNDA{T <: Number, C, IC} <: AbstractSparseRegressionAlgorithm
26+
"""Exponential forgetting factor."""
27+
λ::T
28+
"""Initial inverse covariance scale or matrix."""
29+
initial_covariance::C
30+
"""Optional initial coefficient vector or matrix."""
31+
initial_coefficients::IC
32+
33+
function WyNDA(
34+
λ::T = 1.0;
35+
initial_covariance::C = 1.0e6,
36+
initial_coefficients::IC = nothing
37+
) where {T <: Number, C, IC}
38+
@assert zero(T) < λ <= one(T) "Forgetting factor λ must satisfy 0 < λ <= 1"
39+
if initial_covariance isa Number
40+
@assert initial_covariance > zero(initial_covariance) "Initial covariance must be positive"
41+
end
42+
return new{T, C, IC}(λ, initial_covariance, initial_coefficients)
43+
end
44+
end
45+
46+
Base.summary(::WyNDA) = "WyNDA"
47+
48+
function _initial_covariance(alg::WyNDA, ::Type{T}, n_features::Int) where {T}
49+
covariance = alg.initial_covariance
50+
if covariance isa Number
51+
return Matrix{T}(I, n_features, n_features) .* T(covariance)
52+
end
53+
@assert size(covariance) == (n_features, n_features) "Initial covariance matrix size must match the number of features"
54+
return Matrix{T}(covariance)
55+
end
56+
57+
function _initial_coefficients(
58+
alg::WyNDA, ::Type{T}, n_targets::Int, n_features::Int
59+
) where {T}
60+
coefficients = alg.initial_coefficients
61+
if coefficients === nothing
62+
return zeros(T, n_targets, n_features)
63+
end
64+
if coefficients isa AbstractVector
65+
@assert n_targets == 1 "Initial coefficient vectors are only valid for single-target problems"
66+
@assert length(coefficients) == n_features "Initial coefficient vector length must match the number of features"
67+
return reshape(T.(coefficients), 1, n_features)
68+
end
69+
@assert size(coefficients) == (n_targets, n_features) "Initial coefficient matrix size must match targets by features"
70+
return Matrix{T}(coefficients)
71+
end
72+
73+
function (alg::WyNDA)(
74+
X::AbstractMatrix, Y::AbstractVecOrMat;
75+
options::DataDrivenCommonOptions = DataDrivenCommonOptions(),
76+
kwargs...
77+
)
78+
Y_matrix = Y isa AbstractVector ? reshape(Y, 1, :) : Y
79+
@assert size(X, 2) == size(Y_matrix, 2) "X and Y must have the same number of observations"
80+
81+
n_features, n_observations = size(X)
82+
n_targets = size(Y_matrix, 1)
83+
T = float(promote_type(eltype(X), eltype(Y_matrix), typeof(alg.λ)))
84+
85+
coefficients = _initial_coefficients(alg, T, n_targets, n_features)
86+
covariance = _initial_covariance(alg, T, n_features)
87+
λ = T(alg.λ)
88+
89+
for k in 1:n_observations
90+
basis_values = view(X, :, k)
91+
targets = view(Y_matrix, :, k)
92+
covariance_basis = covariance * basis_values
93+
denominator = λ + dot(basis_values, covariance_basis)
94+
gain = covariance_basis ./ denominator
95+
residual = targets .- coefficients * basis_values
96+
coefficients .+= residual * adjoint(gain)
97+
covariance .-= gain * adjoint(covariance_basis)
98+
covariance ./= λ
99+
end
100+
101+
return coefficients, alg.λ, n_observations
102+
end

lib/DataDrivenSparse/test/Core/sparse_linear_solve.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,34 @@ end
9797
end
9898
end
9999

100+
@testset "WyNDA online estimator" begin
101+
rng = StableRNG(20260708)
102+
t = 0.0:0.02:8.0
103+
X = permutedims(
104+
reduce(
105+
hcat,
106+
(
107+
ones(length(t)), sin.(0.7 .* t), cos.(1.3 .* t),
108+
exp.(-0.2 .* t), randn(rng, length(t)),
109+
)
110+
)
111+
)
112+
A = [0.5 -1.2 0.0 0.75 0.0; -0.25 0.0 1.5 0.0 0.1]
113+
Y = A * X
114+
115+
coefficients, λ, iterations = WyNDA(1.0; initial_covariance = 1.0e8)(X, Y)
116+
117+
@test λ == 1.0
118+
@test iterations == size(X, 2)
119+
# Finite initial covariance is equivalent to a small ridge prior in RLS.
120+
@test coefficients A atol = 1.0e-6
121+
122+
problem = DirectDataDrivenProblem(X, Y)
123+
result = @test_nowarn solve(problem, WyNDA(1.0; initial_covariance = 1.0e8))
124+
@test result isa DataDrivenSolution
125+
@test result.residuals <= 1.0e-8
126+
end
127+
100128
# Issue #564: Test that solve doesn't throw MethodError when coefficients are all zero
101129
# This can happen with very small data values or high regularization
102130
@testset "Zero coefficients handling (Issue #564)" begin

0 commit comments

Comments
 (0)