Skip to content

Commit

Permalink
v0.6.1
Browse files Browse the repository at this point in the history
* Document sparse regression

* Add eval_expression to solve and build_solutions

* Update wrong sizing in tests

* v0.6.1
  • Loading branch information
AlCap23 authored Jul 2, 2021
1 parent dc32f12 commit 5ab2a62
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 35 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "DataDrivenDiffEq"
uuid = "2445eb08-9709-466a-b3fc-47e12bd697a2"
authors = ["Julius Martensen <[email protected]>"]
version = "0.6.0"
version = "0.6.1"

[deps]
Compat = "34da2185-b29b-5c13-b0c7-acf172513d20"
Expand Down
12 changes: 12 additions & 0 deletions docs/src/prob_and_solve.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ ps = parameter_map(res)

## [Optional Arguments](@id optional_arguments)

!!! info
The keyword argument `eval_expression` controls the function creation
behavior. `eval_expression=true` means that `eval` is used, so normal
world-age behavior applies (i.e. the functions cannot be called from
the function that generates them). If `eval_expression=false`,
then construction via GeneralizedGenerated.jl is utilized to allow for
same world-age evaluation. However, this can cause Julia to segfault
on sufficiently large basis functions. By default eval_expression=false.

Koopman based algorithms can be called without a [`Basis`](@ref), resulting in dynamic mode decomposition like methods, or with a basis for extened dynamic mode decomposition :

