Skip to content

Commit 29afdbe

Browse files
rusandrisDatseris
andauthored
Instantaneous Lyapunov exponent for systems with parameter drift (#345)
* add EAPD * revised version and tests (still need docs) * add some docstrings * fix docstring part * include EAPD test * minor fixes, improve docstring, tests * fix dummy parameter test * increase tolerance for testing * Update lyapunovs.jl: relax test * fix comment * add EAPD to docs * fix citation * add changelog and increase version number * correct vertsion number Updated version from v3.3.2 to v3.4 in CHANGELOG.md. * Correct version number --------- Co-authored-by: George Datseris <[email protected]>
1 parent 489c805 commit 29afdbe

File tree

8 files changed

+294
-4
lines changed

8 files changed

+294
-4
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# main
22

3+
# v3.4
4+
5+
- Added instantaneous Lyapunov exponent for systems with parameter drift: `lyapunov_instant` uses slope of the ensemble-averaged pairwise distance function returned by `ensemble_averaged_pairwise_distance` to assess chaoticity at a certain time
6+
37
# v3.3.1
48

59
After 7 years, we only now realized that `lyapunov` gave incorrect results for fixed points of continuous time systems. We've now fixed that. Unfortunately this decreases the computational performance of the function overall, but correctness is more important.

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ChaosTools"
22
uuid = "608a59af-f2a3-5ad4-90b4-758bdf3122a7"
33
repo = "https://github.com/JuliaDynamics/ChaosTools.jl.git"
4-
version = "3.3.1"
4+
version = "3.4.0"
55

66
[deps]
77
Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"

docs/src/lyapunovs.md

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ Colorbar(fig[1,2], obj)
134134
fig
135135
```
136136

137-
138137
## Lyapunov exponent from data
139138

140139
```@docs
@@ -192,4 +191,75 @@ fig
192191

193192
As you can see, using `τ = 15` is not a great choice! The estimates with
194193
`τ = 7` though are very good (the actual value is around `λ ≈ 0.89...`).
195-
Notice that above a linear regression was done over the whole curves, which doesn't make sense. One should identify a linear scaling region and extract the slope of that one. The function `linear_region` from [FractalDimensions.jl](https://github.com/JuliaDynamics/FractalDimensions.jl) does this!
194+
Notice that above a linear regression was done over the whole curves, which doesn't make sense. One should identify a linear scaling region and extract the slope of that one. The function `linear_region` from [FractalDimensions.jl](https://github.com/JuliaDynamics/FractalDimensions.jl) does this!
195+
196+
197+
## Instantaneous Lyapunov exponent for non-autonomous systems
198+
```@docs
199+
ensemble_averaged_pairwise_distance
200+
```
201+
202+
```@docs
203+
lyapunov_instant
204+
```
205+
206+
Let's see first if the ensemble approach is equivalent to the usual time-averaging case (Benettin algorithm) in the autonomous case.
207+
Here is a simple example using the Henon map:
208+
```@example MAIN
209+
henon_rule(x, p, n) = SVector{2}(1.0 - p[1]*x[1]^2 + x[2], p[2]*x[1])
210+
ds = DeterministicIteratedMap(henon_rule, zeros(2), [1.4, 0.3, 0.0])
211+
212+
init_states = StateSpaceSet(0.2 .* rand(1000,2))
213+
pidx = 3 # set to dummy, not used anywhere (no drift)
214+
ρ,times = ensemble_averaged_pairwise_distance(ds,init_states,100,pidx;Ttr=5000)
215+
λ_inst = lyapunov_instant(ρ,times;interval=20:30) #fit to middle part of the curve (slope is constant until saturation)
216+
λ = lyapunov(ds,1000;Ttr=5000) #standard (Benettin) way
217+
@show λ_inst, λ
218+
```
219+
220+
Now look at the nonautonomous Duffing map with drifting ε parameter:
221+
```@example MAIN
222+
223+
using CairoMakie
224+
using ChaosTools
225+
226+
function duffing_drift(u0 = [0.1, 0.25]; ω = 1.0, β = 0.2, ε0 = 0.4, α=0.00045)
227+
return CoupledODEs(duffing_drift_rule, u0, [ω, β, ε0, α])
228+
end
229+
230+
@inbounds function duffing_drift_rule(x, p, t)
231+
ω, β, ε0, α = p
232+
dx1 = x[2]
233+
dx2 = (ε0+α*t)*cos(ω*t) + x[1] - x[1]^3 - 2β * x[2]
234+
return SVector(dx1, dx2)
235+
end
236+
237+
duffing = duffing_drift()
238+
duffing_map = StroboscopicMap(duffing,2π)
239+
init_states = randn(5000,2) #use an ensemble of 5000
240+
pidx = 4 #ε is the fourth parameter
241+
ρ,times = ensemble_averaged_pairwise_distance(duffing_map,StateSpaceSet(init_states),100,pidx;Ttr=20)
242+
243+
#measure slope of ρ at two places
244+
λ_inst = lyapunov_instant(ρ,times;interval=5:10)
245+
λ_inst2 = lyapunov_instant(ρ,times;interval=22:25)
246+
247+
fig,ax,obj = scatter(times, ρ,
248+
markersize = 6,
249+
color = :gray10,
250+
label = L"\omega = 1, \beta = 0.2, \epsilon_0 = 0.4, \alpha=0.00045",
251+
axis = (xlabel = L"t", ylabel = L"\rho(t)",xlabelsize = 20,ylabelsize = 20))
252+
253+
lines!(ax, times[5:10], ρ[5] .+ λ_inst*[0:5;], color = (:red, 0.7),linewidth = 3)
254+
lines!(ax, times[22:25], ρ[22] .+ λ_inst2*[0:3;],color = (:red, 0.7), linewidth = 3)
255+
256+
text!(ax, times[5]+8, ρ[10],text = L"\lambda = %$(round(λ_inst;digits=3))",
257+
color = :red,align = (:left, :center),fontsize = 18)
258+
259+
text!(ax, times[22]+8, ρ[24],text = L"\lambda = %$(round(λ_inst2;digits=3))",
260+
color = :red, align = (:left, :center), fontsize = 18)
261+
262+
axislegend(ax, position = :rb, nbanks = 2,labelsize = 18)
263+
fig
264+
```
265+

src/ChaosTools.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ include("periodicity/davidchacklai.jl")
2626
include("rareevents/mean_return_times/mrt_api.jl")
2727

2828
include("chaosdetection/lyapunovs/lyapunov.jl")
29+
include("chaosdetection/lyapunovs/EAPD.jl")
2930
include("chaosdetection/lyapunovs/lyapunov_from_data.jl")
3031
include("chaosdetection/lyapunovs/lyapunovspectrum.jl")
3132
include("chaosdetection/lyapunovs/local_growth_rates.jl")
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
export ensemble_averaged_pairwise_distance,lyapunov_instant
2+
3+
"""
4+
ensemble_averaged_pairwise_distance(ds, init_states::StateSpaceSet, T, pidx;kwargs...) -> ρ,t
5+
6+
Calculate the ensemble-averaged pairwise distance function `ρ` for non-autonomous dynamical systems
7+
with a time-dependent parameter, using the metod described by [^Jánosi,Tél2024]. Time-dependence is assumed to be a linear drift. The rate of change
8+
of the parameter needs to be stored in the parameter container of the system `p = current_parameters(ds)`,
9+
at the index `pidx`. In case of autonomous systems (with no drift), `pidx` can be set to any index as a dummy.
10+
To every member of the ensemble `init_states`, a perturbed initial condition is assigned.
11+
`ρ(t)` is the natural log of state space distance between the original and perturbed states averaged
12+
over all pairs, calculated for all time steps up to `T`.
13+
14+
15+
## Keyword arguments
16+
17+
* `initial_params = deepcopy(current_parameters(ds))`: initial parameters
18+
* `Ttr = 0`: transient time used to evolve initial states to reach
19+
initial autonomous attractor (without drift)
20+
* `perturbation = perturbation_normal`: if given, it should be a function `perturbation(ds,ϵ)`,
21+
which outputs perturbed state vector of `ds` (preferrably `SVector`). If not given, a normally distributed
22+
random perturbation with norm `ϵ` is added.
23+
* `Δt = 1`: step size
24+
* `ϵ = sqrt(dimension(ds))*1e-10`: initial distance between pairs of original and perturbed initial conditions
25+
26+
27+
## Description
28+
In non-autonomous systems with parameter drift, long time averages are less useful to assess chaoticity.
29+
Thus, quantities using time averages are rather calculated using ensemble averages. Here, a new
30+
quantity called the Ensemble-averaged pairwise distance (EAPD) is used to measure chaoticity of
31+
the snapshot attractor/ state space object traced out by the ensemble [^JánosiTél2024].
32+
33+
To any member of the original ensemble (`init_states`) a close neighbour (test) is added at an initial distance `ϵ`. Quantity `d(t)` is the
34+
state space distance between a test particle and an ensemble member at time t .
35+
If `init_states` are randomly initialized (far from the attractor at the initial parameter), and there's no transient,
36+
the first few time steps cannot be used to calculate any reliable averages.
37+
The function of the EAPD `ρ(t)` is defined as the average logarithmic distance between original and
38+
perturbed initial conditions at every time step: `ρ(t) = ⟨ln d(t)⟩`
39+
40+
An analog of the classical largest Lyapunov exponent can be extracted from the
41+
EAPD function `ρ`: the local slope can be considered an instantaneous Lyapunov exponent.
42+
43+
## Example
44+
Example of a rate-dependent (linearly drifting parameter) system
45+
46+
```julia
47+
#r parameter is replaced by r(n) = r0 + R*n drift term
48+
function drifting_logistic(x,p,n)
49+
r0,R = p
50+
return SVector( (r0 + R*n)*x[1]*(1-x[1]) )
51+
end
52+
53+
r0 = 3.8 #inital parameter
54+
R = 0.001 #rate parameter
55+
p = [r0,R] # pidx = 2
56+
57+
init_states = StateSpaceSet(rand(1000)) #initial states of the ensemble
58+
ds = DeterministicIteratedMap(drifting_logistic, [0.1], p)
59+
ρ,times = ensemble_averaged_pairwise_distance(ds,init_states,100,2;Ttr=1000)
60+
```
61+
62+
[^JánosiTél2024]: Dániel Jánosi, Tamás Tél, Phys. Rep. **1092**, pp 1-64 (2024)
63+
"""
64+
function ensemble_averaged_pairwise_distance(ds,init_states::StateSpaceSet,T,pidx;
65+
initial_params = deepcopy(current_parameters(ds)),Ttr=0,perturbation=perturbation_normal,Δt = 1=sqrt(dimension(ds))*1e-10)
66+
67+
set_parameters!(ds,initial_params)
68+
original_rate = current_parameter(ds, pidx)
69+
N = length(init_states)
70+
d = dimension(ds)
71+
dimension(ds) != d && throw(AssertionError("Dimension of `ds` doesn't match dimension of states in init_states!"))
72+
73+
nt = length(0:Δt:T) #number of time steps
74+
ρ = zeros(nt) #store ρ(t)
75+
times = zeros(nt) #store t
76+
77+
#duplicate every state
78+
#(add test particle to every ensemble member)
79+
init_states_plus_copies = StateSpaceSet(vcat(init_states,init_states))
80+
81+
#create a pds for the ensemble
82+
#pds is a ParallelDynamicalSystem
83+
pds = ParallelDynamicalSystem(ds,init_states_plus_copies)
84+
85+
#set to non-drifting for initial ensemble
86+
set_parameter!(pds,pidx,0.0)
87+
88+
#step system pds to reach attractor(non-drifting)
89+
#system starts to drift at t0=0.0
90+
for _ in 0:Δt:Ttr
91+
step!(pds,Δt,true)
92+
end
93+
94+
#rescale test states
95+
#add perturbation to test states
96+
for i in 1:N
97+
state_i = current_state(pds,i)
98+
perturbed_state_i = state_i .+ perturbation(ds,ϵ)
99+
#set_state!(pds.systems[N+i],perturbed_state_i)
100+
set_state!(pds,perturbed_state_i,N+i)
101+
end
102+
103+
#set to drifting for initial ensemble
104+
set_parameter!(pds,pidx,original_rate)
105+
106+
#set back time to t0 = 0
107+
reinit!(pds,current_states(pds))
108+
109+
#calculate EAPD for each time step
110+
ensemble_averaged_pairwise_distance!(ρ,times,pds,T,Δt)
111+
return ρ,times
112+
113+
end
114+
115+
#calc distance for every time step until T
116+
function ensemble_averaged_pairwise_distance!(ρ,times,pds,T,Δt)
117+
for (i,t) in enumerate(0:Δt:T)
118+
ρ[i] = ensemble_averaged_pairwise_distance(pds)
119+
times[i] = current_time(pds)
120+
step!(pds,Δt,true)
121+
end
122+
end
123+
124+
#calc distance for current states of pds
125+
function ensemble_averaged_pairwise_distance(pds)
126+
127+
states = current_states(pds)
128+
N = Int(length(states)/2)
129+
130+
#calculate distance averages
131+
ρ = 0.0
132+
for i in 1:N
133+
ρ += log.(norm(states[i] - states[N+i]))
134+
end
135+
return ρ/N
136+
137+
end
138+
139+
function perturbation_normal(ds,ϵ)
140+
D, T = dimension(ds), eltype(ds)
141+
p0 = randn(SVector{D, T})
142+
p0 = ϵ * p0 / norm(p0)
143+
end
144+
145+
"""
146+
lyapunov_instant(ρ,times;interval=1:length(times)) -> λ(t)
147+
148+
Calculates the instantaneous Lyapunov exponent by taking the slope of
149+
the ensemble-averaged pairwise distance function `ρ` wrt. to the saved time points `times` in `interval`.
150+
151+
"""
152+
function lyapunov_instant(ρ,times;interval=eachindex(times))
153+
_,s = linreg(times[interval], ρ[interval]) #return estimated slope
154+
return s
155+
end

test/chaosdetection/EAPD.jl

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using ChaosTools, Test
2+
using LinearAlgebra
3+
4+
henon_rule(x, p, n) = SVector{2}(1.0 - p[1]*x[1]^2 + x[2], p[2]*x[1])
5+
henon() = DeterministicIteratedMap(henon_rule, zeros(2), [1.4, 0.3, 0.0]) # add third dummy parameter
6+
7+
#test if ensemble averaging gives the same as
8+
#the usual lyapunov exponent for autonomous system
9+
@testset "time averaged and ensemble averaged lyapunov exponent" begin
10+
ds = henon()
11+
12+
#eapd slope
13+
init_states = StateSpaceSet(0.2 .* rand(1000,2))
14+
pidx = 3 # set to dummy, not used anywhere (no drift)
15+
ρ,times = ensemble_averaged_pairwise_distance(ds,init_states,100,pidx;Ttr=5000)
16+
lyap_instant = lyapunov_instant(ρ,times;interval=20:30)
17+
18+
#lyapunov exponent
19+
λ = lyapunov(ds,1000;Ttr=5000)
20+
@test isapprox(lyap_instant,λ;atol=0.05)
21+
end
22+
23+
#test sliding Duffing map
24+
#-------------------------duffing stuff-----------------------
25+
#https://doi.org/10.1016/j.physrep.2024.09.003
26+
27+
function duffing_drift(u0 = [0.1, 0.25]; ω = 1.0, β = 0.2, ε0 = 0.4, α=0.00045)
28+
return CoupledODEs(duffing_drift_rule, u0, [ω, β, ε0, α])
29+
end
30+
31+
@inbounds function duffing_drift_rule(x, p, t)
32+
ω, β, ε0, α = p
33+
dx1 = x[2]
34+
dx2 = (ε0+α*t)*cos*t) + x[1] - x[1]^3 - 2β * x[2]
35+
return SVector(dx1, dx2)
36+
end
37+
38+
@testset "Duffing map" begin
39+
#----------------------------------hamiltonian case--------------------------------------
40+
duffing = duffing_drift(;β = 0.0=0.0,ε0=0.08) #no dissipation -> Hamiltonian case
41+
duffing_map = StroboscopicMap(duffing,2π)
42+
init_states_auto,_ = trajectory(duffing_map,5000,[-0.85,0.0];Ttr=0) #initial condition for a snapshot torus
43+
#set system to sliding
44+
set_parameter!(duffing_map,4,0.0005)
45+
pidx=4
46+
47+
ρ,times = ensemble_averaged_pairwise_distance(duffing_map,init_states_auto,100,pidx;Ttr=0)
48+
lyap_instant = lyapunov_instant(ρ,times;interval=50:60)
49+
@test isapprox(lyap_instant,0.87;atol=0.01) #0.87 approximate value from article
50+
51+
#-----------------------------------dissipative case------------------------------------
52+
duffing = duffing_drift()
53+
duffing_map = StroboscopicMap(duffing,2π)
54+
init_states = randn(5000,2)
55+
ρ,times = ensemble_averaged_pairwise_distance(duffing_map,StateSpaceSet(init_states),100,pidx;Ttr=20)
56+
lyap_instant = lyapunov_instant(ρ,times;interval=2:20)
57+
@test isapprox(lyap_instant,0.61;atol=0.01) #0.61 approximate value from article
58+
59+
end

test/chaosdetection/lyapunovs.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ end
6464
end
6565

6666
ds = CoupledODEs(lorenz_rule, fill(10.0, 3), [10, 20, 8/3])
67-
@test lyapunov(ds, 1000, Ttr = 100) 0 atol = 1e-3
67+
@test lyapunov(ds, 1000, Ttr = 100) 0 atol = 1e-2
6868
end
6969
end
7070

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ testfile("chaosdetection/gali.jl")
2525
testfile("chaosdetection/partially_predictable.jl")
2626
testfile("chaosdetection/01test.jl")
2727
testfile("chaosdetection/expansionentropy.jl")
28+
testfile("chaosdetection/EAPD.jl")
2829

2930
testfile("stability/fixedpoints.jl")
3031
testfile("periodicity/periodicorbits.jl")

0 commit comments

Comments
 (0)