Exploring Chaos with our Duffing Simulator

Exploring Chaos with our Duffing Simulator#

Here is my version of the Duffing Simulator. I added a few extra bells and whistles, but it is essentially the function you wrote in class.

New elements:

  • I draw the potential surface

  • When you click, you produce not only an oscillator with the parameters corresponding to that point, but a whole ring of initial conditions around it.

  • I have sliders to control everything.

Note: I put pretty much everything in my constructor as one monolithic block. It makes more sense to do it like we did with the pendulum, and write functions that do each piece.

using GLMakie
using DataStructures: CircularBuffer
using LaTeXStrings

mutable struct duffing
    # Basic Scene info
    fig # the Makie Figure object
    ax # the Makie Axis object
    # Time evolution info
    xv # the phase space position of the oscillator
    t # the current time
    dt # the step size
    step # our stepper -- we will use rk4step as the default
    dxdt # the derivitive rule
    p # parameters of the differential equation -- a struct
      # with field F,ω,α,β,γ
    # UI features
    delay # how long to wait between frames
    ball # the observable which will be plotted
    traj # stores past points for drawing a "tail"
    ptraj # not used
    sliders # named tuple of UI elements to control force and frequency
            # Can again use the same struct that we used for p,
            # with attributes F,ω,α,β,γ
    run # button to turn on and off animation
    isrunning # is simulation running?
    potential # stored values of the potential for plotting
    grad # stored values of the gradient for plotting
    bg
    burst
end

mutable struct dp
    F ::Float64
    ω ::Float64
    α ::Float64
    β ::Float64
    γ ::Float64
end

dp(;F,ω,α,β,γ)=dp(F,ω,α,β,γ)

function rk4step(x,p,dxdt,t,deltat)
    k1=dxdt(x,p,t)
    k2=dxdt(x+k1*(deltat/2),p,t+deltat/2)
    k3=dxdt(x+k2*(deltat/2),p,t+deltat/2)
    k4=dxdt(x+k3*deltat,p,t+deltat)
    return x+(k1+2*k2+2*k3+k4)*(deltat/6)
end

function duf_dxvdt(xv,p,t)
    x,v = xv
    return[v,-2*p.γ*v-p.α*x-p.β*x^3+p.F*cos(p.ω*t)]
end

function setxv!(d::duffing,xv)
    d.xv=xv
    d.ball[]=Point2f(xv...)
    push!(d.traj[], Point2f(xv...))
    notify(d.traj) # needed to tell GLMakie that we updated traj -- push! doesn't send that signal
    d
end

function step!(d::duffing)
    xv=d.step(d.xv,d.p,d.dxdt,d.t,d.dt)
    setxv!(d,xv)
    d.t+=d.dt
    p=d.p
    F=p.F
    t=d.t
    ω=p.ω
    d.bg[]=d.potential-F*cos(ω*t).*d.grad
    d.burst[]=[d.step(w,d.p,d.dxdt,d.t,d.dt) for w in d.burst[]]
    d
end

