Code
using NonlinearSolve
using DifferentialEquations
using SciMLBase
using StaticArrays
using ForwardDiff
using LinearAlgebra
using PlotlyJSAlicia
May 21, 2025
Example 7.3.2 is an example system of oscillations in glycolysis. It demonstrates the Poincaré-Bendixson Theorem and shows that changes in the two parameters of the system cause the formation of either a stable limit cycle or a stable fixed point. Further, the values of these parameters can be analyzed in such a way that predicts the behavior of the system based on where the points fall in parameter space. This post explores this example.
The system of equations is:
\[ \dot x = -x + ay + x^2y \]
\[ \dot y = b - ay -x^2y \]
Both a and b are parameters and a, b > 0.
function find_fixed_points(
    system_of_eqs;
    guess_xs::AbstractRange,
    guess_ys::AbstractRange,
    ps::Vector{Float64},
)
    fixed_points = []
    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
    return fixed_points
end;function classify_jacobian(A::Matrix{Float64})
    τ = tr(A)
    Δ = det(A)
    discriminant = tr(A)^2 - 4*det(A)
    if Δ < 0.0
        return "Saddle"
    else
        if isapprox(discriminant, 0.0)
            return "Star, Degenerate"
        elseif discriminant > 0.0
            if isapprox(τ, 0.0)
                return "Neutral Stable"
            elseif τ < 0.0
                return "Stable"
            else
                return "Unstable"
            end
        else
            return "Spiral"
        end
    end
    return "Unknown"
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 phase_portrait(;
    title,
    fps,
    As,
    contour_xs,
    contour_ys,
    contour_f_xy,
    contour_g_xy,
    slope_start_xys,
    slope_end_xys,
    trajectories,
)
    # annotations = []
    # for (fp, A) ∈ zip(fps, As)
    #     classification = classify_jacobian(A)
    #     annotation = attr(x = fp[1], y = fp[2], text = "<b>$classification</b>")
    #     push!(annotations, annotation)
    # end
    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, A)) ∈ enumerate(zip(fps, As))
        classification = classify_jacobian(A)
        size = 15
        color = "darkorchid"
        symbol = classification == "Saddle" ? "circle-open" : "circle"
        showlegend = i == 1
        trace_fp = scatter(
            x = [fp[1]],
            y = [fp[2]],
            mode = "markers",
            marker = attr(color = color, symbol = symbol, 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,
        ),
        # annotations = annotations,
    )
    return plot(traces, layout)
end;function fig_7_3_7(a_vals)
    b2_plus = 0.5 .* (1 .- 2 .* a_vals .+ sqrt.(max.(0.0, 1 .- 8 .* a_vals)))
    b2_minus = 0.5 .* (1 .- 2 .* a_vals .- sqrt.(max.(0.0, 1 .- 8 .* a_vals)))
    traces::Vector{GenericTrace} = []
    trace1 = scatter(
        x = a_vals,
        y = b2_plus,
        mode = "lines",
        name = "b<sup>2</sup> + branch",
        fill = "none",
        line = attr(color = "purple"),
        showlegend = false,
    )
    push!(traces, trace1)
    trace2 = scatter(
        x = a_vals,
        y = b2_minus,
        mode = "lines",
        name = "b<sup>2</sup> - branch",
        fill = "tonexty",
        line = attr(color = "purple"),
        showlegend = false,
    )
    push!(traces, trace2)
    annotation_bgcolor = "white"
    annotation_bordercolor = "black"
    annotation_font = attr(size = 18)
    annotation_borderwidth = 1
    annotations = [
        attr(
            x = 0.06,
            y = 0.5,
            text = "<b>stable limit cycle</b>",
            bgcolor = annotation_bgcolor,
            bordercolor = annotation_bordercolor,
            borderwidth = annotation_borderwidth,
            font = annotation_font,
            showarrow = false,
        ),
        attr(
            x = 0.09,
            y = 0.0,
            text = "<b>stable fixed point</b>",
            bgcolor = annotation_bgcolor,
            bordercolor = annotation_bordercolor,
            borderwidth = annotation_borderwidth,
            font = annotation_font,
            showarrow = false,
        ),
        attr(
            x = 0.09,
            y = 0.9,
            text = "<b>stable fixed point</b>",
            bgcolor = annotation_bgcolor,
            bordercolor = annotation_bordercolor,
            borderwidth = annotation_borderwidth,
            font = annotation_font,
            showarrow = false,
        ),
    ]
    plot_bgcolor = "white"
    paper_bgcolor = "white"
    border_width = 1
    gridwidth = 1
    border_color = "black"
    gridcolor = "lightgray"
    layout = Layout(
        width = 500,
        height = 500,
        annotations = annotations,
        plot_bgcolor = plot_bgcolor,
        paper_bgcolor = paper_bgcolor,
        xaxis = attr(
            title = "a",
            showline = true,
            linewidth = border_width,
            linecolor = border_color,
            mirror = true,
            showgrid = true,
            gridcolor = gridcolor,
            gridwidth = gridwidth,
        ),
        yaxis = attr(
            title = "b<sup>2</sup>",
            showline = true,
            linewidth = border_width,
            linecolor = border_color,
            mirror = true,
            showgrid = true,
            gridcolor = gridcolor,
            gridwidth = gridwidth,
        ),
    )
    plot(traces, layout)
end;function fig_7_3_8(a, b)
    # Define parameters of functions
    ps = [a, b]
    # Min, max of calculations
    min_x, max_x = 0.0, 3.0
    min_y, max_y = 0.0, 4.0
    # Find fixed points
    eqs_01(u, p) = SA[-u[1]+p[1]*u[2]+u[1]^2*u[2], p[2]-p[1]*u[2]-u[1]^2*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 Jacobians
    eqs_02(u, p) = [-u[1]+p[1]*u[2]+u[1]^2*u[2], p[2]-p[1]*u[2]-u[1]^2*u[2]]
    As = find_jacobians(eqs_02, fps, ps)
    println(As)
    # Find contours to plot nullclines and slope field
    f(u::Union{Vector{Float64},Tuple{Float64,Float64}}) = -u[1]+ps[1]*u[2]+u[1]^2*u[2]
    g(u::Union{Vector{Float64},Tuple{Float64,Float64}}) = ps[2]-ps[1]*u[2]-u[1]^2*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] = -u[1]+p[1]*u[2]+u[1]^2*u[2]
        du[2] = p[2]-p[1]*u[2]-u[1]^2*u[2]
    end
    u0s = [[0.333, 0.889], [0.667, 2.667], [0.0, 0.222]]
    tspans = [(0.0, 20.0), (0.0, 20.0), (0.0, 20.0)]
    trajectories = calculate_trajectories(trajectory_eqs!, u0s, tspans, ps)
    # Create final plot
    return phase_portrait(;
        title = "<b>a=$(ps[1]), b=$(ps[2])</b>",
        fps = fps,
        As = As,
        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;First, we can predict what type of fixed point will be on the plot by plotting the a, b parameter space. I will leave the derivation for the book, bu suffice to say that the plot ends up looking like this and the fixed point follows the behavior of where the parameters are in.
Here is the behavior of the plot at a = 0.08, b = 0.6, in the stable limit cycle region of the parameter plot:
Finally, here is a stable fixed point with parameters a = 0.12, b = 0.2.