Code
using NonlinearSolve
using DifferentialEquations
using SciMLBase
using StaticArrays
using ForwardDiff
using LinearAlgebra
using Suppressor
using PlotlyJS
Alicia
May 15, 2025
Figure 8.1.6 of Strogatz is an exmaple of a supercritical pitchfork, wherein a bifurcation happens that splits a single fixed point into 3 fixed points depending on the value of a parameter in the system of equations.
The systems of equations in question is:
\[ \dot x = \mu x - x^3 \] \[ \dot y = -y \]
In this post I examine phase portraits of this system with multiple values for mu to show this behavior.
But first, I willd define several functions to draw the phase portraits.
function find_fixed_points(
system_of_eqs;
guess_xs::AbstractRange,
guess_ys::AbstractRange,
ps::Vector{Float64},
)
fixed_points = []
@suppress begin
for (guess_x, guess_y) ∈ Base.product(guess_xs, guess_ys)
u0 = [guess_x, guess_y]
prob = NonlinearProblem(system_of_eqs, u0, ps)
sol = solve(prob, TrustRegion(), maxiters = 1_000_000)
if SciMLBase.successful_retcode(sol)
found = false
for fixed_point ∈ fixed_points
if isapprox(fixed_point[1], sol.u[1], atol = 1e-3) &&
isapprox(fixed_point[2], sol.u[2], atol = 1e-3)
found = true
break
end
end
if !found
push!(fixed_points, sol.u)
end
# else
# println("$u0 $(sol.retcode)")
end
end
end
return fixed_points
end;
function calculate_trajectories(trajectory_eqs!, u0s, tspans, ps)
trajectories = []
for (u0, tspan) ∈ zip(u0s, tspans)
trajectory_prob = ODEProblem(trajectory_eqs!, u0, tspan, ps)
trajectory_sol = solve(trajectory_prob, RK4(), dt = 0.01)
push!(trajectories, trajectory_sol.u)
end
return trajectories
end;
function final_plot(;
title,
fps,
contour_xs,
contour_ys,
contour_f_xy,
contour_g_xy,
slope_start_xys,
slope_end_xys,
trajectories,
)
traces::Vector{GenericTrace} = []
trace_fxy = contour(
x = contour_xs,
y = contour_ys,
z = contour_f_xy',
contours_start = 0,
contours_end = 0,
contours_coloring = "lines",
colorscale = [[0, "gold"], [1.0, "white"]],
line = attr(width = 2),
name = "f(x,y)",
showlegend = false,
)
push!(traces, trace_fxy)
trace_gxy = contour(
x = contour_xs,
y = contour_ys,
z = contour_g_xy',
contours_start = 0,
contours_end = 0,
contours_coloring = "lines",
colorscale = [[0, "darkorange"], [1.0, "white"]],
line = attr(width = 2),
name = "g(x,y)",
showlegend = false,
)
push!(traces, trace_gxy)
for (i, (start_xy, end_xy)) ∈ enumerate(zip(slope_start_xys, slope_end_xys))
showlegend = i == 1
trace_slope = scatter(
x = [start_xy[1], end_xy[1]],
y = [start_xy[2], end_xy[2]],
mode = "lines",
line = attr(color = "lawngreen"),
name = "slope",
showlegend = showlegend,
)
push!(traces, trace_slope)
end
for (i, trajectory) ∈ enumerate(trajectories)
showlegend = i == 1
trace_start = scatter(
x = [trajectory[1][1]],
y = [trajectory[1][2]],
mode = "markers",
marker = attr(color = "blue", size = 10),
name = "start",
showlegend = showlegend,
)
trace_trajectory = scatter(
x = [x for (x, _) ∈ trajectory],
y = [y for (_, y) ∈ trajectory],
mode = "lines",
line = attr(color = "black"),
name = "trajectory",
showlegend = showlegend,
)
trace_end = scatter(
x = [trajectory[end][1]],
y = [trajectory[end][2]],
mode = "markers",
marker = attr(color = "red", size = 10),
name = "end",
showlegend = showlegend,
)
push!(traces, trace_start)
push!(traces, trace_trajectory)
push!(traces, trace_end)
end
for (i, fp) ∈ enumerate(fps)
size = 15
color = "darkorchid"
showlegend = i == 1
trace_fp = scatter(
x = [fp[1]],
y = [fp[2]],
mode = "markers",
marker = attr(color = color, size = size),
showlegend = showlegend,
name = "Fixed Points",
)
push!(traces, trace_fp)
end
plot_bgcolor = "white"
paper_bgcolor = "white"
border_width = 1
gridwidth = 1
border_color = "black"
gridcolor = "lightgray"
layout = Layout(
title = title,
width = 550,
height = 500,
plot_bgcolor = plot_bgcolor,
paper_bgcolor = paper_bgcolor,
xaxis = attr(
showline = true,
linewidth = border_width,
linecolor = border_color,
mirror = true,
showgrid = true,
gridcolor = gridcolor,
gridwidth = gridwidth,
),
yaxis = attr(
showline = true,
linewidth = border_width,
linecolor = border_color,
mirror = true,
showgrid = true,
gridcolor = gridcolor,
gridwidth = gridwidth,
),
)
return plot(traces, layout)
end;
function fig_8_1_6(μ)
# Define parameters of functions
ps = [μ]
# Min, max of calculations
min_x, max_x = -2.0, 2.0
min_y, max_y = -1.0, 1.0
# Find fixed points
eqs_01(u, p) = SA[p[1]*u[1]-u[1]^3, -u[2]]
fps = find_fixed_points(
eqs_01;
guess_xs = range(min_x, max_x, 5),
guess_ys = range(min_y, max_y, 5),
ps = ps,
)
# println(fps)
# Find contours to plot nullclines and slope field
f(u::Union{Vector{Float64},Tuple{Float64,Float64}}) = ps[1]*u[1]-u[1]^3
g(u::Union{Vector{Float64},Tuple{Float64,Float64}}) = -u[2]
contour_xs = range(min_x, max_x, 100)
contour_ys = range(min_y, max_y, 100)
contour_f_xy, contour_g_xy = nullcline_contours(f, g, contour_xs, contour_ys)
start_xys, end_xys = slope_field(f, g, range(min_x, max_x, 10), range(min_y, max_y, 10))
# Compute trajectories
function trajectory_eqs!(du, u, p, t)
du[1] = p[1]*u[1]-u[1]^3
du[2] = -u[2]
end
u0s = [
[-2.0, 0.0],
[2.0, 0.0],
[0.0, -1.0],
[0.0, 1.0],
[-0.778, -0.556],
[0.778, -0.556],
[-0.778, 0.556],
[0.778, 0.556],
]
tspans = [
(0.0, 10.0),
(0.0, 10.0),
(0.0, 10.0),
(0.0, 10.0),
(0.0, 10.0),
(0.0, 10.0),
(0.0, 10.0),
(0.0, 10.0),
]
trajectories = calculate_trajectories(trajectory_eqs!, u0s, tspans, ps)
# Create final plot
return final_plot(;
title = "<b>μ=$(ps[1])</b>",
fps = fps,
contour_xs = contour_xs,
contour_ys = contour_ys,
contour_f_xy = contour_f_xy,
contour_g_xy = contour_g_xy,
slope_start_xys = start_xys,
slope_end_xys = end_xys,
trajectories = trajectories,
)
end;