Basis
Many of the methods require the definition of a Basis
on observables or functional forms. A Basis
is generated via:
Basis(eqs::AbstractVector, states::AbstractVector;
parameters::AbstractArray = [], iv = nothing,
simplify = false, linear_independent = false, name = gensym(:Basis),
pins = [], observed = [], eval_expression = false,
kwargs...)
where eqs
is either a vector containing symbolic functions using 'ModelingToolkit.jl' or a general function with the typical DiffEq signature h(u,p,t)
, which can be used with an Num
or vector of Num
. states
are the dependent variables used to describe the Basis, and parameters
are the optional parameters in the Basis
. iv
represents the independent variable of the system - in most cases the time. Additional arguments are simplify
, which simplifies eqs
before creating a Basis
. linear_dependent
breaks up eqs
in linear independent elements which are unique. name
is an optional name for the Basis
, pins
and observed
can be using in accordance to ModelingToolkits documentation. eval_expression
is used to generate a callable function from the eqs. If set to false
, callable code will be returned. true
will use eval
on code returned from the function, which might cause worldage issues.
DataDrivenDiffEq.Basis
— Typemutable struct Basis <: ModelingToolkit.AbstractSystem
A basis over the variables u
with parameters p
and independent variable iv
. It extends an AbstractSystem
as defined in ModelingToolkit.jl
. f
can either be a Julia function which is able to use ModelingToolkit variables or a vector of eqs
. It can be called with the typical DiffEq signature, meaning out of place with f(u,p,t)
or in place with f(du, u, p, t)
. If linear_independent
is set to true
, a linear independent basis is created from all atom function in f
. If simplify_eqs
is set to true
, simplify
is called on f
. Additional keyworded arguments include name
, which can be used to name the basis, pins
used for connections and observed
for defining observeables.
Fields
eqs
The equations of the basis
states
Dependent (state) variables
ps
Parameters
pins
observed
iv
Independent variable
f_
Internal function representation of the basis
name
Name of the basis
systems
Internal systems
Example
using ModelingToolkit
using DataDrivenDiffEq
@parameters w[1:2] t
@variables u[1:2](t)
Ψ = Basis([u; sin.(w.*u)], u, parameters = p, iv = t)
Note
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.
Example
We start by crearting some variables and parameters using ModelingToolkit
.
using LinearAlgebra
using DataDrivenDiffEq
using Plots
using ModelingToolkit
@variables u[1:3]
@parameters w[1:2]
(ModelingToolkit.Num[w₁, w₂],)
To define a basis, simply write down the equations you want to be included as a Vector
. Possible used parameters have to be given to the constructor.
h = [u[1]; u[2]; cos(w[1]*u[2]+w[2]*u[3])]
b = Basis(h, u, parameters = w)
\begin{align} \varphi{1} =& u{1} \ \varphi{2} =& u{2} \ \varphi{3} =& \cos\left( u{2} w{1} + u{3} w{_2} \right) \end{align}
Basis
are callable with the signature of functions to be used in DifferentialEquations
. So, the function value at a single point looks like:
x = b([1;2;3])
3-element Array{Any,1}:
1
2
cos(2w₁ + 3w₂)
Or, in place
dx = similar(x)
b(dx, [1;2;3])
Notice that since we did not use any numerical values for the parameters, the basis uses the symbolic values in the result.
To use numerical values, simply pass this on in the function call. Here, we evaluate over a trajectory with two parameters and 40 timestamps.
X = randn(3, 40)
Y = b(X, [2;4], 0:39)
Suppose we want to add another equation, say sin(u[1])
. A Basis
behaves like an array, so we can simply
push!(b, sin(u[1]))
size(b)
(4,)
To ensure that a basis is well-behaved, functions already present are not included again.
push!(b, sin(u[1]))
size(b)
(4,)
We can also define functions of the independent variable and add them
t = independent_variable(b)
push!(b, cos(t*π))
println(b)
##Basis#253 : 5 dimensional basis in ["u₁", "u₂", "u₃"]
Parameters : SymbolicUtils.Sym{ModelingToolkit.Parameter{Real}}[w₁, w₂]
Independent variable: t
Equations
φ₁ = u₁
φ₂ = u₂
φ₃ = cos(u₂*w₁ + u₃*w₂)
φ₅ = sin(u₁)
φ₅ = cos(πt)
Additionally, we can iterate over a Basis
using [eq for eq in basis]
or index specific equations, like basis[2]
.
We can also chain Basis
via just using it in the constructor
@variables x[1:2]
y = [sin(x[1]); cos(x[1]); x[2]]
t = independent_variable(b)
b2 = Basis(b(y, parameters(b), t), x, parameters = w, iv = t)
println(b2)
##Basis#282 : 5 dimensional basis in ["x₁", "x₂"]
Parameters : SymbolicUtils.Sym{ModelingToolkit.Parameter{Real}}[w₁, w₂]
Independent variable: t
Equations
φ₁ = sin(x₁)
φ₂ = cos(x₁)
φ₃ = cos(w₁*cos(x₁) + w₂*x₂)
φ₄ = sin(sin(x₁))
φ₅ = cos(3.141592653589793getindex(t, 1))
You can also use merge
to create the union of two Basis
:
b3 = merge(b, b2)
println(b3)
##Basis#290 : 10 dimensional basis in ["u₁", "u₂", "u₃", "x₁", "x₂"]
Parameters : SymbolicUtils.Sym{ModelingToolkit.Parameter{Real}}[w₁, w₂]
Independent variable: t
Equations
φ₁ = u₁
φ₂ = u₂
φ₃ = cos(u₂*w₁ + u₃*w₂)
φ₄ = sin(u₁)
...
φ₁₀ = cos(3.141592653589793getindex(t, 1))
which combines all the used variables and parameters ( and assumes the same independent_variable ):
variables(b)
3-element Array{SymbolicUtils.Sym{Real},1}:
u₁
u₂
u₃
parameters(b)
2-element Array{SymbolicUtils.Sym{ModelingToolkit.Parameter{Real}},1}:
w₁
w₂
If you have a function already defined as pure code, you can use this also to create a Basis
. Only the signature has to be consistent, so use f(u,p,t)
.
f(u, p, t) = [u[1]; u[2]; cos(p[1]*u[2]+p[2]*u[3])]
b_f = Basis(f, u, parameters = w)
println(b_f)
##Basis#298 : 3 dimensional basis in ["u₁", "u₂", "u₃"]
Parameters : SymbolicUtils.Sym{ModelingToolkit.Parameter{Real}}[w₁, w₂]
Independent variable: t
Equations
φ₁ = u₁
φ₂ = u₂
φ₃ = cos(u₂*w₁ + u₃*w₂)
This works for every function defined over Num
s. So to create a Basis
from a Flux
model, simply extend the activations used:
using Flux
NNlib.σ(x::Num) = 1 / (1+exp(-x))
c = Chain(Dense(3,2,σ), Dense(2, 1, σ))
ps, re = Flux.destructure(c)
@parameters p[1:length(ps)]
g(u, p, t) = re(p)(u)
b = Basis(g, u, parameters = p)
Functions
DataDrivenDiffEq.jacobian
— Functionjacobian(basis)
Returns a function representing the jacobian matrix / gradient of the `Basis` with respect to the
dependent variables as a function with the common signature `f(u,p,t)` for out of place and `f(du, u, p, t)` for in place computation.
DataDrivenDiffEq.dynamics
— Functiondynamics(basis)
Returns the internal function representing the dynamics of the `Basis`.
Base.push!
— Functionpush!(basis, eq, simplify_eqs = true; eval_expression = false)
Push the equations(s) in `eq` into the basis and update all internal fields accordingly.
`eq` can either be a single equation or an array. If `simplify_eq` is true, the equation will be simplified.
Base.deleteat!
— Functiondeleteat!(basis, inds, eval_expression = false)
Delete the entries specified by `inds` and update the `Basis` accordingly.
Base.merge
— Functionmerge(x::Basis, y::Basis; eval_expression = false)
Return a new `Basis`, which is defined via the union of `x` and `y` .
Base.merge!
— Functionmerge!(x::Basis, y::Basis; eval_expression = false)
Updates `x` to include the union of both `x` and `y`.