Swift–Hohenberg: onset of rolls, weakly nonlinear amplitude, roll branch by continuation, 2D rolls

Family: patterns · Script: examples/patterns/swift_hohenberg.py

Obtained

threshold (r, q) on a single pencil (q a parameter, RealFourier); A(r = 0.1) = 0.3652433 (A² − 4r/3 → r²/144); periodic NLBVP (declared phase) = odd subspace to 1e-12; 2D IVP on the branch to 1e-9

Reference

Swift & Hohenberg 1977; Cross & Hohenberg 1993

du/dt = r u - (1 + lap)^2 u - u^3                         (Swift & Hohenberg 1977)

(a) Linear threshold. A normal mode exp(i q x + s t) of the state u = 0 grows at
    s = r - (1 - q^2)^2, so the neutral curve is r = (1 - q^2)^2 and the onset is r_c = 0 at
    q_c = 1 (Cross & Hohenberg 1993, Rev. Mod. Phys. 65, 851, section on the SH model). Computed
    here by linearize() of the IVP around u = 0 and one parametric pencil in (r, q): the Fourier
    wavenumber q is a declared pencil parameter (``wavenumbers={"x": q}``), so critical() minimizes
    over q without rediscretizing (one pencil per q before). RealFourier: each group
    holds the (cos, sin) pair, a double eigenvalue (translation invariance); track() follows a
    declared member of the eigenspace (it raised LinAlgError before).

(b) Roll branch. Steady rolls u = A sin x + B sin 3x + ... obey, projecting on sin x and sin 3x,
    B = A^3 / (4 (64 - r)) and r = 3 A^2 / 4 - 3 A B / 4 + ..., i.e.

        A^2 = 4 r / 3 + r^2 / 144 + O(r^3)

    (leading term: Cross & Hohenberg 1993; the r^2/144 correction is the third-harmonic feedback
    derived above). The rolls are solved as a periodic NLBVP (PeriodicNewtonSolver: dense coupled
    Jacobian, translation fixed by a declared phase condition, the -sin coefficient of mode 1 set
    to zero, with its multiplier reported), and, independently, in the odd subspace (the banded
    NewtonSolver refuses the periodic form: "varies along a Fourier direction"), *exactly
    equivalent* for the periodic rolls and followed in r by continuation: on x in [0, pi] with
    u = u'' = 0 at both ends, every solution extends to an odd 2 pi-periodic one (sin(n x) modes;
    (1 + d_xx)^2 and u^3 preserve oddness), and conversely. Chebyshev in x, four BCs for the
    fourth-order operator. The amplitude A = (2/pi) int_0^pi u sin x dx is an extra unknown of the
    NLBVP, so it is carried by Newton and by the continuation.

(c) 2D rolls. The same equation as an IVP on a doubly periodic 2 pi x 2 pi box (RealFourier x
    RealFourier, dealias = 2 declared for the cubic term), started from a mixture of x- and
    y-rolls: the y-rolls decay, the x-rolls saturate at the amplitude of the branch (b).

Run:  python examples/patterns/swift_hohenberg.py

