Usage
The base of the numerical set oriented methods of this framework are BoxSet
(the discretization of a set of boxes) and BoxMap
(the discretization of a map). Thus, in the following, we will have a closer look at the two and other useful things to know when using GAIO.jl.
To create a Box
given its center point c = (c_1, c_2, ..., c_d)
as well as its "radius" in every axis direction r = (r_1, r_2, ..., c_d)
, simply type
julia> using GAIO
julia> c, r = (0.5, 0.5), (0.5, 0.5)
((0.5, 0.5), (0.5, 0.5))
julia> Q = Box(c, r)
Box{2, Float64}: center: [0.5, 0.5] radius: [0.5, 0.5]
This creates a set $Q = [c_1 - r_1, c_1 + r_1), \times \ldots \times [c_d - r_d, c_d + r_d)$. Conversely, one can get back the vectors c
and r
by calling
julia> c, r = Q
Box{2, Float64}: center: [0.5, 0.5] radius: [0.5, 0.5]
BoxPartition
Most algorithms in GAIO.jl revolve around a partition of the domain $Q$ into small boxes. To create an $n_1 \times \ldots \times n_d$ - element equidistant grid of boxes, we can pass the tuple $n = (n_1, \ldots, n_d)$ into the function BoxPartition
julia> n = (4, 2)
(4, 2)
julia> P = BoxPartition(Q, n)
4 x 2 - element BoxPartition
BoxPartition
s use a cartesian indexing structure to be memory-efficient. These indices are accessed and used through the API:
julia> x = (0.2, 0.1)
(0.2, 0.1)
julia> key = point_to_key(P, x) # x is some point in the domain Q
(1, 1)
julia> box = key_to_box(P, key) # cover the point x with a box from P
Box{2, Float64}: center: [0.125, 0.25] radius: [0.125, 0.25]
julia> box = point_to_box(P, x) # performs both above functions
Box{2, Float64}: center: [0.125, 0.25] radius: [0.125, 0.25]
TreePartition
For partitions of $Q$ into variably sized boxes, one can use TreePartition
:
julia> P2 = TreePartition(Q)
TreePartition of depth 1
A TreePartition
uses a binary tree structure to store a partition of the domain. Every Box of a TreePartition
can be split using the command
julia> subdivide!(P2)
TreePartition of depth 2
julia> subdivide!(P2)
TreePartition of depth 3
julia> subdivide!(P2)
TreePartition of depth 4
The axis direction along which to subdivide cycles through with the depth, i.e. subdividing at depth 1 splits along dimension 1, subdividing at depth d+1
splits along dimension 1 again.
The TreePartition
created above is equivalent to a 4x2 BoxPartition
. One can retrieve this using
julia> P3 = BoxPartition(P2)
4 x 2 - element BoxPartition
TreePartition
s use indices of the type (depth, cartesian_index)
where cartesian_index
is the equivalent index of a BoxPartition
with the same size as a TreePartition
subdivided depth
times. In other words,
julia> key_to_box( P, (1, 1) ) == key_to_box( P2, (4, (1, 1)) )
true
julia> key_to_box( P, (4, 2) ) == key_to_box( P2, (4, (4, 2)) )
true
BoxSet
The core idea behind GAIO.jl is to approximate a subset of the domain via a collection of small boxes. To construct BoxSet
s, there are two main options: getting all boxes in the partition, or locating a box surrounding a point $x \in Q$
julia> B = cover(P, x) # one box surrounding the point x
1 - element BoxSet in 4 x 2 - element BoxPartition
julia> B = cover(P, :) # set of all boxes in P
8 - element BoxSet in 4 x 2 - element BoxPartition
One can also create a Boxset
from an iterable of Box
es. This will cover every element of the iterable with boxes from P
:
julia> x1 = (0.2, 0.1)
(0.2, 0.1)
julia> box1 = point_to_box(P, x1)
Box{2, Float64}: center: [0.125, 0.25] radius: [0.125, 0.25]
julia> x2 = (0.3, 0.6)
(0.3, 0.6)
julia> box2 = point_to_box(P, x2)
Box{2, Float64}: center: [0.375, 0.75] radius: [0.125, 0.25]
julia> B = cover(P, [box1, box2])
2 - element BoxSet in 4 x 2 - element BoxPartition
BoxSet
is a highly memory-efficient way of storing boxes. However, should you want to access the boxes or their internal data, this can be done via iteration:
julia> for box in B center, radius = box # do something end # get an array of boxes
julia> arr_of_boxes = collect(B) # get an array of box centers
2-element Vector{Box{2, Float64}}: [0.0, 0.25) × [0.0, 0.5) [0.25, 0.5) × [0.5, 1.0)
julia> arr_of_centers = collect(box.center for box in B) # get an array of box radii
2-element Vector{SVector{2, Float64}}: [0.125, 0.25] [0.375, 0.75]
julia> arr_of_radii = collect(box.radius for box in B) # (memory-efficiently) create a matrix where each center is a column
2-element Vector{SVector{2, Float64}}: [0.125, 0.25] [0.125, 0.25]
julia> mat_of_centers = reinterpret(reshape, eltype(arr_of_centers[1]), arr_of_centers)
2×2 reinterpret(reshape, Float64, ::Vector{SVector{2, Float64}}) with eltype Float64: 0.125 0.375 0.25 0.75
BoxMap
A BoxMap is a function which maps boxes to boxes. Given a pointmap f : ℝᵈ → ℝᵈ
which accepts an SVector
from StaticArrays.jl
(or just an NTuple
) and returns the same,
julia> function f(u) # the Baker transformation x, y = u if x < 0.5 (2x, y/2) else (2x - 1, y/2 + 1/2) end end
f (generic function with 1 method)
initialize the corresponding BoxMap
F
by
julia> F = BoxMap(f, Q)
SampledBoxMap with 2 sample points
This will generate a BoxMap
which attempts to calculate setwise images of f
. One can also use a DynamicalSystem
from DynamicalSystems.jl
:
julia> using DynamicalSystems: DiscreteDynamicalSystem
julia> using StaticArrays
julia> dynamic_rule(u::Vec, p, t) where Vec = Vec( f(u) ) # DynamicalSystems.jl syntax
dynamic_rule (generic function with 1 method)
julia> u0 = SA_F64[0, 0] # this is dummy data
2-element SVector{2, Float64} with indices SOneTo(2): 0.0 0.0
julia> p0 = SA_F64[0, 0] # parameters, in case they are needed
2-element SVector{2, Float64} with indices SOneTo(2): 0.0 0.0
julia> system = DiscreteDynamicalSystem(dynamic_rule, u0, p0)
2-dimensional DeterministicIteratedMap deterministic: true discrete time: true in-place: false dynamic rule: dynamic_rule parameters: [0.0, 0.0] time: 0 state: [0.0, 0.0]
julia> F̃ = BoxMap(system, Q)
SampledBoxMap with 2 sample points
The same works for a continuous dynamical system.
Currently, you must hard-code your systems, and cannot rely on DifferentialEquations
or DynamicalSystems
for GPU-acceleration.
GAIO.jl ALWAYS performs integration over one time unit! To perform smaller steps, rescale your dynamical system accordingly!
julia> using DynamicalSystems: ContinuousDynamicalSystem
julia> using OrdinaryDiffEq
julia> function lorenz_dudx(u::Vec, p, t, Δt) where Vec x,y,z = u σ,ρ,β = p dudx = Vec(( σ*(y-x), ρ*x-y-x*z, x*y-β*z )) return Δt * dudx end
lorenz_dudx (generic function with 1 method)
julia> Δt = 0.2
0.2
julia> lorenz(u, p=p0, t=0) = lorenz_dudx(u, p, t, Δt)
lorenz (generic function with 3 methods)
julia> u0 = SA_F64[0, 0, 0]
3-element SVector{3, Float64} with indices SOneTo(3): 0.0 0.0 0.0
julia> p0 = SA_F64[10, 28, 0.4]
3-element SVector{3, Float64} with indices SOneTo(3): 10.0 28.0 0.4
julia> diffeq = (alg = RK4(), dt = 1/20, adaptive = false)
(alg = RK4(; stage_limiter! = trivial_limiter!, step_limiter! = trivial_limiter!, thread = static(false),), dt = 0.05, adaptive = false)
julia> lorenz_system = ContinuousDynamicalSystem(lorenz, u0, p0; diffeq)
3-dimensional CoupledODEs deterministic: true discrete time: false in-place: false dynamic rule: lorenz ODE solver: RK4 ODE kwargs: (dt = 0.05, adaptive = false) parameters: [10.0, 28.0, 0.4] time: 0.0 state: [0.0, 0.0, 0.0]
julia> Q̄ = Box((0,0,25), (30,30,30))
Box{3, Float64}: center: [0.0, 0.0, 25.0] radius: [30.0, 30.0, 30.0]
julia> F̄ = BoxMap(lorenz_system, Q̄)
SampledBoxMap with 2 sample points
By default, GAIO.jl will try to adaptively choose test points to compute setwise images by approximating the (local) Lipschitz constant of the map. There are many other types of BoxMap
discretizations available, see the section on BoxMaps for more information.
Using BoxMap
Now, one can map a BoxSet
via the BoxMap
F
by simply calling F
as a function
julia> C = F(B)
7 - element BoxSet in 4 x 2 - element BoxPartition
where the output C
is also a BoxSet
.
For long running computations, GAIO.jl can also display a progress meter
julia> using ProgressMeter
julia> C = F(B; show_progress = true)
7 - element BoxSet in 4 x 2 - element BoxPartition
(Adding a progress meter adds a little bit of overhead, so for super short computations like the above it isn't recommended)
TransferOperator
The Perron-Frobenius operator (or transfer operator) [5] is discretized in GAIO.jl using the TransferOperator
type. To initialize a TransferOperator
that acts on a subdomain of $Q$, type
julia> B = cover(P, :)
8 - element BoxSet in 4 x 2 - element BoxPartition
julia> T = TransferOperator(F, B) # T operates on the domain covered by the box set B
8 x 8 TransferOperator over 4 x 2 - element BoxPartition with 33 stored entries: ⎡⢿⣇⠶⠆⎤ ⎣⠘⠛⠘⡿⎦
In this case, the codomain is generated automatically. This is not always ideal (e.g. in eigenvalue calculations), so the codomain should often be specified as the third argument
julia> T = TransferOperator(F, B, B)
8 x 8 TransferOperator over 4 x 2 - element BoxPartition with 33 stored entries: ⎡⢻⣧⠩⠥⎤ ⎣⢤⣄⢤⣗⎦
Again, a progress meter can be displayed for long computations
julia> using ProgressMeter
julia> T = TransferOperator(F, B, B; show_progress = true)
8 x 8 TransferOperator over 4 x 2 - element BoxPartition with 33 stored entries: ⎡⢻⣧⠩⠥⎤ ⎣⢤⣄⢤⣗⎦
To convert this to the underlying transfer matrix described in [6], one can simply call the sparse
function from SparseArrays
julia> using SparseArrays
julia> sparse(T)
8×8 SparseMatrixCSC{Float64, Int64} with 33 stored entries: 0.333333 0.166667 0.166667 ⋅ 0.5 0.25 0.214286 ⋅ 0.333333 0.166667 0.166667 ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ 0.166667 0.166667 0.333333 ⋅ 0.25 0.214286 0.428571 ⋅ 0.166667 0.166667 0.333333 ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ 0.214286 ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ 0.214286 0.428571 0.333333 0.166667 0.166667 ⋅ 0.5 0.25 0.0714286 ⋅ ⋅ 0.166667 0.166667 0.333333 ⋅ 0.25 0.0714286 0.142857
To find an approximate invariant measure over B
use the eigs
function from Arpack.jl
. All keyword arguments from Arpack.eigs
are supported.
julia> # for the Baker trafo, the Lebesgue measure # - i.e. the constant-weight measure - is invariant λ, ev = eigs(T);
julia> λ
3-element Vector{ComplexF64}: 0.9999999999999991 + 0.0im 0.49999999999999967 + 0.0im -0.37896131637649966 + 0.0im
julia> ev
3-element Vector{BoxMeasure{Box{2, Float64}, Tuple{Int64, Int64}, ComplexF64, BoxPartition{2, Float64, Int64}, OrderedCollections.OrderedDict{Tuple{Int64, Int64}, ComplexF64}}}: BoxMeasure in 4 x 2 - element BoxPartition with 8 boxes in its suport BoxMeasure in 4 x 2 - element BoxPartition with 8 boxes in its suport BoxMeasure in 4 x 2 - element BoxPartition with 8 boxes in its suport
julia> μ = ev[1] # ev is an array of measures, grab the first one
BoxMeasure in 4 x 2 - element BoxPartition with 8 boxes in its suport
This can also be done with the adjoint Koopman operator T'
.
BoxMeasure
The second output of eigs(T)
is a discrete measure, a BoxMeasure
. This measure is absolutely continuous w.r.t. the volume (i.e. Lebesgue) measure and its density is piecewise constant on the boxes of the domain. One can let T
act on a BoxMeasure
simply through multiplication
julia> ν = T*μ
BoxMeasure in 4 x 2 - element BoxPartition with 8 boxes in its suport
Of course, the same holds for the the Koopman operator as well.
julia> ν = T'μ
BoxMeasure in 4 x 2 - element BoxPartition with 8 boxes in its suport
One can evaulate a BoxMeasure
on a BoxSet
julia> B
8 - element BoxSet in 4 x 2 - element BoxPartition
julia> μ(B)
-2.6065137370063387 + 0.0im
Similarly, one can integrate a function with respect to a BoxMeasure by calling
julia> sum(x -> sin(x[1] + 2x[2]), μ)
-2.267631039885399 + 0.0im
Marginal distributions can be accessed using the marginal
function
julia> marginal(μ; dim=1)
BoxMeasure in 2 - element BoxPartition with 2 boxes in its suport
The measures can also be associated with a (Lebesgue) density
julia> p = density(μ)
(::GAIO.var"#eval_density#181"{BoxMeasure{Box{2, Float64}, Tuple{Int64, Int64}, ComplexF64, BoxPartition{2, Float64, Int64}, OrderedCollections.OrderedDict{Tuple{Int64, Int64}, ComplexF64}}, BoxPartition{2, Float64, Int64}}) (generic function with 1 method)
Since a measure $\mu$ is a function defined over measurable sets, composite measures $g \circ \mu$ are well-defined for functions $g : \mathbb{R} \to \mathbb{R}$ (or $g : \mathbb{C} \to \mathbb{C}$). This is supported in GAIO.jl for BoxMeasures
julia> η = exp ∘ μ
BoxMeasure in 4 x 2 - element BoxPartition with 8 boxes in its suport
For multiple BoxMeasures, the concatenation operator ∘
can be applied to each one using julia's broadcasting functionality
julia> real_ev = real .∘ ev
3-element Vector{BoxMeasure{Box{2, Float64}, Tuple{Int64, Int64}, Float64, BoxPartition{2, Float64, Int64}, OrderedCollections.OrderedDict{Tuple{Int64, Int64}, Float64}}}: BoxMeasure in 4 x 2 - element BoxPartition with 8 boxes in its suport BoxMeasure in 4 x 2 - element BoxPartition with 8 boxes in its suport BoxMeasure in 4 x 2 - element BoxPartition with 8 boxes in its suport
Similarly, finite signed measures can be given a vector space structure. This is also supported in GAIO.jl
julia> ν + μ
BoxMeasure in 4 x 2 - element BoxPartition with 8 boxes in its suport
julia> 2ν - μ/2
BoxMeasure in 4 x 2 - element BoxPartition with 8 boxes in its suport
A BoxMeasure
is implemented by a dictionary, mapping boxes to weights
Iterators.take(μ, 3)
To access this structure oneself one can call
P = μ.partition
key_val_pairs = pairs(μ)
Graphs of Boxes
One could equivalently view the transfer operator as a weighted directed graph. That is, a transfer matrix in GAIO.jl is the (transposed) weighted adjacency matrix for a graph. This graph can be constructed using the MetaGraphsNext.jl
package
julia> using Graphs, MetaGraphsNext
WARNING: using Graphs.density in module Main conflicts with an existing identifier.
julia> G = MetaGraph(T)
Meta graph based on a SimpleDiGraph{Int64} with vertex labels of type Tuple{Int64, Int64}, vertex metadata of type Nothing, edge metadata of type Float64, graph metadata given by 4 x 2 - element BoxPartition, and default weight 1.0
See the MetaGraphsNext documentation for how to interface with this data type.
Plotting
GAIO.jl offers both Plots
or Makie
for plotting. To plot a BoxSet
or a BoxMeasure
, simply choose either Plots or a Makie backend, eg. GLMakie
, and call plot
on a BoxSet
or BoxMeasure
using GLMakie: plot
plot(B)
Plotting works with all the functionality of either package. This means you can set box plots as subplots, add colorbars, etc., using the Plots or Makie interface. For an example, see examples/invariant_measure_2d.jl
.