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()
Loading BokehJS ...

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