Run it: python examples/patterns/swift_hohenberg.py (in the conatus environment).

  1"""Swift–Hohenberg: onset of rolls, weakly nonlinear amplitude, roll branch by continuation, 2D rolls.
  2
  3    du/dt = r u - (1 + lap)^2 u - u^3                         (Swift & Hohenberg 1977)
  4
  5(a) Linear threshold. A normal mode exp(i q x + s t) of the state u = 0 grows at
  6    s = r - (1 - q^2)^2, so the neutral curve is r = (1 - q^2)^2 and the onset is r_c = 0 at
  7    q_c = 1 (Cross & Hohenberg 1993, Rev. Mod. Phys. 65, 851, section on the SH model). Computed
  8    here by linearize() of the IVP around u = 0 and one parametric pencil in (r, q): the Fourier
  9    wavenumber q is a declared pencil parameter (``wavenumbers={"x": q}``), so critical() minimizes
 10    over q without rediscretizing (one pencil per q before). RealFourier: each group
 11    holds the (cos, sin) pair, a double eigenvalue (translation invariance); track() follows a
 12    declared member of the eigenspace (it raised LinAlgError before).
 13
 14(b) Roll branch. Steady rolls u = A sin x + B sin 3x + ... obey, projecting on sin x and sin 3x,
 15    B = A^3 / (4 (64 - r)) and r = 3 A^2 / 4 - 3 A B / 4 + ..., i.e.
 16
 17        A^2 = 4 r / 3 + r^2 / 144 + O(r^3)
 18
 19    (leading term: Cross & Hohenberg 1993; the r^2/144 correction is the third-harmonic feedback
 20    derived above). The rolls are solved as a periodic NLBVP (PeriodicNewtonSolver: dense coupled
 21    Jacobian, translation fixed by a declared phase condition, the -sin coefficient of mode 1 set
 22    to zero, with its multiplier reported), and, independently, in the odd subspace (the banded
 23    NewtonSolver refuses the periodic form: "varies along a Fourier direction"), *exactly
 24    equivalent* for the periodic rolls and followed in r by continuation: on x in [0, pi] with
 25    u = u'' = 0 at both ends, every solution extends to an odd 2 pi-periodic one (sin(n x) modes;
 26    (1 + d_xx)^2 and u^3 preserve oddness), and conversely. Chebyshev in x, four BCs for the
 27    fourth-order operator. The amplitude A = (2/pi) int_0^pi u sin x dx is an extra unknown of the
 28    NLBVP, so it is carried by Newton and by the continuation.
 29
 30(c) 2D rolls. The same equation as an IVP on a doubly periodic 2 pi x 2 pi box (RealFourier x
 31    RealFourier, dealias = 2 declared for the cubic term), started from a mixture of x- and
 32    y-rolls: the y-rolls decay, the x-rolls saturate at the amplitude of the branch (b).
 33
 34Run:  python examples/patterns/swift_hohenberg.py
 35"""
 36import numpy as np
 37
 38from conatus.expert import (IVP, NLBVP, CartesianCoordinates, Chebyshev, ContinuationSettings,
 39                            Domain, Field, IVPSolver, NewtonSolver, NLBVPContinuation, Parameter,
 40                            ParametricEVP, PeriodicNewtonSolver, RealFourier, critical, diff,
 41                            discretize, dt, integ, interp, lap, linearize)
 42from conatus.ir import Zero
 43from conatus.solvers import PhaseCondition
 44
 45
 46def swift_hohenberg_ivp(domain, lap_):
 47    """du/dt - r u + (1 + lap)^2 u = -u^3 on ``domain`` (lap_ = the Laplacian of that domain)."""
 48    u, r = Field("u", domain), Parameter("r", 0.1)
 49
 50    def one_plus_lap(f):
 51        return f + lap_(f)
 52
 53    ivp = IVP([u], name="Swift-Hohenberg")
 54    ivp.add_equation((dt(u) - r * u + one_plus_lap(one_plus_lap(u)), -u * u * u))
 55    return ivp, u, r
 56
 57
 58# ------------------------------------------------------------------ (a) linear threshold
 59def growth_pencil():
 60    """Pencil in (r, q) of the perturbation of u = 0; q = wavenumber of x (a pencil parameter,
 61    so the period of the domain plays no role)."""
 62    cs = CartesianCoordinates("x")
 63    x = cs["x"]
 64    D = Domain(cs, [RealFourier(x, 8, (0, 2 * np.pi), dealias=1)])
 65    ivp, u, r = swift_hohenberg_ivp(D, lambda f: diff(diff(f, x), x))
 66    evp = linearize(ivp, around={u: Zero(())}, eigenvalue=Parameter("s", dtype=complex))
 67    return ParametricEVP(evp, [r], wavenumbers={"x": Parameter("q", 1.0)}), evp
 68
 69
 70def onset():
 71    """(q_c, r_c): neutral r of the least stable mode, minimized over q on one pencil."""
 72    cp = critical(growth_pencil()[0], "r", bracket=(-0.5, 1.0), over={"q": (0.5, 1.5)}, xtol=1e-10)
 73    return cp.over["q"], cp.value
 74
 75
 76# ------------------------------------------------------------------ (b) roll branch (NLBVP)
 77def rolls(N: int):
 78    """Steady rolls in the odd subspace: x in [0, pi], u = u'' = 0; unknowns u and amplitude A."""
 79    cs = CartesianCoordinates("x")
 80    x = cs["x"]
 81    D = Domain(cs, [Chebyshev(x, N, (0, np.pi), dealias=1)])
 82    u, A, sinx, r = Field("u", D), Field("A", D.without(x)), Field("sinx", D), Parameter("r", 0.1)
 83
 84    def one_plus_lap(f):
 85        return f + diff(diff(f, x), x)
 86
 87    pb = NLBVP([u, A], name="Swift-Hohenberg rolls (odd subspace)")
 88    pb.add_equation((r * u - one_plus_lap(one_plus_lap(u)) - u * u * u, 0))
 89    pb.add_equation((A - (2 / np.pi) * integ(sinx * u), 0))      # A = first sine coefficient
 90    uxx = diff(diff(u, x), x)
 91    pb.add_bc(interp(u, x, 0), interp(u, x, np.pi), interp(uxx, x, 0), interp(uxx, x, np.pi), into=0)
 92    return pb, u, {sinx: np.sin}
 93
 94
 95def rolls_periodic(N: int, r: float):
 96    """Steady rolls on the 2 pi-periodic line (RealFourier, dealias 2 for the cubic term), phase
 97    fixed by msin_1 = 0; returns the solution (first harmonic via .harmonic("u", x, 1))."""
 98    cs = CartesianCoordinates("x")
 99    x = cs["x"]
