0

I'm relatively new to Python and Stackoverflow so apologies if this question has already been asked but I've searched for quite a while now and can't really find a solution to what I'm trying to do.

Problem:

I've been trying to create a very basic model of the COVID-19 epidemic to familiarise myself with Python. My intention is to produce a basic SIR model that calculates susceptible, infected and removed individuals in a population. So far so good.

My problem is that I would like the plot to have an interactive slider that alters one of the constants in the differential equation.

I am using Bokeh and am trying to use Javascript Callbacks for this however I am having difficulty with the Javascript. All examples I have seen so far use line equations where y is a function of x, and which are relatively straightforward to code. In my case, since its a system of differential equations I'm not sure how I should be going about this.

I've also tried using scipy but I still encounter the same problem.

Code below. Any help/feedback/suggestions would be greatly appreciated.

Thanks!

from bokeh.layouts import column, row
from bokeh.models import CustomJS, Slider
from bokeh.plotting import ColumnDataSource, figure, output_file, show

t = []

for i in range(200):
    t.append(i)

slst = []
ilst = []
rlst = []

s = 489599/489609
i = 10/489609
r = 0/489609
bet = 0.28
gam = 0.189

for f in range(200):
    ds = (-bet * (s * i))
    di = ((bet * (s * i)) - (gam * i))
    dr = gam * i
    s = s + (ds)
    slst.append(s)
    i = i + (di)
    ilst.append(i)
    r = r + (dr)
    rlst.append(r)

source = ColumnDataSource(data=dict(x=t, y=[], s=slst, i=ilst))

plot = figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)

callback = CustomJS(args=dict(source=source), code="""

                    ????????????

    """)

slider = Slider(start=0.1, end=4, value=1, step=.1, title="Beta ")
slider.js_on_change('value', callback)

layout = column(slider, plot)

show(layout)
YP41
  • 35
  • 5
  • 2
    Your equation code is in Python, for it to work without rewriting it to JavaScript you need to use `bokeh serve`. Or are you sure you need to have a static HTML file? Also, you have the `y` column but you never populate it. – Eugene Pakhomov Apr 17 '20 at 14:43
  • 1
    I did a SEILR model (susceptible, exposed, infected, ill, recovered/removed) with "distancing" in https://math.stackexchange.com/a/3618757/115115. The general control structure of using call-backs to change parameters and trigger a re-computation should stay the same, even if the specific library functions change. – Lutz Lehmann Apr 17 '20 at 16:35
  • 1
    As @EugenePakhomov said, if you do not want to use a server, you will need to implement the ODE model and an ODE solver like the ubiquitous fixed-step RK4, in a `CostumJS` object so that the computation for each update of the model parameters can be performed from the "static" web page in the browser JS engine. Please – Lutz Lehmann Apr 17 '20 at 17:16
  • @EugenePakhomov my intention is to embed it on a news portal, so I just need to be able to embed it. RE the y column, that a mistake. The code was originally written with only x and y in the ColumnDataSource x being time (t) and y being the list of values for infected people (ilst). When the beta constant is changed, each new value for i will need to be recalculated in terms of s and i so I was trying to find a way of introducing both of them as variables however I can't code Javascript so not really too sure what I'm doing. – YP41 Apr 17 '20 at 17:49
  • @LutzLehmann. I thought that would make more sense. I've done the same thing using scipy odeint but since Bokeh requires Javascript code, I am not able to use it within the callback. I can get it to create the plot I want, but nothing moves when I move the slider around because obviously the code inside is wrong. Sorry if some of this is really obvious but as I said, I'm quite new to this and learning through practice. – YP41 Apr 17 '20 at 18:11
  • Some javascript implementations of mine, a specialized one in https://stackoverflow.com/questions/29830807/runge-kutta-problems-in-js/29832180#29832180 and a more general one in https://math.stackexchange.com/a/2028785/115115, so it is possible with relatively little effort. – Lutz Lehmann Apr 17 '20 at 19:20
  • @YP41 To be clear - Bokeh doesn't require JS by itself. It's the HTML embedding that you want that requires it. – Eugene Pakhomov Apr 18 '20 at 05:16

2 Answers2

3