function duffing(;
        xv=[0.5,0.],
        p=dp(α=-1,β=1,γ=0.1,F=0.,ω=1.),
        t=0.,
        dt=0.02,
        delay=0.00001,
        step=rk4step,
        dxdt=duf_dxvdt,tail=300)
    # Create Figure object
    # 
    fig = Figure(); display(fig)
    ax = Axis(fig[1,1])
    ax.title = "Duffing"
    ax.aspect = DataAspect() 
    # create background
    xlist=range(-2,2,100)
    ylist=range(-2,2,100)
    potential=[p.α*x^2/2+p.β*x^4/4+y^2/2 for x in xlist, y in ylist];
    grad=[x for x in xlist, y in ylist];
    bg=Observable(potential+0. .* grad);
    image!(ax,(-2,2),(-2,2),bg)
    contour!(ax,(-2,2),(-2,2),bg,levels=20)
    #
    # Create ball object, and add it to scene
    #
    ball=Observable(Point2f(xv...))
    scatter!(ax, ball; marker = :circle, strokewidth = 2, 
            strokecolor = :purple,
            color = :black, markersize = 8)
    # run button
    # run button
    isrunning = Observable(false)
    label = map(cond -> cond ? "Stop" : "Run", isrunning)
    run = Button(fig[2,1]; label = label, tellwidth = false)
    on(run.clicks) do clicks; isrunning[] = !isrunning[]; end
    # Make Sliders
    Label(fig[1,2][1,1],L"\frac{dv}{dt}=-2\gamma v-\alpha x-\beta x^3 +F \cos(\omega t).")
    sg=SliderGrid(fig[1,2][2,1],
    (label="F",range=0.:0.01:1.,format = "{:.2f}",startvalue=p.F),
    (label="ω",range=0.01:0.01:2,format = "{:.2f}",startvalue=p.ω),
    (label="γ",range=0.0:0.01:1,format = "{:.2f}",startvalue=p.γ),
    width=200,tellheight=false)
    Fobserver=sg.sliders[1].value;
    ωobserver=sg.sliders[2].value;
    γobserver=sg.sliders[3].value;
    on(Fobserver) do Fobserver
        p.F=Fobserver[]
    end
    on(ωobserver) do ωobserver
        p.ω=ωobserver[]
    end
    on(γobserver) do γobserver
        p.γ=γobserver[]
    end
    sg2=SliderGrid(fig[1,2][3,1],
    (label="α",range=-2.:0.01:2.,format = "{:.2f}",startvalue=p.α),
    (label="β",range=0.:0.01:1.,format = "{:.2f}",startvalue=p.β),
    width=200,tellheight=false)
    αobserver=sg2.sliders[1].value;
    βobserver=sg2.sliders[2].value;
    sg3=SliderGrid(fig[2,2],
    (label="dt",range=0.:0.002:0.2,format = "{:.2f}",startvalue=dt),
    width=200,tellheight=false)
    dtobserver=sg3.sliders[1].value;
    # code moved below instantiation
    # Now lets make the trail
    traj = CircularBuffer{Point2f}(tail)
    fill!(traj, Point2f(xv...))
    traj = Observable(traj)
    c = to_color(:purple)
    tailcol = [RGBAf(c.r, c.g, c.b, (i/tail)^2) for i in 1:tail]
    lines!(ax, traj; linewidth = 3, color = tailcol)
    #Burst
    burst=Observable([Point2f(xv[1]+0.1*cos(θ),xv[2]+0.1*sin(θ)) for θ in 0.:2*pi/1000:2*pi])
    scatter!(ax,burst)
    #Create object
    d= duffing(
        fig #= the Makie Figure object that you created=#,
        ax #= the Makie Axis object that you created=#,
        xv #= initial conditions passed from constructor=#,
        t #= the current time passed from constructor=#,
        dt #= the step size passed from constructor=#,
        step #= our stepper passed from constructor=#,
        dxdt #= the derivitive rule passed from constructor=#,
        p #= passed from constructor=#,
        delay #= passed from constructor=#,
        ball #= the observable that you created =#,
        traj #=traj=#,
        [] #=ptraj -- empty list =#,
        sg #=sliders  =#,
        run #=run  =#,
        isrunning #=isrunning=#,
        potential #=potential=#,
        grad #=gradient=#,
        bg,burst)
    on(dtobserver) do dtobserver
        d.dt=dtobserver[]
    end
    on(αobserver) do αobserver
        p.α=αobserver[]
        xlist=range(-2,2,100)
        ylist=range(-2,2,100)
        d.potential=[p.α*x^2/2+p.β*x^4/4+y^2/2 for x in xlist, y in ylist];
    end
    on(βobserver) do βobserver
        p.β=βobserver[]
        xlist=range(-2,2,100)
        ylist=range(-2,2,100)
        d.potential=[p.α*x^2/2+p.β*x^4/4+y^2/2 for x in xlist, y in ylist];
    end
    # Set up interactive
    Makie.deactivate_interaction!(ax, :rectanglezoom)
    spoint = select_point(ax.scene, marker = :circle)
    on(spoint) do z
        setxv!(d,z)
        burst[]=[Point2f(z[1]+0.1*cos(θ),z[2]+0.1*sin(θ)) for θ in 0.:2*pi/1000:2*pi]
    end
    startsim(d)
    xlims!(ax,-2,2)
    ylims!(ax,-2,2)
    #
    return d
end

function startsim(p::duffing)
    isrunning_notifier = Condition()
    on(cond -> cond && notify(isrunning_notifier), p.isrunning)
        errormonitor(@async while true
        if p.isrunning[]
            isopen(p.fig.scene) || break # stops if window is closed
            step!(p)
            sleep(p.delay) 
        else
            wait(isrunning_notifier)
        end
    end)
