Skip to content

Instantaneous Lyapunov exponent for systems with parameter drift #345

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/ChaosTools.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ include("periodicity/davidchacklai.jl")
include("rareevents/mean_return_times/mrt_api.jl")

include("chaosdetection/lyapunovs/lyapunov.jl")
include("chaosdetection/lyapunovs/EAPD.jl")
include("chaosdetection/lyapunovs/lyapunov_from_data.jl")
include("chaosdetection/lyapunovs/lyapunovspectrum.jl")
include("chaosdetection/lyapunovs/local_growth_rates.jl")
Expand Down
116 changes: 116 additions & 0 deletions src/chaosdetection/lyapunovs/EAPD.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
export ensemble_averaged_pairwise_distance,lyapunov_instant

"""
lyapunov_instant(ρ,times;interval=1:length(times)) -> λ(t)

Convenience function that calculates the instantaneous Lyapunov exponent by taking the slope of
the ensemble-averaged pairwise distance function `ρ` wrt. to the saved time points `times` in `interval`.
"""
function lyapunov_instant(ρ,times;interval=1:length(times))
_,s = linreg(times[interval], ρ[interval]) #return estimated slope
return s
end

"""
ensemble_averaged_pairwise_distance(ds,init_states::StateSpaceSet,T;kwargs...) -> ρ,t

Calculate the ensemble-averaged pairwise distance function `ρ` for non-autonomous dynamical systems
with a time-dependent parameter. Time-dependence is assumed to be linear (sliding). To every member
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this function is supposed to do just by reading the docstring. We need to clarify some things: is the input ds expected to be autonomous or NON- ? And, is it mandatory that the system is in a parameter drift scenario, or there can be arbitrary non-autonomous behavior? If it can only be a drift, does the parameter drift happens inside ds or do you create it inside this new function? If it happens inside the ds, why do you need sliding_param_rate_index? And then you set this parameter to 0...?

Then inside the function I see this is done so that the ds reaches an attractor. But this is not said in the function.

I think this function is non-trivial and therefore warrants a ## Description section.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I didn't explain the assumptions I'd made about the function itself.
I assumed that the parameter drift happens inside ds, meaning that the user already has a linearly drifting parameter in the definition. Let's take the system from above as an example here:

function duffing_drift(u0 = [0.1, 0.25]; ω = 1.0, β = 0.2, ε0 = 0.4, α=0.00045)
   return CoupledODEs(duffing_drift_rule, u0, [ω, β, ε0, α])
end

@inbounds function duffing_drift_rule(x, p, t)
           ω, β, ε0, α = p
           dx1 = x[2]
           dx2 = (ε0+α*t)*cos*t) + x[1] - x[1]^3 - 2β * x[2]
           return SVector(dx1, dx2)
end

where ε parameter is replaced by the drifting term (ε0+α*t). I also assume that the drift rate α is in p. This is a simple way to accomplish drifting without extending the phase space.

I am also wondering why you do this "convergence to attractor" step first. Is it described in the paper?

Yes. You need some time to reach the snapshot attractor.
In practice, the convergence time is the time after which the averages of the phase space variables taken with respect to the ensembles are practically identical, although time dependent.
If init_states are randomly initialized (far from the attractor at the initial parameter), and there's no transient, the first few time steps cannot be used to calculate any reliable averages.

Because of this, there is a Ttr transient time option, which is spent on converging to the attractor at ε0, before the drift even starts happening. In other words, the drift needs to be switched off (drift rate α = 0) for this step. In order to switch it off, I need to know which parameter I need to set to 0, this is why sliding_param_rate_index
exists. This was the simplest way I could think of, but maybe there's a better way.

Now, what if you want to use this function in an autonomous setting (like I did in the tests with the Henon system)? Then you don't have a drift rate among the parameters, there is no need to switch off anything, in which case the default is sliding_param_rate_index=0 and the function still works.
Let me know if this makes sense.

As far as I know there is no standard way to create a drifting system. (Maybe here but I'm not sure.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears to me that CriticalTransitions.jl discusses a "drift" system in the opening image but nowhere in the stable docs is this mentioned or implemented. So we can safely ignore that.

Right, your approach is fine with me: we expect a parameter index pidx (mandatory input argument, and not optional keyword argument), and simply explain how this process works. that it is expected that p[pidx] multiplies time t and it is the "rate" parameter. For the non-drifiting henon map test just make a 3-parameter henon map where the 3rd parmaeter is not used anywhere. Then give pidx = 3.

Do you mind going ahead with adding the descriptions etc in the source code/docstring?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @reykboerner or @ryandeeley I don't know if you are still working on CriticalTransitions.jl and what is the status for the drifting systems.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh and @rusandris you shouldn't mess with the whole parameter container. Just do original_rate = current_parameter(ds, pidx) and then after setting it to zero do set_parameter!(ds, pidx, original_rate) to restore it.

Copy link

@ryandeeley ryandeeley Apr 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @reykboerner or @ryandeeley I don't know if you are still working on CriticalTransitions.jl and what is the status for the drifting systems.

Hi, the status regarding "drifting systems" is that @raphael-roemer and I have developed code for passing a user-defined ds::CoupledODEs (describing the system of interest in an autonomous form) and a rp::RateProtocol (describing the desired time-dependent forcing, or "drifting", one wishes to apply to the system) to a function RateSystem(), which returns a CoupledODEs describing the corresponding non-autonomous system. In particular the function RateSystem() creates a modified drift function f(u,p,t) from the information of ds.integ.f and rp. The code is available in the RateSystem branch of CriticalTransitions.jl (https://github.com/JuliaDynamics/CriticalTransitions.jl/tree/RateSystem), specifically in the file /test/ratesystem/RateSystemDraft2.jl, and the discussion surrounding this can be found in this pull request: JuliaDynamics/CriticalTransitions.jl#117. Documentation hasn't been written yet, I suppose in an ideal world that'll be done soon. I'm happy to help with anything if you'd like to implement this and cannot understand things just from the scripts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that's good to know. @rusandris does this method you are implementing rely on a linear drift? I.e., it mandates a term p*t with t time? Or time dependence can be arbitrary?

For now we will proceed as is in this PR with pidx. In the future, when the drifting system is available as public API we can use it instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it relies on linear drift, but it can be generalized further in the future if needed.

of the ensemble `init_states`, a perturbed initial condition is assigned. `ρ(t)` is the natural log
of phase space distance between the original and perturbed states averaged over all pairs, calculated
for all time steps up to `T`.

This function implements the method described in
https://doi.org/10.1016/j.physrep.2024.09.003.

## Keyword arguments

* `sliding_param_rate_index = 0`: index of the parameter that gives the rate of change of the sliding parameter
* `initial_params = deepcopy(current_parameters(ds))`: initial parameters
* `Ttr = 0`: transient time used to evolve initial states to reach
initial autonomous attractor (without sliding)
* `perturbation = perturbation_uniform`: if given, it should be a function `perturbation(ds,ϵ)`,
which outputs perturbed state vector of `ds` (preferrably `SVector`). If not given, a normally distributed
random perturbation with norm `ϵ` is added.
* `Δt = 1`: step size
* `ϵ = sqrt(dimension(ds))*1e-10`: initial distance between pairs of original and perturbed initial conditions
"""
function ensemble_averaged_pairwise_distance(ds,init_states::StateSpaceSet,T;sliding_param_rate_index=0,
initial_params = deepcopy(current_parameters(ds)),Ttr=0,perturbation=perturbation_uniform,Δt = 1,ϵ=sqrt(dimension(ds))*1e-10)