Here's what I've come up with. It's interesting to see that with high values of beta, the susceptible line goes below 0. Maybe I've made a mistake while porting your code to JavaScript - please correct me if so.

from bokeh.core.property.instance import Instance
from bokeh.io import save
from bokeh.layouts import column
from bokeh.model import Model
from bokeh.models import CustomJS, Slider, Callback
from bokeh.plotting import ColumnDataSource, figure

source = ColumnDataSource(data=dict(t=[], s=[], i=[], r=[]))

plot = figure(plot_width=400, plot_height=400)
plot.line('t', 's', source=source, line_width=3, line_alpha=0.6)
plot.line('t', 'i', source=source, line_width=3, line_alpha=0.6, color='orange')
plot.line('t', 'r', source=source, line_width=3, line_alpha=0.6, color='green')

callback = CustomJS(args=dict(source=source), code="""\
    const N = 200;
    let s = 489599 / 489609;
    let i = 10 / 489609;
    let r = 0 / 489609;
    const bet = cb_obj.value;
    const gam = 0.189;
    const tlst = source.data.t = [];
    const slst = source.data.s = [];
    const ilst = source.data.i = [];
    const rlst = source.data.r = [];
    for (let t = 0; t < N; ++t) {
        s -= bet * s * i;
        i += bet * s * i - gam * i;
        r += gam * i;
        tlst.push(t);
        slst.push(s);
        ilst.push(i);
        rlst.push(r);
    }
    source.change.emit();
""")

slider = Slider(start=0.1, end=4, value=1, step=.1, title="Beta ")
slider.js_on_change('value', callback)


class IdleDocObserver(Model):
    """Work around https://github.com/bokeh/bokeh/issues/4272."""
    on_idle = Instance(Callback)

    # language=TypeScript
    __implementation__ = """\
        import {View} from "core/view"
        import {Model} from "model"
        import * as p from "core/properties"

        export class IdleDocObserverView extends View {}

        export namespace IdleDocObserver {
            export type Attrs = p.AttrsOf<Props>
            export type Props = Model.Props & {on_idle: p.Property<any>}
        }

        export interface IdleDocObserver extends IdleDocObserver.Attrs {}

        export class IdleDocObserver extends Model {
            static init_IdleDocObserver(): void {
                this.prototype.default_view = IdleDocObserverView
                this.define<IdleDocObserver.Props>({on_idle: [p.Any]})
            }

            _doc_attached(): void {
                super._doc_attached()
                const execute = () => this.on_idle!.execute(this)
                if (this.document!.is_idle)
                    execute();
                else
                    this.document!.idle.connect(execute);
            }
        }
    """


idle_doc_observer = IdleDocObserver(on_idle=CustomJS(args=dict(callback=callback, slider=slider),
                                                     code="callback.execute(slider);"))

layout = column(slider, plot)
save([idle_doc_observer, layout])
Eugene Pakhomov
  • 9,309
  • 3
  • 27
  • 53
  • 1
    That the susceptible go to zero or negative values is impossible for the exact solution. The plane `S=0` is completely filled with solutions that stay in that plane, no other solution can cross it (By the uniqueness part of Picard-Lindelöf). Here is only an artifact of the use of the Euler method with a rather large step size. – Lutz Lehmann Apr 18 '20 at 08:45
  • @Eugene. Thank you very much for this. Just reading through it. The code is returning an error when I run it. Will slowly go through it to understand exactly what's happening. Thanks for the help, much appreciated! – YP41 Apr 21 '20 at 08:25
  • Can you tell what the error is? It worked just fine for me on Bokeh 2.0.1. – Eugene Pakhomov Apr 21 '20 at 08:39
  • Does not work as a standalone example anymore. `Compilation failed:IdleDocObserver.ts:15:13:reserved word 'static'` – user2589273 Apr 29 '20 at 19:58
  • @user2589273 Works fine for me with Bokeh 2.0.2 and Node 12.16.3. – Eugene Pakhomov May 02 '20 at 20:25
1

As per my comments, one can use RK4 in a javascript implementation to integrate the ODE in the html document. There appears no way in bokeh to implement javascript functions outside of callbacks, utility functions and common computations. So to avoid code duplication one has to make the one callback universal enough that it can serve for all slider change events. (Alternatively, one could implement a "recompute" button.)