end
startsim (generic function with 1 method)
d1=duffing()
duffing(Scene (600px, 450px):
  0 Plots
  6 Child Scenes:
    ├ Scene (600px, 450px)
    ├ Scene (600px, 450px)
    ├ Scene (600px, 450px)
    ├ Scene (600px, 450px)
    ├ Scene (600px, 450px)
    └ Scene (600px, 450px), Axis (6 plots), [0.5, 0.0], 0.0, 0.02, rk4step, duf_dxvdt, dp(0.0, 1.0, -1.0, 1.0, 0.1), 1.0e-5, Observable(Float32[0.5, 0.0]), Observable(Point{2, Float32}[[0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0]  …  [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0], [0.5, 0.0]]), Any[], SliderGrid(), Button(), Observable(false), [4.0 3.9200081624324046 … 3.9200081624324046 4.0; 3.7664231813746545 3.6864313438070595 … 3.6864313438070595 3.7664231813746545; … ; 3.7664231813746545 3.6864313438070595 … 3.6864313438070595 3.7664231813746545; 4.0 3.9200081624324046 … 3.9200081624324046 4.0], [-2.0 -2.0 … -2.0 -2.0; -1.9595959595959596 -1.9595959595959596 … -1.9595959595959596 -1.9595959595959596; … ; 1.9595959595959596 1.9595959595959596 … 1.9595959595959596 1.9595959595959596; 2.0 2.0 … 2.0 2.0], Observable([4.0 3.9200081624324046 … 3.9200081624324046 4.0; 3.7664231813746545 3.6864313438070595 … 3.6864313438070595 3.7664231813746545; … ; 3.7664231813746545 3.6864313438070595 … 3.6864313438070595 3.7664231813746545; 4.0 3.9200081624324046 … 3.9200081624324046 4.0]), Observable(Point{2, Float32}[[0.6, 0.0], [0.599998, 0.0006283144], [0.5999921, 0.001256604], [0.59998226, 0.0018848439], [0.59996843, 0.0025130096], [0.5999507, 0.0031410758], [0.599929, 0.0037690182], [0.5999033, 0.004396812], [0.5998737, 0.0050244317], [0.59984016, 0.0056518535]  …  [0.59984016, -0.0056518535], [0.5998737, -0.0050244317], [0.5999033, -0.004396812], [0.599929, -0.0037690182], [0.5999507, -0.0031410758], [0.59996843, -0.0025130096], [0.59998226, -0.0018848439], [0.5999921, -0.001256604], [0.599998, -0.0006283144], [0.6, -2.4492935f-17]]))

## In-Class Activity 1

Everyone in class should start this running. Get in groups of 2 and play with the following. I’ll have one or two groups explain what they saw afterwards.

  1. Set \(\alpha=1,\beta=0,F=0,\gamma=0\). This is the simple harmonic oscillator. How does it behave?

  2. Turn on \(\gamma\), how does that change the behavior?

  3. Now turn on \(F\). You can increase/decrease the timestep to speed up or slow down the motion. Can you see resonant behavior? What happens when \(\omega\) is very small? Very big?

## In-Class Activity 2

  1. Now set \(\alpha=-1,\beta=1,F=0,\omega=1, \gamma=0\). There are 3 types of trajectories: Those that circle \((x,v)=(1,0)\), those that circle \((x,v)=(0,1)\), and those that circle both points.

  2. Turn on \(\gamma\). What happens to each of those 3 types of trajectories? Click on different starting points, and see what the “basins of attraction” are for the various fixed points. You can change dt to speed up or slow down the smulation.

  3. Now turn on a weak driving. Try \(F=0.1,\gamma=0.07\). Up the timestep so that the transients decay quickly. How many different stable orbits can you find? I found 4.

  4. Increase \(F\) to \(0.2\). The transients should start to get interesting. Run with a fast time-step to see if they die out.

  5. Now go to \(F=0.29\). You should get chaotic behavior here (at least for the right initial conditions). What is happening with the blue points? Can you come up with a story about what is happening with the phase space volume? Does this shed light on the “strange attractor” that you saw in the Poincare section?

## In-Class Activity 3

If you set the parameters just right, one can find regular trajectories whose period is a multiple of the drive period. The following gave me a trajectory whose period is twice the drive period: \(F=0.32,\omega=1.4,\gamma=0.05,\alpha=-1,\beta=1\). You might need to make a large timestep for a while to get rid of the transients. You may also need to try a couple initial conditions.

If I go to \(F=0.34\) I get one whose period is 4 times the drive period. To see it, up the timestep, so that the trails outline the whole multiple loop trajectory.

This is the “period doubling” route to chaos. As one approaches the chaotic point the periods get longer and longer, until they are infinite.