02E. Bifurcation
Mingyang Lu
01/07/2024
Consider one self-activating gene,
.Method 1: (direct method) find all roots¶
% dX is used to estimate the derivatives of f(X) near the steady states
% All k values to be sampled (control parameter)
fun = @(X) derivs_k(X,k);
X1(j) = fzero(fun,X0_all(j));
if sign(derivs_k(X + dX, k)) < 0
results(n_points,:) = [k, X, stability];
gscatter(results(:, 1), results(:, 2), results(:,3), 'br', 'o', 5);
xlabel('$k$ (Minute$^{-1}$)', 'Interpreter', 'latex');
ylabel('$Xs$ (nM)', 'Interpreter', 'latex');
legend('Stable', 'Unstable', 'Location', 'northeast');
Method 2: Get bifurcation curve by simulations
See NumericalR for details of the algorithm.
gap = 100.0; % the stop criterion for a bifurcation point
t_max = 1000.0; % Maximum ODE simulation time
dk = 0.001; % k step size
dX = 1; % small step in X to estimate df/dX
X0 = randi([100, 600]); % Random initial condition of X
ind = 0; % Record the index of steady states
% Initialize results storage
% Simulate until a stable steady state is reached
options = odeset('RelTol', 1e-6, 'AbsTol', 1e-6);
[~, X] = ode45(@(t, X) derivs_k_adj(t, X, k, d), [0, t_max], X0, options);
% Check for bifurcation point, switch the sampling direction of k.
if ind > 0 && abs(Xi - X0) > gap
% Update X0, with slight perturbation along the same direction from X0 to Xi in the current step
X0 = X0 + dX * sign(Xi - X0);
% Determine stability of the steady state
f_x_plus_dx = derivs_k_adj(0, Xi + dX, k, 1);
if sign(f_x_plus_dx) < 0 % df/dX < 0, stable
% Record the steady state
results(ind, :) = [k, Xi, stability];
% Update k and the initial X for the next iteration
% Trim excess NaNs from results matrix
results = results(1:ind, :);
% Plot the bifurcation diagram
gscatter(results(:, 1), results(:, 2), results(:,3), 'br', 'o', 5);
xlabel('$k$ (Minute$^{-1}$)', 'Interpreter', 'latex');
ylabel('$Xs$ (nM)', 'Interpreter', 'latex');
legend('Stable', 'Unstable', 'Location', 'northeast');
Method 3: numerical continuation
Starting from a steady state X for a specific k, we can obtain the slope of the bifurcation curve
from
.This allows us to find the initial guess of the next (k, X) point.
We can then use a correction method, such as Newton's method, to find the nearby solution.
Newton's method
We solve
starting from an initial guess at
.
.Thus,
.Numerically, we perform the following calculation iteratively:
, until
, where ϵ is a small constant. Below shows the implementation of Newton's method for the current system.% Example usage of find_root_Newton
X_root1 = find_root_Newton(500, @derivs_k, @dfdX, [0, 800], 1e-3, 0.1);
% Bistable, first root near 500
X_root2 = find_root_Newton(500, @derivs_k, @dfdX, [0, 800], 1e-3, 0.15);
X_root3 = find_root_Newton(100, @derivs_k, @dfdX, [0, 800], 1e-3, 0.15);
% The last root (unstable) near 150
X_root4 = find_root_Newton(150, @derivs_k, @dfdX, [0, 800], 1e-3, 0.15);
disp(['Root 1: ', num2str(X_root1)]);
disp(['Root 2: ', num2str(X_root2)]);
disp(['Root 3: ', num2str(X_root3)]);
disp(['Root 4: ', num2str(X_root4)]);
An implementation of the continuation method
Now, we implement the numerical continuation method by uniformly varying k.
% Define the partial derivative df/dk
X_init = 0; % Initial condition of X
k_range = [0.1, 0.2]; % Range of parameter k
dk = 0.001; % Step size for parameter k
nmax_cycle = (k_range(2) - k_range(1))/dk + 1; % Maximum number of cycles
% Create a matrix to store results
results_1 = NaN(nmax_cycle, 3);
% Obtain initial steady-state X
X_new = fzero(@(X) derivs_k(X,k),X_init);
results_1(cycle, :) = [k, X_new, -sign(dfdX(X_new, k))];
while ( (k - k_range(1)) * (k - k_range(2)) <= 0 ) % Check if k is in the range of [k_min, k_max]
% Compute the slope of the bifurcation curve
slope = -dfdk(X_new, k) / dfdX(X_new, k);
X_init = X_new + dk * slope;
% Correction using Newton's method
X_new = find_root_Newton(X_init, @derivs_k, @dfdX, [0, 800], 1e-3, k);
results_1(cycle, :) = [k, X_new, -sign(dfdX(X_new, k))];
results_1 = results_1(1:cycle, :);
% Plot the bifurcation diagram
gscatter(results_1(:, 1), results_1(:, 2), (3-results_1(:,3)/2), 'br', 'o', 5);
xlabel('$k$ (Minute$^{-1}$)', 'Interpreter', 'latex');
ylabel('$Xs$ (nM)', 'Interpreter', 'latex');
legend('Stable', 'Unstable', 'Location', 'northeast');
Warning: Ignoring extra legend entries.

