Continuation and bifurcations

Branches of solutions followed through folds by pseudo-arclength, with branch points, Hopf points, cycles and codimension-two curves located on the way.

Pseudo-arclength continuation follows a curve of solutions \(G(x, p) = 0\) by stepping along its tangent and correcting with Newton on the bordered system, so that folds are passed rather than hit. Events (folds, branch points, Hopf points) are located on the curve by bisection on test functions.

A nonlinear boundary-value problem

The Bratu problem \(u'' + \lambda e^u = 0\), \(u(\pm 1) = 0\), written natively and compiled at a declared resolution:

import numpy as np

import conatus as cn
from conatus.expert import ContinuationSettings, NewtonSolver, NLBVPContinuation

m = cn.Model(cn.Box(x=cn.walls(-1, 1)), name="Bratu", lam=0.3)
m.scalar("u")
m.equations("dx(dx(u)) + lam*exp(u) = 0")
m.at("walls", u=0)
compiled = m.compile(48, dealias=1)        # a nonlinear boundary-value problem, N = 48
problem, u = compiled.problem, compiled.fields["u"]

dealias=1 is declared here: exp(u) is not a polynomial nonlinearity, so no padding makes it exact. Newton works on the exact Jacobian of the discretised problem:

newton = NewtonSolver(problem, tol=1e-13, damping=1.0, max_iterations=20,
                      params={"lam": 0.3})
sol = newton.solve({u: lambda x: 0 * x})                # initial guess u = 0
print(sol.explain())                                    # residual per iteration, orders

Following the branch through the fold

settings = ContinuationSettings(ds=0.2, ds_min=1e-8, ds_max=0.6, adapt=1.5,
                                fast_iterations=3, tolerance=1e-12, max_newton=15,
                                max_points=12, dp_fd=1e-6)
branch = NLBVPContinuation(newton, "lam", settings).run(sol.coeffs, 0.3, +1,
                                                        p_bounds=(0.3, 1.0))
(fold,) = branch.folds()
print(branch.explain())
print(f"fold at lam = {fold.p:.12f}")

Every step-size rule is in ContinuationSettings: initial, minimum and maximum steps, adaptation factor, Newton budget and tolerance, the finite-difference step of \(\partial_p G\) (or None when exact derivatives exist).

Bifurcations of a dynamical system

For a system of ODEs, or a PDE reduced to one by ir_system, Conatus computes the exact Jacobian, \(\partial_p G\) and the multilinear forms used by the normal forms.

from conatus.expert import continue_equilibria, ir_system, ode_problem

lorenz = ode_problem(["x", "y", "z"], {"sigma": 10.0, "r": 0.5, "b": 8 / 3},
                     lambda X, P: [P["sigma"] * (X["y"] - X["x"]),
                                   P["r"] * X["x"] - X["y"] - X["x"] * X["z"],
                                   X["x"] * X["y"] - P["b"] * X["z"]], name="Lorenz")
L = ir_system(lorenz, ["r"])            # G(x; r), exact Jacobian, dG/dr, multilinear forms
cfg = ContinuationSettings(ds=0.1, ds_min=1e-9, ds_max=2.0, adapt=1.3, fast_iterations=4,
                           tolerance=1e-11, max_newton=12, max_points=60, dp_fd=None)
origin = continue_equilibria(L, np.zeros(3), "r", cfg, marginal_tol=1e-9, bounds=(0, 3))
(bp,) = origin.events_of("branch point")

Branch switching, Hopf points and cycles

from conatus.expert import CollocationMesh, branch_switch, normal_form

S = np.diag([-1.0, -1.0, 1.0])          # (x, y, z) -> (-x, -y, z), a symmetry of Lorenz
cplus = branch_switch(origin, bp, cfg, step=0.05, symmetry=S, bounds=(0, 30))
(hopf,) = cplus.events_of("hopf")
hf = normal_form(L, hopf.point.z[:-1], kind="hopf", par={"r": hopf.point.z[-1]},
                 parameter="r")
print(hf.explain())                     # first Lyapunov coefficient: sub- or supercritical

cfo = ContinuationSettings(ds=0.2, ds_min=1e-9, ds_max=1.0, adapt=1.3, fast_iterations=4,
                           tolerance=1e-9, max_newton=12, max_points=12, dp_fd=None)
cycles = branch_switch(cplus, hopf, cfo, amplitude=0.2,
                       mesh=CollocationMesh.uniform(40, 4), tests=("lpc",))
print(cycles.explain())                 # periodic orbits born at the Hopf point

At a symmetry-breaking pitchfork the symmetry is declared (symmetry=S) and the branch point is classified. At a Hopf point the first Lyapunov coefficient decides between sub- and supercritical; the cycles born there are continued by orthogonal collocation on a declared mesh. A branch of cycles can also start from any orbit given as a collocation state z0 (the mesh values, the period and the parameter): continue_orbits(PeriodicOrbitProblem(system, "r", mesh), z0, settings), with the same tests ("lpc", "pd", "ns", "bpc") and explain().

Codimension two

from conatus.expert import continue_codim2

bt = ir_system(ode_problem(["x", "y"], {"b1": -0.5, "b2": -1.0},
                           lambda X, P: [X["y"], P["b1"] + P["b2"] * X["x"]
                                         + X["x"] * X["x"] - X["x"] * X["y"]],
                           name="Bogdanov-Takens"), ["b1", "b2"])
cfg2 = ContinuationSettings(ds=0.05, ds_min=1e-9, ds_max=0.3, adapt=1.3, fast_iterations=3,
                            tolerance=1e-11, max_newton=12, max_points=40, dp_fd=None)
eq = continue_equilibria(bt, np.array([(1 - np.sqrt(3)) / 2, 0.0]), "b1", cfg2,
                         marginal_tol=1e-9, bounds=(-2, 2))
fold_pt = eq.events_of("fold")[0]
folds = continue_codim2(bt, fold_pt.point.z[:-1], "fold", ("b1", "b2"), cfg2,
                        par={"b1": fold_pt.point.z[-1]}, bounds={"b2": (-1.5, 1.5)})
print(folds.explain())                  # the fold curve b1 = b2^2 / 4 in the (b1, b2) plane

Fold and Hopf curves are continued in two parameters; codimension-two points on them (Bogdanov–Takens, cusp, generalized Hopf) are reported as events.

Amplitude equations (amplitude_equation, ginzburg_landau) reduce a PDE near onset to its Landau or Ginzburg–Landau form; tutorial 10. Bifurcations: branch switching, cycles, codimension 2, normal forms checks them on Swift–Hohenberg.

See also: tutorials 4. Nonlinear boundary-value problems and continuation: Bratu and 10. Bifurcations: branch switching, cycles, codimension 2, normal forms; ContinuationSettings, NLBVPContinuation, continue_equilibria, branch_switch, continue_codim2, normal_form.