```julia
Expand All @@ -81,6 +90,9 @@ Possible keyworded arguments include
+ `digits` controls the digits / rounding used for deriving the system equations (`digits = 1` would round `10.02` to `10.0`)
+ `operator_only` returns a `NamedTuple` containing the operator, input and output mapping and matrices used for updating the operator as described [here](https://arxiv.org/pdf/1406.7187.pdf)

!!! info
If `eval_expression` is set to `true`, the returning result of the Koopman based inference will not contain a parametrized equation, but rather use the numeric values of the operator/generator.

SINDy based algorithms can be called like :

```julia
Expand Down
12 changes: 9 additions & 3 deletions src/optimizers/sparseregression.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
$(SIGNATURES)
Implements a sparse regression, given an `AbstractOptimizer` or `AbstractSubspaceOptimizer`.
`maxiter` indicate the maximum iterations for each call of the optimizer, `abstol` the absolute tolerance of
`X` denotes the coefficient matrix, `A` the design matrix and `Y` the matrix of observed or target values.
`X` can be derived via `init(opt, A, Y)`.
`maxiter` indicates the maximum iterations for each call of the optimizer, `abstol` the absolute tolerance of
the difference between iterations in the 2 norm. If the optimizer is called with a `Vector` of thresholds, each `maxiter` indicates
the maximum iterations for each threshold.
If `progress` is set to `true`, a progressbar will be available.
If `progress` is set to `true`, a progressbar will be available. `progress_outer` and `progress_offset` are used to compute the initial offset of the
progressbar.
If used with a `Vector` of thresholds, the functions `f` with signature `f(X, A, Y)` and `g` with signature `g(x, threshold) = G(f(X, A, Y))` with the arguments given as stated above can be passed in. These are
used for finding the pareto-optimal solution to the sparse regression.
"""
function sparse_regression!(X, A, Y, opt::AbstractOptimizer{T};
maxiter::Int = maximum(size(A)),
Expand Down Expand Up @@ -83,7 +89,7 @@ function sparse_regression!(X, A, Y, opt::AbstractOptimizer{T};
)
end
end

for i in 1:size(Y, 2)
@views clip_by_threshold!(X[:, i], λs[i])
end
Expand Down
56 changes: 36 additions & 20 deletions src/solution.jl
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ function Base.print(io::IO, r::DataDrivenSolution, fullview::DataType)
println(io, "")
print(io, r.res)
println(io, "")
if length(r.res.ps) > 0
if length(r.res.ps) > 0
x = parameter_map(r)
println(io, "Parameters:")
for v in x
Expand Down Expand Up @@ -154,14 +154,15 @@ end


# Explicit sindy
function build_solution(prob::DataDrivenProblem, Ξ::AbstractMatrix, opt::Optimize.AbstractOptimizer, b::Basis)
function build_solution(prob::DataDrivenProblem, Ξ::AbstractMatrix, opt::Optimize.AbstractOptimizer, b::Basis;
eval_expression = false)
if all(iszero(Ξ))
@warn "Sparse regression failed! All coefficients are zero."
return DataDrivenSolution(
nothing , :failed, nothing, opt, Ξ, (Problem = prob, Basis = b, nothing),
nothing , :failed, nothing, opt, Ξ, (Problem = prob, Basis = b, nothing),
)
end

eqs, ps, p_ = build_parametrized_eqs(Ξ, b)

# Build the lhs
Expand All @@ -176,7 +177,8 @@ function build_solution(prob::DataDrivenProblem, Ξ::AbstractMatrix, opt::Optimi
eqs, states(b),
parameters = [parameters(b); p_], iv = independent_variable(b),
controls = controls(b), observed = observed(b),
name = gensym(:Basis)
name = gensym(:Basis),
eval_expression = eval_expression
)

sparsity = norm(Ξ, 0)
Expand Down Expand Up @@ -225,15 +227,15 @@ function build_solution(prob::DataDrivenProblem, Ξ::AbstractMatrix, opt::Optimi
end

function build_solution(prob::DataDrivenProblem, Ξ::AbstractMatrix, opt::Optimize.AbstractSubspaceOptimizer,
b::Basis, implicits::Vector{Num})
b::Basis, implicits::Vector{Num}; eval_expression = false)

if all(iszero(Ξ))
@warn "Sparse regression failed! All coefficients are zero."
return DataDrivenSolution(
nothing , :failed, nothing, opt, Ξ, (Problem = prob, Basis = b, nothing),
nothing , :failed, nothing, opt, Ξ, (Problem = prob, Basis = b, nothing),
)
end

eqs, ps, p_ = build_parametrized_eqs(Ξ, b)
eqs = [0 .~ eq for eq in eqs]

Expand All @@ -242,7 +244,8 @@ function build_solution(prob::DataDrivenProblem, Ξ::AbstractMatrix, opt::Optimi
eqs, states(b),
parameters = [parameters(b); p_], iv = independent_variable(b),
controls = controls(b), observed = observed(b),
name = gensym(:Basis)
name = gensym(:Basis),
eval_expression = eval_expression
)

sparsity = norm(Ξ, 0)
Expand Down Expand Up @@ -298,8 +301,8 @@ function _round!(x::AbstractArray{T, N}, digits::Int) where {T, N}
end

#function build_solution(prob::DataDrivenProblem, Ξ::AbstractMatrix, opt::AbstractKoopmanAlgorithm)
function build_solution(prob::DataDrivenProblem, k, C, B, Q, P, inds, b::AbstractBasis, alg::AbstractKoopmanAlgorithm; digits::Int = 10)
# Build parameterized equations
function build_solution(prob::DataDrivenProblem, k, C, B, Q, P, inds, b::AbstractBasis, alg::AbstractKoopmanAlgorithm; digits::Int = 10, eval_expression = false)
# Build parameterized equations, inds indicate the location of basis elements containing an input
Ξ = zeros(eltype(B), size(C,2), length(b))

Ξ[:, inds] .= real.(Matrix(k))
Expand All @@ -308,7 +311,14 @@ function build_solution(prob::DataDrivenProblem, k, C, B, Q, P, inds, b::Abstrac
end

# Transpose because of the nature of build_parametrized_eqs
eqs, ps, p_ = build_parametrized_eqs(_round!(C*Ξ, digits)', b)
if !eval_expression
eqs, ps, p_ = build_parametrized_eqs(_round!(C*Ξ, digits)', b)
else
= _round!(C*Ξ, digits)
eqs =*Num[states(b); controls(b)]
p_ = []
ps = [K̃...]
end

# Build the lhs
if length(eqs) == length(states(b))
Expand All @@ -317,18 +327,25 @@ function build_solution(prob::DataDrivenProblem, k, C, B, Q, P, inds, b::Abstrac
eqs = [d(xs[i]) ~ eq for (i,eq) in enumerate(eqs)]
end


res_ = Koopman(eqs, states(b),
parameters = [parameters(b); p_],
controls = controls(b), iv = independent_variable(b),
K = k, C = C, Q = Q, P = P, lift = b.f)
K = k, C = C, Q = Q, P = P, lift = b.f,
eval_expression = eval_expression)


retcode = :success
pnew = !isempty(parameters(b)) ? [prob.p; ps] : ps
# Equation space
retcode = :sucess
X = prob.DX
X_, _, t, U = get_oop_args(prob)
Y = res_(X_, pnew, t, U)
X_, p_, t, U = get_oop_args(prob)

pnew = !isempty(parameters(b)) ? [p_; ps] : ps

if !eval_expression
# Equation space
Y = res_(X_, pnew, t, U)
else
Y =*b(X_, p_, t, U)
end

# Build the metrics
error = norm(X-Y, 2)
Expand Down Expand Up @@ -357,5 +374,4 @@ function build_solution(prob::DataDrivenProblem, k, C, B, Q, P, inds, b::Abstrac
return DataDrivenSolution(
res_, retcode, pnew, alg, Ξ, inputs, metrics
)
return K
end
6 changes: 4 additions & 2 deletions src/solve/koopman.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

function DiffEqBase.solve(prob::DataDrivenProblem{dType}, alg::AbstractKoopmanAlgorithm;
B::AbstractArray = [], digits::Int = 10, operator_only::Bool = false,
eval_expression = false,
kwargs...) where {dType <: Number}
# Check the validity
@assert is_valid(prob) "The problem seems to be ill-defined. Please check the problem definition."
Expand Down Expand Up @@ -39,11 +40,12 @@ function DiffEqBase.solve(prob::DataDrivenProblem{dType}, alg::AbstractKoopmanAl

operator_only && return (K = k, C = C, B = B, Q = Q, P = P)

return build_solution(prob, k, C, B, Q, P, inds, b, alg, digits = digits)
return build_solution(prob, k, C, B, Q, P, inds, b, alg, digits = digits, eval_expression = eval_expression)
end

function DiffEqBase.solve(prob::DataDrivenProblem{dType}, b::Basis, alg::AbstractKoopmanAlgorithm;
digits::Int = 10, operator_only::Bool = false,
eval_expression = false,
kwargs...) where {dType <: Number}
# Check the validity
@assert is_valid(prob) "The problem seems to be ill-defined. Please check the problem definition."
Expand Down Expand Up @@ -98,5 +100,5 @@ function DiffEqBase.solve(prob::DataDrivenProblem{dType}, b::Basis, alg::Abstrac

operator_only && return (K = k, C = C, B = B, Q = Q, P = P)

return build_solution(prob, k, C, B, Q, P, inds, b, alg, digits = digits)
return build_solution(prob, k, C, B, Q, P, inds, b, alg, digits = digits, eval_expression = eval_expression)
end
8 changes: 5 additions & 3 deletions src/solve/sindy.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ end
# Main
function DiffEqBase.solve(p::DataDrivenProblem{dType}, b::Basis, opt::Optimize.AbstractOptimizer;
normalize::Bool = false, denoise::Bool = false, maxiter::Int = 0,
round::Bool = true, kwargs...) where {dType <: Number}
round::Bool = true,
eval_expression = false, kwargs...) where {dType <: Number}
# Check the validity
@assert is_valid(p) "The problem seems to be ill-defined. Please check the problem definition."

Expand All @@ -48,7 +49,7 @@ function DiffEqBase.solve(p::DataDrivenProblem{dType}, b::Basis, opt::Optimize.A

# Build solution Basis
return build_solution(
p, Ξ, opt, b
p, Ξ, opt, b, eval_expression = eval_expression
)
end

Expand Down Expand Up @@ -78,6 +79,7 @@ end
@views function DiffEqBase.solve(p::DataDrivenProblem{dType}, b::Basis,
opt::Optimize.AbstractSubspaceOptimizer, implicits::Vector{Num} = Num[];
normalize::Bool = false, denoise::Bool = false, maxiter::Int = 0,
eval_expression = false,
round::Bool = true, kwargs...) where {dType <: Number}
# Check the validity
@assert is_valid(p) "The problem seems to be ill-defined. Please check the problem definition."
Expand Down Expand Up @@ -119,6 +121,6 @@ end
normalize ? rescale_xi!(Ξ, scales, round) : nothing
# Build solution Basis
return build_solution(
p, Ξ, opt, b, implicits
p, Ξ, opt, b, implicits, eval_expression = eval_expression
)
end
14 changes: 12 additions & 2 deletions test/dmd/linear_autonomous.jl
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ end
u0 = [10.0; -20.0]
prob = ODEProblem(f, u0, (0.0, 10.0))
sol = solve(prob, Tsit5(), saveat = 0.001)

prob = DataDrivenProblem(sol)

for alg in [DMDPINV(), DMDSVD(), TOTALDMD()]
Expand All @@ -45,7 +45,7 @@ end
@test isempty(estimator.B)
res = solve(prob, alg , operator_only = false)
m = metrics(res)
@test m.Error ./ size(X, 2) < 3e-1
@test m.Error ./ size(sol, 2) < 3e-1
@test Matrix(result(res)) Matrix(estimator.K)
end
end
Expand Down Expand Up @@ -74,6 +74,16 @@ end
end
end

@testset "Big System" begin
# Creates a big system which would resulting in a segfault otherwise
X = rand([0, 1], 128, 936);
T = collect(LinRange(0, 4.367058580858928, 936));
problem = DiscreteDataDrivenProblem(X, T);
res1 = solve(problem, DMDSVD(), eval_expression = true)
res2 = solve(problem, DMDSVD(), operator_only = true)
@test Matrix(result(res1)) == real.(Matrix(res2.K))
end

# TODO Include the Big System test
# This fails to generate an equation right now...
#@testset "Big System" begin
Expand Down
4 changes: 2 additions & 2 deletions test/dmd/nonlinear_autonomous.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
b = result(res)
m = metrics(res)
@test isapprox(eigvals(b), [2*p[1]; p[1]; p[2]], atol = 1e-1)
@test m.Error/size(X, 2) < 1e-1
@test m.Error/size(solution, 2) < 1e-1

_prob = ODEProblem((args...)->b(args...), u0, tspan, parameters(res))
_sol = solve(_prob, Tsit5(), saveat = solution.t)
@test norm(solution - _sol)/size(X, 2) < 1e-1
@test norm(solution - _sol)/size(solution, 2) < 1e-1
end
end
4 changes: 2 additions & 2 deletions test/dmd/nonlinear_forced.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

problem = ODEProblem(slow_manifold, u0, tspan, p)
solution = solve(problem, Tsit5(), saveat = 0.01)

ufun(u,p,t) = sin(t^2)

prob = ContinuousDataDrivenProblem(solution, U = ufun)
Expand All @@ -24,7 +24,7 @@
b = result(res)
m = metrics(res)
@test isapprox(eigvals(b), [2*p[1]; p[1]; p[2]], atol = 1e-1)
@test m.Error/size(X, 2) < 3e-1
@test m.Error/size(solution, 2) < 3e-1

# TODO This does not work right now, but it should
#sdict = Dict([y[1] => sin(t^2)])
Expand Down

0 comments on commit 5ab2a62

Please sign in to comment.