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,

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.

mutable 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.


  • eqs

    The equations of the basis

  • states

    Dependent (state) variables

  • ps


  • pins

  • observed

  • iv

    Independent variable

  • f_

    Internal function representation of the basis

  • name

    Name of the basis

  • systems

    Internal systems


using ModelingToolkit
using DataDrivenDiffEq

@parameters w[1:2] t
@variables u[1:2]

Ψ = Basis([u; sin.(w.*u)], u, parameters = p, iv = t)


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.



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)
##Basis#253 : 3 dimensional basis in ["u₁", "u₂", "u₃"]

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}:
  cos((w₁ * 2) + (w₂ * 3))

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]))

To ensure that a basis is well-behaved, functions already present are not included again.

push!(b, sin(u[1]))

We can also define functions of the independent variable and add them

t = independent_variable(b)
push!(b, cos(t*π))
##Basis#253 : 5 dimensional basis in ["u₁", "u₂", "u₃"]
Parameters : SymbolicUtils.Sym{ModelingToolkit.Parameter{Real}}[w₁, w₂]

Independent variable: t
φ₁ = u₁
φ₂ = u₂
φ₃ = cos((w₁ * u₂) + (w₂ * u₃))
φ₅ = 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)
##Basis#282 : 5 dimensional basis in ["x₁", "x₂"]
Parameters : SymbolicUtils.Sym{ModelingToolkit.Parameter{Real}}[w₁, w₂]

Independent variable: t
φ₁ = sin(x₁)
φ₂ = cos(x₁)
φ₃ = cos((w₁ * cos(x₁)) + (w₂ * x₂))
φ₄ = sin(sin(x₁))
φ₅ = cos(getindex(t, 1) * π)

You can also use merge to create the union of two Basis:

b3 = merge(b, b2)
##Basis#290 : 10 dimensional basis in ["u₁", "u₂", "u₃", "x₁", "x₂"]
Parameters : SymbolicUtils.Sym{ModelingToolkit.Parameter{Real}}[w₁, w₂]

Independent variable: t
φ₁ = u₁
φ₂ = u₂
φ₃ = cos((w₁ * u₂) + (w₂ * u₃))
φ₄ = sin(u₁)
φ₁₀ = cos(getindex(t, 1) * π)

which combines all the used variables and parameters ( and assumes the same independent_variable ):

3-element Array{SymbolicUtils.Sym{Real},1}:
2-element Array{SymbolicUtils.Sym{ModelingToolkit.Parameter{Real}},1}:

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)
##Basis#298 : 3 dimensional basis in ["u₁", "u₂", "u₃"]
Parameters : SymbolicUtils.Sym{ModelingToolkit.Parameter{Real}}[w₁, w₂]

Independent variable: t
φ₁ = u₁
φ₂ = u₂
φ₃ = cos((w₁ * u₂) + (w₂ * u₃))

This works for every function defined over Nums. 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)



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.
push!(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.
deleteat!(basis, inds, eval_expression = false)

Delete the entries specified by `inds` and update the `Basis` accordingly.
merge(x::Basis, y::Basis; eval_expression = false)

Return a new `Basis`, which is defined via the union of `x` and `y` .
merge!(x::Basis, y::Basis; eval_expression = false)

Updates `x` to include the union of both `x` and `y`.