Exercise 4.7: Making a stand-alone app for exploring parameters
Recall that in our lesson on dashboarding, we built a dashboard investigating how fold change in gene expression varies with repressor copy number \(R\) and inducer concentration \(c\). The theoretical curve is
\begin{align} \text{fold change} = \left[1 + \frac{\frac{R}{K}\left(1 + c/K_\mathrm{d}^\mathrm{A}\right)^2}{\left(1 + c/K_\mathrm{d}^\mathrm{A}\right)^2 + K_\mathrm{switch}\left(1 + c/K_\mathrm{d}^\mathrm{I}\right)^2}\right]^{-1}, \end{align}
with parameters given in the table below.
Parameter |
Description |
---|---|
\(K_\mathrm{d}^\mathrm{A}\) |
dissoc. const. for active repressor binding IPTG |
\(K_\mathrm{d}^\mathrm{I}\) |
dissoc. const. for inactive repressor binding IPTG |
\(K_\mathrm{switch}\) |
equil. const. for switching active/inactive |
\(K\) |
dissoc. const. for active repressor binding operator |
\(R\) |
number of repressors in cell |
Rebuild that dashboard, but use JavaScript callbacks so you can have full interactivity with a stand-alone HTML file.
Solution
[1]:
import numpy as np
import bokeh.io
import bokeh.layouts
import bokeh.models
import bokeh.plotting
bokeh.io.output_notebook()
To build our app, we will first code up the functions to compute the fold change in Python so we can make the starting plot.
[2]:
def bohr_parameter(c, R, K, KdA, KdI, Kswitch):
"""Compute Bohr parameter based on MWC model."""
# Big nasty argument of logarithm
log_arg = (1 + c / KdA) ** 2 / ((1 + c / KdA) ** 2 + Kswitch * (1 + c / KdI) ** 2)
return -np.log(R / K) - np.log(log_arg)
def fold_change(c, R, K, KdA, KdI, Kswitch):
"""Compute theoretical fold change for MWC model."""
return 1.0 / (1.0 + np.exp(-bohr_parameter(c, R, K, KdA, KdI, Kswitch)))
Next, we can make the sliders we want. We will again vary the parameters on a logarithmic scale, but will use a clever trick with the labels to we do not display the base ten logarithm of the parameter value, but rather the value itself. To do this, we use a bokeh.models.FuncTickFormatter
to give a snippet of JavaScript code to display the value of the slider. We will raise ten to the value of the log slider and then display the resulting value with a precision of 3.
[3]:
slider_format = bokeh.models.FuncTickFormatter(
code="return Math.pow(10, tick).toPrecision(3)"
)
log_R_slider = bokeh.models.Slider(
title="R (1/cell)", start=0, end=3, step=0.1, value=2, format=slider_format,
)
log_K_slider = bokeh.models.Slider(
title="K (1/cell)", start=-6, end=3, step=0.1, value=0, format=slider_format,
)
log_KdA_slider = bokeh.models.Slider(
title="KdA (1/mM)", start=-6, end=3, step=0.1, value=-2, format=slider_format,
)
log_KdI_slider = bokeh.models.Slider(
title="KdI (1/mM)", start=-6, end=3, step=0.1, value=-2, format=slider_format,
)
log_Kswitch_slider = bokeh.models.Slider(
title="Kswitch", start=-3, end=6, step=0.1, value=1, format=slider_format,
)
Now, let’s build out plot, using a ColumnDataSource
so we can manipulate the values.
[4]:
c = np.logspace(-6, 2, 200)
fc = fold_change(
c,
10 ** log_R_slider.value,
10 ** log_K_slider.value,
10 ** log_KdA_slider.value,
10 ** log_KdI_slider.value,
10 ** log_Kswitch_slider.value,
)
cds = bokeh.models.ColumnDataSource(dict(c=c, fc=fc))
p = bokeh.plotting.figure(
frame_height=250,
frame_width=350,
x_axis_label="[IPTG] (mM)",
y_axis_label="fold change",
x_range=[1e-6, 1e2],
y_range=[-0.05, 1.05],
x_axis_type="log",
)
p.line(source=cds, x="c", y="fc", line_width=2);
Next, we code up the JavaScript code to get triggered when the sliders change values. The JS code is pretty straightforward, directly following what we have seen in the lesson.
[5]:
jscode = """
function bohr_parameter(c, R, K, KdA, KdI, Kswitch){
var log_arg = (1 + c / KdA) ** 2 / ((1 + c / KdA) ** 2 + Kswitch * (1 + c / KdI) ** 2);
return -Math.log(R / K) - Math.log(log_arg);
}
function fold_change(c, R, K, KdA, KdI, Kswitch) {
return 1 / (1 + Math.exp(-bohr_parameter(c, R, K, KdA, KdI, Kswitch)));
}
// For convenience, get views into the ColumnDataSource data
var c = cds.data['c'];
var fc = cds.data['fc'];
// Pull the values off of the sliders
var R = 10 ** log_R_slider.value;
var K = 10 ** log_K_slider.value;
var KdA = 10 ** log_KdA_slider.value;
var KdI = 10 ** log_KdI_slider.value;
var Kswitch = 10 ** log_Kswitch_slider.value;
// Loop through and update
var cLen = c.length;
for (var i = 0; i < cLen; i++) {
fc[i] = fold_change(c[i], R, K, KdA, KdI, Kswitch);
}
cds.change.emit();
"""
Finally, we link the code to the sliders as a bokeh.models.CustomJS
instance, making sure to specify the arguments that the JS code needs.
[6]:
args = dict(
cds=cds,
log_R_slider=log_R_slider,
log_K_slider=log_K_slider,
log_KdA_slider=log_KdA_slider,
log_KdI_slider=log_KdI_slider,
log_Kswitch_slider=log_Kswitch_slider,
)
code = bokeh.models.CustomJS(code=jscode, args=args)
log_R_slider.js_on_change("value", code)
log_K_slider.js_on_change("value", code)
log_KdA_slider.js_on_change("value", code)
log_KdI_slider.js_on_change("value", code)
log_Kswitch_slider.js_on_change("value", code)
Great! Now, let’s lay out our app and take a look!
[7]:
layout = bokeh.layouts.row(
p,
bokeh.layouts.Spacer(width=15),
bokeh.layouts.column(
log_R_slider,
log_K_slider,
log_KdA_slider,
log_KdI_slider,
log_Kswitch_slider,
width=200,
),
)
bokeh.io.show(layout)
Computing environment
[8]:
%load_ext watermark
%watermark -v -p numpy,bokeh,jupyterlab
Python implementation: CPython
Python version : 3.9.12
IPython version : 8.3.0
numpy : 1.21.5
bokeh : 2.4.2
jupyterlab: 3.3.2