String method on the Muller-Brown landscape using a gradient function
This example demonstrates how to use CriticalTransitions.string_method when you do not have a DynamicalSystem, but instead have access to a potential landscape $V(x)$ and its gradient $\nabla V(x)$. Together with the implementation of the string method, this examples is inspired by Gideon Simpson package StringMethod.jl.
The string method implementation in CriticalTransitions expects a drift (vector field) b(x). For a gradient system, the natural choice is the gradient-descent flow
\[\dot{x} = -\nabla V(x),\]
so we pass b(x) = -gradV(x).
using CriticalTransitions
using CairoMakie
using ForwardDiff
using LinearAlgebra: norm
using NLsolve
using Optim
using Statistics: meanMuller-Brown potential
We use the standard 4-Gaussian Muller-Brown potential
\[V(x,y) = \sum_{i=1}^4 A_i \exp\left(a_i(x-x_i)^2 + b_i(x-x_i)(y-y_i) + c_i(y-y_i)^2\right).\]
function Muller(x)
aa = (-1.0, -1.0, -6.5, 0.7)
bb = (0.0, 0.0, 11.0, 0.6)
cc = (-10.0, -10.0, -6.5, 0.7)
AA = (-200.0, -100.0, -170.0, 15.0)
XX = (1.0, 0.0, -0.5, -1.0)
YY = (0.0, 0.5, 1.5, 1.0)
x1, x2 = x
V = 0.0
@inbounds for i in 1:4
dx = x1 - XX[i]
dy = x2 - YY[i]
V += AA[i] * exp(aa[i] * dx^2 + bb[i] * dx * dy + cc[i] * dy^2)
end
return V
end
V = x -> Muller(x)#1 (generic function with 1 method)The gradient can be computed with ForwardDiff.
gradV = x -> ForwardDiff.gradient(V, x)#3 (generic function with 1 method)Locate minima and saddles (for plotting)
This part is not strictly necessary for computing the string, but it helps for visualization and for choosing endpoints.
min1 = optimize(V, [-0.75, 1.5])
x1 = min1.minimizer
min2 = optimize(V, [0.6, 0.0])
x2 = min2.minimizer
min3 = optimize(V, [0.0, 0.5])
x3 = min3.minimizer
saddle1 = nlsolve(gradV, [-1.0, 0.5])
s1 = saddle1.zero
saddle2 = nlsolve(gradV, [0.25, 0.3])
s2 = saddle2.zero
x1, x2, x3, s1, s2([-0.5582223315786567, 1.441727697793157], [0.6235028759609716, 0.028037347734474527], [-0.05001052926654174, 0.46668995606833874], [-0.8220015587327318, 0.6243128028148712], [0.2124865820008195, 0.29298832510745915])Run the string method using a gradient function
The string method expects a drift field, so we wrap the gradient into gradient-descent drift.
b(x) = -gradV(x)b (generic function with 1 method)Choose endpoints between two minima (left well to right well).
xa = x1
xb = x22-element Vector{Float64}:
0.6235028759609716
0.028037347734474527Construct an initial string as a 2×Nt matrix, with a small transverse perturbation to make the convergence visually obvious.
function linear_string(a, b, Nt)
xs = range(a[1], b[1]; length = Nt)
ys = range(a[2], b[2]; length = Nt)
return vcat(xs', ys')
end
Nt = 50
x_initial = linear_string(xa, xb, Nt)
x_initial[2, :] .+= 0.15 .* sin.(range(0, π; length = Nt))50-element view(::Matrix{Float64}, 2, :) with eltype Float64:
1.441727697793157
1.4224874073196787
1.4032076251046623
1.3838490216866066
1.3643725914972415
1.3447398131437733
1.324912807701551
1.3048544943666933
1.2845287428290648
1.263900521739471
1.2429360426609857
1.2216028989128864
1.1998701987366558
1.1777086922368447
1.1550908915751665
1.1319911839239198
1.1083859367145932
1.0842535947491558
1.0595747687749926
1.0343323151594992
1.008511406336949
0.9820995917381381
0.9550868489524467
0.9274656249120735
0.8992308669292117
0.8703800434586262
0.8409131545003175
0.81083273159952
0.7801438274440405
0.7488539951016808
0.7169732569830605
0.6845140636573828
0.6514912426903756
0.6179219377146421
0.5838255379827982
0.549223598692874
0.5141397524133815
0.4785996119720218
0.4426306652070817
0.4062621620140105
0.369524994151325
0.3324515682997481
0.29507567289620573
0.25743233928989273
0.21955769779094444
0.18148882920324205
0.14326361245143648
0.10492056892832116
0.06649870420216687
0.028037347734474544The parameter stepsize is the (fixed) time step used internally for the evolution step. The Muller-Brown potential has steep regions, so small stepsize is recommended.
stepsize = 1.0e-4
maxiters = 2500
string = CriticalTransitions.string_method(
b, x_initial; stepsize, maxiters, show_progress = false
)Minimum action Path of length 50 in 2 dimensionsA simple convergence diagnostic: average step along the string.
string_m = Matrix(string.path)
dmean = mean(norm(string_m[i + 1, :] .- string_m[i, :]) for i in 1:(size(string_m, 1) - 1))
dmean0.05472215599356862Visualize on the potential landscape
xx = LinRange(-1.5, 1.5, 250)
yy = LinRange(-0.5, 2.0, 250)
V_vals = [V([x, y]) for y in yy, x in xx]
V_clip = min.(V_vals, 500)
fig = Figure(; size = (700, 450), fontsize = 13)
ax = Axis(fig[1, 1]; xlabel = "x", ylabel = "y", aspect = 1.2)
contour!(ax, xx, yy, V_clip; levels = range(-150, 500, 35), colormap = :viridis)
lines!(ax, x_initial[1, :], x_initial[2, :]; color = :black, linewidth = 2, linestyle = :dash) # Initial string (dashed) and converged string (solid)
lines!(ax, string_m[:, 1], string_m[:, 2]; color = :black, linewidth = 3)
scatter!(ax, [x1[1], x2[1], x3[1]], [x1[2], x2[2], x3[2]]; color = :red, markersize = 10) # Minima and saddles
scatter!(ax, [s1[1], s2[1]], [s1[2], s2[2]]; color = :green, marker = :diamond, markersize = 10)
limits!(ax, extrema(xx)..., extrema(yy)...)
fig