set_parameters!(ds,initial_params)
N = length(init_states)
d = dimension(ds)
dimension(ds) != d && throw(AssertionError("Dimension of `ds` doesn't match dimension of states in init_states!"))

nt = length(0:Δt:T) #number of time steps
ρ = zeros(nt) #store ρ(t)
times = zeros(nt) #store t

#duplicate every state
#(add test particle to every ensemble member)
init_states_plus_copies = StateSpaceSet(vcat(init_states,init_states))

#create a pds for the ensemble
#pds is a ParallelDynamicalSystem
pds = ParallelDynamicalSystem(ds,init_states_plus_copies)

#set to non-drifting for initial ensemble
sliding_param_rate_index != 0 && set_parameter!(pds,sliding_param_rate_index,0.0)

#step system pds to reach attractor(non-drifting)
#system starts to drift at t0=0.0
for _ in 0:Δt:Ttr
step!(pds,Δt,true)
end

#rescale test states
#add perturbation to test states
for i in 1:N
state_i = current_state(pds,i)
perturbed_state_i = state_i .+ perturbation(ds,ϵ)
#set_state!(pds.systems[N+i],perturbed_state_i)
set_state!(pds,perturbed_state_i,N+i)
end

#set to drifting for initial ensemble
set_parameters!(pds,initial_params)