This algorithm doesn't work at the bifurcation point, where the the slope of the curve becomes infinity.
Improved continuaton method using arc length
We describe the bifurcation curve as the function of arc length s, instead of the control parameter k. Previously we aim to find
, but here we will find
and
. Therefore, we get
,where
. In this case, even when h is infinity,
and
are small enough. The choice of
would depend on which direction we want to go. Here, we set
to be in the same direction as that in the previous step. X_init = 0; % Initial condition of X
k_range = [0.1, 0.2]; % Range of parameter k
ds = 0.3; % Step size for the arc length
nmax_cycle = 10000; % Maximum number of cycles
% Create a matrix to store results
results_2 = NaN(nmax_cycle, 3);
step_k_previous = 1; % Initial step_k is positive
% Obtain initial steady-state X
X_new = fzero(@(X) derivs_k(X,k),X_init);
results_2(cycle, :) = [k, X_new, -sign(dfdX(X_new, k))];
while (k_range(1) <= k && k <= k_range(2) && cycle < nmax_cycle)
h = -dfdk(X_new, k) / dfdX(X_new, k);
step_k = ds / sqrt(1 + h^2);
% The direction of change should be the same along the search
if (step_k_previous * step_k + step_X_previous * step_X < 0)
step_k_previous = step_k;
step_X_previous = step_X;
% Correction using Newton's method
X_new = find_root_Newton(X_init, @derivs_k, @dfdX, [0, 800], 1e-3, k);
results_2(cycle, :) = [k, X_new, -sign(dfdX(X_new, k))];
results_2 = results_2(1:cycle, :);
% Plot the bifurcation diagram
gscatter(results_2(:, 1), results_2(:, 2), (3-results_2(:,3)/2), 'br', 'o', 5);
xlabel('$k$ (Minute$^{-1}$)', 'Interpreter', 'latex');
ylabel('$Xs$ (nM)', 'Interpreter', 'latex');
legend('Stable', 'Unstable', 'Location', 'northeast');
Define MATLAB functions
1. Define the derivative function
function dXdt = derivs_k(X, k)
dXdt = 10 + 45 * (1 - 1 / (1 + (X / 200)^4)) - k * X;
2. Define the derivative function (ajusted to allow searching for stable/unstable steady states from ODE simulations)
% k: control parameter; d: 1 - stable steady state; 2 - unstable steady state
function dXdt = derivs_k_adj(t, X, k, d)
dXdt = d * (10 + 45 * (1 - 1 / (1 + (X / 200)^4)) - k * X);
3. Implementation of Newton's method to find a root of the function "func"
function X_root = find_root_Newton(X, func, dfunc, X_range, error, varargin)
f = func(X, varargin{:});
X = X - f/dfunc(X, varargin{:});
% Check if X is within X_range
if (X - X_range(1)) * (X - X_range(2)) > 0
break; % Avoid potential infinite loop
f = func(X, varargin{:});
4.Define dfdX function
function result = dfdX(X, k)
result = 180 /X * x_frac / (1 + x_frac)^2 - k;