# Optimization of Dissipative Qubit Reset

```python
# NBVAL_IGNORE_OUTPUT
%load_ext watermark
import qutip
import numpy as np
import scipy
import matplotlib
import matplotlib.pylab as plt
import krotov
%watermark -v --iversions
```

    qutip       4.3.1
    numpy       1.15.4
    scipy       1.1.0
    matplotlib  3.0.2
    matplotlib.pylab  1.15.4
    krotov      0.0.1
    CPython 3.6.7
    IPython 7.2.0

$\newcommand{tr}[0]{\operatorname{tr}}
\newcommand{diag}[0]{\operatorname{diag}}
\newcommand{abs}[0]{\operatorname{abs}}
\newcommand{pop}[0]{\operatorname{pop}}
\newcommand{aux}[0]{\text{aux}}
\newcommand{int}[0]{\text{int}}
\newcommand{opt}[0]{\text{opt}}
\newcommand{tgt}[0]{\text{tgt}}
\newcommand{init}[0]{\text{init}}
\newcommand{lab}[0]{\text{lab}}
\newcommand{rwa}[0]{\text{rwa}}
\newcommand{bra}[1]{\langle#1\vert}
\newcommand{ket}[1]{\vert#1\rangle}
\newcommand{Bra}[1]{\left\langle#1\right\vert}
\newcommand{Ket}[1]{\left\vert#1\right\rangle}
\newcommand{Braket}[2]{\left\langle #1\vphantom{#2} \mid
#2\vphantom{#1}\right\rangle}
\newcommand{op}[1]{\hat{#1}}
\newcommand{Op}[1]{\hat{#1}}
\newcommand{dd}[0]{\,\text{d}}
\newcommand{Liouville}[0]{\mathcal{L}}
\newcommand{DynMap}[0]{\mathcal{E}}
\newcommand{identity}[0]{\mathbf{1}}
\newcommand{Norm}[1]{\lVert#1\rVert}
\newcommand{Abs}[1]{\left\vert#1\right\vert}
\newcommand{avg}[1]{\langle#1\rangle}
\newcommand{Avg}[1]{\left\langle#1\right\rangle}
\newcommand{AbsSq}[1]{\left\vert#1\right\vert^2}
\newcommand{Re}[0]{\operatorname{Re}}
\newcommand{Im}[0]{\operatorname{Im}}$
This example provides an example for an optimization in an open quantum system,
where the dynamics is governed by the Liouville-von Neumann equation. Hence,
states are represented by density matrices $\op{\rho}(t)$ and the time-evolution
operator is given by a general dynamical map $\DynMap$.

## Define parameters

```python
omega_q = 1.0  # qubit level splitting
omega_T = 3.0  # TLS level splitting
J       = 0.1  # qubit-TLS coupling
kappa   = 0.04 # TLS decay rate
beta    = 1.0  # inverse bath temperature
T       = 25.0 # final time
nt      = 2500 # number of time steps
```

## Define the Liouvillian

The system is given by a qubit with Hamiltonian
$\op{H}_{q}(t) = - \omega_{q} \op{\sigma}_{z} - \epsilon(t) \op{\sigma}_{z}$,
where $\omega_{q}$ is a static energy level splitting which can time-dependently
be modified by $\epsilon(t)$. This qubit couples strongly to another two-level
system (TLS) with Hamiltonian $\op{H}_{t} = - \omega_{t} \op{\sigma}_{z}$ with
static energy level splitting $\omega_{t}$. The coupling strength between both
systems is given by $J$ with the interaction Hamiltonian given by $\op{H}_{\int}
= J \op{\sigma}_{x} \otimes \op{\sigma}_{x}$. The Hamiltonian for the system of
qubit and TLS is

\begin{equation}
  \op{H}(t) = \op{H}_{q}(t) \otimes
\identity_{t} + \identity_{q} \otimes \op{H}_{t} + \op{H}_{\int}.
\end{equation}
In addition, the TLS is embedded in a heat bath with inverse temperature
$\beta$. The TLS couples to the bath with rate $\kappa$. In order to simulate
the dissipation arising from this coupling, we consider the two Lindblad
operators $\op{L}_{1} =
\sqrt{\kappa (N_{th}+1)} \identity_{q} \otimes
\ket{0}\bra{1}$ and $\op{L}_{2} =
\sqrt{\kappa N_{th}} \identity_{q} \otimes
\ket{1}\bra{0}$ with $N_{th} =
1/(e^{\beta \omega_{t}} - 1)$. The dynamics of
the qubit-TLS system state
$\op{\rho}(t)$ is then governed by the Liouville-von
Neumann equation
\begin{equation}
  \frac{\partial}{\partial t} \op{\rho}(t) =
\Liouville(t) \op{\rho}(t)
=
  - i \left[\op{H}(t), \op{\rho}(t)\right]
  +
\sum_{k=1,2} \left(
\op{L}_{k} \op{\rho}(t) \op{L}_{k}^\dagger
  - \frac{1}{2}
\op{L}_{k}^\dagger
\op{L}_{k} \op{\rho}(t)
  - \frac{1}{2} \op{\rho}(t)
\op{L}_{k}^\dagger
\op{L}_{k}
  \right).
\end{equation}

```python
def liou_and_states(omega_q, omega_T, J, kappa, beta):
    """Liouvillian for the coupled system of qubit and TLS"""

    # drift qubit Hamiltonian
    H0_q = 0.5*omega_q*np.diag([-1,1])
    # drive qubit Hamiltonian
    H1_q = 0.5*np.diag([-1,1])

    # drift TLS Hamiltonian
    H0_T = 0.5*omega_T*np.diag([-1,1])

    # Lift Hamiltonians to joint system operators
    H0 = np.kron(H0_q, np.identity(2)) + np.kron(np.identity(2), H0_T)
    H1 = np.kron(H1_q, np.identity(2))

    # qubit-TLS interaction
    H_int = J*np.fliplr(np.diag([0,1,1,0]))

    # convert Hamiltonians to QuTiP objects
    H0 = qutip.Qobj(H0+H_int)
    H1 = qutip.Qobj(H1)

    # Define Lindblad operators
    N = 1.0/(np.exp(beta*omega_T)-1.0)
    # Cooling on TLS
    L1 = np.sqrt(kappa * (N+1))\
        * np.kron(np.identity(2), np.array([[0,1],[0,0]]))
    # Heating on TLS
    L2 = np.sqrt(kappa * N)\
        * np.kron(np.identity(2), np.array([[0,0],[1,0]]))

    # convert Lindblad operators to QuTiP objects
    L1 = qutip.Qobj(L1)
    L2 = qutip.Qobj(L2)

    # generate the Liouvillian
    L0 = qutip.liouvillian(H=H0, c_ops=[L1,L2])
    L1 = qutip.liouvillian(H=H1)

    # define qubit-TLS basis in Hilbert space
    psi_00 = qutip.Qobj(np.kron(np.array([1,0]), np.array([1,0])))
    psi_01 = qutip.Qobj(np.kron(np.array([1,0]), np.array([0,1])))
    psi_10 = qutip.Qobj(np.kron(np.array([0,1]), np.array([1,0])))
    psi_11 = qutip.Qobj(np.kron(np.array([0,1]), np.array([0,1])))

    # take as guess field a filed putting qubit and TLS into resonance
    eps0 = lambda t, args: omega_T - omega_q

    return ([L0, [L1, eps0]], psi_00, psi_01, psi_10, psi_11)

L, psi_00, psi_01, psi_10, psi_11= liou_and_states(
    omega_q=omega_q, omega_T=omega_T, J=J, kappa=kappa, beta=beta)
```

```python
proj_00 = psi_00 * psi_00.dag()
proj_01 = psi_01 * psi_01.dag()
proj_10 = psi_10 * psi_10.dag()
proj_11 = psi_11 * psi_11.dag()
```

## Define the optimization target

The time grid is given by `nt` equidistant
time steps between $t=0$ and $t=T$.

```python
tlist = np.linspace(0, T, nt)
```

The initial state of qubit and TLS are assumed to be in thermal equilibrium with
the heat bath (although only the TLS is directly interacting with the bath).
Both states are given by

\begin{equation}
  \op{\rho}_{\alpha}^{th} =
\frac{e^{x_{\alpha}} \ket{0}\bra{0} + e^{-x_{\alpha}} \ket{1}\bra{1}}{2
\cosh(x_{\alpha})},
  \qquad
  x_{\alpha} = \frac{\omega_{\alpha} \beta}{2},
\end{equation}

with $\alpha = q,t$. The initial state of the bipartite system
of qubit and TLS is given by the thermal state $\op{\rho}_{th} =
\op{\rho}_{q}^{th} \otimes \op{\rho}_{t}^{th}$.

```python
x_q = omega_q*beta/2.0
rho_q_th = np.diag([np.exp(x_q),np.exp(-x_q)])/(2*np.cosh(x_q))

x_T = omega_T*beta/2.0
rho_T_th = np.diag([np.exp(x_T),np.exp(-x_T)])/(2*np.cosh(x_T))

rho_th = qutip.Qobj(np.kron(rho_q_th, rho_T_th))
```

Since we are in the end only interested in the state of the qubit, we define
`trace_TLS`, which calculates the partial trace over the TLS degrees of freedom.
Hence, by passing the state $\op{\rho}$ of the bipartite system of qubit and
TLS, it returns the reduced state of the qubit, $\op{\rho}_{q} =
\tr_{t}\{\op{\rho}\}$.

```python
def trace_TLS(rho):
    """Partial trace over the TLS degrees of freedom"""
    rho_q = np.zeros(shape=(2,2), dtype=np.complex_)
    rho_q[0,0] = rho[0,0] + rho[1,1]
    rho_q[0,1] = rho[0,2] + rho[1,3]
    rho_q[1,0] = rho[2,0] + rho[3,1]
    rho_q[1,1] = rho[2,2] + rho[3,3]
    return qutip.Qobj(rho_q)
```

As target state we take (temporarily) the ground state of the bipartite system,
i.e., $\op{\rho}_{\tgt} = \ket{00}\bra{00}$. Note that in the end we will only
optimize the reduced state of the qubit.

```python
rho_q_trg = np.diag([1,0])
rho_T_trg = np.diag([1,0])
rho_trg = np.kron(rho_q_trg, rho_T_trg)
rho_trg = qutip.Qobj(rho_trg)
```

Next, the list of `objectives` is defined, which contains the initial and target
state and the Liouvillian $\Liouville(t)$ determining the evolution of the
system.

```python
objectives = [
    krotov.Objective(initial_state=rho_th, target=rho_trg, H=L)
]
```

In the following, we define the shape function $S(t)$, which we use in order to
ensure a smooth switch on and off in the beginning and end. Note that at times
$t$ where $S(t)$ vanishes, the updates of the field is suppressed. A priori,
this has
nothing to do with the shape of the field on input.

```python
def S(t):
    """Shape function for the field update"""
    return krotov.shapes.flattop(
        t, t_start=0, t_stop=T,
        t_rise=0.05*T, t_fall=0.05*T,
        func='sinsq')
```

However, we also want to start with a shaped field on input. Hence, we use the
previously defined shape function $S(t)$ to shape $\epsilon_{0}(t)$.

```python
def shape_field(eps0):
    """Applies the shape function S(t) to the guess field"""
    eps0_shaped = lambda t, args: eps0(t, args)*S(t)
    return eps0_shaped

L[1][1] = shape_field(L[1][1])
```

At last, before heading to the actual optimization below, we assign the shape
function $S(t)$ to the OCT parameters of the control and choose `lambda_a`, a
numerical
parameter that controls the field update magnitude in each iteration.

```python
pulse_options = {
    L[1][1]: krotov.PulseOptions(lambda_a=0.01, shape=S)
}
```

## Simulate the dynamics of the guess field

```python
def plot_pulse(pulse, tlist):
    fig, ax = plt.subplots()
    if callable(pulse):
        pulse = np.array([pulse(t, args=None) for t in tlist])
    ax.plot(tlist, pulse)
    ax.set_xlabel('time')
    ax.set_ylabel('pulse amplitude')
    plt.show(fig)
```

The following plot shows the guess field $\epsilon_{0}(t)$, which is, as chosen
above, just a constant field that puts qubit and TLS into resonance (with a
smooth switch-on and switch-off)

```python
plot_pulse(L[1][1], tlist)
```

Before optimizing, we solve the equation of motion for the guess field
$\epsilon_{0}(t)$.

```python
guess_dynamics = objectives[0].mesolve(
    tlist, e_ops=[proj_00, proj_01, proj_10, proj_11])
```

By inspecting the population dynamics of qubit and TLS ground state, we see that
both are oscillating and especially the qubit's ground state population reaches
a maximal value at intermediate times $t < T$. This maximum is indeed the
maximum that is physically possible. However, we want to reach this maximum at
final time $T$ (not before), so the guess field is not doing the right thing so
far.

```python
def plot_population(result):
    fig, ax = plt.subplots()
    ax.plot(result.times, np.array(result.expect[0])+np.array(result.expect[1]),
            label='qubit 0')
    ax.plot(result.times, np.array(result.expect[0])+np.array(result.expect[2]),
            label='TLS 0')
    ax.legend()
    ax.set_xlabel('time')
    ax.set_ylabel('population')
    plt.show(fig)

plot_population(guess_dynamics)
```

## Optimize

Our optimization target is the ground state $\ket{\Psi_{q}^{\tgt}}
= \ket{0}$ of the qubit, irrespective of the state of the TLS. Thus, our
optimization functional reads

\begin{equation}
  F_{re} = 1 -
\Braket{\Psi_{q}^{\tgt}}{\tr_{t}\{\op{\rho}(T)\} \,|\; \Psi_{q}^{\tgt}}
\end{equation}

and we first define `print_qubit_error`, which prints out the
above functional after each iteration.

```python
def print_qubit_error(**args):
    """Utility function writing the qubit error to screen"""
    taus = []
    for state_T in args['fw_states_T']:
        state_q_T = trace_TLS(state_T)
        taus.append(state_q_T[0,0].real)
    J_T_re = 1 - np.average(taus)
    print("Iteration %d: \tJ_T = %f" % (args['iteration'], J_T_re))
    return J_T_re
```

In order to minimize the above functional, we need to provide the correct
`chi_constructor` for the Krotov optimization, as it contains all necessary
information. Given our bipartite system and choice of $F_{re}$, the
$\op{\chi}(T)$ reads

\begin{equation}
  \op{\chi}(T) =
  \sum_{k=0,1} a_{k}
\op{\rho}_{q}^{\tgt} \otimes \ket{k}\bra{k}
\end{equation}

with $\{\ket{k}\}$ a
basis for the TLS Hilbert space.

```python
def TLS_onb_trg():
    """Returns the tensor product of qubit target state
    and a basis for the TLS Hilbert space"""
    rho1 = qutip.Qobj(np.kron(rho_q_trg, np.diag([1,0])))
    rho2 = qutip.Qobj(np.kron(rho_q_trg, np.diag([0,1])))
    return [rho1, rho2]

TLS_onb = TLS_onb_trg()

def chis_qubit(states_T, objectives, tau_vals):
    """Calculate chis for the chosen functional"""
    chis = []
    for state_i_T in states_T:
        chis_i = np.zeros(shape=(4,4), dtype=np.complex_)
        for state_k in TLS_onb:
            a_i_k = krotov.optimize._overlap(state_i_T, state_k)
            chis_i += a_i_k * state_k
        chis.append(qutip.Qobj(chis_i))
    return chis
```

The following carries out the optimization.

```python
oct_result = krotov.optimize_pulses(
    objectives, pulse_options, tlist,
    propagator=krotov.propagators.expm,
    chi_constructor=chis_qubit,
    info_hook=krotov.info_hooks.chain(
        print_qubit_error),
    check_convergence=krotov.convergence.check_monotonic_error,
    iter_stop=5)
```

    Iteration 0: 	J_T = 0.110918
    Iteration 1: 	J_T = 0.105496
    Iteration 2: 	J_T = 0.066806
    Iteration 3: 	J_T = 0.050200
    Iteration 4: 	J_T = 0.049164
    Iteration 5: 	J_T = 0.048533

```python
oct_result
```

    Krotov Optimization Result
    --------------------------
    - Started at 2018-12-20 08:35:29
    - Number of objectives: 1
    - Number of iterations: 5
    - Reason for termination: Reached 5 iterations
    - Ended at 2018-12-20 08:36:23

## Simulate the dynamics of the optimized field

The plot of the optimized field
shows, that the optimization slightly shifts the field such that qubit and TLS
are no longer perfectly in resonance.

```python
plot_pulse(oct_result.optimized_controls[0], tlist)
```

This slight shift of qubit and TLS out of resonance delays the population
oscillations between
qubit and TLS ground state such that the qubit ground state
is maximally
populated at final time $T$.

```python
optimized_dynamics = oct_result.optimized_objectives[0].mesolve(
    tlist, e_ops=[proj_00, proj_01, proj_10, proj_11])

plot_population(optimized_dynamics)
```


## My Model

My own model for 2019-nCov
Based on SEIR model

main innovation:
1. construct a two stage model, which can describe the hudge transportation before Spring Festival and the strict regulation of government.
2. according to the news, patients in incubation period can infect other people. My research try to model this feature.

模型符号
S: 易感  beta，sigma: 易感-潜伏 sigma=r·beta  (r>1) r由正常人群和隔离人群接触人数比例确定
E：潜伏  αlpha：感染-确诊（隔离）  拟合优化确定
I：隔离  gamma：隔离-移除  根据已有数据确定
R：移除
T: 春运
F：封城

模型方程：
SEIR_modified 模型：

$$
\begin{align}
&\frac{dS}{dt} = -β·\frac{I·S}{N} - σ·\frac{I·E}{N} \\
&\frac{dE}{dt} = β·\frac{I·S}{N} + σ·\frac{I·E}{N} - α·E \\
&\frac{dI}{dt} = α·E - γ·I \\
&\frac{dR}{dt}=γ·I \\
&σ=r·β
\end{align}
$$

```python
import pandas as pd
import numpy as np
import warnings
import matplotlib.pyplot as plt
from IPython.core.interactiveshell import InteractiveShell
warnings.filterwarnings('ignore')
InteractiveShell.ast_node_interactivity = "all"
```

```python
import seaborn as sns
sns.set_style('darkgrid')
sns.mpl.rcParams['figure.figsize'] = (12.0, 8.0)
sns.mpl.rcParams['savefig.dpi'] = 90
sns.mpl.rcParams['font.family'] = 'sans-serif'
sns.mpl.rcParams['font.size'] = 14
```

```python
# detail data load
china_detail_data=pd.read_hdf('..\\data\\detail_data_store.h5',key='china_detail_data')
gs_detail_data=pd.read_hdf('..\\data\\detail_data_store.h5',key='gs_detail_data')
sx_detail_data=pd.read_hdf('..\\data\\detail_data_store.h5',key='sx_detail_data')
cq_detail_data=pd.read_hdf('..\\data\\detail_data_store.h5',key='cq_detail_data')
gd_detail_data=pd.read_hdf('..\\data\\detail_data_store.h5',key='gd_detail_data')
hb_detail_data=pd.read_hdf('..\\data\\detail_data_store.h5',key='hb_detail_data')
```

```python
# national daily data load
data=pd.read_hdf('..\\data\\data.h5')
data_new=pd.read_hdf('..\\data\\data_new.h5')
data_init=pd.read_hdf('..\\data\\data_init.h5')
```

```python
china_detail_data.head(3)
```

      确诊
      疑似
      当日医学观察
      累计医学观察
      全国数据来源

      2020-01-20
      NaN
      NaN
      NaN
      NaN
      NaN

      2020-01-21
      440
      NaN
      922
      1739
      国家卫健委

      2020-01-22
      473
      NaN
      1394
      2197
      国家卫健委

```python
# visualize the ratio of accumulated number of people under medical observation and the number of diagnosed patients
plt.figure(figsize=(10,8))
plt.plot(china_detail_data['确诊'],marker='o',markersize=5) ## plot 确诊
plt.plot(china_detail_data['累计医学观察'],marker='+',markersize=6) ## plot 医学累计观察
plt.title('2019-nCov prediction detail data: China',fontsize=16)
plt.xlabel('date',fontsize=14)
plt.ylabel('number',fontsize=14)
plt.legend(['diagnosed','observation_all'])
import pylab as pl
pl.xticks(rotation=270)
```

```python
# compute number of medical observation/diagnosed
## note that for Shanxi and Hubei province,we only have accumulated observation
china_k=china_detail_data['累计医学观察']/china_detail_data['确诊']
hb_k=hb_detail_data['累计医学观察']/hb_detail_data['确诊']
gd_k=gd_detail_data['当日医学观察']/gd_detail_data['确诊']  ## 累计医学观察为nan，用当日医学观察代替
cq_k=cq_detail_data['累计医学观察']/cq_detail_data['确诊']
sx_k=sx_detail_data['当日医学观察']/(sx_detail_data['确诊']+10**(-10)) ## 有含0的情况
gs_k=gs_detail_data['累计医学观察']/(gs_detail_data['确诊']+10**(-10))
```

```python
k_sample=pd.DataFrame([china_k,hb_k,gd_k,cq_k,sx_k,gs_k]).T
k_sample.columns=['China','hb','gd','cq','sx','gs']
```

```python
k_sample
```

      China
      hb
      gd
      cq
      sx
      gs

      2020-01-20
      NaN
      NaN
      NaN
      NaN
      0.000000
      NaN

      2020-01-21
      3.952273
      NaN
      NaN
      NaN
      0.000000
      NaN

      2020-01-22
      4.644820
      NaN
      NaN
      10.444444
      17.666667
      NaN

      2020-01-23
      7.104819
      NaN
      4.867925
      8.296296
      25.000000
      NaN

      2020-01-24
      11.808081
      NaN
      4.435897
      7.087719
      11.933333
      NaN

      2020-01-25
      11.863797
      7.594106
      5.734694
      10.840000
      12.409091
      11.857143

      2020-01-26
      11.952988
      7.304287
      4.849315
      10.054545
      15.971429
      14.214286

      2020-01-27
      10.594241
      6.228445
      5.420213
      10.962121
      19.543478
      14.684211

      2020-01-28
      10.970372
      6.206809
      5.568465
      16.748299
      23.000000
      16.500000

      2020-01-29
      11.502140
      6.275621
      4.135048
      21.933333
      31.412698
      21.769231

      2020-01-30
      11.718840
      6.053049
      3.852417
      20.024272
      31.942529
      24.137931

      2020-01-31
      11.617929
      5.742346
      3.778846
      24.655462
      35.287129
      26.600000

      2020-02-01
      11.393880
      5.352766
      4.003311
      27.965649
      39.945455
      28.125000

```python
plt.figure(figsize=(12,8))
plt.plot(k_sample,marker='o',markersize=5)
plt.title('2019-nCov prediction detail data: China,hubei,guangdong,chongqing,shanxi,gansu',fontsize=16)
plt.xlabel('date',fontsize=14)
plt.ylabel('number',fontsize=14)
plt.legend(['China','Hubei','Guangdong','Chongqing','Shanxi','Gansu'])
import pylab as pl
pl.xticks(rotation=315)

plt.savefig('k_value_province.jpg')
```

根据疫情4-7天的潜伏期（丁香医生），24日以前完成的春运(20日至24日集中)接触效果在2020-01-25 -- 2020-02-01显现，从图像上看，多地确实显现出了春运带来的接触者影响，k值有所上升。
值得注意的时，2020-01-24及以后，全国的数据k值平稳，可以作为平均接触人数的参考

#### 考虑到在2020-01-23，政府采取了封城举措，全国多地启动一级响应，可以把春运和封城举动综合考虑

```python
china_k[china_k.index>='2020-01-24'].mean()
k_original=11.5
```

    11.491363226848797

在封城和春运之后,k值是有所变化的,这个变化可以用封城春运后的分层抽样来刻画.
注:这个时候已经不能用全国的值了,因为湖北多个地方被封城了,因该选取每层的分层抽样来刻画.

k_T计算：
分层采样的加权平均：
一级区域：湖北
二级区域:浙江 广东 湖南 河南 >400
三级区域:安徽 江西 重庆 江苏 四川 山东 200-400
四级区域:北京 上海 福建 陕西 广西 河北 云南 黑龙江 90-200
五级区域:辽宁 海南 山西 天津 甘肃 贵州 宁夏 内蒙古 吉林 新疆 <90

```python
level_data=[9074,2221,1627,1049,408]
# k 增加比例
# 按2020-01-25 到 2020-02-01的均值
def period_mean(k_data):
    # 2020-01-25 to 2020-02-01
    return k_data[(k_data.index>='2020-01-25')&(k_data.index<='2020-02-01')].mean()
level_k=[period_mean(hb_k),period_mean(gd_k),period_mean(cq_k),period_mean(sx_k),period_mean(gs_k)]
```

```python
level_k
```

    [6.34467869394357,
     4.6677886735908265,
     17.897960275527986,
     26.188976018462753,
     19.735975112590957]

```python
k_new=sum([level_data[i]*level_k[i] for i in range(0,len(level_data))])/sum(level_data)
k_new
```

    9.22061813550857

可以看到，在春运和疫情管控综合作用下，k值是降低的

```python
# 春运+管控 影响系数：
k_change_ratio=k_original/k_new
k_change_ratio
```

    1.2472048870252568

然而，另一方面,春运的存在，势必增加了易感人群的数量，也就是第二阶段（stage 2）的N值会增加
基于一个简单的估计：
根据信息：武汉市人口1100W，封城前有500W人离开，截止2月1日24时武汉市有4109人感染，设两种人群（离开和非离开）的比例相同，则500W人中
有 4109×500/600 人感染，不妨设湖北流出感染者是其两倍，共约为7000人
根据平均k值，一个人密切接触者11.5人，则易感人群增加：7000×11.5约8W人
因此认为: N_stage_2=N_stage_1+80000

#### a single stage example

```python
param_dict={
    'beta':0.09,
    'alpha':0.25,
    'gamma':0.03,
    'init_S':100000,
    'init_I':44,
    'init_E':300,
    'init_R':0
}
```

```python
from class_SEIR_modified import *
my_model=seir_modified_model(param_dict)
result=my_model.seir_modified_model_pre()
```

```python
# predicted I
##
pre_I=pd.Series(result[2][:50])

## date index
date_index=pd.date_range(start='2020-01-15',periods=pre_I.shape[0]).tolist()
date_index=np.array([time.date() for time in date_index],dtype='str')

##
pre_I.index=date_index

pre_I
```

    2020-01-15       44.000000
    2020-01-16      125.281930
    2020-01-17      225.282396
    2020-01-18      350.358332
    2020-01-19      508.720760
    2020-01-20      710.843729
    2020-01-21      969.965352
    2020-01-22     1303.035687
    2020-01-23     1731.274885
    2020-01-24     2279.937966
    2020-01-25     2979.468628
    2020-01-26     3868.431001
    2020-01-27     4987.382629
    2020-01-28     6378.165034
    2020-01-29     8082.057620
    2020-01-30    10133.134888
    2020-01-31    12562.012978
    2020-02-01    15373.012600
    2020-02-02    18541.471613
    2020-02-03    22013.745027
    2020-02-04    25716.657526
    2020-02-05    29550.125450
    2020-02-06    33390.204249
    2020-02-07    37129.918641
    2020-02-08    40680.097065
    2020-02-09    43969.371692
    2020-02-10    46937.017553
    2020-02-11    49529.890797
    2020-02-12    51751.546249
    2020-02-13    53609.865731
    2020-02-14    55111.610210
    2020-02-15    56262.419801
    2020-02-16    57075.921457
    2020-02-17    57597.740510
    2020-02-18    57862.270035
    2020-02-19    57900.251991
    2020-02-20    57740.082359
    2020-02-21    57408.127264
    2020-02-22    56932.193108
    2020-02-23    56332.595535
    2020-02-24    55627.175243
    2020-02-25    54833.625337
    2020-02-26    53969.491328
    2020-02-27    53052.074038
    2020-02-28    52093.344913
    2020-02-29    51099.947352
    2020-03-01    50078.941812
    2020-03-02    49037.685203
    2020-03-03    47983.830888
    2020-03-04    46925.286046
    dtype: float64

```python
# visualize predata
## plot
plt.plot(pre_I,marker='o',markersize=5)
plt.plot(list(data['确诊']),marker='+',markersize=6)
plt.title('2019-nCov predicted infected number',fontsize=16)
plt.xlabel('date',fontsize=14)
plt.ylabel('number',fontsize=14)

plt.legend(['predicted','real'])

import pylab as pl
pl.xticks(range(0,pre_I.shape[0],5),rotation=315)
```

## Two Stage Infection

### stage one:
before 2020-01-23
拟合日期(参数优化)： [2020-01-20,2020-01-24]+4~7 = [2020-01-24 - 2020-01-31]

**optimize best param by using hyperopt**

```python
fixed_param_stage_1={
    'gamma':0.025,
    'init_S':100000,
    'init_I':44,
    'init_E':300,
    'init_R':2
}
seir_modified_param_opt_class=seir_modified_param_opt(data,fixed_param_stage_1)
best_param_stage_1,trial=seir_modified_param_opt_class.seir_hp()
```

    100%|█████████████████████████████████████████████| 1000/1000 [01:15<00:00,  6.85trial/s, best loss: 899822.0500477956]

```python
best_param_stage_1
```

    {'alpha': 0.15, 'beta': 0.08}

```python
param_stage_1={**best_param_stage_1,**fixed_param_stage_1}
param_stage_1
```

    {'alpha': 0.15,
     'beta': 0.08,
     'gamma': 0.025,
     'init_S': 100000,
     'init_I': 44,
     'init_E': 300,
     'init_R': 2}

```python
mymodel_stage_1=seir_modified_model(param_stage_1)
result_stage_1=mymodel_stage_1.seir_modified_model_pre()
pre_stage_1=result_stage_1[2]
```

```python
plot=result_visualize(data,result_stage_1,pre_start_date='2020-01-15')
```

```python
# global predicted I visualization
plot.pre_visualize_global()
```

```python
# local predicted I visualization
plot.pre_visualize_local(start_date='2020-01-25',end_date='2020-02-05')
```

```python
# whole pre result S,E,I,R
plot.visualize_all_result()
```

### stage two

```python
result_stage_1=pd.DataFrame(result_stage_1).T
date_index = pd.date_range(start=data.index[0], periods=result_stage_1.shape[0]).tolist()
date_index = np.array([time.date() for time in date_index],dtype='str')
result_stage_1.index= date_index
result_stage_1.columns=['S','E','I','R']
```

```python
# predicted S & E at stage two start date
result_stage_1[result_stage_1.index=='2020-01-25']
```

      S
      E
      I
      R

      2020-01-25
      93441.027626
      4513.400925
      2216.028589
      175.54286

```python
# update parameters for stage 2
param_stage_2={
    'beta':param_stage_1['beta'],
    'alpha':param_stage_1['alpha']*2,
    'gamma':0.03,
    'init_S':93441+10000,
    'init_I':1975,
    'init_E':4513,
    'init_R':49
}
```

```python
param_stage_2
```

    {'beta': 0.08,
     'alpha': 0.3,
     'gamma': 0.03,
     'init_S': 103441,
     'init_I': 1975,
     'init_E': 4513,
     'init_R': 49}

```python
# model
my_model=seir_modified_model(param_stage_2,k_change_ratio=k_change_ratio)
result_stage_2=my_model.seir_modified_model_pre()
```

```python
pre_stage_2=result_stage_2[2]
```

```python
plot_stage_2=result_visualize(data,result_stage_2,pre_start_date='2020-01-25')
```

```python
# global predicted I visualization - stage 2
plot_stage_2.pre_visualize_global()
```

```python
# local predicted I visualization - stage 2
plot_stage_2.pre_visualize_local(start_date='2020-01-25',end_date='2020-02-05')
```

```python
plot_stage_2.visualize_all_result()
```

### 结论
在2月底三月上旬到达峰值,大约在5W人左右


# newton ip

```python
import numpy as np
import sympy as sy
sy.init_printing()
from matplotlib import pyplot as plt
%matplotlib inline
```

```python
xs=[0.0,0.2,0.4,0.6,0.8,1.2,1.4,1.6,1.8,2.0]
ys=[2.0,2.1,1.6,2.6,1.5,2.7,0.67,3.5,0.94,2.0]
size=len(xs)
```

```python
x=sy.symbols('x')
prod_polys=[sy.prod([x-xs[i] for i in range(k)] ) for k in range(size)]
prod_polys
```

```python
a=np.empty((size,size))
for i,y in enumerate(ys):
    a[i][0]=y
a
```

    array([[  2.        ,   3.5101219 ,   4.26801633,   4.49987487,
              4.38363044,   4.05651471,   3.62175494,   3.15447872,
              2.70689101,   2.31278516],
           [  2.1       ,   1.75099979,   1.59126426,   1.50614551,
              1.48563025,   1.51741523,   1.5882149 ,   1.68478515,
              1.79469838,   1.90690283],
           [  1.6       ,   2.10294297,   2.17415758,   2.22248287,
              2.24657953,   2.24684946,   2.22520944,   2.18483082,
              2.12985927,   2.06512661],
           [  2.6       ,   1.92743443,   1.86506545,   1.8136324 ,
              1.77745285,   1.76012246,   1.76438566,   1.79204398,
              1.84390237,   1.91975297],
           [  1.5       ,   2.13768899,   2.27464158,   2.4255193 ,
              2.58598565,   2.75125835,   2.91628175,   3.07590947,
              3.22509233,   3.35906622],
           [  2.7       ,   3.56484131,   3.63012625,   3.66746287,
              3.67596916,   3.65589031,   3.60864831,   3.53685564,
              3.44429022,   3.33582991],
           [  0.67      ,   3.09554987,   2.97781049,   2.87191572,
              2.78580892,   2.72728737,   2.70367351,   2.7214645 ,
              2.78596808,   2.90093391],
           [  3.5       ,   3.28730685,   3.55527305,   3.86624933,
              4.21136811,   4.57862901,   4.95290208,   5.31606432,
              5.64729577,   5.92356373],
           [  0.94      ,   6.21248689,   6.1756407 ,   5.9876432 ,
              5.63055027,   5.09296877,   4.37286618,   3.48088856,
              2.44423972,   1.31117724],
           [  2.        ,  -0.91412014,  -1.75426842,  -2.17235788,
             -1.92152043,  -0.69037119,   1.90764317,   6.34607968,
             13.19860492,  23.15346258]])

```python
row,col=size,size
for j in range(1,col):
    for i in range(row-j):
        a[i][j]=(a[i+1][j-1]-a[i][j-1])/(xs[i+j]-xs[i])
print(a)

```

    [[  2.00000000e+00   5.00000000e-01  -7.50000000e+00   4.37500000e+01
       -1.48437500e+02   2.28298611e+02  -2.94704861e+02   3.57259115e+02
       -4.41240045e+02   5.47970834e+02]
     [  2.10000000e+00  -2.50000000e+00   1.87500000e+01  -7.50000000e+01
        1.25520833e+02  -1.84288194e+02   2.76909722e+02  -4.36972966e+02
        6.54701623e+02   1.90690283e+00]
     [  1.60000000e+00   5.00000000e+00  -2.62500000e+01   5.05208333e+01
       -9.56250000e+01   2.03385417e+02  -4.22247024e+02   7.41489955e+02
        2.12985927e+00   2.06512661e+00]
     [  2.60000000e+00  -5.50000000e+00   1.41666667e+01  -4.51041667e+01
        1.48437500e+02  -3.87760417e+02   7.64136905e+02   1.79204398e+00
        1.84390237e+00   1.91975297e+00]
     [  1.50000000e+00   3.00000000e+00  -2.19166667e+01   1.03333333e+02
       -3.16875000e+02   6.82031250e+02   2.91628175e+00   3.07590947e+00
        3.22509233e+00   3.35906622e+00]
     [  2.70000000e+00  -1.01500000e+01   6.07500000e+01  -2.13541667e+02
        5.01562500e+02   3.65589031e+00   3.60864831e+00   3.53685564e+00
        3.44429022e+00   3.33582991e+00]
     [  6.70000000e-01   1.41500000e+01  -6.73750000e+01   1.87708333e+02
        2.78580892e+00   2.72728737e+00   2.70367351e+00   2.72146450e+00
        2.78596808e+00   2.90093391e+00]
     [  3.50000000e+00  -1.28000000e+01   4.52500000e+01   3.86624933e+00
        4.21136811e+00   4.57862901e+00   4.95290208e+00   5.31606432e+00
        5.64729577e+00   5.92356373e+00]
     [  9.40000000e-01   5.30000000e+00   6.17564070e+00   5.98764320e+00
        5.63055027e+00   5.09296877e+00   4.37286618e+00   3.48088856e+00
        2.44423972e+00   1.31117724e+00]
     [  2.00000000e+00  -9.14120135e-01  -1.75426842e+00  -2.17235788e+00
       -1.92152043e+00  -6.90371192e-01   1.90764317e+00   6.34607968e+00
        1.31986049e+01   2.31534626e+01]]

```python
diff_array=a[0]
diff_array
```

    array([  2.00000000e+00,   5.00000000e-01,  -7.50000000e+00,
             4.37500000e+01,  -1.48437500e+02,   2.28298611e+02,
            -2.94704861e+02,   3.57259115e+02,  -4.41240045e+02,
             5.47970834e+02])

```python
prod_polys
newton_abst=np.dot(diff_array,prod_polys)
newton_abst
```

```python
newton_x=sy.Lambda(x,newton_abst.subs({x:x}))
```

```python
fig,ax=plt.subplots()
pltx=np.linspace(0,2.0,100)
plty=np.vectorize(newton_x)(pltx)
ax.plot(pltx,plty)
ax.plot(xs,ys,'gx')
```

# final code

```python
import numpy as np
import sympy as sy
sy.init_printing()
from matplotlib import pyplot as plt
%matplotlib inline
```

```python
xs=[0.0,0.2,0.4,0.6,0.8,1.2,1.4,1.6,1.8,2.0]
ys=[2.0,2.1,1.6,2.6,1.5,2.7,0.67,3.5,0.94,2.0]
size=len(xs)
x=sy.symbols('x')
prod_polys=[sy.prod([x-xs[i] for i in range(k)] ) for k in range(size)]
a=np.empty((size,size))
for i,y in enumerate(ys):
    a[i][0]=y
row,col=size,size
# do newton ip core algorithm
for j in range(1,col):
    for i in range(row-j):
        a[i][j]=(a[i+1][j-1]-a[i][j-1])/(xs[i+j]-xs[i])
diff_array=a[0]
prod_polys
newton_abst=np.dot(diff_array,prod_polys)
newton_abst.simplify()
```

```python
newton_x=sy.Lambda(x,newton_abst.subs({x:x}))
fig,ax=plt.subplots()
pltx=np.linspace(0,2.0,100)
plty=np.vectorize(newton_x)(pltx)
ax.plot(pltx,plty)
ax.plot(xs,ys,'gx')
```

```python

```


# CONTROL LIBRARY EXAMPLES

# Preliminaries

```python
from controlSBML.control_extensions.state_space_tf import StateSpaceTF
import controlSBML as ctl

import control
import numpy as np
import pandas as pd
import sympy
import tellurium as te
import matplotlib.pyplot as plt
```

# Antimony Models

## LINEAR_MDL

```python
# Need to write the model in the correct way so that get a Jacobian with all state.
# In particular, cannot use fixed concentrations. Instead, generate the species at the same rate at which it is consumed.
LINEAR_MDL = """
J0:  -> S1; E_J0
J1: S1 -> S2; k1*S1*E_J1
J2: S2 -> S3; k2*S2*E_J2
J3: S3 -> S4; k3*S3*E_J3
J4: S4 -> $S5; k4*S4*E_J4

k1 = 1;
k2 = 1
k3 = 1
k4 = 1
S0 = 1
k0 = 1
S1 = 10
S2 = 0
S3 = 0
S4 = 0
S5 = 0
E_J0 = 1
E_J1 = 1
E_J2 = 1
E_J3 = 1
E_J4 = 1
"""

LINEAR_CTL = ctl.ControlSBML(LINEAR_MDL, input_names=["J0"], output_names=["S4", "S1"])
LINEAR_CTL.plotTrueModel(figsize=(10, 5), end_time=10)
```

## NONLINEAR_MDL

```python
# Nonlinear model with one quadratic term
NONLINEAR_MDL = """
J0: -> S1; k1*E_J0
J1: S1 -> S2; k2*S1*S1
J2: S2 -> S3; k3*S2
J3: S3 -> S4; k3*S3
J4: S4 -> $S5; k4*S4

k1 = 1;
k2 = 1
k3 = 1
k4 = 1
E_J0 = 1
S1 = 10
S2 = 0
S3 = 0
S4 = 0
S5 = 2
"""

NONLINEAR_CTL = ctl.ControlSBML(NONLINEAR_MDL, output_names=["S1", "S4"], input_names=["J0"])
NONLINEAR_CTL.plotTrueModel(figsize=(10, 5), end_time=10, ylim=[0, 10])
```

```python
nonlinear_sys = NONLINEAR_CTL.makeNonlinearIOSystem("nonlinear", effector_dct={"J0": "E_J0"})
times = [0.1*n for n in range(0, 51)]
NONLINEAR_CTL.setTime(0)
result = control.input_output_response(nonlinear_sys, times, X0=NONLINEAR_CTL.state_ser.values)
df = pd.DataFrame(np.transpose(result.outputs), columns = NONLINEAR_CTL.output_names)
_ = plt.plot(result.t, df)
```

# Simulation Effectors

```python
length = 100
dt = 0.1
rr = LINEAR_CTL.roadrunner
rr.reset()
datas = []
for idx in range(length):
    err = 5.75 - rr["S1"]
    rr["E_J0"] += max(0, 2*err)
    #print((rr["S1"], err, rr["E_J0"], rr["J1"]))
    data = rr.simulate(idx*dt, (idx+1)*dt, 2)
    datas.append(data[0, :])
df = pd.DataFrame(np.array(datas))
df.columns = ["time", "S1", "S2", "S3", "S4"]
df = df.set_index("time")
df
```

      S1
      S2
      S3
      S4

      time

      0.0
      10.000000
      0.000000
      0.000000
      0.000000

      0.1
      9.143537
      0.909516
      0.045397
      0.001512

      0.2
      8.368577
      1.654985
      0.164895
      0.010973

      0.3
      7.667364
      2.259391
      0.336968
      0.033603

      0.4
      7.032881
      2.742832
      0.544182
      0.072277

      ...
      ...
      ...
      ...
      ...

      9.5
      9.095488
      9.087263
      9.051590
      8.946985

      9.6
      9.095579
      9.088049
      9.055019
      8.957096

      9.7
      9.095661
      9.088769
      9.058193
      8.966558

      9.8
      9.095736
      9.089427
      9.061131
      8.975411

      9.9
      9.095803
      9.090030
      9.063848
      8.983689

100 rows × 4 columns

# Constructing a Cascade of Systems

Notes
1. First create a ``NonlinearIOSystem``. Simulation is done via ``input_output_response``.
1. Eacn system, system input, system output, and system state must have a name.
1. Connections between systems are done by name
1. If there is more than one input, then you must specify the input for all time steps.
1. The number of states in the combined system is the sum of the number of states in each component system.
1. WHen specifying connections, give the input signal and then the outputs.

## Simple, Synthetic System

```python
NUM_STATE = 3
END_TIME = 10
# Create a simple system
def generateRandomState(time, x_vec, u_vec, params=None):
    num_state = len(x_vec)
    new_state = np.random.rand(num_state)
    new_state *= u_vec
    return new_state
#
state_names = ["s%d" % n for n in range(NUM_STATE)]
input_names = ["u%d" % n for n in range(NUM_STATE)]
out_sys = control.NonlinearIOSystem(generateRandomState, None, states=state_names,
                                             inputs=input_names, outputs=state_names, name="out_sys")
inp_sys = control.NonlinearIOSystem(generateRandomState, None, states=state_names,
                                             inputs=input_names, outputs=state_names, name="inp_sys")
```

```python
X0 = [np.random.rand(1) for _ in range(NUM_STATE)]
u_vec = np.array([np.array(np.random.rand(NUM_STATE)) for _ in  range(END_TIME)])
u_vec = np.array([np.repeat(100, NUM_STATE) for _ in  range(END_TIME)])
u_vec = np.transpose(u_vec)
times = np.array([n*0.1 for n in range(END_TIME)])
t, y = control.input_output_response(inp_sys, times, u_vec, X0)
```

```python
io_serial = control.interconnect(
  [inp_sys, out_sys],
  connections=[
    ['out_sys.u0', 'inp_sys.s0'],
      ['out_sys.u1', 'inp_sys.s1'],
      ['out_sys.u2', 'inp_sys.s2'],
  ],
  inplist=['inp_sys.u1', 'inp_sys.u2', 'inp_sys.u0'],
  outlist=['out_sys.s1', 'out_sys.s2', 'out_sys.s0'],
)
```

```python
io_serial.nstates, io_serial.noutputs, io_serial.ninputs
```

    (6, 3, 3)

```python
num_state = 2*NUM_STATE
X0 = [np.random.rand(1) for _ in range(num_state)]
u_vec = np.array([np.repeat(1000, NUM_STATE) for _ in  range(END_TIME)])
u_vec = np.transpose(u_vec)
times = np.array([n*0.1 for n in range(END_TIME)])
t, y = control.input_output_response(io_serial, times, u_vec, X0)
```

```python
y
```

    array([[  0.82922537,   2.18195419,   5.90474182,  12.00052293,
             22.10145422,  33.40448455,  48.38767024,  64.35418133,
             82.45840714, 102.6802236 ],
           [  0.94011294,   2.099622  ,   6.39260856,  11.98082604,
             19.74071681,  30.58274083,  44.36124499,  63.42370361,
             82.83035997, 101.8390174 ],
           [  0.12971092,   1.5002206 ,   5.94047201,  12.34745719,
             19.60485331,  30.33585838,  47.66476255,  66.3943658 ,
             88.46972839, 114.16729013]])

```python
io_serial = control.interconnect(
  [tellurium, inp_sys],
  connections=[
  ['tellurium.J0', 'inp_sys.out'],
  ],
  inplist=['inp_sys.inp'],
  outlist=ctlsb.output_names,
)
```

```python
io_serial.connections
```

```python
ctlsb = ctl.ControlSBML(NONLINEAR_MDL, input_names=["J0"], output_names=["S1", "S2"])
effector_dct = {"J0": "E_J0"}
params = {"ctlsb": ctlsb, "effector_dct": effector_dct}
num_state = 1 + ctlsb.num_state
X0 = [np.random.rand(1) for _ in range(num_state)]
u_vec = np.array([1000 for _ in  range(END_TIME)])
u_vec = np.transpose(u_vec)
times = np.array([n*0.1 for n in range(END_TIME)])
t, y = control.input_output_response(io_serial, times, u_vec, X0, params=params)
df = pd.DataFrame(y, columns=ctlsb.output_names)
```

```python
y
```

```python
df = pd.DataFrame(y, index=ctlsb.output_names).T
df
```

# Controller Construction

## Uncontrolled System

```python
nonlinear_sys = NONLINEAR_CTL.makeNonlinearIOSystem("nonlinear", effector_dct={"J0": "E_J0"})
```

```python
# Uncontrolled system
X0 = nonlinear_sys.getStateSer().values
result = control.input_output_response(nonlinear_sys, times, X0=X0)
df = pd.DataFrame(np.transpose(result.states), columns=nonlinear_sys.state_names)
_ = plt.plot(result.t, df)
plt.legend(df.columns)
```

## Regulating S1

```python
xeq = [4]
def updfcn(t, x, u, _):
    # x[0] - current error
    # x[1] - integral of error
    err, acc_err = x
    new_err = xeq - u
    acc_err += new_err
    return new_err, acc_err
def outfcn(t, x, u, _):
    # State is accumulated error
    control_error = x[0] + x[1]
    control_error = min(control_error, 25)
    return control_error
controller = control.NonlinearIOSystem(
  updfcn,
  outfcn,
  states=["error", "accumulated_error"],
  inputs=['in'],
  outputs=['out'], name='controller')
```

```python
# Create the closed loop system
closed_outputs = list(nonlinear_sys.outlist)
closed_outputs.append("controller.out")
closed = control.interconnect(
  [nonlinear_sys, controller],       # systems
  connections=[
    ['nonlinear.J0', 'controller.out'],
    ['controller.in',  'nonlinear.S1'],
  ],
  inplist=["controller.in"],
  outlist=closed_outputs,
)
```

```python
closed.syslist
```

    [nonlinear, controller]

```python
size = 101
X0 = list(nonlinear_sys.getStateSer().values)
X0.extend([0,0])
times = np.array([n*0.1 for n in range(size)])
result = control.input_output_response(closed, times, X0=X0)
#t, y = control.input_output_response(tellurium, times, U, X0)
columns = nonlinear_sys.state_names
columns.extend(["control_err", "accumulated_err"])
df = pd.DataFrame(np.transpose(result.states), columns=columns)
plt.plot(result.t, df)
plt.legend(df.columns)
plt.ylim([0, 30])
```

# Predatory Prey

## Original Model

```python
def predprey_rhs(t, x, u, params):
    # Parameter setup
    a = params.get('a', 3.2)
    b = params.get('b', 0.6)
    c = params.get('c', 50.)
    d = params.get('d', 0.56)
    k = params.get('k', 125)
    r = params.get('r', 1.6)

    # Map the states into local variable names
    H = x[0]
    L = x[1]

    # Compute the control action (only allow addition of food)
    u_0 = u if u > 0 else 0

    # Compute the discrete updates
    dH = (r + u_0) * H * (1 - H/k) - (a * H * L)/(c + H)
    dL = b * (a * H *  L)/(c + H) - d * L

    return [dH, dL]
```

```python
io_predprey = control.NonlinearIOSystem(
    predprey_rhs, None, inputs=('u'), outputs=('H', 'L'),
    states=('H', 'L'), name='predprey')
```

```python
# Simulate the system
X0 = [25, 20]                 # Initial H, L
T = np.linspace(0, 70, 500)   # Simulation 70 years of time

# Simulate the system
t, y = control.input_output_response(io_predprey, T, 0, X0)

# Plot the response
plt.figure(1)
plt.plot(t, y[0])
plt.plot(t, y[1])
plt.legend(['Hare', 'Lynx'])
plt.show(block=False)
```

```python
np.shape(T)
```

```python
# Create a feedback controller. Step 1: Find an equalibrium point
eqpt = control.find_eqpt(io_predprey, X0, 0)
xeq = eqpt[0]                         # choose the nonzero equilibrium point
lin_predprey = control.linearize(io_predprey, xeq, 0)
```

```python
# Step 2: Construct the controller via pole placement
K = control.place(lin_predprey.A, lin_predprey.B, [-0.1, -0.2])
A, B = lin_predprey.A, lin_predprey.B
C = np.array([[0, 1]])                # regulated output = number of lynxes
kf = -1/(C @ np.linalg.inv(A - B @ K) @ B)
```

```python
# Construct the controller
io_controller = control.NonlinearIOSystem(
  None,
  lambda t, x, u, params: -K @ (u[1:] - xeq) + kf * (u[0] - xeq[1]),
  inputs=('Ld', 'u1', 'u2'), outputs=1, name='control')

# Create the closed loop system
io_closed = control.interconnect(
  [io_predprey, io_controller],       # systems
  connections=[
    ['predprey.u', 'control.y[0]'],
    ['control.u1',  'predprey.H'],
    ['control.u2',  'predprey.L']
  ],
  inplist=['control.Ld'],
  outlist=['predprey.H', 'predprey.L', 'control.y[0]']
)

# Simulate the system
t, y = control.input_output_response(io_closed, T, 30, [15, 20])

# Plot the response
plt.figure(2)
plt.subplot(2, 1, 1)
plt.plot(t, y[0])
plt.plot(t, y[1])
plt.legend(['Hare', 'Lynx'])
plt.subplot(2, 1, 2)
plt.plot(t, y[2])
plt.legend(['input'])
plt.show(block=False)
```

## Disturbance

```python
# Add a distrubance
disturbance = control.NonlinearIOSystem(
  None,
  lambda t, x, u, params: 0 if (t < 20 or t >= 25) else 2, inputs=0,
  outputs='out', name='disturbance')

# Create the closed loop system
io_closed_disturbance = control.interconnect(
  [io_predprey, io_controller, disturbance],       # systems
  connections=[
    ['predprey.u', 'control.y[0]', 'disturbance.out'],
    ['control.u1',  'predprey.H'],
    ['control.u2',  'predprey.L']
  ],
  inplist=['control.Ld'],
  outlist=['predprey.H', 'predprey.L', 'control.y[0]', 'disturbance.out']
)

# Simulate the system
results = control.input_output_response(io_closed_disturbance, T, 30, [15, 20])
outputs = results.outputs
times  = results.t

# Plot the response
plt.figure(2)
plt.subplot(2, 1, 1)
plt.plot(times, outputs[0])
plt.plot(times, outputs[1])
plt.legend(['Hare', 'Lynx'])
plt.subplot(2, 1, 2)
plt.plot(times, outputs[2])
plt.plot(times, outputs[3])
plt.legend(['input', 'disturbance'])
plt.show(block=False)
```

# BioModels 823

```python
ctlsb_823 = ctl.ControlSBML("https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000823.2?filename=Varusai2018.xml")
```

```python
sys_823 = ctl.NonlinearIOSystem("sys_823", ctlsb_823)
```

```python
df.head()
```

```python
def plotBiomodel(name, url, is_plot=True):
    ctlsb = ctl.ControlSBML(url)
    sys = ctl.NonlinearIOSystem(name, ctlsb)
    times = [0.1*n for n in range(0, 51)]
    results = control.input_output_response(sys, times, X0=sys.getStateSer().values)
    df = pd.DataFrame(np.transpose(results.states), columns=sys.state_names)
    _, ax = plt.subplots(1)
    _ = ax.plot(results.t, df)
    ax.set_title(name)
    ax.legend(df.columns)
    if not is_plot:
        plt.close()

# Tests
plotBiomodel("823", "https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000823.2?filename=Varusai2018.xml",
            is_plot=False)
```

```python
URL_DCT = {
    816: "https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000816.5?filename=model.xml",
    823: "https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000823.2?filename=Varusai2018.xml",
    640: "https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000640.2?filename=BIOMD0000000640_url.xml",
    206: "https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000206.2?filename=BIOMD0000000206_url.xml",
    861: "https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000861.2?filename=Bachmann2011.xml",
}
```

```python
for key, value in URL_DCT.items():
    print(key)
    plotBiomodel(str(key), value)
```


# Characterization of Systems in the Spectral Domain

*This Jupyter notebook is part of a [collection of notebooks](../index.ipynb) in the bachelors module Signals and Systems, Communications Engineering, Universität Rostock. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## The Transfer Function

Different time-domain representations can be used to characterize linear time-invariant (LTI) systems. For instance, the constant coefficients of ordinary partial differential equations (ODEs), the [impulse response](../systems_time_domain/impulse_response.ipynb) and the [step response](../systems_time_domain/step_response.ipynb). The [transfer function](../systems_time_domain/eigenfunctions.ipynb#Transfer-Function) denotes the complex factor $H(s)$ an eigenfunction $e^{s t}$ at the input of an LTI system is weighted with when passing through the system. Since the Laplace transform decomposes signals with respect to these eigenfunctions, the transfer function constitutes a representation in the spectral (Fourier or Laplace) domain. The links between the transfer function and the time-domain characterizations of LTI systems are discussed in this section.

### Relation to Impulse Response and In-/Output Signal

It was already outlined in the context of the [convolution theorem of the Fourier](../fourier_transform/theorems.ipynb#Convolution-Theorem) and [Laplace transform](../laplace_transform/theorems.ipynb#Convolution-Theorem), that an LTI system can be represented equivalently in the temporal and spectral domain. In the temporal domain the output of an LTI system is given by convolving the input signal $x(t)$ with the [impulse response](../systems_time_domain/impulse_response.ipynb) $h(t)$ of the system

\begin{equation}
y(t) = x(t) * h(t)
\end{equation}

Using the convolution theorem of the Laplace transform yields

\begin{equation}
Y(s) = X(s) \cdot H(s)
\end{equation}

where $X(s) = \mathcal{L} \{ x(t) \}$ and $Y(s) = \mathcal{L} \{ y(t) \}$ are the Laplace transforms of the input and output signal, respectively. The transfer function $H(s)$ is given as the Laplace transform of the impulse response $h(t)$

\begin{equation}
H(s) = \mathcal{L} \{ h(t) \}
\end{equation}

This may also be concluded from the [previously derived link between the impulse response and the transfer function](../systems_time_domain/eigenfunctions.ipynb#Link-between-Transfer-Function-and-Impulse-Response). The derived results show that an LTI system can be fully characterized either in the temporal or spectral domain by its impulse response $h(t)$ or transfer function $H(s)$ or $H(j \omega)$, respectively.

It can furthermore be concluded that the transfer function is given as the quotient between output $Y(s)$ and input signal $X(s)$ in the Laplace domain

\begin{equation}
H(s) = \frac{Y(s)}{X(s)}
\end{equation}

for $X(s) \neq 0$. The same relations hold in the Fourier domain if the region of convergence (ROC) of the impulse response includes the imaginary axis $\Re \{ s \} = 0$. In general, the transfer function can be derived by divison of the spectra of the output and input signal. This can be used to measure the transfer function of a system by specific input signals that fulfill $X(s) \neq 0$ with $X(s) \approx$ constant.

**Example**

The [transfer function $H(s)$ of the 2nd order low-pass filter](../systems_time_domain/network_analysis.ipynb#Transfer-Function) was derived as

\begin{equation}
H(s) = \frac{1}{C L s^2 + C R s + 1}
\end{equation}

According to above findings, the impulse response $h(t)$ of the low-pass is given by inverse Laplace transform of the transfer function $H(s)$. First the transfer function is defined in `SymPy`

```python
%matplotlib inline
import sympy as sym
sym.init_printing()

R, L, C = sym.symbols('R L C', positive=True)
s = sym.symbols('s', complex=True)

H = 1/(C*L*s**2 + C*R*s + 1)
H
```

Then the inverse Laplace transform is computed for the specific normalized values $L = .5$, $R = 1$, $C = .4$ of the network elements

```python
t = sym.symbols('t', real=True)
RLC = {R: 1, L: sym.Rational('.5'), C: sym.Rational('.4')}

h = sym.inverse_laplace_transform(H.subs(RLC), s, t)
h
```

This result is plotted for illustration

```python
sym.plot(h, (t,0,10), xlabel='$t$', ylabel='h(t)');
```

Note, the result is equal to the [impulse response derived by explicit solution of the underlying ODE](../systems_time_domain/network_analysis.ipynb#Impulse-Response).

### Relation to Differential Equations

Linear ODEs with constant coefficients describing the relation between input $x(t)$ and output $y(t)$ represent an LTI system. Such a system is characterized by the constants $\alpha_n$ and $\beta_m$ of the ODE

\begin{equation}
\sum_{n=0}^{N} \alpha_n \frac{d^n}{d t^n} y(t) = \sum_{m=0}^{M} \beta_m \frac{d^m}{d t^m} x(t)
\end{equation}

where $\alpha_N \neq 0$ with $N$ denoting the degree of the ODE. Initial values are discarded in the following. Applying a two-sided Laplace transform to the left- and right-hand-side and using the [differentiation theorem](../laplace_transform/theorems.ipynb#Differentiation-Theorem) yields

\begin{equation}
Y(s) \sum_{n=0}^{N} \alpha_n s^n = X(s) \sum_{m=0}^{M} \beta_m s^m
\end{equation}

Rearranging this result derives the transfer function $H(s)$ of the system as

\begin{equation}
H(s) = \frac{Y(s)}{X(s)} = \frac{\sum_{m=0}^{M} \beta_m s^m}{\sum_{n=0}^{N} \alpha_n s^n}
\end{equation}

The transfer function of an LTI system whose in- and output relation can be characterized by an ODE is given as rational function in $s$. The coefficients of its numerator and denominator polynomial are given by the coefficients $\beta_m$ and $\alpha_n$ of the ODE. The conversion between both representations of an LTI system is hence straightforward.

**Example**

The coefficients for the 2nd order low-pass filter can be determined [from its ODE](../systems_time_domain/network_analysis.ipynb#Differential-Equation) or from above transfer function as

\begin{align}
\beta_0 &= 1 & \alpha_0 &= 1 \\
& & \alpha_1 &= C R \\
& & \alpha_2 &= C L
\end{align}

From theses coefficients the transfer function $H(s)$ is constructed in `SymPy`

```python
R, L, C = sym.symbols('R L C', positive=True)
beta = 1
alpha = L*C, R*C, 1

H = sym.Poly(beta, s) / sym.Poly(alpha, s)
H
```

Many software packages for signal processing feature functionality for the computation of the transfer function from the coefficients $\beta_m$ and $\alpha_n$ of a system. The Python Signal Processing Toolbox [`scipy.signal`](http://docs.scipy.org/doc/scipy/reference/signal.html) for instance provides a function to numerically evaluate the transfer function $H(j \omega)$ for a given set of coefficients. This is illustrated in the following for the normalized values $R$, $L$ and $C$ used above.

```python
from scipy import signal

R = 1
L = 0.5
C = 0.4
beta = 1
alpha = L*C, R*C, 1

w, H = signal.freqresp((beta, alpha))
```

This results in the vector `w` of angular frequencies $\omega$ and the vector `H` with the values of $H(j \omega)$ at these frequencies. The latter is plotted with the Python plotting library [`matplotlib`](http://matplotlib.org/).

```python
import matplotlib.pyplot as plt

plt.plot(w, abs(H))
plt.xlabel('$\omega$ in $s^{-1}$')
plt.ylabel('$|H(j \omega)|$')
plt.grid();
```

### Determination from Internal Structure

The transfer function $H(s)$ can be determined from the internal structure of an LTI system, if known. Either by derivation of the ODE characterizing the system, as shown above, or by direct analysis of the system in the Laplace/Fourier domain. For passive electrical networks this can be conducted by representing the signals and elements of the network by their complex impedances $Z(s)$ and applying Kirchhoff's circuit laws. This was illustrated for instance by the previous [Laplace domain analysis of a 2nd order low-pass filter](../laplace_transform/network_analysis.ipynb#Example:-Second-Order-Low-Pass-Filter). Similar procedures can be applied to other types of LTI systems, e.g. mechanical systems.

The transfer function is then given as the quotient between the output signal $Y(s)$ and the input signal $X(s)$ in the Laplace/Fourier domain.

**Copyright**

The notebooks are provided as [Open Educational Resource](https://de.wikipedia.org/wiki/Open_Educational_Resources). Feel free to use the notebooks for your own educational purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Lecture Notes on Signals and Systems* by Sascha Spors.


#  Neuromatch Academy: Week 1, Day 1, Tutorial 3
# Model Types: "Why" models
__Content creators:__ Matt Laporte, Byron Galbraith, Konrad Kording

__Content reviewers:__ Dalin Guo, Aishwarya Balwani, Madineh Sarvestani, Maryam Vaziri-Pashkam, Michael Waskom

We would like to acknowledge [Steinmetz _et al._ (2019)](https://www.nature.com/articles/s41586-019-1787-x) for sharing their data, a subset of which is used here.

___
# Tutorial Objectives
This is tutorial 3 of a 3-part series on different flavors of models used to understand neural data. In parts 1 and 2 we explored mechanisms that would produce the data. In this tutorial we will explore models and techniques that can potentially explain *why* the spiking data we have observed is produced the way it is.

To understand why different spiking behaviors may be beneficial, we will learn about the concept of entropy. Specifically, we will:

- Write code to compute formula for entropy, a measure of information
- Compute the entropy of a number of toy distributions
- Compute the entropy of spiking activity from the Steinmetz dataset

```python
#@title Video 1: “Why” models
from IPython.display import IFrame
class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
        self.id=id
        src = "//player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
        super(BiliVideo, self).__init__(src, width, height, **kwargs)

video = BiliVideo(id='BV16t4y1Q7DR', width=854, height=480, fs=1)
print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
video

```

    Video available at https://www.bilibili.com/video/BV16t4y1Q7DR

# Setup

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
```

```python
#@title Figure Settings
import ipywidgets as widgets #interactive display

%matplotlib inline
%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")

```

```python
#@title Helper Functions

def plot_pmf(pmf,isi_range):
  """Plot the probability mass function."""
  ymax = max(0.2, 1.05 * np.max(pmf))
  pmf_ = np.insert(pmf, 0, pmf[0])
  plt.plot(bins, pmf_, drawstyle="steps")
  plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
  plt.title(f"Neuron {neuron_idx}")
  plt.xlabel("Inter-spike interval (s)")
  plt.ylabel("Probability mass")
  plt.xlim(isi_range);
  plt.ylim([0, ymax])
```

```python
#@title Download Data
import io
import requests
r = requests.get('https://osf.io/sy5xt/download')
if r.status_code != 200:
  print('Could not download data')
else:
  steinmetz_spikes = np.load(io.BytesIO(r.content), allow_pickle=True)['spike_times']
```

# Section 1: Optimization and Information

Neurons can only fire so often in a fixed period of time, as the act of emitting a spike consumes energy that is depleted and must eventually be replenished. To communicate effectively for downstream computation, the neuron would need to make good use of its limited spiking capability. This becomes an optimization problem:

What is the optimal way for a neuron to fire in order to maximize its ability to communicate information?

In order to explore this question, we first need to have a quantifiable measure for information. Shannon introduced the concept of entropy to do just that, and defined it as

\begin{align}
  H_b(X) &= -\sum_{x\in X} p(x) \log_b p(x)
\end{align}

where $H$ is entropy measured in units of base $b$ and $p(x)$ is the probability of observing the event $x$ from the set of all possible events in $X$. See the Appendix for a more detailed look at how this equation was derived.

The most common base of measuring entropy is $b=2$, so we often talk about *bits* of information, though other bases are used as well e.g. when $b=e$ we call the units *nats*.

First, let's explore how entropy changes between some simple discrete probability distributions. In the rest of this tutorial we will refer to these as probability mass functions (PMF), where $p(x_i)$ equals the $i^{th}$ value in an array, and mass refers to how much of the distribution is contained at that value.

For our first PMF, we will choose one where all of the probability mass is located in the middle of the distribution.

```python
n_bins = 50  # number of points supporting the distribution
x_range = (0, 1)  # will be subdivided evenly into bins corresponding to points

bins = np.linspace(*x_range, n_bins + 1)  # bin edges

pmf = np.zeros(n_bins)
pmf[len(pmf) // 2] = 1.0  # middle point has all the mass

# Since we already have a PMF, rather than un-binned samples, `plt.hist` is not
# suitable. Instead, we directly plot the PMF as a step function to visualize
# the histogram:
pmf_ = np.insert(pmf, 0, pmf[0])  # this is necessary to align plot steps with bin edges
plt.plot(bins, pmf_, drawstyle="steps")
# `fill_between` provides area shading
plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
plt.xlabel("x")
plt.ylabel("p(x)")
plt.xlim(x_range)
plt.ylim(0, 1);
```

If we were to draw a sample from this distribution, we know exactly what we would get every time. Distributions where all the mass is concentrated on a single event are known as *deterministic*.

How much entropy is contained in a deterministic distribution?

## Exercise 1: Computing Entropy

Your first exercise is to implement a method that computes the entropy of a discrete probability distribution, given its mass function. Remember that we are interested in entropy in units of _bits_, so be sure to use the correct log function.

Recall that $\log(0)$ is undefined. When evaluated at $0$, NumPy log functions (such as `np.log2`) return `np.nan` ("Not a Number"). By convention, these undefined terms— which correspond to points in the distribution with zero mass—are excluded from the sum that computes the entropy.

```python
def entropy(pmf):
  """Given a discrete distribution, return the Shannon entropy in bits.

  This is a measure of information in the distribution. For a totally
  deterministic distribution, where samples are always found in the same bin,
  then samples from the distribution give no more information and the entropy
  is 0.

  For now this assumes `pmf` arrives as a well-formed distribution (that is,
  `np.sum(pmf)==1` and `not np.any(pmf < 0)`)

  Args:
    pmf (np.ndarray): The probability mass function for a discrete distribution
      represented as an array of probabilities.
  Returns:
    h (number): The entropy of the distribution in `pmf`.

  """
  ############################################################################
  # Exercise for students: compute the entropy of the provided PMF
  #   1. Exclude the points in the distribution with no mass (where `pmf==0`).
  #      Hint: this is equivalent to including only the points with `pmf>0`.
  #   2. Implement the equation for Shannon entropy (in bits).
  #  When ready to test, comment or remove the next line
  raise NotImplementedError("Excercise: implement the equation for entropy")
  ############################################################################

  # reduce to non-zero entries to avoid an error from log2(0)
  pmf = ...

  # implement the equation for Shannon entropy (in bits)
  h = ...

  # return the absolute value (avoids getting a -0 result)
  return np.abs(h)

# Uncomment to test your entropy function
# print(f"{entropy(pmf):.2f} bits")
```

```python
# to_remove solution
def entropy(pmf):
  """Given a discrete distribution, return the Shannon entropy in bits.

  This is a measure of information in the distribution. For a totally
  deterministic distribution, where samples are always found in the same bin,
  then samples from the distribution give no more information and the entropy
  is 0.

  For now this assumes `pmf` arrives as a well-formed distribution (that is,
  `np.sum(pmf)==1` and `not np.any(pmf < 0)`)

  Args:
    pmf (np.ndarray): The probability mass function for a discrete distribution
      represented as an array of probabilities.
  Returns:
    h (number): The entropy of the distribution in `pmf`.
  """
  # reduce to non-zero entries to avoid an error from log2(0)
  pmf = pmf[pmf > 0]
  # implement the equation for shannon entropy (in bits)
  h = -np.sum(pmf * np.log2(pmf))
  # return the absolute value (avoids getting a -0 result)
  return np.abs(h)

print(f"{entropy(pmf):.2f} bits")
```

    0.00 bits

We expect zero surprise from a deterministic distribution. If we had done this calculation by hand, it would simply be

$-1\log_2 1 = -0=0$

Note that changing the location of the peak (i.e. the point and bin on which all the mass rests) doesn't alter the entropy. The entropy is about how predictable a sample is with respect to a distribution. A single peak is deterministic regardless of which point it sits on.

```python
pmf = np.zeros(n_bins)
pmf[2] = 1.0  # arbitrary point has all the mass

pmf_ = np.insert(pmf, 0, pmf[0])
plt.plot(bins, pmf_, drawstyle="steps")
plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
plt.xlabel("x")
plt.ylabel("p(x)")
plt.xlim(x_range)
plt.ylim(0, 1);
```

What about a distribution with mass split equally between two points?

```python
pmf = np.zeros(n_bins)
pmf[len(pmf) // 3] = 0.5
pmf[2 * len(pmf) // 3] = 0.5

pmf_ = np.insert(pmf, 0, pmf[0])
plt.plot(bins, pmf_, drawstyle="steps")
plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
plt.xlabel("x")
plt.ylabel("p(x)")
plt.xlim(x_range)
plt.ylim(0, 1);
```

Here, the entropy calculation is

$-(0.5 \log_2 0.5 + 0.5\log_2 0.5)=1$

There is 1 bit of entropy. This means that before we take a random sample, there is 1 bit of uncertainty about which point in the distribution the sample will fall on: it will either be the first peak or the second one.

Likewise, if we make one of the peaks taller (i.e. its point holds more of the probability mass) and the other one shorter, the entropy will decrease because of the increased certainty that the sample will fall on one point and not the other:

$-(0.2 \log_2 0.2 + 0.8\log_2 0.8)\approx 0.72$

Try changing the definition of the number and weighting of peaks, and see how the entropy varies.

If we split the probability mass among even more points, the entropy continues to increase. Let's derive the general form for $N$ points of equal mass, where $p_i=p=1/N$:

\begin{align}
 -\sum_i p_i \log_b p_i&= -\sum_i^N \frac{1}{N} \log_b \frac{1}{N}\\
                       &= -\log_b \frac{1}{N} \\
                       &= \log_b N
\end{align}
$$$$

If we have $N$ discrete points, the _uniform distribution_ (where all points have equal mass) is the distribution with the highest entropy: $\log_b N$. This upper bound on entropy is useful when considering binning strategies, as any estimate of entropy over $N$ discrete points (or bins) must be in the interval $[0, \log_b N]$.

```python
pmf = np.ones(n_bins) / n_bins  # [1/N] * N

pmf_ = np.insert(pmf, 0, pmf[0])
plt.plot(bins, pmf_, drawstyle="steps")
plt.fill_between(bins, pmf_, step="pre", alpha=0.4)
plt.xlabel("x")
plt.ylabel("p(x)")
plt.xlim(x_range);
plt.ylim(0, 1);
```

Here, there are 50 points and the entropy of the uniform distribution is $\log_2 50\approx 5.64$. If we construct _any_ discrete distribution $X$ over 50 points (or bins) and calculate an entropy of $H_2(X)>\log_2 50$, something must be wrong with our implementation of the discrete entropy computation.

# Section 2: Information, neurons, and spikes

```python
#@title Video 2: Entropy of different distributions
from IPython.display import IFrame
class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
        self.id=id
        src = "//player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
        super(BiliVideo, self).__init__(src, width, height, **kwargs)

video = BiliVideo(id='BV1df4y1976g', width=854, height=480, fs=1)
print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
video
```

    Video available at https://www.bilibili.com/video/BV1df4y1976g

Recall the discussion of spike times and inter-spike intervals (ISIs) from Tutorial 1. What does the information content (or distributional entropy) of these measures say about our theory of nervous systems?

We'll consider three hypothetical neurons that all have the same mean ISI, but with different distributions:

1. Deterministic
2. Uniform
3. Exponential

Fixing the mean of the ISI distribution is equivalent to fixing its inverse: the neuron's mean firing rate. If a neuron has a fixed energy budget and each of its spikes has the same energy cost, then by fixing the mean firing rate, we are normalizing for energy expenditure. This provides a basis for comparing the entropy of different ISI distributions. In other words: if our neuron has a fixed budget, what ISI distribution should it express (all else being equal) to maximize the information content of its outputs?

Let's construct our three distributions and see how their entropies differ.

```python
n_bins = 50
mean_isi = 0.025
isi_range = (0, 0.25)

bins = np.linspace(*isi_range, n_bins + 1)
mean_idx = np.searchsorted(bins, mean_isi)

# 1. all mass concentrated on the ISI mean
pmf_single = np.zeros(n_bins)
pmf_single[mean_idx] = 1.0

# 2. mass uniformly distributed about the ISI mean
pmf_uniform = np.zeros(n_bins)
pmf_uniform[0:2*mean_idx] = 1 / (2 * mean_idx)

# 3. mass exponentially distributed about the ISI mean
pmf_exp = stats.expon.pdf(bins[1:], scale=mean_isi)
pmf_exp /= np.sum(pmf_exp)
```

```python
#@title
#@markdown Run this cell to plot the three PMFs
fig, axes = plt.subplots(ncols=3, figsize=(18, 5))

dists =  [# (subplot title, pmf, ylim)
          ("(1) Deterministic", pmf_single, (0, 1.05)),
          ("(1) Uniform", pmf_uniform, (0, 1.05)),
          ("(1) Exponential", pmf_exp, (0, 1.05))]

for ax, (label, pmf_, ylim) in zip(axes, dists):
  pmf_ = np.insert(pmf_, 0, pmf_[0])
  ax.plot(bins, pmf_, drawstyle="steps")
  ax.fill_between(bins, pmf_, step="pre", alpha=0.4)
  ax.set_title(label)
  ax.set_xlabel("Inter-spike interval (s)")
  ax.set_ylabel("Probability mass")
  ax.set_xlim(isi_range);
  ax.set_ylim(ylim);
```

```python
print(
  f"Deterministic: {entropy(pmf_single):.2f} bits",
  f"Uniform: {entropy(pmf_uniform):.2f} bits",
  f"Exponential: {entropy(pmf_exp):.2f} bits",
  sep="\n",
)
```

    Deterministic: 0.00 bits
    Uniform: 3.32 bits
    Exponential: 3.77 bits

```python
#@title Video 3: Probabilities from histogram
from IPython.display import IFrame
class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
        self.id=id
        src = "//player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
        super(BiliVideo, self).__init__(src, width, height, **kwargs)

video = BiliVideo(id='BV1Jk4y1B7cz', width=854, height=480, fs=1)
print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
video
```

    Video available at https://www.bilibili.com/video/BV1Jk4y1B7cz

In the previous example we created the PMFs by hand to illustrate idealized scenarios. How would we compute them from data recorded from actual neurons?

One way is to convert the ISI histograms we've previously computed into discrete probability distributions using the following equation:

\begin{align}
p_i = \frac{n_i}{\sum\nolimits_{i}n_i}
\end{align}

where $p_i$ is the probability of an ISI falling within a particular interval $i$ and $n_i$ is the count of how many ISIs were observed in that interval.

### Exercise 2: Probabilty Mass Function

Your second exercise is to implement a method that will produce a probability mass function from an array of ISI bin counts.

To verify your solution, we will compute the probability distribution of ISIs from real neural data taken from the Steinmetz dataset.

```python
neuron_idx = 283

isi = np.diff(steinmetz_spikes[neuron_idx])
bins = np.linspace(*isi_range, n_bins + 1)
counts, _ = np.histogram(isi, bins)
```

```python
def pmf_from_counts(counts):
  """Given counts, normalize by the total to estimate probabilities."""
  ###########################################################################
  # Exercise: Compute the PMF. Remove the next line to test your function
  raise NotImplementedError("Student excercise: compute the PMF from ISI counts")
  ###########################################################################

  pmf = ...

  return pmf

# Uncomment when ready to test your function
# pmf = pmf_from_counts(counts)
# plot_pmf(pmf,isi_range)
```

```python
# to_remove solution
def pmf_from_counts(counts):
  """Given counts, normalize by the total to estimate probabilities."""
  pmf = counts / np.sum(counts)
  return pmf

pmf = pmf_from_counts(counts)
with plt.xkcd():
  plot_pmf(pmf,isi_range)
```

# Section 3: Calculate entropy from a PMF

```python
#@title Video 4: Calculating entropy from pmf
from IPython.display import IFrame
class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
        self.id=id
        src = "//player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
        super(BiliVideo, self).__init__(src, width, height, **kwargs)

video = BiliVideo(id='BV1vA411e7Cd', width=854, height=480, fs=1)
print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
video
```

    Video available at https://www.bilibili.com/video/BV1vA411e7Cd

Now that we have the probability distribution for the actual neuron spiking activity, we can calculate its entropy.

```python
print(f"Entropy for Neuron {neuron_idx}: {entropy(pmf):.2f} bits")
```

    Entropy for Neuron 283: 3.36 bits

## Interactive Demo: Entropy of neurons

We can combine the above distribution plot and entropy calculation with an interactive widget to explore how the different neurons in the dataset vary in spiking activity and relative information. Note that the mean firing rate across neurons is not fixed, so some neurons with a uniform ISI distribution may have higher entropy than neurons with a more exponential distribution.

```python
#@title
#@markdown **Run the cell** to enable the sliders.

def _pmf_from_counts(counts):
  """Given counts, normalize by the total to estimate probabilities."""
  pmf = counts / np.sum(counts)
  return pmf

def _entropy(pmf):
  """Given a discrete distribution, return the Shannon entropy in bits."""
  # remove non-zero entries to avoid an error from log2(0)
  pmf = pmf[pmf > 0]
  h = -np.sum(pmf * np.log2(pmf))
  # absolute value applied to avoid getting a -0 result
  return np.abs(h)

@widgets.interact(neuron=widgets.IntSlider(0, min=0, max=(len(steinmetz_spikes)-1)))
def steinmetz_pmf(neuron):
  """ Given a neuron from the Steinmetz data, compute its PMF and entropy """
  isi = np.diff(steinmetz_spikes[neuron])
  bins = np.linspace(*isi_range, n_bins + 1)
  counts, _ = np.histogram(isi, bins)
  pmf = _pmf_from_counts(counts)

  plot_pmf(pmf,isi_range)
  plt.title(f"Neuron {neuron}: H = {_entropy(pmf):.2f} bits")

```

---
# Summary

```python
#@title Video 5: Summary of model types
from IPython.display import IFrame
class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
        self.id=id
        src = "//player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
        super(BiliVideo, self).__init__(src, width, height, **kwargs)

video = BiliVideo(id='BV1F5411e7ww', width=854, height=480, fs=1)
print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
video
```

    Video available at https://www.bilibili.com/video/BV1F5411e7ww

Congratulations! You've finished your first NMA tutorial. In this 3 part tutorial series, we used different types of models to understand the spiking behavior of neurons recorded in the Steinmetz data set.

 - We used "what" models to discover that the ISI distribution of real neurons is closest to an exponential distribution
 - We used "how" models to discover that balanced excitatory and inbhitiory inputs, coupled with a leaky membrane, can give rise to neuronal spiking with exhibiting such an exponential ISI distribution
 - We used "why" models to discover that exponential ISI distributions contain the most information when the mean spiking is constrained

# Bonus

### The foundations for Entropy

In his foundational [1948 paper](https://en.wikipedia.org/wiki/A_Mathematical_Theory_of_Communication) on information theory, Claude Shannon began with three criteria for a function $H$ defining the entropy of a discrete distribution of probability masses $p_i\in p(X)$ over the points $x_i\in X$:
1. $H$ should be continuous in the $p_i$.
  - That is, $H$ should change smoothly in response to smooth changes to the mass $p_i$ on each point $x_i$.
2. If all the points have equal shares of the probability mass, $p_i=1/N$, $H$ should be a non-decreasing function of $N$.
  - That is, if $X_N$ is the support with $N$ discrete points and $p(x\in X_N)$ assigns constant mass to each point, then $H(X_1) < H(X_2) < H(X_3) < \dots$
3. $H$ should be preserved by (invariant to) the equivalent (de)composition of distributions.
  - For example (from Shannon's paper) if we have a discrete distribution over three points with masses $(\frac{1}{2},\frac{1}{3},\frac{1}{6})$, then their entropy can be represented in terms of a direct choice between the three and calculated $H(\frac{1}{2},\frac{1}{3},\frac{1}{6})$. However, it could also be represented in terms of a series of two choices:
    1. either we sample the point with mass $1/2$ or not (_not_ is the other $1/2$, whose subdivisions are not given in the first choice),
    2. if (with probability $1/2$) we _don't_ sample the first point, we sample one of the two remaining points, masses $1/3$ and $1/6$.

    Thus in this case we require that $H(\frac{1}{2},\frac{1}{3},\frac{1}{6})=H(\frac{1}{2},\frac{1}{2}) + \frac{1}{2}H(\frac{1}{3}, \frac{1}{6})$

There is a unique function (up to a linear scaling factor) which satisfies these 3 requirements:

\begin{align}
  H_b(X) &= -\sum_{x\in X} p(x) \log_b p(x)
\end{align}

Where the base of the logarithm $b>1$ controls the units of entropy. The two most common cases are $b=2$ for units of _bits_, and $b=e$ for _nats_.

We can view this function as the expectation of the self-information over a distribution:

$$H_b(X) = \mathbb{E}_{x\in X} \left[I_b(x)\right]$$

$$I_b(x)=-\log_b p(x)$$

Self-information is just the negative logarithm of probability, and is a measure of how surprising an event sampled from the distribution would be. Events with $p(x)=1$ are certain to occur, and their self-information is zero (as is the entropy of the distribution they compose) meaning they are totally unsurprising. The smaller the probability of an event, the higher its self-information, and the more surprising the event would be to observe.


```python
from IPython.display import Image
from IPython.core.display import HTML
from sympy import *; x,h,t,y,z,a,b,c   = symbols("x h t y z a b c", real=True)
Image(url= "https://i.imgur.com/Mdbi1PW.png")
```

```python
f  = a*x**3 - b*y**5 - c**3
f
```

$\displaystyle a x^{3} - b y^{5} - c^{3}$

```python
idiff(f,y,x)
```

$\displaystyle \frac{3 a x^{2}}{5 b y^{4}}$

```python
print(idiff(f,y,x))
```

    3*a*x**2/(5*b*y**4)

```python
Image(url= "https://i.imgur.com/NG1u8Dm.png")
```

```python

```


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license (c)2014 L.A. Barba, G.F. Forsyth, C.D. Cooper.

# Spreading out

Welcome back! This is the third lesson of the course [Module 4](https://github.com/numerical-mooc/numerical-mooc/tree/master/lessons/04_spreadout), _Spreading out: parabolic PDEs,_ where we study the numerical solution of diffusion problems.

In the first two notebooks, we looked at the 1D heat equation, and solved it numerically using [*explicit*](https://github.com/numerical-mooc/numerical-mooc/blob/master/lessons/04_spreadout/01_Heat_Equation_1D_Explicit.ipynb) and [*implicit*](https://github.com/numerical-mooc/numerical-mooc/blob/master/lessons/04_spreadout/02_Heat_Equation_1D_Implicit.ipynb) schemes. We learned that implicit schemes are unconditionally stable, and we are free to choose any time step. —Wait: _any time step?_ Remember, we still want to capture the physics of the problem accurately. So although stability concerns do not limit the time step, it still hast to be small enough to satisfy any accuracy concerns.

We are now ready to graduate to two dimensions! In the remaining lessons of this course module, we will study the 2D heat equation and reaction-diffusion equation. Like before, we start with explicit methods (this lesson) and then move to implicit methods (next lesson). Let's get started.

## 2D Heat conduction

The equation of heat conduction in 2D is:

\begin{equation}
\rho c_p \frac{\partial T}{\partial t} = \frac{\partial}{\partial x} \left( \kappa_x \frac{\partial T}{\partial x} \right) + \frac{\partial}{\partial y} \left(\kappa_y \frac{\partial T}{\partial y} \right)
\end{equation}

where $\rho$ is the density, $c_p$ is the heat capacity and $\kappa$ is the thermal conductivity.

If the thermal conductivity $\kappa$ is constant, then we can take it outside of the spatial derivative and the equation simplifies to:

\begin{equation}
\frac{\partial T}{\partial t} = \alpha \left(\frac{\partial^2 T}{\partial x^2} + \frac{\partial^2 T}{\partial y^2} \right)
\end{equation}

where $\alpha = \frac{\kappa}{\rho c_p}$ is the thermal diffusivity.  The thermal diffusivity describes the ability of a material to conduct heat vs. storing it.

Does that equation have a familiar look to it?  That's because it's the same as the diffusion equation.  There's a reason that $\alpha$ is called the thermal *diffusivity*! We're going to set up a interesting problem where 2D heat conduction is important, and set about to solve it with explicit finite-difference methods.

### Problem statement

Removing heat out of micro-chips is a big problem in the computer industry. We are at a point in technology where computers can't run much faster because the chips might start failing due to the high temperature. This is a big deal! Let's study the problem more closely.

We want to understand how heat is dissipated from the chip with a very simplified model. Say we consider the chip as a 2D plate of size $1{\rm cm}\times 1{\rm cm}$, made of Silicon: $\kappa = 159{\rm W/m C}$, $c_p = 0.712\cdot 10^3 {\rm J/kg C}$, $\rho = 2329{\rm kg/m}^3$, and diffusivity $\alpha \approx 10^{-4}{\rm m}^2{/rm s}$. Silicon melts at $1414{\rm C}$, but chips should of course operate at much smaller temperatures. The maximum temperature allowed depends on the processor make and model; in many cases, the maximum temperature is somewhere between $60{\rm C}$ and $\sim70{\rm C}$, but better CPUs are recommended to operate at a [maximum of $80{\rm C}$](http://www.pugetsystems.com/blog/2009/02/26/intel-core-i7-temperatures/) (like the Intel Core i7, for example).

We're going to set up a somewhat artificial problem, just to demonstrate an interesting numerical solution. Say the chip is in a position where on two edges (top and right) it is in contact with insulating material.  On the other two edges the chip is touching other components that have a constant temperature of $T=100{\rm C}$ when the machine is operating. Initially, the chip is at room temperature $(20{\rm C})$. *How long does it take for the center of the chip to reach $70{\rm C}$?*

####Figure 1: Simplified microchip problem setup.

Let's use what we have learned to tackle this problem!

## 2D Finite differences

Everything you learned about finite-difference schemes in [Notebook 1 of Module 2](http://nbviewer.ipython.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/02_spacetime/02_01_1DConvection.ipynb) still applies, but now there are two spatial dimensions.  We will need to build a 2D grid of discrete points to compute the solution on.

We will use a 2D Cartesian grid: one that consists of two families of (grid) lines parallel to the two spatial directions. Two lines (of different families) intersect on one and only one grid node (this is called a _structured_ grid). In the $x$ direction, the discretization uses $i=0, \cdots N_x$ lines, and in the $y$ direction we have $j=0, \cdots N_y$ lines. A given node on the grid will now have two spatial coordinates, and we need two indices: for the two lines that intersect at that node.  For example, the middle point in the figure below would be $T_{i,j}$.

#### Figure 2. Nodal coordinates in 2 dimensions

### Explicit scheme in 2D

Recall from above that the 2D heat equation is

$$\frac{\partial T}{\partial t} = \alpha \left(\frac{\partial^2 T}{\partial x^2} + \frac{\partial^2 T}{\partial y^2} \right)$$

Let's write this out discretized using forward difference in time, and central difference in space,  using an explicit scheme. You should be able write this out yourself, without looking—if you need to look, it means you still need to write more difference equations by your own hand!

\begin{equation}
\frac{T^{n+1}_{i,j} - T^n_{i,j}}{\Delta t} = \alpha \left( \frac{T^n_{i+1, j} - 2T^n_{i,j} + T^n_{i-1,j}}{\Delta x^2} + \frac{T^n_{i, j+1} - 2T^n_{i,j} + T^n_{i,j-1}}{\Delta y^2}\right)
\end{equation}

Rearranging the equation to solve for the value at the next time step, $T^{n+1}_{i,j}$, yields

\begin{equation}
T^{n+1}_{i,j}= T^n_{i,j} + \alpha \left( \frac{\Delta t}{\Delta x^2} (T^n_{i+1, j} - 2T^n_{i,j} + T^n_{i-1,j}) + \frac{\Delta t}{\Delta y^2} (T^n_{i, j+1} - 2T^n_{i,j} + T^n_{i,j-1})\right)
\end{equation}

That's a little messier than 1D, but still recognizable.

Up until now, we've used stencils to help visualize how a scheme will advance the solution for one timestep.  Stencils in 2D are a little harder to draw, but hopefully the figure below will guide your understanding of this method: we are using five grid points at time step $n$ to obtain the solution on one point at time step $n+1$.

####Figure 3: 2D Explicit Stencil

Similar to all of the 1D explicit methods we've used, the solution at $T^{n+1}_{i,j}$ is updated using only known values from the current solution at time $n$. This is straightforward to implement in code, but will be subject to stability limitations on the time step that you can choose. We'll study an implicit method in the next lesson.

### Boundary Conditions

Whenever we reach a point that interacts with the boundary, we apply the boundary condition. As in the previous notebook, if the boundary has Dirichlet conditions, we simply impose the prescribed temperature at that point. If the boundary has Neumann conditions, we approximate them with a finite-difference scheme.

Remember, Neumann boundary conditions prescribe the derivative in the normal direction. For example, in the problem described above, we have $\frac{\partial T}{\partial y} = q_y$ in the top boundary and $\frac{\partial T}{\partial x} = q_x$ in the right boundary, with $q_y = q_x = 0$ (insulation).

Thus, at every time step, we need to enforce

\begin{equation}
T_{i,end} = q_y\cdot\Delta y + T_{i,end-1}
\end{equation}

and

\begin{equation}
T_{end,j} = q_x\cdot\Delta x + T_{end-1,j}
\end{equation}

Write the finite-difference discretization of the boundary conditions yourself, and confirm that you can get the expressions above.

### Stability

Before doing any coding, let's revisit stability constraints. We saw in the first notebook of this series that the 1D explicit discretization of the diffusion equation was stable as long as $\alpha \frac{\Delta t}{(\Delta x)^2} \leq \frac{1}{2}$. In 2D, this constraint is even tighter, as we need to add them in both directions:

\begin{equation}
\alpha \frac{\Delta t}{(\Delta x)^2} + \alpha \frac{\Delta t}{(\Delta y)^2} < \frac{1}{2}.
\end{equation}

Say that the mesh has the same spacing in $x$ and $y$, $\Delta x = \Delta y = \delta$. In that case, the stability condition is:

\begin{equation}
\alpha \frac{\Delta t}{\delta^2} < \frac{1}{4}
\end{equation}

## Code implementation

### Array storage

The physical problem has two dimensions, so we also store the temperatures in two dimensions: in a 2D array.

We chose to store it with the $y$ coordinates corresponding to the rows of the array and $x$ coordinates varying with the columns (this is just a code design decision!). If we are consistent with the stencil formula (with $x$ corresponding to index $i$ and $y$ to index $j$), then $T_{i,j}$ will be stored in array format as `T[j,i]`.

This might be a little confusing as most of us are used to writing coordinates in the format $(x,y)$, but our preference is to have the data stored so that it matches the physical orientation of the problem. Then, when we make a plot of the solution, the visualization will make sense to us, with respect to the geometry of our set-up. That's just nicer than to have the plot rotated!

####Figure 4: Row-column data storage

As you can see on Figure 4 above, if we want to access the value $18$ we would write those coordinates as $(x_2, y_3)$.  You can also see that its location is the 3rd row, 2nd column, so its array address would be `T[3,2]`.

Again, this is a design decision.  However you can choose to manipulate and store your data however you like; just remember to be consistent!

### Code time!

Now, to some coding! First, we have a little function that will advance the solution in time with a forward-time, centered-space scheme, and will monitor the center of the plate to tell us when it reaches $70{\rm C}$. Let's start by setting up our Python compute environment.

```
import numpy
import matplotlib.pyplot as plt
%matplotlib inline
from matplotlib import rcParams
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
```

```
def ftcs(T, nt, alpha, dt, dx, dy):

    j_mid = (numpy.shape(T)[0])/2
    i_mid = (numpy.shape(T)[1])/2

    for n in range(nt):
        Tn = T.copy()
        T[1:-1,1:-1] = Tn[1:-1,1:-1] + alpha *\
            (dt/dy**2 * (Tn[2:,1:-1] - 2*Tn[1:-1,1:-1] + Tn[:-2,1:-1]) +\
             dt/dx**2 * (Tn[1:-1,2:] - 2*Tn[1:-1,1:-1] + Tn[1:-1,:-2]))

        # Enforce Neumann BCs
        T[-1,:] = T[-2,:]
        T[:,-1] = T[:,-2]

        # Check if we reached T=70C
        if T[j_mid, i_mid] >= 70:
            print ("Center of plate reached 70C at time {0:.2f}s.".format(dt*n))
            break

    if T[j_mid, i_mid]<70:
        print ("Center has not reached 70C yet, it is only {0:.2f}C.".format(T[j_mid, i_mid]))

    return T
```

See the [`break`](https://docs.python.org/2/tutorial/controlflow.html) statement? It exits the `for` loop at the closest time iteration when the plate reaches $70{\rm C}$.

In the code cell below, we define our initial conditions according to the problem set up, and choose the discretization parameters. We start with only 20 spatial steps in each coordinate direction and advance for 500 time steps. You should later experiments with these parameters at your leisure!

```
L = 1.0e-2
H = 1.0e-2

nx = 21
ny = 21
nt = 500

dx = L/(nx-1)
dy = H/(ny-1)

x = numpy.linspace(0,L,nx)
y = numpy.linspace(0,H,ny)

alpha = 1e-4

Ti = numpy.ones((ny, nx))*20
Ti[0,:]= 100
Ti[:,0] = 100
```

We don't want our solution blowing up, so let's find a time step with $\frac{\alpha \Delta t}{\Delta x^2} = \frac{\alpha \Delta t}{\Delta y^2} = \frac{1}{4}$.

```
sigma = 0.25
dt = sigma * min(dx, dy)**2 / alpha
T=Ti.copy()
T = ftcs(T, nt, alpha, dt, dx, dy)
```

    Center of plate reached 70C at time 0.18s.

### Visualize the results

By now, you're no doubt *very* familiar with the `plt.plot` command.  It's great for line plots, scatter plots, etc., but what about when we have two spatial dimensions and another value (temperature) to display?

Are you thinking contour plot?  We're thinking contour plot.  Check out the documentation on [`plt.contourf`](http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.contour) (the 'f' denotes "filled" contours).

First, though, we need to create a `meshgrid` purely for plotting purposes:   [`numpy.meshgrid`](http://docs.scipy.org/doc/numpy/reference/generated/numpy.meshgrid.html) is a function that takes two vectors (`x` and `y`, say) and returns two 2D arrays of $x$ and $y$ coordinates that we then use to create the contour plot.

```
#my, mx = numpy.meshgrid(y,x)
mx, my = numpy.meshgrid(x,y)
```

```
plt.figure(figsize=(8,5))
plt.contourf(my,mx,T,20)
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.colorbar();
```

That looks pretty cool!  Note that in the call to `plt.contourf` you can specify the number of contour levels to display (we chose 20).  Look at that visualization: does it make physical sense to you, considering that the upper and right sides of the chip are insulated, in our problem?

##### Dig deeper

In the problem we just demonstrated, the chip reaches a temperature of $70{\rm C}$ at a given time, but will it keep increasing? That spells trouble.

Imagine that you have a heat sink instead of an insulator acting on the upper and right sides. What should be the heat flux that the heat sink achieves there, so that the temperature does not exceed $70{\rm C}$ at the center of the chip?

---
###### The cell below loads the style of the notebook

```
from IPython.core.display import HTML
css_file = '../../styles/numericalmoocstyle.css'
HTML(open(css_file, "r").read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 750px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Alegreya Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:600px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Nixie One', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 45pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}
.text_cell_render h2 {
    font-family: 'Nixie One', serif;
    font-weight: 400;
    font-size: 30pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Nixie One', serif;
    margin-top:16px;
	font-size: 22pt;
    font-weight: 600;
    margin-bottom: 3px;
    font-style: regular;
    color: rgb(102,102,0);
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Nixie One', serif;
    font-size: 14pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Nixie One', sans-serif;
    font-weight: 400;
    font-size: 16pt;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
        font-family: "PT Mono";
        font-size: 90%;
}

</style>