#set back time to t0 = 0
reinit!(pds,current_states(pds))

#calculate EAPD for each time step
ensemble_averaged_pairwise_distance!(ρ,times,pds,T,Δt)
return ρ,times

end

#calc distance for every time step until T
function ensemble_averaged_pairwise_distance!(ρ,times,pds,T,Δt)
for (i,t) in enumerate(0:Δt:T)
ρ[i] = ensemble_averaged_pairwise_distance(pds)
times[i] = current_time(pds)
step!(pds,Δt,true)
end
end

#calc distance for current states of pds
function ensemble_averaged_pairwise_distance(pds)

states = current_states(pds)
N = Int(length(states)/2)

#calculate distance averages
ρ = 0.0
for i in 1:N
ρ += log.(norm(states[i] - states[N+i]))
end
return ρ/N

end

function perturbation_uniform(ds,ϵ)
D, T = dimension(ds), eltype(ds)
p0 = randn(SVector{D, T})
p0 = ϵ * p0 / norm(p0)
end
57 changes: 57 additions & 0 deletions test/chaosdetection/EAPD.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using ChaosTools, Test
using LinearAlgebra

henon_rule(x, p, n) = SVector{2}(1.0 - p[1]*x[1]^2 + x[2], p[2]*x[1])
henon() = DeterministicIteratedMap(henon_rule, zeros(2), [1.4, 0.3])

#test if ensemble averaging gives the same as
#the usual lyapunov exponent for autonomous system
@testset "time averaged and ensemble averaged lyapunov exponent" begin
ds = henon()

#eapd slope
init_states = StateSpaceSet(0.2 .* rand(5000,2))
ρ,times = ensemble_averaged_pairwise_distance(ds,init_states,100;Ttr=1000,sliding_param_rate_index=0)
lyap_instant = lyapunov_instant(ρ,times;interval=10:20)

#lyapunov exponent
λ = lyapunov(ds,1000;Ttr=1000)
@test isapprox(lyap_instant,λ;atol=0.01)
end

#test sliding Duffing map
#-------------------------duffing stuff-----------------------
#https://doi.org/10.1016/j.physrep.2024.09.003

function duffing_drift(u0 = [0.1, 0.25]; ω = 1.0, β = 0.2, ε0 = 0.4, α=0.00045)
return CoupledODEs(duffing_drift_rule, u0, [ω, β, ε0, α])
end

@inbounds function duffing_drift_rule(x, p, t)
ω, β, ε0, α = p
dx1 = x[2]
dx2 = (ε0+α*t)*cos(ω*t) + x[1] - x[1]^3 - 2β * x[2]
return SVector(dx1, dx2)
end

@testset "Duffing map" begin
#----------------------------------hamiltonian case--------------------------------------
duffing = duffing_drift(;β = 0.0,α=0.0,ε0=0.08) #no dissipation -> Hamiltonian case
duffing_map = StroboscopicMap(duffing,2π)
init_states_auto,_ = trajectory(duffing_map,5000,[-0.85,0.0];Ttr=0) #initial condition for a snapshot torus
#set system to sliding
set_parameter!(duffing_map,4,0.0005)

ρ,times = ensemble_averaged_pairwise_distance(duffing_map,init_states_auto,100;Ttr=0,sliding_param_rate_index=4)
lyap_instant = lyapunov_instant(ρ,times;interval=50:60)
@test isapprox(lyap_instant,0.87;atol=0.01) #0.87 approximate value from article

#-----------------------------------dissipative case------------------------------------
duffing = duffing_drift() #no dissipation -> Hamiltonian case
duffing_map = StroboscopicMap(duffing,2π)
init_states = randn(5000,2)
ρ,times = ensemble_averaged_pairwise_distance(duffing_map,StateSpaceSet(init_states),100;Ttr=20,sliding_param_rate_index=4)
lyap_instant = lyapunov_instant(ρ,times;interval=2:20)
@test isapprox(lyap_instant,0.61;atol=0.01) #0.61 approximate value from article

end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ testfile("chaosdetection/gali.jl")
testfile("chaosdetection/partially_predictable.jl")
testfile("chaosdetection/01test.jl")
testfile("chaosdetection/expansionentropy.jl")
testfile("chaosdetection/EAPD.jl")

testfile("stability/fixedpoints.jl")
testfile("periodicity/periodicorbits.jl")
Expand Down
Loading