Computing Periodic Orbits (Oscillations) Using BifurcationKit.jl

Previously, we have shown how to compute bifurcation diagrams using BifurcationKit.jl. In this example we will consider a system which exhibits an oscillation and show how to use BifurcationKit to track not just the system's (potentially unstable) steady state, but also the periodic orbit itself. More information on how to track periodic orbits can be found in the BifurcationKit documentation.

Computing the bifurcation diagram for the Repressilator

We will first compute the bifurcation diagram, using the same approach as in the corresponding tutorial. For this example, we will use the oscillating Repressilator model.

using Catalyst
repressilator = @reaction_network begin
    hillr(Z,v,K,n), ∅ --> X
    hillr(X,v,K,n), ∅ --> Y
    hillr(Y,v,K,n), ∅ --> Z
    d, (X, Y, Z) --> ∅
end

\[ \begin{align*} \varnothing &\xrightarrow{\frac{K^{n} v}{K^{n} + Z^{n}}} \mathrm{X} \\ \varnothing &\xrightarrow{\frac{K^{n} v}{K^{n} + X^{n}}} \mathrm{Y} \\ \varnothing &\xrightarrow{\frac{K^{n} v}{K^{n} + Y^{n}}} \mathrm{Z} \\ \mathrm{X} &\xrightarrow{d} \varnothing \\ \mathrm{Y} &\xrightarrow{d} \varnothing \\ \mathrm{Z} &\xrightarrow{d} \varnothing \end{align*} \]

Next, we create a BifurcationProblem for our model. We will compute the bifurcation diagram with respect to the parameter $v$, and plot the species $X$ in the diagram.

using BifurcationKit
bif_par = :v
u_guess = [:X => 20.0, :Y => 15.0, :Z => 15.0]
p_start = [:v => 10.0, :K => 15.0, :n => 3, :d => 0.2]
plot_var = :X
bprob = BifurcationProblem(repressilator, u_guess, p_start, bif_par; plot_var)

We compute the bifurcation diagram using the bifurcationdiagram function. We will compute it across the interval $v \in (0,20)$.

v_span = (0.0, 20.0)
opts_br = ContinuationPar(p_min = v_span[1], p_max = v_span[2])
bifdia = bifurcationdiagram(bprob, PALC(), 3, opts_br; bothside = true)

Finally, we plot the bifurcation diagram.

using Plots
plot(bifdia; xguide = bif_par, yguide = plot_var, xlimit = v_span, branchlabel = "Steady state concentration",
    linewidthstable = 5)
Example block output

We note that for small values of $v$ the system's single steady state is stable (where the line is thicker). After a Hopf bifurcation (the red point), the state turns unstable (where the line is thinner). For chemical reaction networks (which mostly are well-behaved) a single unstable steady state typically corresponds to an oscillation. We can confirm that the system oscillates in the unstable region (while it reaches a stable steady state in the stable region) using simulations:

using OrdinaryDiffEqDefault
p_nosc = [:v => 5.0, :K => 15.0, :n => 3, :d => 0.2]
p_osc = [:v => 15.0, :K => 15.0, :n => 3, :d => 0.2]
prob_nosc = ODEProblem(repressilator, u_guess, 80.0, p_nosc)
prob_osc = ODEProblem(repressilator, u_guess, 80.0, p_osc)
sol_nosc = OrdinaryDiffEqDefault.solve(prob_nosc)
sol_osc = OrdinaryDiffEqDefault.solve(prob_osc)
plot(plot(sol_nosc; title = "v = 5"), plot(sol_osc; title = "v = 15"), size = (1000,400), lw = 4)
Example block output

Tracking the periodic orbits

Next, we will use BifurcationKit.jl's continuation function (the bifurcationdiagram function, which we previously have used, works by calling continuation recursively) to track the periodic orbit which appears with the Hopf bifurcation point.

First, we set the options for the continuation. Just like for bifurcation diagrams we must set our continuation parameters. Here we will use the same one as for the initial diagram (however, additional ones can be supplied).

opts_po = ContinuationPar(opts_br)

During the continuation we will compute the periodic orbit using the Collocation method (however, the Trapezoid and Shooting method also exist, with each having their advantages and disadvantages). For this, we create a PeriodicOrbitOCollProblem which we will supply to our continuation computation.

poprob = PeriodicOrbitOCollProblem(50, 4)

Finally, we will also create a record_from_solution function. This is a function which records information of the solution at each step of the continuation (which we can later plot or investigate using other means).

X_idx = findfirst(isequal(repressilator.X), unknowns(complete(convert(NonlinearSystem, repressilator))))
function record_from_solution(x, p; kwargs...)
 xtt = get_periodic_orbit(p.prob, x, p.p)
 min, max = extrema(xtt[1,:])
 period = getperiod(p.prob, x, p.p)
    return (; min, max, period)
end

Here, get_periodic_orbit computes the system's periodic orbit. From it we extract the $X$ species's minimum and maximum values (using extrema) and the period length (using getperiod). We return these quantities as a NamedTuple.

We can now compute the periodic orbit. First we must extract a branch from our bifurcation diagram (using get_branch), as continuation do not work on bifurcation diagrams directly (our bifurcation diagram consists of a single branch, which we here extract). Next, we can call continuation on it, designating that we wish to do continuation from the second point on the branch (which corresponds to the Hopf bifurcation, the first point is one of the branch's two endpoints).

branch = get_branch(bifdia, ()).γ
br_po = continuation(branch, 2, opts_po, poprob; record_from_solution)

Finally, we can plot the periodic orbit. We use the vars argument to designate what we wish to plot. Here we first provide (:param, :min) (designating that we wish to plot the min value returned by record_from_solution, i.e. the minimum value throughout the periodic orbit) against the continuation parameter's ($v$) value. Next, we plot the maximum periodic orbit value using (:param, :max).

plot(bifdia; xlimit = v_span, branchlabel = "Steady state concentration", linewidthstable = 5)
plot!(br_po, vars = (:param, :min); color = 2, branchlabel = "Oscillation amplitude (min/max)")
plot!(br_po, vars = (:param, :max); color = 2, xguide = bif_par, yguide = plot_var, branchlabel = "")
Example block output

Here we can see that, as $v$ increases, the oscillation amplitude increases with it.

Previously, we had record_from_solution record the periodic orbit's period. This means that we can plot it as well. Here, we plot it against $v$ using vars = (:param, :period).

plot(br_po, vars = (:param, :period); xguide = bif_par, yguide = "Period length", xlimit = v_span, ylimit = (0.0, Inf))
Example block output

In the plot we see that the period starts at around $18$ time units, and slowly increase with $v$.


Citation

If you use BifurcationKit.jl for your work, we ask that you cite the following paper!! Open source development strongly depends on this. It is referenced on HAL-Inria with bibtex entry CITATION.bib.