100    u, rr = Field("u", Domain(cs, [RealFourier(x, N, (0, 2 * np.pi), dealias=2)])), Parameter("r", r)
101
102    def one_plus_lap(f):
103        return f + diff(diff(f, x), x)
104
105    pb = NLBVP([u], name="Swift-Hohenberg rolls (periodic)")
106    pb.add_equation((rr * u - one_plus_lap(one_plus_lap(u)) - u * u * u, 0))
107    solver = PeriodicNewtonSolver(pb, tol=1e-13, damping=1.0, max_iterations=20,
108                                  phase=PhaseCondition(u, x, kind="sin1"))
109    return solver.solve({u: lambda X: np.sqrt(4 * r / 3) * np.cos(X)}), x
110
111
112def roll_solver(N: int, r: float):
113    """Newton for the rolls at r, started from the leading-order amplitude sqrt(4 r / 3)."""
114    pb, u, ncc = rolls(N)
115    solver = NewtonSolver(pb, tol=1e-13, damping=1.0, max_iterations=30, params={"r": r}, ncc=ncc)
116    return solver, solver.solve({u: lambda X: np.sqrt(4 * r / 3) * np.sin(X)})
117
118
119def roll_branch(N: int, r0: float, r1: float, ds_max: float):
120    """Pseudo-arclength continuation of the rolls in r from r0 to r1: [(r, A, residual)]."""
121    solver, sol = roll_solver(N, r0)
122    cfg = ContinuationSettings(ds=0.02, ds_min=1e-6, ds_max=ds_max, adapt=1.5, fast_iterations=3,
123                               tolerance=1e-12, max_newton=15, max_points=40, dp_fd=1e-6)
124    cont = NLBVPContinuation(solver, "r", cfg)
125    branch = cont.run(sol.coeffs, r0, +1, p_bounds=(r0, r1))
126    pts = [(q.p, float(np.real(cont.coeffs_of(q.x)["A"]).ravel()[0]), q.residual) for q in branch.points]
127    return pts, branch
128
129
130# ------------------------------------------------------------------ (c) 2D rolls (IVP)
131def rolls_2d(N: int, r: float, timestep: float, steps: int):
132    """IVP on the 2 pi-periodic square; returns (A_x, A_y, |du/dt| at the end, solver)."""
133    cs = CartesianCoordinates("x", "y")
134    x, y = cs.coords
135    D = Domain(cs, [RealFourier(x, N, (0, 2 * np.pi), dealias=2), RealFourier(y, N, (0, 2 * np.pi), dealias=2)])
136    ivp, u, _ = swift_hohenberg_ivp(D, lap)
137    solver = IVPSolver(discretize(ivp, params={"r": r}), "SBDF2", timestep)
138    solver.set_state({u: lambda X, Y: 0.05 * np.cos(X) + 0.04 * np.cos(Y) + 0.01 * np.sin(2 * X + Y)})
139    solver.run(steps - 1)
140    before = solver.grid("u")
141    solver.run(1)
142    g = solver.grid("u")
143    e = np.exp(-1j * 2 * np.pi * np.arange(N) / N)
144    A_x = 2 * abs(np.mean(g * e[:, None]))           # |cos x, sin x| content (phase free)
145    A_y = 2 * abs(np.mean(g * e[None, :]))
146    return A_x, A_y, float(np.abs(g - before).max() / timestep), solver
147
148
149if __name__ == "__main__":
150    q_c, r_c = onset()
151    print(f"(a) onset: q_c = {q_c:.8f}, r_c = {r_c:.2e}  (theory: q_c = 1, r_c = 0)")
152    assert abs(q_c - 1) < 1e-4 and abs(r_c) < 1e-8, (q_c, r_c)    # Swift & Hohenberg (1977)
153    P, evp = growth_pencil()
154    print(evp.explain())
155    print(P.explain())
156    print("(b) rolls at r = 0.1, convergence in N (A = first sine coefficient):")
157    for N in (16, 20, 24, 32):
158        _, sol = roll_solver(N, 0.1)
159        print(f"  N = {N}: A = {sol.value('A'):.15f}, Newton residual {sol.residual:.1e}")
160    A = sol.value("A")                                             # N = 32, converged in N
161    sol, x = rolls_periodic(16, 0.1)
162    print(f"  periodic NLBVP (N = 16, phase msin_1 = 0): A = {sol.harmonic('u', x, 1):.15f}")
163    assert abs(sol.harmonic("u", x, 1) - A) < 1e-12               # periodic NLBVP = odd subspace
164    print(sol.explain())
165    pts, branch = roll_branch(24, 0.02, 0.5, 0.05)
166    print(branch.explain())
167    print("  r         A              A^2 - 4r/3     (A^2 - 4r/3)/r^2  (theory 1/144 = 0.006944)")
168    for r, A_r, res in pts:
169        gap = A_r * A_r - 4 * r / 3
170        print(f"  {r:.5f}  {A_r:.12f}  {gap:.3e}      {gap / r ** 2:.6f}   residual {res:.1e}")
171        assert abs(gap) < 0.01 * r ** 2, (r, gap)                 # A^2 = 4r/3 + O(r^2) (Cross & Hohenberg 1993)
172    A_x, A_y, rate, s = rolls_2d(16, 0.1, 0.5, 800)
173    print(f"(c) 2D IVP at r = 0.1, t = {s.t}: A_x = {A_x:.12f}, A_y = {A_y:.1e}, |du/dt| = {rate:.1e}")
174    assert rate < 1e-8 and A_y < 1e-6 and abs(A_x - A) < 1e-9, (rate, A_y, A_x, A)   # 2D IVP = 1D rolls
175    print(s.explain())