We're now going to use some of these tools to look at a class of network motifs, called Feed Forward Loops (FFLs), found in signaling and regulatory networks. FFLs involve interactions between three components, with the basic topology illustrated below. Depending on the signs of the edges (whether activating or repressing) we can classify FFLs as "coherent" or "incoherent." We'll take a look at an example of each class.
Using our logic approximation framework we will model the coherent FFL network illustrated above as follows.
In [7]:
# import statements to make numeric and plotting functions available
%matplotlib inline
from numpy import *
from matplotlib.pyplot import *
In [8]:
## We'll specify the behavior of X as a series of pulse of different length
## so we'll define a function to generate pulses
def pulse(ontime, offtime, ntimes, onval=1):
if ontime >= offtime:
raise Exception("Invalid on/off times.")
signal = np.zeros(ntimes)
signal[ontime:offtime] = onval
return signal
In [13]:
nsteps = 150
short_pulse = pulse(20, 23, nsteps) # 5 sec pulse
long_pulse = pulse(50, 100, nsteps) # 50 sec pulse
X = short_pulse + long_pulse # we can then add the pulses to create
# a single time trace
plot(X, color='black')
xlabel('Time units')
ylabel('Amount of Gene Product')
ylim(0, 1.5)
pass
In [14]:
## Write Python functions for dY/dt and dZ/dt
def dY(B,K,a,X,Y):
pass ## replace this line with your function definition
def dZ(B,Kx,Ky,a,X,Y,Z):
pass ## replace this line with your function definition
def dY(B,K,a,X,Y):
if X > K:
theta = 1
else:
theta = 0
return B * theta - a * Y
def dZ(B,Kx,Ky,a,X,Y,Z):
theta = 0
if (X > Kx) and (Y > Ky):
theta = 1
return B * theta - a * Z
In [17]:
## Plot X, Y, and Z on the same time scale
nsteps = 150
short_pulse = pulse(20, 23, nsteps) # 5 sec pulse
long_pulse = pulse(50, 100, nsteps) # 50 sec pulse
X = short_pulse + long_pulse
# setup parameters for Y and Z
Y = [0]
betay, alphay = 0.2, 0.1
Kxy = 0.5
Z = [0]
betaz, alphaz = 0.2, 0.1
Kxz = 0.5
Kyz = 1
for i in range(nsteps):
xnow = X[i]
ynow, znow = Y[-1], Z[-1]
ynew = ynow + dY(betay, Kxy, alphay, xnow, ynow)
znew = znow + dZ(betaz, Kxz, Kyz, alphaz, xnow, ynow, znow)
Y.append(ynew)
Z.append(znew)
plot(X, 'k--', label='X', linewidth=1.5)
plot(Y, 'b', label='Y')
plot(Z, 'r', label='Z')
ylim(-0.1, 2.5)
xlabel("Time")
ylabel("Concentration")
legend()
pass
How do the dynamics of $Y$ and $Z$ differ in the simulation above?
Try varying the length of the first (short) pulse? How does changing the length of the pulse affect the dynamics of $Y$ and $Z$?
In [74]:
nsteps = 150
p1start = 10
p1duration = 5
p2start = 50
p2duration = 50
short_pulse = pulse(p1start, p1start + p1duration, nsteps) # short pulse
long_pulse = pulse(p2start, p2start + p2duration, nsteps) # long pulse
X = short_pulse + long_pulse
# change this `scale` argument to increase/decrease noise
noise = np.random.normal(loc=0, scale=0.2, size=nsteps) # mean=0, sd=0.2
X = X + noise
# setup parameters for Y and Z
Y = [0]
betay, alphay = 0.2, 0.1
Kxy = 0.5
Z = [0]
betaz, alphaz = 0.2, 0.1
Kxz = 0.5
Kyz = 1
for i in range(nsteps):
xnow = X[i]
ynow, znow = Y[-1], Z[-1]
ynew = ynow + dY(betay, Kxy, alphay, xnow, ynow)
znew = znow + dZ(betaz, Kxz, Kyz, alphaz, xnow, ynow, znow)
Y.append(ynew)
Z.append(znew)
# draw each trace as a subfigure
# subfigures stacked in a vertical grid
subplot2grid((3,1),(0,0))
plot(X, 'k', label='X', linewidth=1)
legend()
subplot2grid((3,1),(1,0))
plot(Y, 'b', label='Y', linewidth=2)
legend()
subplot2grid((3,1),(2,0))
plot(Z, 'r', label='Z', linewidth=2)
vlines(p1start, min(Z),max(Z)*1.1,color='black',linestyle='dashed')
annotate("pulse 1 on", xy=(p1start,1),xytext=(40,20),
textcoords='offset points',
horizontalalignment="center",
verticalalignment="bottom",
arrowprops=dict(arrowstyle="->",color='black',
connectionstyle='arc3,rad=0.5',
linewidth=1))
vlines(p2start, min(Z),max(Z)*1.1,color='black',linestyle='dashed')
annotate("pulse 2 on", xy=(p2start,1),xytext=(-40,0),
textcoords='offset points',
horizontalalignment="center",
verticalalignment="bottom",
arrowprops=dict(arrowstyle="->",color='black',
connectionstyle='arc3,rad=0.5',
linewidth=1))
legend()
pass
As before we can solve for Y as a function of time and calculate what its steady state value will be:
$$ Y(t) = Y_{st}(1-e^{-\alpha_{y}t}) $$and
$$ Y_{st}=\frac{\beta_y}{\alpha_y} $$Since $Z$ is governed by an AND function it needs both $X$ and $Y$ to be above their respective thresholds, $K_{xz}$ and $K_{yz}$. For the sake of simplicity let's assume that both $Y$ and $Z$ have the same threshold with respect to $X$, i.e. $K_{xy} = K_{xz}$. This allows us just to consider how long it takes for $Y$ to reach the threshold value $K_{yz}$. Given this we can calculate the delay before $Z$ turns on, $T_{\mathrm{on}}$ as follows.
$$ Y(T_{\mathrm{on}}) = Y_{st}(1-e^{-\alpha_y T_{\mathrm{on}}}) = K_{yz} $$and solving for $T_{\mathrm{on}}$ we find:
$$ T_{\mathrm{on}} = \frac{1}{\alpha_y} \log\left[\frac{1}{(1-K_{yz}/Y_{st})}\right] $$Thus we see that the delay before $Z$ turns on is a function of the degradation rate of $Y$ and the ratio between $Y_{st}$ and $K_{yz}$.
From the above formula, we see that there are two parameters that affect the turn-on time of $Z$ -- $\alpha_y$ (the scaling factor for the decay rate of $Y$) and the compound parameter $K_{yz}/Y_{st}$ (the threshold concentration where $Y$ activate $Z$ relative to the steady state of $Y$). To explore the two-dimensional parameter space of $Z's$ $T_on$ we can create a contour plot.
In [38]:
def Ton(alpha, KYratio):
return (1.0/alpha) * log(1.0/(1.0-KYratio))
## Create a contour plot for a range of alpha and Kyz/Yst
x = alpha = linspace(0.01, 0.2, 100)
y = KYratio = linspace(0.01, 0.99, 100)
X,Y = meshgrid(x, y)
Z = Ton(X,Y)
levels = MaxNLocator(nbins=20).tick_values(Z.min(), Z.max())
im = contourf(X,Y,Z, cmap=cm.inferno_r, levels=levels)
contour(X, Y, Z, levels,
colors=('k',),
linewidths=(0.5,))
colorbar(im)
xlabel('alpha')
ylabel("Kyz/Yst")
pass
As discussed in the article by Shen-Orr et al. a feed forward loop of the type we've just discussed can act as a type of filter -- a sign-sensitive delay that keeps $Z$ from firing in response to transient noisy signals from $X$, but shuts down $Z$ immediately once the signal from $X$ is removed.
Consider the FFL illustrated in the figure below.
In this incoherent FFL, the logic function that regulates $Z$ is "X AND NOT Y". That is $Z$ turns on once $X$ is above a given threshold, but only stays on fully as long as $Y$ is below another threshold. Again for simplicity we assume $K_{xy} = K_{yz}$.
To describe $Z$ we consider two phases - 1) while $Y < K_{yz}$ and 2) while $Y > K_{yz}$.
For the first phase:
$$ \frac{dZ}{dt} = \beta_z\ \Theta(X > K_{xz}) - \alpha_{z}Z $$and
$$ Z(t) = Z_{m}(1-e^{-\alpha_{z}t}) $$As we did in the case of the coherent FFL, we can calculate the time until $Y$ reaches the treshold $K_{yz}$. We'll call this $T_{\mathrm{rep}}$ and it is the same formula we found for $T_{\mathrm{on}}$ previously.
$$ T_{\mathrm{rep}} = \frac{1}{\alpha_y \log[\frac{1}{1-K_{yz}/Y_{st}}]} $$After a delay, $T_{\mathrm{rep}}$, $Y$ starts to repress the transcription of $Z$ and $Z$ decays to a new lower steady state, $Z_{st} = \beta_{z}^{'}/\alpha$. The value of $\beta_{z}^{'}$ depends on how leaky the repression of $Z$ is by $Y$.
The dynamics of $Z$ in Phase 2 is given by:
$$ Z(t) = Z_{st} + (Z_0 - Z_{st})e^{-\alpha_{z}(t-T_{\mathrm{rep}})} $$where $$ Z_0 = Z_{m}(1-e^{-\alpha_{z}T_{\mathrm{rep}}}) $$
In [79]:
## Write a Python function that represents dZ/dt for the Incoherent FFL
## our dY function previously defined stays the same
def dZ_incoh(B1,B2,Kx,Ky,a,X,Y,Z):
pass # define the function here
def dZ_incoh(B1,B2,Kx,Ky,a,X,Y,Z):
theta = 0
B = 0
if (X > Kx) and (Y < Ky):
theta = 1
B = B1
elif (X > Kx) and (Y >= Ky):
theta = 1
B = B2
return B * theta - a * Z
In [85]:
## Write your simulation here
nsteps = 150
short_pulse = pulse(20, 25, nsteps) # 5 sec pulse
long_pulse = pulse(50, 100, nsteps) # 50 sec pulse
X = short_pulse + long_pulse
# setup parameters for Y and Z
Y = [0]
betay, alphay = 0.2, 0.1
Kxy = 0.5
Z = [0]
betaz1, betaz2 = 0.2, 0.001
alphaz = 0.1
Kxz = 0.5
Kyz = 0.5
for i in range(nsteps):
xnow = X[i]
ynow, znow = Y[-1], Z[-1]
ynew = ynow + dY(betay, Kxy, alphay, xnow, ynow)
znew = znow + dZ_incoh(betaz1, betaz2, Kxz, Kyz, alphaz, xnow, ynow, znow)
Y.append(ynew)
Z.append(znew)
# draw each trace as a subfigure
# subfigures stacked in a vertical grid
subplot2grid((3,1),(0,0))
plot(X, 'k', label='X', linewidth=1)
legend()
ylim(0,1.1)
subplot2grid((3,1),(1,0))
plot(Y, 'b', label='Y', linewidth=2)
legend()
ylim(0,2.1)
subplot2grid((3,1),(2,0))
plot(Z, 'r', label='Z', linewidth=2)
legend()
ylim(0,0.7)
pass
Note that the stimulus amount of $Z$ in the system initially increases, but then decreases to a lower steady even when the initial stimulus persists. This system thus generates pulse-like dynamics to a persistent signal. How pulse-like the signal is depends on the ratio of $\beta_z$ to $\beta_{z}^{'}$. We define the repression factor, $F$, as follows:
$$ F = \frac{\beta_z}{\beta_{z}^{'}} = \frac{Z_m}{Z_{st}} $$See if you can come up with a reasonably small set of coupled ODEs for one of the signaling or regulatory networks you've learned about in this mini-term.