To look more professional, make 2 plots, one for all components, and one for I alone.

# Set up the plots and their data source
source = ColumnDataSource(data=dict(T=[], S=[], I=[], R=[]))

SIR_plot = figure(plot_width=400, plot_height=400)
SIR_plot.line('T', 'S', source=source, legend_label="S", line_width=3, line_alpha=0.6, color='blue')
SIR_plot.line('T', 'I', source=source, legend_label="I", line_width=3, line_alpha=0.6, color='orange')
SIR_plot.line('T', 'R', source=source, legend_label="R", line_width=3, line_alpha=0.6, color='green')

I_plot = figure(plot_width=400, plot_height=400)
I_plot.line('T', 'I', source=source, line_width=3, line_alpha=0.6, color='orange')

Next set up 4 sliders for parameters one is likely to want to influence

# declare the interactive interface elements
trans_rate = Slider(start=0.01, end=0.4, value=0.3, step=.01, title="transmission rate ")
recov_rate = Slider(start=0.01, end=0.4, value=0.1, step=.01, title="recovery rate")

I_init = Slider(start=0.01, end=0.1, value=0.05, step=.002, title="initial infected [proportion] ")
max_time = Slider(start=10, end=200, value=50, step=1, title="time range [days] ")

Now as the main change to the answer of Eugene Pakhomov, make one call-back for all sliders (see bokeh gallery slider demo) and use a vectorized RK4 method to do the ODE integration

callback = CustomJS(args=dict(source=source, I_init=I_init, max_time=max_time, 
                              trans_rate=trans_rate, recov_rate=recov_rate), 
                    code="""\
    let i = I_init.value;
    let s = 1-i;
    let r = 0;
    const bet = trans_rate.value;
    const gam = recov_rate.value;
    let tf = max_time.value;
    const dt = 0.1;
    const tlst = source.data.T = [0];
    const slst = source.data.S = [s];
    const ilst = source.data.I = [i];
    const rlst = source.data.R = [r];

    function odefunc(t,sir) {
        let tr = bet*sir[0]*sir[1];
        let rc = gam*sir[1];
        return [-tr, tr-rc, rc];
    }
    let sir = [s,i,r];
    for (let t = 0; t < tf; t+=dt) {
        sir = RK4Step(t,sir,dt);
        tlst.push(t+dt);
        slst.push(sir[0]);
        ilst.push(sir[1]);
        rlst.push(sir[2]);
    }
    source.change.emit();

    function axpy(a,x,y) { 
        // returns a*x+y for arrays x,y of the same length
        var k = y.length >>> 0;
        var res = new Array(k);
        while(k-->0) { res[k] = y[k] + a*x[k]; }
        return res;
    }

    function RK4Step(t,y,h) {
        var k0 = odefunc(t      ,               y );
        var k1 = odefunc(t+0.5*h, axpy(0.5*h,k0,y));
        var k2 = odefunc(t+0.5*h, axpy(0.5*h,k1,y));
        var k3 = odefunc(t+    h, axpy(    h,k2,y));
        // ynext = y+h/6*(k0+2*k1+2*k2+k3);
        return axpy(h/6,axpy(1,k0,axpy(2,k1,axpy(2,k2,k3))),y);
    }

""")
trans_rate.js_on_change('value', callback)
recov_rate.js_on_change('value', callback)

I_init.js_on_change('value', callback)
max_time.js_on_change('value', callback)

Finally, string all together in some layout

# generate the layout

parameters_panel = column(trans_rate, recov_rate)
initials_panel = column(I_init,max_time)

plots = row(SIR_plot, I_plot)
inputs = row(parameters_panel, initials_panel)

simulation = column(plots, inputs)

show(simulation)

To avoid the initial empty plots I refer to the answer of Eugene Pakhomov, as it is the plots appear after the first slider is moved.

Lutz Lehmann
  • 25,219
  • 2
  • 22
  • 51
  • Just seeing this. Thank you very much, this seems to work. I haven't really had time to go through the full code to determine what exactly is happening but I intend to do so that I can adapt it to other uses. Thanks again! – YP41 Apr 21 '20 at 08:19