# Interactive notebook of basic `helmpy` examples

The mathematics of the model which `helmpy` uses are described in [Hardwick et al. (2021)](https://www.sciencedirect.com/science/article/pii/S1755436521000013).

In this notebook we will run through all of the basic features in `helmpy` as well as providing insights into its construction along the way. Note that the using `helmpy` for statistical inference is more advanced, and so there is another notebook written for this topic. Throughout we shall be running the algorithm only once to generate our plots, however for best results it is recommended to rerun `helmpy` a few more times (fewer than $10$ iterations) and to combine the output. Because a number of realisations may be run at once (e.g., $250$), this can lead to very good statistics in the output (overall $10 \times 250$ realisations) with high efficiency.

First we must tell the system where the `helmpy` directory is...

```python
import sys
path_to_helmpy = '/Users/Rob/work/helmpy' # Give your path to helmpy here
sys.path.append(path_to_helmpy + '/source/')
from helmpy import helmpy

import numpy as np
import matplotlib.pyplot as plt
import scipy.special as spec
import time
```

## Running basic simulations in three steps

First note that the majority of the examples in this notebook will consider the soil-transmitted helminthiasis (STH) however the `helmpy` class also supports schistosomiasis (SCH) and soon will include lymphatic filariasis (LF).

### Step 1

Initialise the `helmpy` class by specifying a disease type and working directory...

```python
hp = helmpy(
            'STH',                              # Set the disease type - types available are: 'STH', 'SCH' and 'LF'
            path_to_helmpy,                     # Set the path to the working directory
            suppress_terminal_output=False      # Set this to 'True' to remove terminal messages
            )
```

### Step 2

Set the parameters and initial conditions for the simulation using the `helmpy.parameter_dictionary` and `helmpy.initial_conditions` dictionaries. List sizes are variable depending on the number of groupings one wishes to apply. Hence age, gender and all other stratifications within a cluster may be programmed in straightforwardly and each grouping may be identified within a given cluster by a spatial index. The available basic options to vary are...

```python
hp.parameter_dictionary['mu'] = [0.014,0.014]   # Human death rate (per year)
hp.parameter_dictionary['mu1'] = [0.5,0.5]      # Adult worm death rate (per year)
hp.parameter_dictionary['mu2'] = [26.0,26.0]    # Reservoir (eggs and larvae) death rate (per year)
hp.parameter_dictionary['R0'] = [3.5,2.1]       # Basic reproduction number within grouping
#hp.parameter_dictionary['R0'] = [3.5,3.0]       # Basic reproduction number within grouping
hp.parameter_dictionary['k'] = [0.3,0.5]        # Inverse-clumping factor within grouping
hp.parameter_dictionary['gam'] = [0.08,0.08]    # Density dependent fecundity: z = exp(-gam)
hp.parameter_dictionary['Np'] = [300,350]       # Number of people within grouping
hp.parameter_dictionary['spi'] = [1,2]          # Spatial index number of grouping

hp.initial_conditions['M'] = [2.9,2.1]          # Initial mean total worm burden within grouping
#hp.initial_conditions['M'] = [2.9,3.1]          # Initial mean total worm burden within grouping
hp.initial_conditions['FOI'] = [1.25,1.1]       # Initial force of infection (per year) within grouping
#hp.initial_conditions['FOI'] = [1.25,1.5]       # Initial force of infection (per year) within grouping
```

So we are running two non-interacting clusters each with a single grouping of people which both contain different numbers of people, different initial conditions, different parasite clumping (controlled through $k$) and different overall values of parasite uptake: here combined into a parameter we call `R0` - note that these are different to the true $R_0$ value in the cluster which can be computed from the weighted sum of these sub-`R0` values.

### Step 3

Run the fully individual-based stochastic simulation by setting the key numerical parameters and using the `helmpy.run_full_stoch` method. For comparison we will also run the mean field `helmpy.run_meanfield` and mean field stochastic `helmpy.run_meanfield_stoch` methods (which are much faster) alongside the full simulation...

```python
runtime = 100.0                               # Set the total time of the run in years
realisations = 250                            # Set the number of stochastic realisations for the model
do_nothing_timescale = 0.01                   # Set a timescale (in years) short enough such that an individual
                                              # is expected to stay in the same state

output_filename = 'default_example'           # Set a filename for the data to be output in data/

output_timesteps = [1000,2000,3000,9000]      # Optional - output binned worm burdens over whole population
                                              # and realisations after a specified number of steps in time
hp.run_full_stoch(
                  runtime,
                  realisations,
                  do_nothing_timescale,
                  output_filename,
                  timesteps_snapshot=output_timesteps
                  )

hp.run_meanfield_stoch(
                       runtime,
                       realisations,
                       do_nothing_timescale,
                       'meanfield_stoch_' + output_filename,
                       timesteps_snapshot=output_timesteps
                       )

hp.run_meanfield(
                 runtime,
                 do_nothing_timescale,
                 'meanfield_' + output_filename
                 )
```

      >>>>      >>>>      >>>>      >>>>      >>>>      >>
     >>  >>    >>  >>    >>  >>    >>  >>    >>  >>    >> >>
    >>    >>>>>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>   >>
    >>                 >>
    >>                 >>
    >>         >>>>    >>      >>>    >>>    >>>>>>   >>   >>
    >>>>>>>   >>  >>   >>     >> >>  >> >>  >>    >>  >>   >>
    >>    >>  >>  >>   >>     >>  >>>>  >>  >>    >>  >>   >>
    >>    >>  >>>>>>   >>     >>   >>   >>  >>>>>>>   >>   >>
    >>    >>  >>       >>     >>        >>  >>        >>   >>
    >>    >>  >>   >>  >>     >>        >>  >>        >>   >>
    >>    >>   >>>>>    >>>>> >>        >>  >>         >>>>>>
                                                          >>
       >>>>      >>>>      >>>>      >>>>      >>>>      >>
      >>  >>    >>  >>    >>  >>    >>  >>    >>  >>    >>
    >>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>

    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                   Author: Robert J. Hardwick
                  DISTRIBUTED UNDER MIT LICENSE
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    Soil-transmitted helminth mode enabled

    Setting initial conditions...

    Total number of individuals: 650
    Number of clusters: 2

    Now running full stochastic simulation for 100.0 years...

    Output snapshot of worm burdens at time t = 9.98 years for cluster 1
    Output snapshot of worm burdens at time t = 9.98 years for cluster 2
    Output snapshot of worm burdens at time t = 19.79 years for cluster 1
    Output snapshot of worm burdens at time t = 19.79 years for cluster 2
    Output snapshot of worm burdens at time t = 29.61 years for cluster 1
    Output snapshot of worm burdens at time t = 29.61 years for cluster 2
    Output snapshot of worm burdens at time t = 90.44 years for cluster 1
    Output snapshot of worm burdens at time t = 90.44 years for cluster 2

      >>>>      >>>>      >>>>      >>>>      >>>>      >>
     >>  >>    >>  >>    >>  >>    >>  >>    >>  >>    >> >>
    >>    >>>>>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>   >>
    >>                 >>
    >>                 >>
    >>         >>>>    >>      >>>    >>>    >>>>>>   >>   >>
    >>>>>>>   >>  >>   >>     >> >>  >> >>  >>    >>  >>   >>
    >>    >>  >>  >>   >>     >>  >>>>  >>  >>    >>  >>   >>
    >>    >>  >>>>>>   >>     >>   >>   >>  >>>>>>>   >>   >>
    >>    >>  >>       >>     >>        >>  >>        >>   >>
    >>    >>  >>   >>  >>     >>        >>  >>        >>   >>
    >>    >>   >>>>>    >>>>> >>        >>  >>         >>>>>>
                                                          >>
       >>>>      >>>>      >>>>      >>>>      >>>>      >>
      >>  >>    >>  >>    >>  >>    >>  >>    >>  >>    >>
    >>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>

    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                   Author: Robert J. Hardwick
                  DISTRIBUTED UNDER MIT LICENSE
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    Soil-transmitted helminth mode enabled

    Setting initial conditions...

    Total number of individuals: 650
    Number of clusters: 2

    Now running mean-field stochastic model for 100.0 years...

    Output snapshot of worm burdens at time t = 10.0 years for cluster 1
    Output snapshot of worm burdens at time t = 10.0 years for cluster 2
    Output snapshot of worm burdens at time t = 20.0 years for cluster 1
    Output snapshot of worm burdens at time t = 20.0 years for cluster 2
    Output snapshot of worm burdens at time t = 30.0 years for cluster 1
    Output snapshot of worm burdens at time t = 30.0 years for cluster 2
    Output snapshot of worm burdens at time t = 90.0 years for cluster 1
    Output snapshot of worm burdens at time t = 90.0 years for cluster 2

      >>>>      >>>>      >>>>      >>>>      >>>>      >>
     >>  >>    >>  >>    >>  >>    >>  >>    >>  >>    >> >>
    >>    >>>>>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>   >>
    >>                 >>
    >>                 >>
    >>         >>>>    >>      >>>    >>>    >>>>>>   >>   >>
    >>>>>>>   >>  >>   >>     >> >>  >> >>  >>    >>  >>   >>
    >>    >>  >>  >>   >>     >>  >>>>  >>  >>    >>  >>   >>
    >>    >>  >>>>>>   >>     >>   >>   >>  >>>>>>>   >>   >>
    >>    >>  >>       >>     >>        >>  >>        >>   >>
    >>    >>  >>   >>  >>     >>        >>  >>        >>   >>
    >>    >>   >>>>>    >>>>> >>        >>  >>         >>>>>>
                                                          >>
       >>>>      >>>>      >>>>      >>>>      >>>>      >>
      >>  >>    >>  >>    >>  >>    >>  >>    >>  >>    >>
    >>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>

    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                   Author: Robert J. Hardwick
                  DISTRIBUTED UNDER MIT LICENSE
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    Soil-transmitted helminth mode enabled

    Setting initial conditions...

    Total number of individuals: 650
    Number of clusters: 2

    Now running mean field model for 100.0 years...

Having run all three models, let us now read in the data and compare the ensemble mean and variance of the mean worm burden realisations in each cluster.

First we compare the predictions of the mean field model to those of the full simulation...

```python
example_output_data = np.loadtxt(path_to_helmpy + '/data/' + output_filename + '.txt')
example_meanfield_output_data = np.loadtxt(path_to_helmpy + '/data/' + 'meanfield_' + output_filename + '.txt')

# Mean of ensemble in cluster 1
plt.plot(example_output_data.T[0],example_output_data.T[1],color='r',\
         label=r'$R_0 =' + str(hp.parameter_dictionary['R0'][0]) + r'\,\,' + \
               r'k =' + str(hp.parameter_dictionary['k'][0]) + r'\,\,' + \
               r'N_{\mathrm{p}} =' + str(hp.parameter_dictionary['Np'][0]) + r'\,\,' + \
               r'M(t_0) =' + str(hp.initial_conditions['M'][0]) + r'\,\,' + \
               r'\Lambda (t_0) =' + str(hp.initial_conditions['FOI'][0]) + r'$')

# Mean of ensemble in cluster 2
plt.plot(example_output_data.T[0],example_output_data.T[2],color='b',\
         label=r'$R_0 =' + str(hp.parameter_dictionary['R0'][1]) + r'\,\,' + \
               r'k =' + str(hp.parameter_dictionary['k'][1]) + r'\,\,' + \
               r'N_{\mathrm{p}} =' + str(hp.parameter_dictionary['Np'][1]) + r'\,\,' + \
               r'M(t_0) =' + str(hp.initial_conditions['M'][1]) + r'\,\,' + \
               r'\Lambda (t_0) =' + str(hp.initial_conditions['FOI'][1]) + r'$')

# Mean field of ensemble in cluster 1
plt.plot(example_meanfield_output_data.T[0],example_meanfield_output_data.T[1],color='r',alpha=0.4)

# Mean field of ensemble in cluster 2
plt.plot(example_meanfield_output_data.T[0],example_meanfield_output_data.T[2],color='b',alpha=0.4)

axes = plt.gca()
axes.set_ylim([0.0,7.5])
axes.set_ylabel(r'$M(t)$')
axes.set_xlabel(r'$t-t_0$')

plt.legend(fontsize = 12)
plt.savefig(path_to_helmpy + '/plots/Mean_meanfield_comp.png',format='png',dpi=500)
plt.show()
```

```python
example_output_data = np.loadtxt(path_to_helmpy + '/data/' + output_filename + '.txt')
example_meanfield_output_data = np.loadtxt(path_to_helmpy + '/data/' + 'meanfield_' + output_filename + '.txt')

# Variance of ensemble in cluster 1
plt.plot(example_output_data.T[0],example_output_data.T[3],color='r',\
         label=r'$R_0 =' + str(hp.parameter_dictionary['R0'][0]) + r'\,\,' + \
               r'k =' + str(hp.parameter_dictionary['k'][0]) + r'\,\,' + \
               r'N_{\mathrm{p}} =' + str(hp.parameter_dictionary['Np'][0]) + r'\,\,' + \
               r'M(t_0) =' + str(hp.initial_conditions['M'][0]) + r'\,\,' + \
               r'\Lambda (t_0) =' + str(hp.initial_conditions['FOI'][0]) + r'$')

# Variance of ensemble in cluster 2
plt.plot(example_output_data.T[0],example_output_data.T[4],color='b',\
         label=r'$R_0 =' + str(hp.parameter_dictionary['R0'][1]) + r'\,\,' + \
               r'k =' + str(hp.parameter_dictionary['k'][1]) + r'\,\,' + \
               r'N_{\mathrm{p}} =' + str(hp.parameter_dictionary['Np'][1]) + r'\,\,' + \
               r'M(t_0) =' + str(hp.initial_conditions['M'][1]) + r'\,\,' + \
               r'\Lambda (t_0) =' + str(hp.initial_conditions['FOI'][1]) + r'$')

# Variance from mean field of ensemble in cluster 1
plt.plot(example_meanfield_output_data.T[0],example_meanfield_output_data.T[3],color='r',alpha=0.4)

# Variance from mean field of ensemble in cluster 2
plt.plot(example_meanfield_output_data.T[0],example_meanfield_output_data.T[4],color='b',alpha=0.4)

axes = plt.gca()
axes.set_ylim([0.0,2.5])
axes.set_ylabel(r'$V(t)$')
axes.set_xlabel(r'$t-t_0$')

plt.legend(fontsize = 12)
plt.savefig(path_to_helmpy + '/plots/Var_meanfield_comp.png',format='png',dpi=500)
plt.show()
```

Secondly, we compare the predictions of a fast stochastic model built from the mean field code to those of the full simulation...

```python
example_output_data = np.loadtxt(path_to_helmpy + '/data/' + output_filename + '.txt')
example_meanfield_stoch_data = np.loadtxt(path_to_helmpy + '/data/' + 'meanfield_stoch_' + output_filename + '.txt')

# Mean of ensemble in cluster 1
plt.plot(example_output_data.T[0],example_output_data.T[1],color='r',\
         label=r'$R_0 =' + str(hp.parameter_dictionary['R0'][0]) + r'\,\,' + \
               r'k =' + str(hp.parameter_dictionary['k'][0]) + r'\,\,' + \
               r'N_{\mathrm{p}} =' + str(hp.parameter_dictionary['Np'][0]) + r'\,\,' + \
               r'M(t_0) =' + str(hp.initial_conditions['M'][0]) + r'\,\,' + \
               r'\Lambda (t_0) =' + str(hp.initial_conditions['FOI'][0]) + r'$')

# Mean of ensemble in cluster 2
plt.plot(example_output_data.T[0],example_output_data.T[2],color='b',\
         label=r'$R_0 =' + str(hp.parameter_dictionary['R0'][1]) + r'\,\,' + \
               r'k =' + str(hp.parameter_dictionary['k'][1]) + r'\,\,' + \
               r'N_{\mathrm{p}} =' + str(hp.parameter_dictionary['Np'][1]) + r'\,\,' + \
               r'M(t_0) =' + str(hp.initial_conditions['M'][1]) + r'\,\,' + \
               r'\Lambda (t_0) =' + str(hp.initial_conditions['FOI'][1]) + r'$')

# 68% CLs of ensemble in cluster 1
plt.plot(example_output_data.T[0],example_output_data.T[5],color='r')
plt.plot(example_output_data.T[0],example_output_data.T[7],color='r')

# 68% CLs of ensemble in cluster 2
plt.plot(example_output_data.T[0],example_output_data.T[6],color='b')
plt.plot(example_output_data.T[0],example_output_data.T[8],color='b')

# Mean of mean-field stochastic ensemble in cluster 1
plt.plot(example_meanfield_stoch_data.T[0],example_meanfield_stoch_data.T[1],color='r',alpha=0.4)

# Mean of mean-field stochastic ensemble in cluster 2
plt.plot(example_meanfield_stoch_data.T[0],example_meanfield_stoch_data.T[2],color='b',alpha=0.4)

# 68% CLs of mean-field stochastic ensemble in cluster 1
plt.plot(example_meanfield_stoch_data.T[0],example_meanfield_stoch_data.T[5],color='r',alpha=0.4)
plt.plot(example_meanfield_stoch_data.T[0],example_meanfield_stoch_data.T[7],color='r',alpha=0.4)

# 68% CLs of mean-field stochastic ensemble in cluster 2
plt.plot(example_meanfield_stoch_data.T[0],example_meanfield_stoch_data.T[6],color='b',alpha=0.4)
plt.plot(example_meanfield_stoch_data.T[0],example_meanfield_stoch_data.T[8],color='b',alpha=0.4)

axes = plt.gca()
axes.set_ylim([0.0,9.5])
axes.set_ylabel(r'$m(t)$')
axes.set_xlabel(r'$t-t_0$')

plt.legend(fontsize = 12, loc = 9)
plt.savefig(path_to_helmpy + '/plots/meanfield_stoch_comp.png',format='png',dpi=500)
plt.show()
```

Now let us also have a look at some snapshots in time of the mean worm burden distribution from the full simulation...

```python
time_values = [1000,2000,3000,9000]
alphlist = [0.2,0.4,0.6,1.0]

for ti in range(0,len(time_values)):
    wb_data = np.loadtxt(path_to_helmpy + '/data/' + output_filename + \
                         '_snapshot_timestep_' + str(time_values[ti]) + '_cluster_1.txt')
    bo = np.histogram((np.sum(wb_data,axis=1)/len(wb_data.T)).astype(float),bins='fd',density=True)
    box = 0.5*(bo[1][:len(bo[0])]+bo[1][1:len(bo[0])+1])
    boy = bo[0]
    plt.plot(box[boy!=0.0],(boy/max(boy))[boy!=0.0],color='r',alpha=alphlist[ti])

for ti in range(0,len(time_values)):
    wb_data = np.loadtxt(path_to_helmpy + '/data/' + output_filename + \
                         '_snapshot_timestep_' + str(time_values[ti]) + '_cluster_2.txt')
    bo = np.histogram((np.sum(wb_data,axis=1)/len(wb_data.T)).astype(float),bins='fd',density=True)
    box = 0.5*(bo[1][:len(bo[0])]+bo[1][1:len(bo[0])+1])
    boy = bo[0]
    plt.plot(box[boy!=0.0],(boy/max(boy))[boy!=0.0],color='b',alpha=alphlist[ti])

axes = plt.gca()
axes.set_xlim([0.0,8.0])
plt.show()
```

...and the equivalent from the mean field stochastic code...

```python
time_values = [1000,2000,3000,9000]
alphlist = [0.2,0.4,0.6,1.0]

for ti in range(0,len(time_values)):
    mwb_data = np.loadtxt(path_to_helmpy + '/data/'  + 'meanfield_stoch_' + output_filename + \
                          '_snapshot_timestep_' + str(time_values[ti]) + '_cluster_1.txt')
    bo = np.histogram(mwb_data.astype(float),bins='fd',density=True)
    box = 0.5*(bo[1][:len(bo[0])]+bo[1][1:len(bo[0])+1])
    boy = bo[0]
    plt.plot(box[boy!=0.0],(boy/max(boy))[boy!=0.0],color='r',alpha=alphlist[ti])

for ti in range(0,len(time_values)):
    mwb_data = np.loadtxt(path_to_helmpy + '/data/'  + 'meanfield_stoch_' + output_filename + \
                          '_snapshot_timestep_' + str(time_values[ti]) + '_cluster_2.txt')
    bo = np.histogram(mwb_data.astype(float),bins='fd',density=True)
    box = 0.5*(bo[1][:len(bo[0])]+bo[1][1:len(bo[0])+1])
    boy = bo[0]
    plt.plot(box[boy!=0.0],(boy/max(boy))[boy!=0.0],color='b',alpha=alphlist[ti])

axes = plt.gca()
axes.set_xlim([0.0,8.0])
plt.show()
```

## Adding inter-cluster migration

To begin with, let us create a new instance of `helmpy` with the same parameters and initial conditions set for comparison in order to continue working distinctly from the previous section...

```python
hpmig = helmpy(
              'STH',                              # Set the disease type - types available are: 'STH', 'SCH' and 'LF'
              path_to_helmpy,                     # Set the path to the working directory
              suppress_terminal_output=False      # Set this to 'True' to remove terminal messages
              )

hpmig.parameter_dictionary['mu'] = [0.014,0.014]   # Human death rate (per year)
hpmig.parameter_dictionary['mu1'] = [0.5,0.5]      # Adult worm death rate (per year)
hpmig.parameter_dictionary['mu2'] = [26.0,26.0]    # Reservoir (eggs and larvae) death rate (per year)
hpmig.parameter_dictionary['R0'] = [3.5,2.1]       # Basic reproduction number within grouping
hpmig.parameter_dictionary['k'] = [0.3,0.5]        # Inverse-clumping factor within grouping
hpmig.parameter_dictionary['gam'] = [0.08,0.08]    # Density dependent fecundity: z = exp(-gam)
hpmig.parameter_dictionary['Np'] = [300,350]       # Number of people within grouping
hpmig.parameter_dictionary['spi'] = [1,2]          # Spatial index number of grouping

hpmig.initial_conditions['M'] = [2.9,2.1]          # Initial mean total worm burden within grouping
hpmig.initial_conditions['FOI'] = [1.25,1.1]       # Initial force of infection (per year) within grouping
```

Having run the basic simulations above for two separate clusters, let us now introduce an inter-cluster migration effect by specifying non-zero migratory rate matrices with the following definitions: Components $(r_+)_{ij}$ and $(r_-)_{ij}$ are the migration rates (per year) of individuals in to cluster $i$ from cluster $j$ and out of cluster $i$ from cluster $j$, respectively. Hence, the matrices for two clusters are

$$
\begin{equation}
{\bf r}_+ = \begin{bmatrix} (r_+)_{11} & (r_+)_{12} \\ (r_+)_{21} & (r_+)_{22}\end{bmatrix}\,,\qquad {\bf r}_- = \begin{bmatrix} (r_-)_{11} & (r_-)_{12} \\ (r_-)_{21} & (r_-)_{22}\end{bmatrix}\,,
\end{equation}
$$

and one may deduce that $(r_+)_{ij}=(r_-)_{ji}$. Let us now set these matrices in the parameter dictionaries of our `helmpy` instance...

```python
hpmig.parameter_dictionary['r+'] = [[0.0,0.0],[52.0,0.0]]  # Migration matrix - the migration rate in (per year)
hpmig.parameter_dictionary['r-'] = [[0.0,52.0],[0.0,0.0]]  # Migration matrix - the migration rate out (per year)
hpmig.parameter_dictionary['Nm'] = [1]                     # Number of migrants per event (global parameter)
```

Now that the migrations have been set, we may simply rerun the code as before...

```python
runtime = 100.0                               # Set the total time of the run in years
realisations = 250                            # Set the number of stochastic realisations for the model
do_nothing_timescale = 0.01                   # Set a timescale (in years) short enough such that an individual
                                              # is expected to stay in the same state

output_filename = 'default_example'           # Set a filename for the data to be output in data/

output_timesteps = [1000,2000,3000,9000]      # Optional - output binned worm burdens over whole population
                                              # and realisations after a specified number of steps in time

hpmig.run_full_stoch(
                    runtime,
                    realisations,
                    do_nothing_timescale,
                    'mig_' + output_filename,
                    timesteps_snapshot=output_timesteps
                    )
```

      >>>>      >>>>      >>>>      >>>>      >>>>      >>
     >>  >>    >>  >>    >>  >>    >>  >>    >>  >>    >> >>
    >>    >>>>>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>   >>
    >>                 >>
    >>                 >>
    >>         >>>>    >>      >>>    >>>    >>>>>>   >>   >>
    >>>>>>>   >>  >>   >>     >> >>  >> >>  >>    >>  >>   >>
    >>    >>  >>  >>   >>     >>  >>>>  >>  >>    >>  >>   >>
    >>    >>  >>>>>>   >>     >>   >>   >>  >>>>>>>   >>   >>
    >>    >>  >>       >>     >>        >>  >>        >>   >>
    >>    >>  >>   >>  >>     >>        >>  >>        >>   >>
    >>    >>   >>>>>    >>>>> >>        >>  >>         >>>>>>
                                                          >>
       >>>>      >>>>      >>>>      >>>>      >>>>      >>
      >>  >>    >>  >>    >>  >>    >>  >>    >>  >>    >>
    >>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>

    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                   Author: Robert J. Hardwick
                  DISTRIBUTED UNDER MIT LICENSE
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    Soil-transmitted helminth mode enabled

    Setting initial conditions...

    Total number of individuals: 650
    Number of clusters: 2

    Inter-cluster migration has been enabled...

    Now running full stochastic simulation for 100.0 years...

    Output snapshot of worm burdens at time t = 10.01 years for cluster 1
    Output snapshot of worm burdens at time t = 10.01 years for cluster 2
    Output snapshot of worm burdens at time t = 20.12 years for cluster 1
    Output snapshot of worm burdens at time t = 20.12 years for cluster 2
    Output snapshot of worm burdens at time t = 30.0 years for cluster 1
    Output snapshot of worm burdens at time t = 30.0 years for cluster 2
    Output snapshot of worm burdens at time t = 90.8 years for cluster 1
    Output snapshot of worm burdens at time t = 90.8 years for cluster 2

```python
example_output_data = np.loadtxt(path_to_helmpy + '/data/' + output_filename + '.txt')
example_output_data_mig = np.loadtxt(path_to_helmpy + '/data/' + 'mig_' + output_filename + '.txt')

# Mean of ensemble in cluster 1 with migration
plt.plot(example_output_data_mig.T[0],example_output_data_mig.T[1],color='r',\
         label=r'$R_0 =' + str(hpmig.parameter_dictionary['R0'][0]) + r'\,\,' + \
               r'k =' + str(hpmig.parameter_dictionary['k'][0]) + r'\,\,' + \
               r'N_{\mathrm{p}} =' + str(hpmig.parameter_dictionary['Np'][0]) + r'\,\,' + \
               r'M(t_0) =' + str(hpmig.initial_conditions['M'][0]) + r'\,\,' + \
               r'\Lambda (t_0) =' + str(hpmig.initial_conditions['FOI'][0]) + r'$')

# Mean of ensemble in cluster 2 with migration
plt.plot(example_output_data_mig.T[0],example_output_data_mig.T[2],color='b',\
         label=r'$R_0 =' + str(hpmig.parameter_dictionary['R0'][1]) + r'\,\,' + \
               r'k =' + str(hpmig.parameter_dictionary['k'][1]) + r'\,\,' + \
               r'N_{\mathrm{p}} =' + str(hpmig.parameter_dictionary['Np'][1]) + r'\,\,' + \
               r'M(t_0) =' + str(hpmig.initial_conditions['M'][1]) + r'\,\,' + \
               r'\Lambda (t_0) =' + str(hpmig.initial_conditions['FOI'][1]) + r'$')

# 68% CLs of ensemble in cluster 1 with migration
plt.plot(example_output_data_mig.T[0],example_output_data_mig.T[5],color='r')
plt.plot(example_output_data_mig.T[0],example_output_data_mig.T[7],color='r')

# 68% CLs of ensemble in cluster 2 with migration
plt.plot(example_output_data_mig.T[0],example_output_data_mig.T[6],color='b')
plt.plot(example_output_data_mig.T[0],example_output_data_mig.T[8],color='b')

# Mean of ensemble in cluster 1
plt.plot(example_output_data.T[0],example_output_data.T[1],color='r',alpha=0.4)

# Mean of ensemble in cluster 2
plt.plot(example_output_data.T[0],example_output_data.T[2],color='b',alpha=0.4)

# 68% CLs of ensemble in cluster 1
plt.plot(example_output_data.T[0],example_output_data.T[5],color='r',alpha=0.4)
plt.plot(example_output_data.T[0],example_output_data.T[7],color='r',alpha=0.4)

# 68% CLs of ensemble in cluster 2
plt.plot(example_output_data.T[0],example_output_data.T[6],color='b',alpha=0.4)
plt.plot(example_output_data.T[0],example_output_data.T[8],color='b',alpha=0.4)

axes = plt.gca()
axes.set_ylim([0.0,8.5])
axes.set_ylabel(r'$m(t)$')
axes.set_xlabel(r'$t-t_0$')

plt.legend(fontsize = 12, loc = 9)
plt.text(50.0,0.5,r'$(r_+,r_-)=(' + \
         str(np.round(hpmig.parameter_dictionary['r+'][0][1]/ \
                      hpmig.parameter_dictionary['mu2'][0],decimals=1)) + '\mu_2,' + \
         str(np.round(hpmig.parameter_dictionary['r-'][0][1]/ \
                      hpmig.parameter_dictionary['mu2'][0],decimals=1)) + '\mu_2)$', \
          color='r',fontsize=15)
plt.savefig(path_to_helmpy + '/plots/mig_comp.png',format='png',dpi=500)
plt.show()
```

Now let once again have a look at some snapshots in time of the mean worm burden distribution from the full simulation...

```python
time_values = [1000,2000,3000,9000]
alphlist = [0.2,0.4,0.6,1.0]

for ti in range(0,len(time_values)):
    wb_data = np.loadtxt(path_to_helmpy + '/data/' + 'mig_' + output_filename + \
                         '_snapshot_timestep_' + str(time_values[ti]) + '_cluster_1.txt')
    bo = np.histogram((np.sum(wb_data,axis=1)/len(wb_data.T)).astype(float),bins='fd',density=True)
    box = 0.5*(bo[1][:len(bo[0])]+bo[1][1:len(bo[0])+1])
    boy = bo[0]
    plt.plot(box[boy!=0.0],(boy/max(boy))[boy!=0.0],color='r',alpha=alphlist[ti])

for ti in range(0,len(time_values)):
    wb_data = np.loadtxt(path_to_helmpy + '/data/' + 'mig_' + output_filename + \
                         '_snapshot_timestep_' + str(time_values[ti]) + '_cluster_2.txt')
    bo = np.histogram((np.sum(wb_data,axis=1)/len(wb_data.T)).astype(float),bins='fd',density=True)
    box = 0.5*(bo[1][:len(bo[0])]+bo[1][1:len(bo[0])+1])
    boy = bo[0]
    plt.plot(box[boy!=0.0],(boy/max(boy))[boy!=0.0],color='b',alpha=alphlist[ti])

axes = plt.gca()
axes.set_xlim([0.0,8.0])
plt.show()
```

## Adding treatment rounds

The `helmpy` class models rounds of mass drug administration (MDA) through Bernoulli trials with a given probability of reducing an individual's worm burden to 0. This is effectively a 'random compliance' model. Let us use this feature to examine the influence of migration on the efficacy of treatment using the system setup from before...

```python
# Change the Boolean to see the effect of migration for simplicity...
with_migration = False

hptrt = helmpy(
              'STH',                              # Set the disease type - types available are: 'STH', 'SCH' and 'LF'
              path_to_helmpy,                     # Set the path to the working directory
              suppress_terminal_output=False      # Set this to 'True' to remove terminal messages
              )

hptrt.parameter_dictionary['mu'] = [0.014,0.014]   # Human death rate (per year)
hptrt.parameter_dictionary['mu1'] = [0.5,0.5]      # Adult worm death rate (per year)
hptrt.parameter_dictionary['mu2'] = [26.0,26.0]    # Reservoir (eggs and larvae) death rate (per year)
hptrt.parameter_dictionary['R0'] = [3.5,2.1]       # Basic reproduction number within grouping
hptrt.parameter_dictionary['k'] = [0.3,0.5]        # Inverse-clumping factor within grouping
hptrt.parameter_dictionary['gam'] = [0.08,0.08]    # Density dependent fecundity: z = exp(-gam)
hptrt.parameter_dictionary['Np'] = [300,350]       # Number of people within grouping
hptrt.parameter_dictionary['spi'] = [1,2]          # Spatial index number of grouping

hptrt.initial_conditions['M'] = [2.9,2.1]          # Initial mean total worm burden within grouping
hptrt.initial_conditions['FOI'] = [1.25,1.1]       # Initial force of infection (per year) within grouping

if with_migration == True:
    hptrt.parameter_dictionary['r+'] = [[0.0,0.0],[52.0,0.0]] # Migration matrix - the migration rate in (per year)
    hptrt.parameter_dictionary['r-'] = [[0.0,52.0],[0.0,0.0]] # Migration matrix - the migration rate out (per year)
    hptrt.parameter_dictionary['Nm'] = [10]                   # Number of migrants per event (global parameter)
```

Having initialised a new instance, let us define 3 rounds of MDA with 60% coverage at years 15, 16 and 17 using the `helmpy.add_treatment_prog` method to see the effect this has on the disease...

```python
treatment_coverages = [[0.6,0.6,0.6],         # A list of lists matching the chosen groupings which gives
                       [0.6,0.6,0.6]]         # the effective coverage fraction in each

treatment_times = [15.0,16.0,17.0]            # A list of treatment times for all clusters

hptrt.add_treatment_prog(
                        treatment_times,
                        treatment_coverages=treatment_coverages,
                        drug_efficacy=1.0
                        )
```

Now we can run the simulation which will output both the mean worm burden dynamics (as before) and the final prevalence realisations at the last round of treatment and at the absolute end of the runs...

```python
runtime = 100.0                               # Set the total time of the run in years
realisations = 250                            # Set the number of stochastic realisations for the model
do_nothing_timescale = 0.01                   # Set a timescale (in years) short enough such that an individual
                                              # is expected to stay in the same state

output_filename = 'default_example'           # Set a filename for the data to be output in data/

output_timesteps = [1000,2000,3000,9000]      # Optional - output binned worm burdens over whole population
                                              # and realisations after a specified number of steps in time

if with_migration == False:
    hptrt.run_full_stoch(
                        runtime,
                        realisations,
                        do_nothing_timescale,
                        'trt_' + output_filename,
                        timesteps_snapshot=output_timesteps
                        )

if with_migration == True:
    hptrt.run_full_stoch(
                        runtime,
                        realisations,
                        do_nothing_timescale,
                        'mig_trt_' + output_filename,
                        timesteps_snapshot=output_timesteps
                        )
```

      >>>>      >>>>      >>>>      >>>>      >>>>      >>
     >>  >>    >>  >>    >>  >>    >>  >>    >>  >>    >> >>
    >>    >>>>>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>   >>
    >>                 >>
    >>                 >>
    >>         >>>>    >>      >>>    >>>    >>>>>>   >>   >>
    >>>>>>>   >>  >>   >>     >> >>  >> >>  >>    >>  >>   >>
    >>    >>  >>  >>   >>     >>  >>>>  >>  >>    >>  >>   >>
    >>    >>  >>>>>>   >>     >>   >>   >>  >>>>>>>   >>   >>
    >>    >>  >>       >>     >>        >>  >>        >>   >>
    >>    >>  >>   >>  >>     >>        >>  >>        >>   >>
    >>    >>   >>>>>    >>>>> >>        >>  >>         >>>>>>
                                                          >>
       >>>>      >>>>      >>>>      >>>>      >>>>      >>
      >>  >>    >>  >>    >>  >>    >>  >>    >>  >>    >>
    >>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>    >>>>>>

    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                   Author: Robert J. Hardwick
                  DISTRIBUTED UNDER MIT LICENSE
    >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

    Soil-transmitted helminth mode enabled

    Setting initial conditions...

    Total number of individuals: 650
    Number of clusters: 2

    Treatments are to be performed on the nearest times to: [15. 16. 17.] years

    Now running full stochastic simulation for 100.0 years...

    Output snapshot of worm burdens at time t = 9.62 years for cluster 1
    Output snapshot of worm burdens at time t = 9.62 years for cluster 2
    Treatment 1 implemented: 15.0 years
    Treatment 1 implemented: 15.0 years
    Treatment 2 implemented: 16.0 years
    Treatment 2 implemented: 16.0 years
    Treatment 3 implemented: 17.0 years
    Treatment 3 implemented: 17.0 years
    Output snapshot of worm burdens at time t = 19.63 years for cluster 1
    Output snapshot of worm burdens at time t = 19.63 years for cluster 2
    Output snapshot of worm burdens at time t = 29.53 years for cluster 1
    Output snapshot of worm burdens at time t = 29.53 years for cluster 2

    /Users/Rob/work/helmpy/source/helmpy.py:1655: RuntimeWarning: invalid value encountered in double_scalars
      ensM_zeros_removed_perclus = np.sum(
    /Users/Rob/work/helmpy/source/helmpy.py:1658: RuntimeWarning: invalid value encountered in double_scalars
      ensV_zeros_removed_perclus = np.sum(

    Output snapshot of worm burdens at time t = 90.65 years for cluster 1
    Output snapshot of worm burdens at time t = 90.65 years for cluster 2

```python
#example_output_data_mig_trt = np.loadtxt(path_to_helmpy + '/data/' + 'mig_trt_' + output_filename + '.txt')
example_output_data_trt = np.loadtxt(path_to_helmpy + '/data/' + 'trt_' + output_filename + '.txt')

# Mean of ensemble in cluster 1 with migration
#plt.plot(example_output_data_mig_trt.T[0],example_output_data_mig_trt.T[1],color='r',\
#         label=r'$R_0 =' + str(hptrt.parameter_dictionary['R0'][0]) + r'\,\,' + \
#               r'k =' + str(hptrt.parameter_dictionary['k'][0]) + r'\,\,' + \
#               r'N_{\mathrm{p}} =' + str(hptrt.parameter_dictionary['Np'][0]) + r'\,\,' + \
#               r'M(t_0) =' + str(hptrt.initial_conditions['M'][0]) + r'\,\,' + \
#               r'\Lambda (t_0) =' + str(hptrt.initial_conditions['FOI'][0]) + r'$')

# Mean of ensemble in cluster 2 with migration
#plt.plot(example_output_data_mig_trt.T[0],example_output_data_mig_trt.T[2],color='b',\
#         label=r'$R_0 =' + str(hptrt.parameter_dictionary['R0'][1]) + r'\,\,' + \
#               r'k =' + str(hptrt.parameter_dictionary['k'][1]) + r'\,\,' + \
#               r'N_{\mathrm{p}} =' + str(hptrt.parameter_dictionary['Np'][1]) + r'\,\,' + \
#               r'M(t_0) =' + str(hptrt.initial_conditions['M'][1]) + r'\,\,' + \
#               r'\Lambda (t_0) =' + str(hptrt.initial_conditions['FOI'][1]) + r'$')

# 68% CLs of ensemble in cluster 1 with migration
#plt.plot(example_output_data_mig_trt.T[0],example_output_data_mig_trt.T[5],color='r')
#plt.plot(example_output_data_mig_trt.T[0],example_output_data_mig_trt.T[7],color='r')

# 68% CLs of ensemble in cluster 2 with migration
#plt.plot(example_output_data_mig_trt.T[0],example_output_data_mig_trt.T[6],color='b')
#plt.plot(example_output_data_mig_trt.T[0],example_output_data_mig_trt.T[8],color='b')

# Mean of ensemble in cluster 1
plt.plot(example_output_data_trt.T[0],example_output_data_trt.T[1],color='r',alpha=0.4)

# Mean of ensemble in cluster 2
plt.plot(example_output_data_trt.T[0],example_output_data_trt.T[2],color='b',alpha=0.4)

# 68% CLs of ensemble in cluster 1
plt.plot(example_output_data_trt.T[0],example_output_data_trt.T[5],color='r',alpha=0.4)
plt.plot(example_output_data_trt.T[0],example_output_data_trt.T[7],color='r',alpha=0.4)

# 68% CLs of ensemble in cluster 2
plt.plot(example_output_data_trt.T[0],example_output_data_trt.T[6],color='b',alpha=0.4)
plt.plot(example_output_data_trt.T[0],example_output_data_trt.T[8],color='b',alpha=0.4)

axes = plt.gca()
axes.set_ylim([0.0,8.5])
axes.set_ylabel(r'$m(t)$')
axes.set_xlabel(r'$t-t_0$')

#plt.legend(fontsize = 12, loc = 9)
#plt.text(50.0,1.0,r'$(r_+,r_-)=(' + str(hptrt.parameter_dictionary['r+'][0][1]) + ',' + \
#                                    str(hptrt.parameter_dictionary['r-'][0][1]) + ')$',color='r',fontsize=15)
plt.savefig(path_to_helmpy + '/plots/treat_comp.png',format='png',dpi=500)
plt.show()
```

```python
example_lasttreat_prev_data = np.loadtxt(path_to_helmpy + '/data/' + 'trt_' + output_filename + \
                                                  '_lasttreat_prevalences_cluster_1.txt')
example_final_prev_data = np.loadtxt(path_to_helmpy + '/data/' + 'trt_' + output_filename + \
                                                  '_final_prevalences_cluster_1.txt')

example_lasttreat_prev_data2 = np.loadtxt(path_to_helmpy + '/data/' + 'trt_' + output_filename + \
                                                  '_lasttreat_prevalences_cluster_2.txt')
example_final_prev_data2 = np.loadtxt(path_to_helmpy + '/data/' + 'trt_' + output_filename + \
                                                  '_final_prevalences_cluster_2.txt')

p1,b1 = np.histogram(example_lasttreat_prev_data,bins='fd')
p2,b2 = np.histogram(example_final_prev_data,bins='fd')
p3,b3 = np.histogram(example_lasttreat_prev_data2,bins='fd')
p4,b4 = np.histogram(example_final_prev_data2,bins='fd')

plt.plot(0.5*(b1[:len(b1)-1]+b1[1:len(b1)]),p1/max(p1),color='r',alpha=0.4)
plt.plot(0.5*(b2[:len(b2)-1]+b2[1:len(b2)]),p2/max(p2),color='r')
plt.plot(0.5*(b3[:len(b3)-1]+b3[1:len(b3)]),p3/max(p3),color='b',alpha=0.4)
plt.plot(0.5*(b4[:len(b4)-1]+b4[1:len(b3)]),p4/max(p4),color='b')
plt.show()
```

```python
#example_lasttreat_prev_data = np.loadtxt(path_to_helmpy + '/data/' + 'mig_trt_' + output_filename + \
#                                                  '_lasttreat_prevalences_cluster_1.txt')
#example_final_prev_data = np.loadtxt(path_to_helmpy + '/data/' + 'mig_trt_' + output_filename + \
#                                                  '_final_prevalences_cluster_1.txt')

#example_lasttreat_prev_data2 = np.loadtxt(path_to_helmpy + '/data/' + 'mig_trt_' + output_filename + \
#                                                  '_lasttreat_prevalences_cluster_2.txt')
#example_final_prev_data2 = np.loadtxt(path_to_helmpy + '/data/' + 'mig_trt_' + output_filename + \
#                                                  '_final_prevalences_cluster_2.txt')

#p1,b1 = np.histogram(example_lasttreat_prev_data,bins='fd')
#p2,b2 = np.histogram(example_final_prev_data,bins='fd')
#p3,b3 = np.histogram(example_lasttreat_prev_data2,bins='fd')
#p4,b4 = np.histogram(example_final_prev_data2,bins='fd')

#plt.plot(0.5*(b1[:len(b1)-1]+b1[1:len(b1)]),p1/max(p1),color='r',alpha=0.4)
#plt.plot(0.5*(b2[:len(b2)-1]+b2[1:len(b2)]),p2/max(p2),color='r')
#plt.plot(0.5*(b3[:len(b3)-1]+b3[1:len(b3)]),p3/max(p3),color='b',alpha=0.4)
#plt.plot(0.5*(b4[:len(b4)-1]+b4[1:len(b4)]),p4/max(p4),color='b')
#plt.show()
```

## Adding individual non-compliance to the treatment rounds

The `helmpy` class also contains models for systematic individual non-compliance to the MDA programme in the form of the conditional probabilities of: individuals in the $j$-th grouping being treated in the $n$-th round given treatment in the $(n-1)$-th round $\alpha^j_{n,n-1}$ and being treated in the $n$-th round given non-treatment in the $(n-1)$-th round $\beta^j_{n,n-1}$ in a Markov model such that the probability of an individual in the $j$-th grouping is treated in the $n$-th round may be written as

$$
\begin{equation}
p_{j,n} = \alpha^j_{n,n-1} p_{j,n-1} + \beta^j_{n,n-1} [1 - p_{j,n-1}]\,.
\end{equation}
$$

Having initialised a `helmpy` instance, let us illustrate how to define the same 3 rounds of MDA with the same effective 60% coverage at years 15, 16 and 17 but with compliance parameters using the `compliance_params` keyword of the `helmpy.add_treatment_prog` method for systematic individual non-compliance...

```python
comp_alpha_betas = [[0.6,0.4,0.97,0.05,0.97,0.05],     # A list of lists matching the chosen groupings which gives
                    [0.6,0.4,0.97,0.05,0.97,0.05]]     # the alpha and beta parameters in the order of
                                                       # alpha_1, beta_1, alpha_2,... in each, where the
                                                       # first round alpha is an initial coverage fraction

treatment_times = [15.0,16.0,17.0]                     # A list of treatment times for all clusters

hp.add_treatment_prog(
    treatment_times,
    compliance_params=comp_alpha_betas,
    drug_efficacy=1.0,
)
```

## Running with ageing process between bins

The default setting in `helmpy` is to not have people's worm counts explicitly 'ageing' out of bins. To examine how this can affect the results, we include this ageing as a global Poisson process which occurs at a rate controlled by the birth rate of new indviduals (with zero worms) into the cluster while keeping the number of people within each grouping constant for all time, we may adapt `helmpy` in the following way...

```python
# Remember to set this to zero for consistency!
hp.parameter_dictionary['mu'] = [0.00,0.00]        # Human death rate (per year)

hp.parameter_dictionary['ari'] = [0,1]             # Group age-ordering index - 0,1,2,3,...
hp.parameter_dictionary['brat'] = [2.0,2.0]        # Birth rate per year into grouping 0
hp.parameter_dictionary['Na'] = [10]               # Number of people ageing per event
```

Including the ageing process typically increases the runtime by around 40%, so for most applications it not recommended. It appears to not make much of a difference in most cases but it is useful for checking the robustness of the model.

## Running large numbers of people and clusters

In order to run `helmpy` with a large number of people and clusters, it is advised to reduce the number of realisations to 1 as the internal vectorisation which optimises the code cannot handle matrices that are too large.

```python

```


# Elementary greenhouse models

____________

## 1. A single layer atmosphere
____________

We will make our first attempt at quantifying the greenhouse effect in the simplest possible greenhouse model: a single layer of atmosphere that is able to absorb and emit longwave radiation.

### Assumptions

- Atmosphere is a single layer of air at temperature $T_a$
- Atmosphere is **completely transparent to shortwave** solar radiation.
- The **surface** absorbs shortwave radiation $(1-\alpha) Q$
- Atmosphere is **completely opaque to infrared** radiation
- Both surface and atmosphere emit radiation as **blackbodies** ($\sigma T_s^4, \sigma T_a^4$)
- Atmosphere radiates **equally up and down** ($\sigma T_a^4$)
- There are no other heat transfer mechanisms

We can now use the concept of energy balance to ask what the temperature need to be in order to balance the energy budgets at the surface and the atmosphere, i.e. the **radiative equilibrium temperatures**.

### Energy balance at the surface

\begin{align}
\text{energy in} &= \text{energy out} \\
(1-\alpha) Q + \sigma T_a^4 &= \sigma T_s^4 \\
\end{align}

The presence of the atmosphere above means there is an additional source term: downwelling infrared radiation from the atmosphere.

We call this the **back radiation**.

### Energy balance for the atmosphere

\begin{align}
\text{energy in} &= \text{energy out} \\
\sigma T_s^4 &= A\uparrow + A\downarrow = 2 \sigma T_a^4
\end{align}

which means that
$$ T_s = 2^\frac{1}{4} T_a \approx 1.2 T_a $$

So we have just determined that, in order to have a purely **radiative equilibrium**, we must have $T_s > T_a$.

*The surface must be warmer than the atmosphere.*

### Solve for the radiative equilibrium surface temperature

Now plug this into the surface equation to find

$$ (1-\alpha) Q + \sigma T_a^4 = 2\sigma T_a^4 $$

and use the definition of the emission temperature $T_e$ to write

$$ (1-\alpha) Q = \sigma T_e^4 $$

*In this model, $T_e$ is identical to the atmospheric temperature $T_a$, since all the OLR originates from this layer.*

Solve for the surface temperature:
$$ T_s = 2^\frac{1}{4} T_e $$

Putting in observed numbers, $T_e = 255$ K gives a surface temperature of
$$T_s = 303 ~\text{K}$$

This model is one small step closer to reality: The surface is warmer than atmosphere, emissions to space are generated in the atmosphere, and the atmosphere is heated from below and helps to keep surface warm.

### Why does this model overpredict the surface temperature?

Our model now overpredicts the surface temperature by about 15ºC (303 K versus the observed 288 K).

Ideas about why?

Basically we just need to read our **list of assumptions** above and realize that none of them are very good approximations:

- Atmosphere absorbs some solar radiation.
- Atmosphere is NOT a perfect absorber of longwave radiation
- Absorption and emission varies strongly with wavelength *(atmosphere does not behave like a blackbody)*.
- Emissions are not determined by a single temperature $T_a$ but by the detailed *vertical profile* of air temperture.
- Energy is redistributed in the vertical by a variety of dynamical transport mechanisms (e.g. convection and boundary layer turbulence).

____________

## 2. Introducing the two-layer grey gas model
____________

Let's generalize the above model just a little bit to build a slighly more realistic model of longwave radiative transfer.

We will address two shortcomings of our single-layer model:
1. No vertical structure
2. 100% longwave opacity

Relaxing these two assumptions gives us what turns out to be a very useful prototype model for **understanding how the greenhouse effect works**.

### Assumptions

- The atmosphere is **transparent to shortwave radiation** (still)
- Divide the atmosphere up into **two layers of equal mass** (the dividing line is thus at 500 hPa pressure level)
- Each layer **absorbs only a fraction $\epsilon$** of whatever longwave radiation is incident upon it.
- We will call the fraction $\epsilon$ the **absorptivity** of the layer.
- Assume $\epsilon$ is the same in each layer

This is called the **grey gas** model, where **grey** here means the emission and absorption have **no spectral dependence** (same at every wavelength).

We can think of this model informally as a "leaky greenhouse".

Note that the assumption that $\epsilon$ is the same in each layer is appropriate if the absorption is actually carried out by a gas that is **well-mixed** in the atmosphere.

Out of our two most important absorbers:

- CO$_2$ is well mixed
- H$_2$O is not (mostly confined to lower troposphere due to strong temperature dependence of the saturation vapor pressure).

But we will ignore this aspect of reality for now.

### Kirchoff's Law

In order to build our model, we need to introduce one additional piece of physics known as **Kirchoff's Law**:

$$ \text{absorptivity} = \text{emissivity} $$

So **if a layer of atmosphere at temperature $T$ absorbs a fraction $\epsilon$** of incident longwave radiation, it must **emit**

$$ \epsilon ~\sigma ~T^4 $$

both up and down.

### A sketch of the radiative fluxes in the 2-layer atmosphere

- Surface temperature is $T_s$
- Atmospheric temperatures are $T_0, T_1$ where $T_0$ is closest to the surface.
- absorptivity of atm layers is $\epsilon$
- Surface emission is $\sigma T_s^4$
- Atmospheric emission is $\epsilon \sigma T_0^4, \epsilon \sigma T_1^4$ (up and down)
- Absorptivity = emissivity for atmospheric layers
- a fraction $(1-\epsilon)$ of the longwave beam is **transmitted** through each layer

____________
## 3. Tracing the upwelling beam of longwave radiation
____________

Let's think about the upwelling beam of longwave radiation, which we denote $U$.

### Surface to layer 0

We start at the surface. The upward flux **from the surface to layer 0** is

$$U_0 = \sigma T_s^4$$

(just the emission from the suface).

### Layer 0 to layer 1

Now **following this beam upward**, we first recognize that a fraction $\epsilon$ of this beam is **absorbed** in layer 0.

The upward flux from layer 0 to layer 1 consists of the sum of two parts:

1. The **transmitted part** of whatever is incident from below (i.e. the part that is **not absorbed**)
2. **New upward emissions** from layer 0

We can write this upward flux from layer 0 to layer 1 as:

$$U_1 = (1-\epsilon) \sigma T_s^4 + \epsilon \sigma T_0^4$$

### Beyond layer 1

Continuing to follow the same beam, we follow the same logic! A fraction $\epsilon$ of $U_1$ is absorbed in layer 1, and therefore the transmitted part is $(1-\epsilon) U_1$.

Including new emissions from layer 1, the upwelling flux above layer 1 is

$$U_2 = (1-\epsilon) U_1 + \epsilon \sigma T_1^4$$

### Outgoing Longwave Radiation

Since there is **no more atmosphere above layer 1**, this upwelling beam is our OLR for this model:

$$OLR = U_2 = (1-\epsilon) U_1 + \epsilon \sigma T_1^4$$

which, plugging in the above expression for $U_1$, works out to

$$OLR= (1-\epsilon)^2 \sigma T_s^4 + \epsilon(1-\epsilon)\sigma T_0^4 + \epsilon \sigma T_1^4$$

Here the three terms represent **contributions to the total OLR** that **originate from each of the three levels**

### Limits of large and small absorptivity/emissivity

Think about the following two questions:

- What happens to this expression if $\epsilon=1$? *What does this represent physically?*
- What about $\epsilon=0$?

By allowing the atmosphere to partially absorb emissions from other levels, we now see that the Outgoing Longwave Radiation to space **includes emissions from every level** - and therefore **affected by temperature at every level**!

____________

## 4. Tuning the grey gas model to observations
____________

In building our new model we have introduced exactly **one parameter**, the absorptivity $\epsilon$. We need to choose a value for $\epsilon$.

We will tune our model so that it **reproduces the observed global mean OLR** given **observed global mean temperatures**.

### Global mean air temperature observations

To get appropriate temperatures for $T_s, T_0, T_1$, let's revisit the global, annual mean lapse rate plot from NCEP Reanalysis data we first encountered in the [Radiation notes](radiation.ipynb).

```python
#  This code is used just to create the skew-T plot of global, annual mean air temperature
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import xarray as xr
from metpy.plots import SkewT
ncep_url = "https://psl.noaa.gov/thredds/dodsC/Datasets/ncep.reanalysis.derived/"
ncep_air = xr.open_dataset( ncep_url + "pressure/air.mon.1981-2010.ltm.nc", use_cftime=True)
#  Take global, annual average and convert to Kelvin
coslat = np.cos(np.deg2rad(ncep_air.lat))
weight = coslat / coslat.mean(dim='lat')
Tglobal = (ncep_air.air * weight).mean(dim=('lat','lon','time'))
```

    Cannot import USCOUNTIES and USSTATES without Cartopy installed.

```python
fig = plt.figure(figsize=(9, 9))
skew = SkewT(fig, rotation=30)
skew.plot(Tglobal.level, Tglobal, color='black', linestyle='-', linewidth=2, label='Observations')
skew.ax.set_ylim(1050, 10)
skew.ax.set_xlim(-75, 45)
# Add the relevant special lines
skew.plot_dry_adiabats(linewidth=0.5)
skew.plot_moist_adiabats(linewidth=0.5)
#skew.plot_mixing_lines()
skew.ax.legend()
skew.ax.set_title('Global, annual mean sounding from NCEP Reanalysis', fontsize = 16);
```

### Target temperatures for our model tuning

First, we set
$$T_s = 288 \text{ K}  $$

From the lapse rate plot, an average temperature for the layer between 1000 and 500 hPa is

$$ T_0 = 275 \text{ K}$$

Defining an average temperature for the layer between 500 and 0 hPa is more ambiguous because of the lapse rate reversal at the tropopause. We will choose

$$ T_1 = 230 \text{ K}$$

From the graph, this is approximately the observed global mean temperature at 275 hPa or about 10 km.

### OLR

From the [observed global energy budget](../images/GlobalEnergyBudget.png) we set

$$OLR = 238.5 \text{ W m}^{-2}$$

### Solving for $\epsilon$

We wrote down the expression for OLR as a function of temperatures and absorptivity in our model above.

All we need to do is plug the observed values into the above expression for OLR, and solve for $\epsilon$.

It is a **quadratic equation** for the unknown $\epsilon$. We could work out the exact solution using the quadratic formula.

But let's do it **graphically**, using Python!

### Exercise: graphical solution to find the best fit value of $\epsilon$

The OLR formula for the leaky greenhouse that we derived above is

$$OLR= (1-\epsilon)^2 \sigma T_s^4 + \epsilon(1-\epsilon)\sigma T_0^4 + \epsilon \sigma T_1^4$$

Do the following:

- Write a Python function that implements this formula
- The function should accept **four input parameters**:
    - The three temperatures $T_s, T_0, T_1$
    - The emissivity $\epsilon$
- Using this function, make a **graph of OLR vs. $\epsilon$** for the observed temperature values $T_s = 288, T_0 = 275, T_1 = 230$
- For the graph, $\epsilon$ should range between 0 and 1.
- From your graph, find the approximate value of $\epsilon$ that gives $OLR = 238.5$

```python

```

Note if you solve the quadratic equation algebraically you will get two solutions:

- $\epsilon \approx 0.586$
- $\epsilon \approx 3.93$

(for details, see [the advanced notes here](sympy-greenhouse.ipynb))

**Why is the second solution not physically meaningful?**

Hopefully your graph shows that $\epsilon = 0.586$ gives the correct value of OLR.

This is the absorptivity that guarantees that **our model reproduces the observed OLR given the observed temperatures**.

____________

## 5. Level of emission
____________

### Contributions from each level to the outgoing radiation

Now that we have tuned up our model, we can see exactly how strongly each level contributes to the OLR.

The three components of the OLR are

\begin{align*}
OLR_s &= (1-\epsilon)^2 \sigma T_s^4  \\
OLR_0 &= \epsilon(1-\epsilon)\sigma T_0^4 \\
OLR_1 &= \epsilon \sigma T_1^4
\end{align*}

which of course add up to the total OLR we wrote down above.

### Exercise: calculate contributions to OLR

Write some **simple** Python code to calculate each term in the OLR using the observed temperatures and the tuned value $\epsilon = 0.586$. Fill out the list below using your calculated numbers.

```python

```

**Contributions to the OLR originating from each level, in W/m2:**

- Surface:
- Level 0:
- Level 1:

```python
# now sum up the numbers to verify you get something very close to 238.5

```

Notice that the largest single contribution is coming from the top layer.

*This is in spite of the fact that the emissions from this layer are weak, because it is so cold.*

###  Changing the level of emission by adding absorbers

Adding some **extra greenhouse absorbers** will mean that a **greater fraction** of incident longwave radiation is absorbed in each layer.

Thus **$\epsilon$ must increase** as we add greenhouse gases.

Suppose we have $\epsilon$ initially, and the absorptivity increases to $\epsilon_2 = \epsilon + \Delta \epsilon$.

Suppose further that this increase happens **abruptly** so that there is no time for the temperatures to respond to this change. **We hold the temperatures fixed** in the column and ask how the radiative fluxes change.

**Question: Do you expect the OLR to increase or decrease?**

### Calculating the change in level of emission

Let's use our two-layer leaky greenhouse model to investigate the answer.

The components of the OLR before the perturbation are

\begin{align*}
OLR_s &= (1-\epsilon)^2 \sigma T_s^4  \\
OLR_0 &= \epsilon(1-\epsilon)\sigma T_0^4 \\
OLR_1 &= \epsilon \sigma T_1^4
\end{align*}

and after the perturbation we have

\begin{align*}
OLR_s &= (1-\epsilon - \Delta \epsilon)^2 \sigma T_s^4  \\
OLR_0 &= (\epsilon + \Delta \epsilon)(1-\epsilon - \Delta \epsilon)\sigma T_0^4 \\
OLR_1 &= (\epsilon + \Delta \epsilon) \sigma T_1^4
\end{align*}

Let's subtract off the original components to get the contributions to the **change in OLR** from each layer:

\begin{align*}
\Delta OLR_s &= \left[(1-\epsilon - \Delta \epsilon)^2 - (1-\epsilon)^2\right]\sigma T_s^4  \\
\Delta OLR_0 &= \left[(\epsilon + \Delta \epsilon)(1-\epsilon - \Delta \epsilon) - \epsilon(1-\epsilon)  \right] \sigma T_0^4 \\
\Delta OLR_1 &= \left[(\epsilon + \Delta \epsilon) - \epsilon \right] \sigma T_1^4
\end{align*}

Now expand this out, but to make things easier to deal with, neglect term in $\Delta \epsilon^2$ (very small - we will be considering changes of less than 10% in $\epsilon$):

\begin{align*}
\Delta OLR_s &\approx (\Delta \epsilon) \left[ -2(1-\epsilon) \right] \sigma T_s^4  \\
\Delta OLR_0 &\approx (\Delta \epsilon) (1 - 2 \epsilon) \sigma T_0^4 \\
\Delta OLR_1 &\approx (\Delta \epsilon) \sigma T_1^4
\end{align*}

Now look at the **sign** of each term. Recall that $0 < \epsilon < 1$. **Which terms in the OLR go up and which go down?**

**THIS IS VERY IMPORTANT, SO STOP AND THINK ABOUT IT.**

The contribution from the **surface** must **decrease**, while the contribution from the **top layer** must **increase**.

**When we add absorbers, the average level of emission goes up!**

____________

## 6. Radiative forcing in the 2-layer grey gas model
____________

### Definition of Radiative Forcing

We now define a very important quantity:

*Radiative forcing is the change in total radiative flux at TOA after adding absorbers.*

In this model, **only the longwave flux can change**, so we calculate the radiative forcing as

$$ R = - \Delta OLR $$

(with the minus sign so that $R$ is **positive when the climate system is gaining extra energy**).

### Connection between radiative forcing and level of emission

We just worked out that whenever we add some extra absorbers, the emissions to space (on average) will originate from higher levels in the atmosphere.

What does this mean for OLR? Will it increase or decrease?

To get the answer, we just have to sum up the three contributions we wrote above:

\begin{align*}
R &= -\Delta OLR_s - \Delta OLR_0 - \Delta OLR_1  \\
  &= -\Delta \epsilon \left[  -2(1-\epsilon) \sigma T_s^4 + (1 - 2 \epsilon) \sigma T_0^4  + \sigma T_1^4 \right]
\end{align*}

Is this a positive or negative number? The key point is this:

**It depends on the temperatures, i.e. on the lapse rate.**

### Greenhouse effect for an isothermal atmosphere

Stop and think about this question:

If the **surface and atmosphere are all at the same temperature**, does the OLR go up or down when $\epsilon$ increases (i.e. we add more absorbers)?

Understanding this question is key to understanding how the greenhouse effect works.

#### Let's solve the isothermal case

We will just set $T_s = T_0 = T_1$ in the above expression for the radiative forcing.

What do you get?

#### The answer is $R=0$

For an isothermal atmosphere, there is **no change** in OLR when we add extra greenhouse absorbers. Hence, no radiative forcing and no greenhouse effect.

Why?

The level of emission still must go up. But since the temperature at the upper level is the **same** as everywhere else, the emissions are exactly the same.

### The radiative forcing (change in OLR) depends on the lapse rate!

For a more realistic example of radiative forcing due to an increase in greenhouse absorbers, we use our observed temperatures and the tuned value for $\epsilon$.

We'll express the answer in W m$^{-2}$ for a 2% increase in $\epsilon$:

$$ \Delta \epsilon = 0.02 \times 0.58 $$

```python
epsilon = 0.586041150248834
delta_epsilon = 0.02 * epsilon
delta_epsilon
```

    0.011720823004976679

Calculate the three components of the radiative forcing:

```python
sigma = 5.67E-8
Ts = 288.
T0 = 275.
T1 = 230.
```

```python
#  Component originating from the surface
Rs = -delta_epsilon * (-2*(1-epsilon)*sigma * Ts**4)
print(Rs)
```

    3.78528239476332

```python
#  Component originating from level 0
R0 = -delta_epsilon * (1-2*epsilon) * sigma * T0**4
print(R0)
```

    0.6540457761108388

```python
#  Component originating from level 1
R1 = -delta_epsilon * sigma * T1**4
print(R1)
```

    -1.8597411929137297

So just add them up to get the total radiative forcing:

```python
R = Rs + R0 + R1
print(R)
```

    2.579586977960429

So in our example, **the OLR decreases by 2.6 W m$^{-2}$**, or equivalently, the **radiative forcing is +2.6 W m$^{-2}$.**

What we have just calculated is this:

*Given the observed lapse rates, a small increase in absorbers will cause a small decrease in OLR.*

The **greenhouse effect** thus gets **stronger**, and energy will begin to accumulate in the system -- which will eventually **cause temperatures to increase** as the system adjusts to a new equilibrium.

____________

## 7. Summary
____________

### Key physical lessons

- Putting a **layer of longwave absorbers** above the surface keeps the **surface substantially warmer**, because of the back radiation from the atmosphere (greenhouse effect).
- The **grey gas** model assumes that each layer absorbs and emits a fraction $\epsilon$ of its blackbody value, independent of wavelength.

- With **incomplete absorption** ($\epsilon < 1$), there are contributions to the OLR from every level and the surface (there is no single level of emission)
- **Adding more absorbers** means that contributions to the OLR from upper levels go up, while contributions from the surface go down.
- This upward shift in the weighting of different levels is what we mean when we say the **level of emission goes up**.

- The **radiative forcing** caused by an increase in absorbers depends on the lapse rate.
- For an **isothermal atmosphere** the radiative forcing is zero and there is **no greenhouse effect**
- The radiative forcing is positive for our atmosphere **because tropospheric temperatures tend to decrease with height**.

____________

## Credits

This notebook is part of [The Climate Laboratory](https://brian-rose.github.io/ClimateLaboratoryBook), an open-source textbook developed and maintained by [Brian E. J. Rose](http://www.atmos.albany.edu/facstaff/brose/index.html), University at Albany. It has been modified by [Nicole Feldl](http://nicolefeldl.com), UC Santa Cruz.

It is licensed for free and open consumption under the
[Creative Commons Attribution 4.0 International (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/) license.

Development of these notes and the [climlab software](https://github.com/brian-rose/climlab) is partially supported by the National Science Foundation under award AGS-1455071 to Brian Rose. Any opinions, findings, conclusions or recommendations expressed here are mine and do not necessarily reflect the views of the National Science Foundation.
____________


```python
from sympy import symbols, diff, integrate, solve, factor, Sum, oo, \
  binomial, sqrt, limit, simplify
```

# Глава 1

## Задача 1
$f(a)=M(\zeta-a)^2, a=M\zeta$

$M(\zeta^2-2\zeta a+a^2)=\\=
M\zeta^2-2M (\zeta a) +Ma^2$

$-2M(\zeta a)+Ma^2 \rightarrow min$

$-2M\zeta+2Ma$

$M\zeta = Ma$

## Задача 2
$M \bar \eta = M(\frac{1}{n}(\eta_1+...+\eta_n))=\\
=\frac{1}{n}(M\eta_1+...+M\eta_n)=0.5$

$D \bar \eta = D(\frac{1}{n}(\eta_1+...+\eta_n))=\\
=M\eta^2-(M\eta)^2=0.5-0.25=0.25$

$M\bar\eta=\int_0^1 dx=$

## Задача 3
$M\eta(n)=M max(\eta_1,...,\eta_n)=$

$F_1(x)=P(\eta_1\leq x)$

$F_{\eta(n)}(\eta_{(n)}\leq a)=\prod_{i=1}^{n}P(\eta_i \leq a) = a^n$

$M\eta_{(n)}=\int_0^1 a^n da=\frac{1}{n+1}a^{n+1}\\=\frac{1}{n+1}1^{n+1}$

$M\bar\eta\ = \int_0^1 a \cdot  F_{\eta} da = \\
=\int_0^1 a^{n+1} da = \frac{1}{n+1}\cdot a$

```python
a, n = symbols('a n', positive =True)
F = a**n
F
```

$\displaystyle a^{n}$

```python
f = diff(F, a)
f
```

$\displaystyle \frac{a^{n} n}{a}$

```python
M = integrate(a*f, (a, 0, 1))
M
```

$\displaystyle \frac{n}{n + 1}$

```python
D = integrate(a**2*f,(a,0,1)) - M**2
D
```

$\displaystyle - \frac{n^{2}}{\left(n + 1\right)^{2}} + \frac{n}{n + 2}$

```python
factor(D)
```

$\displaystyle \frac{n}{\left(n + 1\right)^{2} \left(n + 2\right)}$

```python
del a,n,F,f,M,D
```

## Задача 4
$F(\mu =  a)=p\cdot \prod_{i=1}^a (1-p)=\\
=p\cdot (1-p)^a$

```python
# число неудач до появления первого успеха и вероятность успеха
p = symbols('p', positive=True)
mu, a = symbols('mu a', positive=True, Integer=True)
F = p*(1-p)**a
F
```

$\displaystyle p \left(1 - p\right)^{a}$

```python
s = Sum(F, (a,1,+oo)).doit()
print(s)
```

    p*Piecewise(((1 - p)/p, Abs(p - 1) < 1), (Sum((1 - p)**a, (a, 1, oo)), True))

```python
del p,mu,a,F,s
```

## Задача 5

```python
# Кол-во тестируемых в группе
k = symbols('k', positive=True, Integer=True)
# Вероятность обнаружения заболевания
p = symbols('p', positive= True)
# Вероятность отсутствия заболевания
q = 1 - p
# Вероястность что группа больна и НЕ больна
pk, qk = 1 - q**k, q**k
# Число всевозможных комбинации больных и здоровых в больной группе
i = symbols('i', positive=True, Integer=True)
npk =  Sum(binomial(k, i), (i, 1, k))
# Кол-во необходимых тестов для одной группы, с учётом того, что
# если в больной группе не выявился больной на k-1 тест, то тест делать не надо
z = 1*  qk + pk * ((k+1) * (npk-1)/npk + k * 1/npk)
z
```

$\displaystyle \left(1 - p\right)^{k} + \left(1 - \left(1 - p\right)^{k}\right) \left(\frac{k}{\sum_{i=1}^{k} {\binom{k}{i}}} + \frac{\left(k + 1\right) \left(\sum_{i=1}^{k} {\binom{k}{i}} - 1\right)}{\sum_{i=1}^{k} {\binom{k}{i}}}\right)$

```python
# Кол-во необходимых тестов для группы из 2-х тестируемых
z2 = z.subs(k,2).doit()
z2
```

$\displaystyle \frac{8}{3} - \frac{5 \left(1 - p\right)^{2}}{3}$

```python
# Критерий по "p" при котором выгоднее тестировать группами чем по отдельности
crit = solve(z2<2,p).args[0]
crit.evalf()
```

$\displaystyle p < 0.367544467966324$

```python
del k,p,q,pk,qk,i,npk,z,z2,crit
```

## Задача 6

```python
x, p = symbols('x, p', positive=True)
```

```python
H = 1+1/x-(1-p)**x
H
```

$\displaystyle - \left(1 - p\right)^{x} + 1 + \frac{1}{x}$

```python
h = diff(H, x)*-1
h
```

$\displaystyle \left(1 - p\right)^{x} \log{\left(1 - p \right)} + \frac{1}{x^{2}}$

```python
# Для док-ва подставим значение предпологаемого кореня и возьмём предел
a = h.subs(x,1/sqrt(p))
limit(a,p,0)
```

$\displaystyle 0$

```python
simplify(H.subs(x, 1/sqrt(p)))
```

$\displaystyle \sqrt{p} - \left(1 - p\right)^{\frac{1}{\sqrt{p}}} + 1$

```python
del x,p,H,h,a
```


# Table of Contents

1  ALGO1 : Introduction à l'algorithmique

2  Cours Magistral 7

2.1  Optimisation de chaînes de multiplications de matrices

2.1.1  Par exemple : avec <span class="MathJax" id="MathJax-Element-27-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>1</mn></msub></math>" role="presentation"><nobr aria-hidden="true">𝐴1</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>1</mn></msub></math> de taille <span class="MathJax" id="MathJax-Element-28-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>10</mn><mo>,</mo><mn>100</mn><mo stretchy="false">)</mo></math>" role="presentation"><nobr aria-hidden="true">(10,100)</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>10</mn><mo>,</mo><mn>100</mn><mo stretchy="false">)</mo></math>, <span class="MathJax" id="MathJax-Element-29-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>2</mn></msub></math>" role="presentation"><nobr aria-hidden="true">𝐴2</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>2</mn></msub></math> de taille <span class="MathJax" id="MathJax-Element-30-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>100</mn><mo>,</mo><mn>5</mn><mo stretchy="false">)</mo></math>" role="presentation"><nobr aria-hidden="true">(100,5)</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>100</mn><mo>,</mo><mn>5</mn><mo stretchy="false">)</mo></math> et <span class="MathJax" id="MathJax-Element-31-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>3</mn></msub></math>" role="presentation"><nobr aria-hidden="true">𝐴3</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>3</mn></msub></math> de taille <span class="MathJax" id="MathJax-Element-32-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>5</mn><mo>,</mo><mn>50</mn><mo stretchy="false">)</mo></math>" role="presentation"><nobr aria-hidden="true">(5,50)</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>5</mn><mo>,</mo><mn>50</mn><mo stretchy="false">)</mo></math>.

2.1.2  Par exemple : avec <span class="MathJax" id="MathJax-Element-33-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>1</mn></msub></math>" role="presentation"><nobr aria-hidden="true">𝐴1</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>1</mn></msub></math> de taille <span class="MathJax" id="MathJax-Element-34-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>10</mn><mo>,</mo><mn>100</mn><mo stretchy="false">)</mo></math>" role="presentation"><nobr aria-hidden="true">(10,100)</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>10</mn><mo>,</mo><mn>100</mn><mo stretchy="false">)</mo></math>, <span class="MathJax" id="MathJax-Element-35-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>2</mn></msub></math>" role="presentation"><nobr aria-hidden="true">𝐴2</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>2</mn></msub></math> de taille <span class="MathJax" id="MathJax-Element-36-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>100</mn><mo>,</mo><mn>5</mn><mo stretchy="false">)</mo></math>" role="presentation"><nobr aria-hidden="true">(100,5)</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>100</mn><mo>,</mo><mn>5</mn><mo stretchy="false">)</mo></math> et <span class="MathJax" id="MathJax-Element-37-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>3</mn></msub></math>" role="presentation"><nobr aria-hidden="true">𝐴3</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>3</mn></msub></math> de taille <span class="MathJax" id="MathJax-Element-38-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>5</mn><mo>,</mo><mn>50</mn><mo stretchy="false">)</mo></math>" role="presentation"><nobr aria-hidden="true">(5,50)</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><mo stretchy="false">(</mo><mn>5</mn><mo>,</mo><mn>50</mn><mo stretchy="false">)</mo></math> et <span class="MathJax" id="MathJax-Element-39-Frame" tabindex="0" style="position: relative;" data-mathml="<math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>4</mn></msub></math>" role="presentation"><nobr aria-hidden="true">𝐴4</nobr><math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>A</mi><mn>4</mn></msub></math> de taille $(50, 200).

2.2  Plus longue sous séquence commune

2.3  Plus longue sous séquence croissante

2.4  Algorithme de Bellman-Ford

2.5  Algorithme de Floyd-Warshall

2.6  Résolution du problème du sac à dos par programmation dynamique

2.7  Mémoïzation générique

2.8  Conclusion

# [ALGO1 : Introduction à l'algorithmique](https://perso.crans.org/besson/teach/info1_algo1_2019/)

- [Page du cours](https://perso.crans.org/besson/teach/info1_algo1_2019/) : https://perso.crans.org/besson/teach/info1_algo1_2019/
- Magistère d'Informatique de Rennes - ENS Rennes - Année 2019/2020
- Intervenants :
  + Cours : [Lilian Besson](https://perso.crans.org/besson/)
  + Travaux dirigés : [Raphaël Truffet](http://perso.eleves.ens-rennes.fr/people/Raphael.Truffet/)
- Références :
  + [Open Data Structures](http://opendatastructures.org/ods-python.pdf)

# Cours Magistral 7

- Ce cours traite des algorithmes par programmation dynamique.

## Optimisation de chaînes de multiplications de matrices

```python
# https://github.com/jilljenn/tryalgo/blob/master/tryalgo/matrix_chain_mult.py
def matrix_mult_opt_order(M):
    """Matrix chain multiplication optimal order

    :param M: list of matrices
    :returns: matrices opt, arg, such that opt[i][j] is the optimal number of
              operations to compute M[i] * ... * M[j] when done in the order
              (M[i] * ... * M[k]) * (M[k + 1] * ... * M[j]) for k = arg[i][j]
    :complexity: :math:`O(n^2)`
    """
    n = len(M)
    r = [len(Mi) for Mi in M]
    c = [len(Mi[0]) for Mi in M]
    opt = [[0 for j in range(n)] for i in range(n)]
    arg = [[None for j in range(n)] for i in range(n)]
    for j_i in range(1, n):   # loop on i, j of increasing j - i = j_i
        for i in range(n - j_i):
            j = i + j_i
            opt[i][j] = float('inf')
            for k in range(i, j):
                alt = opt[i][k] + opt[k + 1][j] + r[i] * c[k] * c[j]
                if opt[i][j] > alt:
                    opt[i][j] = alt
                    arg[i][j] = k
    return opt, arg
```

```python
def matrix_chain_mult(M):
    """Matrix chain multiplication

    :param M: list of matrices
    :returns: M[0] * ... * M[-1], computed in time optimal order
    :complexity: whatever is needed by the multiplications
    """
    opt, arg = matrix_mult_opt_order(M)
    return apply_order(M, arg, 0, len(M)-1)
```

```python
def apply_order(M, arg, i, j):
    # --- multiply matrices from M[i] to M[j] included
    if i == j:
        return M[i]
    else:
        k = arg[i][j]        # --- follow placement of parentheses
        A = apply_order(M, arg, i, k)
        B = apply_order(M, arg, k + 1, j)
        row_A = range(len(A))
        row_B = range(len(B))
        col_B = range(len(B[0]))
        return [[sum(A[a][b] * B[b][c] for b in row_B)
                 for c in col_B] for a in row_A]
```

Et juste pour avoir une comparaison honnête avec les multiplications de Numpy, on écrit les mêmes fonctions mais utilisant `A @ B` (`numpy.dot`) pour multiplier les matrices :

```python
def apply_order_numpy(M, arg, i, j):
    # --- multiply matrices from M[i] to M[j] included
    if i == j:
        return M[i]
    else:
        k = arg[i][j]        # --- follow placement of parentheses
        A = apply_order_numpy(M, arg, i, k)
        B = apply_order_numpy(M, arg, k + 1, j)
        return A @ B
```

```python
def matrix_chain_mult_numpy(M):
    """Matrix chain multiplication

    :param M: list of matrices
    :returns: M[0] * ... * M[-1], computed in time optimal order
    :complexity: whatever is needed by the multiplications
    """
    opt, arg = matrix_mult_opt_order(M)
    return apply_order_numpy(M, arg, 0, len(M)-1)
```

### Par exemple : avec $A_1$ de taille $(10,100)$, $A_2$ de taille $(100,5)$ et $A_3$ de taille $(5,50)$.

```python
import numpy as np
```

```python
A1 = np.random.randint(-1000, 1000, size=(10, 100))
A2 = np.random.randint(-1000, 1000, size=(100, 5))
A3 = np.random.randint(-1000, 1000, size=(5, 50))
```

```python
np.shape(A1)
np.shape(A2)
np.shape(A3)
```

    (10, 100)

    (100, 5)

    (5, 50)

```python
prod0 = A1 @ A2 @ A3  # let Python decide the order !

prod1 = (A1 @ A2) @ A3
assert np.alltrue(prod0 == prod1)

prod2 = A1 @ (A2 @ A3)
assert np.alltrue(prod0 == prod2)

assert np.alltrue(prod1 == prod2)
```

```python
np.shape(prod1)
np.shape(prod2)
```

    (10, 50)

    (10, 50)

```python
%timeit A1 @ A2 @ A3
%timeit (A1 @ A2) @ A3
%timeit A1 @ (A2 @ A3)
```

    15.5 µs ± 841 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
    15.5 µs ± 1.93 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
    118 µs ± 4.73 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

Python utilise l'associativité à gauche, par défaut, comme le montre les calculs ci dessus : `A1 @ A2 @ A3` est calculé comme `(A1 @ A2) @ A3`.
On peut le vérifier avec un autre exemple :

```python
10 - 3 - 2
(10 - 3) - 2  # x - y - z = (x - y) - z oui
10 - (3 - 2)  # x - y - z != x - (y - z) non
```

    5

    5

    9

```python
M = [A1, A2, A3]
%timeit matrix_chain_mult(M)
%timeit matrix_chain_mult_numpy(M)
```

    12.1 ms ± 371 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    38.3 µs ± 1.16 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

```python
print("Finding the optimal order to multiply chain of matrices of dimensions")
print([np.shape(Mi) for Mi in M])
opt, arg = matrix_mult_opt_order(M)
print("opt =", opt)
print("arg =", arg)
```

    Finding the optimal order to multiply chain of matrices of dimensions
    [(10, 100), (100, 5), (5, 50)]
    opt = [[0, 5000, 7500], [0, 0, 25000], [0, 0, 0]]
    arg = [[None, 0, 1], [None, None, 1], [None, None, None]]

Ce vecteur `arg` nous dit qu'il faut d'abord multiplier `M[0]` avec `M[1]` (soit `A1 @ A2`), puis multiplier le résultat avec `M[2]` (soit `(A1 @ A2) @ A3` comme trouvé ci dessus).

### Par exemple : avec $A_1$ de taille $(10,100)$, $A_2$ de taille $(100,5)$ et $A_3$ de taille $(5,50)$ et $A_4$ de taille $(50, 200).

```python
A1 = np.random.randint(-1000, 1000, size=(10, 100))
A2 = np.random.randint(-1000, 1000, size=(100, 5))
A3 = np.random.randint(-1000, 1000, size=(5, 50))
A4 = np.random.randint(-1000, 1000, size=(50, 200))
```

```python
prod0 = A1 @ A2 @ A3 @ A4  # let Python decide the order !

prod1 = ((A1 @ A2) @ A3) @ A4
assert np.alltrue(prod0 == prod1)

prod2 = (A1 @ (A2 @ A3)) @ A4
assert np.alltrue(prod0 == prod2)

prod3 = (A1 @ A2) @ (A3 @ A4)
assert np.alltrue(prod0 == prod3)

prod4 = A1 @ ((A2 @ A3) @ A4)
assert np.alltrue(prod0 == prod4)

prod5 = A1 @ (A2 @ (A3 @ A4))
assert np.alltrue(prod0 == prod5)
```

```python
np.shape(prod1)
np.shape(prod2)
np.shape(prod3)
np.shape(prod4)
np.shape(prod5)
```

    (10, 200)

    (10, 200)

    (10, 200)

    (10, 200)

    (10, 200)

```python
%timeit A1 @ A2 @ A3 @ A4

%timeit ((A1 @ A2) @ A3) @ A4
%timeit (A1 @ (A2 @ A3)) @ A4
%timeit (A1 @ A2) @ (A3 @ A4)
%timeit A1 @ ((A2 @ A3) @ A4)
%timeit A1 @ (A2 @ (A3 @ A4))
```

    201 µs ± 8.84 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
    196 µs ± 2.94 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
    319 µs ± 5.78 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
    139 µs ± 5.27 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
    2.2 ms ± 241 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
    673 µs ± 22.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

```python
M = [A1, A2, A3, A4]
%timeit matrix_chain_mult(M)
%timeit matrix_chain_mult_numpy(M)
```

    80.4 ms ± 9.9 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
    141 µs ± 12.1 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

```python
print("Finding the optimal order to multiply chain of matrices of dimensions")
print([np.shape(Mi) for Mi in M])
opt, arg = matrix_mult_opt_order(M)
print("opt =", opt)
print("arg =", arg)
```

    Finding the optimal order to multiply chain of matrices of dimensions
    [(10, 100), (100, 5), (5, 50), (50, 200)]
    opt = [[0, 5000, 7500, 65000], [0, 0, 25000, 150000], [0, 0, 0, 50000], [0, 0, 0, 0]]
    arg = [[None, 0, 1, 1], [None, None, 1, 1], [None, None, None, 2], [None, None, None, None]]

---
## Plus longue sous séquence commune

Je ne rentre pas dans les détails, allez voir [sur Internet](https://fr.wikipedia.org/wiki/Plus_longue_sous-s%C3%A9quence_commune), ou notamment des développements préparés par des élèves de l'ENS de Rennes candidat-e-s à l'agrégation de mathématiques option informatique : [ici](https://minerve.ens-rennes.fr/images/Dvt_plsc.pdf) ou [ici](https://minerve.ens-rennes.fr/index.php/Plus_longue_sous-s%C3%A9quence_commune).

```python
def longest_common_subsequence(x, y):
    """longest common subsequence

    Dynamic programming

    :param x:
    :param y: x, y are lists or strings
    :returns: longest common subsequence in form of a string
    :complexity: `O(|x|*|y|)`
    """
    n = len(x)
    m = len(y)
    #                      -- compute optimal length
    A = [[0 for j in range(m + 1)] for i in range(n + 1)]
    for i in range(n):
        for j in range(m):
            if x[i] == y[j]:
                A[i + 1][j + 1] = A[i][j] + 1
            else:
                A[i + 1][j + 1] = max(A[i][j + 1],  A[i + 1][j])
    #                      -- extract solution
    sol = []
    i, j = n, m
    while A[i][j] > 0:
        if A[i][j] == A[i - 1][j]:
            i -= 1
        elif A[i][j] == A[i][j - 1]:
            j -= 1
        else:
            i -= 1
            j -= 1
            sol.append(x[i])
    return ''.join(sol[::-1])    # inverse solution
```

Exemples :

```python
longest_common_subsequence("BABAR", "ACAB")
```

    'AB'

```python
longest_common_subsequence("J'aime bien les oiseaux !", "Les oiseaux sont des animaux à protéger.")
```

    'e ien es iaux '

```python
longest_common_subsequence("Je suis amoureux de la vie", "Tu n'es pas amoureux d'elle ?")
```

    'e s amoureux del '

---
## Plus longue sous séquence croissante

On traite le sous-problème suivant : $L(k)$ est la longueur de la plus longue sous séquence croissante de $[a_1,\dots,a_k]$.

$$L(1) = 1$$
$$\forall k > 0, L(k+1) = \max(L(k), \max(L(i) + 1 : a[sol[i][-1]] <= a[k+1], i <= k))$$

C'est donc très facile d'écrire la solution :

```python
def longest_increasing_sequence(a):
    n = len(a)
    L = [0] * n
    sol = [[i] for i in range(n)]
    # L[i] = length of longer increasing sequence of a[0]..a[i]
    L[0] = 1
    sol[0] = [0]  # just <a[0]> is increasing for a[0]..a[0]
    for j in range(1, n):
        # L[j] = max(L[j-1], max(L[i]+1 for i < j such that a[sol[i][-1]] <= a[j]))
        ell = 1
        for i in range(j):
            if a[sol[i][-1]] <= a[j] and L[i] + 1 > ell:
                ell = L[i] + 1
                sol[j] = sol[i] + [j]  # sol of a[0]..a[i] + a[j]
        if ell < L[j-1]:
            ell = L[j-1]
            sol[j] = sol[i][::]
        L[j] = ell
    return [a[i] for i in sol[n-1]], sol[n-1], max(L)
```

Avec l'exemple utilisé en cours :

```python
sequence = [19, 1, 9, 27, 26]
```

```python
for i in range(1, len(sequence) + 1):
    print("Pour la séquence", sequence[:i], "la plus longue sous séquence croissante est", longest_increasing_sequence(sequence[:i]))
```

    Pour la séquence [19] la plus longue sous séquence croissante est ([19], [0], 1)
    Pour la séquence [19, 1] la plus longue sous séquence croissante est ([1], [1], 1)
    Pour la séquence [19, 1, 9] la plus longue sous séquence croissante est ([1, 9], [1, 2], 2)
    Pour la séquence [19, 1, 9, 27] la plus longue sous séquence croissante est ([1, 9, 27], [1, 2, 3], 3)
    Pour la séquence [19, 1, 9, 27, 26] la plus longue sous séquence croissante est ([1, 9, 26], [1, 2, 4], 3)

---
## Algorithme de Bellman-Ford

**Chemin de $k$ sommets** : on considère le sous-problème $P(k)$ suivant : chercher la longueur des plus courts chemins comportant au plus $k$ sommets ($k \in \{1,\dots,n\}$).

On fixe l'origine des chemins, $s$ ; on note $\delta(k, t)$ la longueur minimale d'un chemin d'au plus $k$ arcs de $s$ à $t$. On a la relation de récurrence suivante :

$$\delta(0, t) = \begin{cases}
    0 & \textrm{ si } s=t \\
    \infty & \textrm { sinon.}
     \end{cases}$$
$$\delta(k + 1, t) = \min(\delta(k, t), \min \{\delta(k, u) + w(u,t), u\in S \}) $$

Cela donne l'algorithme suivant, qui fixe $s\in S$ le sommet de départ.
L'algorithme de Bellman-Ford s'exécute en mémoire $\mathcal{O}(|S|)$ et en temps $\mathcal{O}(|S| * |A|)$.

Donc avec tous les sommets $s\in S$ différents, cela donne $\mathcal{O}(|S|^2 |A|) \leq \mathcal{O}(|S|^4)$.

```python
def bellman_ford(graph, weight, source=0):
    """ Single source shortest paths by Bellman-Ford

    :param graph: directed graph in listlist or listdict format
    :param weight: can be negative.
                   in matrix format or same listdict graph
    :returns: distance table, precedence table, bool
    :explanation: bool is True if a negative circuit is
                  reachable from the source, circuits
                  can have length 2.
    :complexity: `O(|S|*|A|)`
    """
    n = len(graph)
    dist = [float('inf')] * n
    prec = [None] * n
    dist[source] = 0
    # this loop is in O(|S|) nb of vertex
    for nb_iterations in range(n):
        changed = False
        # these two loops are in O(\sum_u degree(u)) = O(|A|) nb of edges
        for node in range(n):
            for neighbor in graph[node]:
                alt = dist[node] + weight[node][neighbor]
                if alt < dist[neighbor]:
                    dist[neighbor] = alt
                    prec[neighbor] = node
                    changed = True
        if not changed:                   # fixed point
            return dist, prec, False
    return dist, prec, True
```

Un exemple de graphe, comme celui utilisé [dans la page Wikipédia](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm#Example) :

```python
oo = float('+inf')

weight = [
    [0, oo, -2, oo],
    [4, 0, 3, oo],
    [oo, oo, 0, 2],
    [oo, -1, oo, 0],
]
```

```python
n = len(weight)
graph = [
    [ v for v in range(n) if weight[u][v] < oo ]
    for u in range(n)
]
```

```python
bellman_ford(graph, weight)
```

    ([0, -1, -2, 0], [None, 3, 0, 2], False)

---
## Algorithme de Floyd-Warshall

**Chemin empruntant les sommets $\{1, \ldots, k\}$**
On suppose que $S=\{1, \ldots, n\}$ (quitte à ré-étiqueter les sommets), on s'intéresse au sous-problème $P(k)$ suivant~: chercher la longueur des plus courts chemins dont les sommets intermédiaires sont dans $\{1, \ldots, k\}$.

On note maintenant $\delta(k, s, t)$ la longueur minimale d'un chemin de $s$ à
$t$ dont les sommets intermédiaires sont dans $\{1, \ldots, k\}$.

On a la relation de récurrence $\delta(k, s, t) = \min(\delta(k-1, s, t), \delta(k-1, s, k) + \delta(k-1, k, t))$
et le cas simple
$$\delta(0, s, t) =
  \begin{cases}
    0  \textrm{ si } s=t \\
    w(s,t) & \textrm { sinon.}
  \end{cases}$$

Cela donne l'algorithme suivant.
L'algorithme de Floyd-Warshall s'exécute en mémoire $\mathcal{O}(|S|^2)$ et en temps $\mathcal{O}(|S|^3)$.

> Cherchez par vous même le lien entre produit de matrice (dans le sous-anneau "tropical" $(\mathbb{R}, \min, +)$) et l'algorithme de Floyd-Warshall.

```python
from copy import deepcopy
```

```python
def floyd_warshall(weight):
    """All pairs shortest paths by Floyd-Warshall

    :param weight: edge weight matrix
    :returns: weight, and True if there are negative cycles
    :complexity: :math:`O(|V|^3)`
    """
    weight = deepcopy(weight)
    V = range(len(weight))
    for k in V:  # considering paths using 0..k
        # to go from u to v
        for u in V:
            for v in V:
                weight[u][v] = min(weight[u][v],
                                   weight[u][k] + weight[k][v])
    for v in V:
        if weight[v][v] < 0:      # negative cycle found
            return weight, True
    return weight, False
```

Un exemple de graphe, comme celui utilisé [dans la page Wikipédia](https://en.wikipedia.org/wiki/Floyd%E2%80%93Warshall_algorithm#Example) :

```python
oo = float('+inf')

weight = [
    [0, oo, -2, oo],
    [4, 0, 3, oo],
    [oo, oo, 0, 2],
    [oo, -1, oo, 0],
]
```

```python
floyd_warshall(weight)
```

    ([[0, -1, -2, 0], [4, 0, 2, 4], [5, 1, 0, 2], [3, -1, 1, 0]], False)

---
## Résolution du problème du sac à dos par programmation dynamique

```python
def knapsack(p, v, cmax):
    """Knapsack problem: select maximum value set of items if total size not
    more than capacity

    :param p: table with size of items
    :param v: table with value of items
    :param cmax: capacity of bag
    :requires: number of items non-zero
    :returns: value optimal solution, list of item indexes in solution
    :complexity: O(n * cmax), for n = number of items
    """
    n = len(p)
    opt = [[0] * (cmax + 1) for _ in range(n + 1)]
    sel = [[False] * (cmax + 1) for _ in range(n + 1)]
    #                               --- basic case
    for cap in range(p[0], cmax + 1):
        opt[0][cap] = v[0]
        sel[0][cap] = True
    #                               --- induction case
    for i in range(1, n):
        for cap in range(cmax + 1):
            if cap >= p[i] and opt[i-1][cap - p[i]] + v[i] > opt[i-1][cap]:
                opt[i][cap] = opt[i-1][cap - p[i]] + v[i]
                sel[i][cap] = True
            else:
                opt[i][cap] = opt[i-1][cap]
                sel[i][cap] = False
    #                               --- reading solution
    cap = cmax
    solution = []
    for i in range(n-1, -1, -1):
        if sel[i][cap]:
            solution.append(i)
            cap -= p[i]
    return (opt[n - 1][cmax], solution)
```

On peut considérer l'exemple de la page Wikipédia :

For example, there are 10 different items and the weight limit is 67.
And with : $$\begin{align}
&w[  1]= 23 ,w[  2]= 26,w[  3]= 20,w[  4]= 18,w[  5]= 32,w[  6]= 27,w[  7]= 29,w[  8]= 26,w[  9]= 30,w[ 10]= 27 \\
&v[  1]=505 ,v[  2]=352,v[  3]=458,v[  4]=220,v[  5]=354,v[  6]=414,v[  7]=498,v[  8]=545,v[  9]=473,v[ 10]=543 \\
\end{align}$$

```python
p = [23, 26, 20, 18, 32, 27, 29, 26, 30, 27]  # param p, table with size of items
v = [505, 352, 458, 220, 354, 414, 498, 545, 473, 543]  # param v, table with value of items
cmax = 67  # param cmax, capacity of bag
```

```python
knapsack(p, v, cmax)
```

    (1270, [7, 3, 0])

$$\begin{align}
&m(10, 67) = 1270\\
&m(9, 67) = 1270, m(9, 40) = 678\\
&m(8, 67) = 1270, m(8, 40) = 678, m(8, 37) = 545\\
&m(7, 67) = 1183, m(7, 41) = 725, m(7, 40) = 678, m(7, 37) = 505\\
&m(6, 67) = 1183, m(6, 41) = 725, m(6, 40) = 678, m(6, 38) = 678, m(6, 37) = 505\\
&m(5, 67) = 1183, m(5, 41) = 725, m(5, 40) = 678, m(5, 38) = 678, m(5, 37) = 505\\
&m(4, 67) = 1183, m(4, 41) = 725, m(4, 40) = 678, m(4, 38) = 678, m(4, 37) = 505, m(4, 35) = 505\\
&m(3, 67) = 963, m(3, 49) = 963, m(3, 41) = 505, m(3, 40) = 505, m(3, 38) = 505, m(3, 37) = 505, m(3, 35) = 505, m(3, 23) = 505, m(3, 22) = 458, m(3, 20) = 458\\
&m(2, 67) = 857, m(2, 49) = 857, m(2, 47) = 505, m(2, 41) = 505, m(2, 40) = 505, m(2, 38) = 505, m(2, 37) = 505, m(2, 35) = 505, m(2, 29) = 505, m(2, 23) = 505\\
&m(1, 67) = 505, m(1, 49) = 505, m(1, 47) = 505, m(1, 41) = 505, m(1, 40) = 505, m(1, 38) = 505, m(1, 37) = 505, m(1, 35) = 505, m(1, 29) = 505, m(1, 23) = 505\\
\end{align}$$

---
## Mémoïzation générique

```python
from time import sleep
```

```python
def f1(n):
    sleep(3)
    return n + 3
```

```python
print("3 secondes...")
print(f1(10))  # 13, 3 secondes après
```

    3 secondes...
    13

```python
def f2(n):
    sleep(4)
    return n * n
```

```python
print("4 secondes...")
print(f2(10))  # 100, 4 secondes après
```

    4 secondes...
    100

Mémoïsation générique, non typée

```python
def memo(f):
    memoire = {}  # dictionnaire vide, {} ou dict()
    def memo_f(n):
        if n not in memoire:
             memoire[n] = f(n)
        return memoire[n]
    return memo_f
```

```python
memo_f1 = memo(f1)

print("3 secondes...")
print(memo_f1(10))  # 13, 3 secondes après
print("0 secondes !")
print(memo_f1(10))  # instantanné !
```

    3 secondes...
    13
    0 secondes !
    13

```python
# différent de ces deux lignes !

print("3 secondes...")
print(memo(f1)(10))
print("3 secondes...")
print(memo(f1)(10))  # 3 secondes aussi !
```

    3 secondes...
    13
    3 secondes...
    13

```python
memo_f2 = memo(f2)

print("4 secondes...")
print(memo_f2(10))  # 100, 4 secondes après
print("0 secondes !")
print(memo_f2(10))  # instantanné !
```

    4 secondes...
    100
    0 secondes !
    100

```python
# bonus : on peut utiliser la syntaxe d'un décorateur en Python :

def fibo(n):
    if n <= 1: return 1
    else: return fibo(n-1) + fibo(n-2)

print("Test de fibo() non mémoisée :")
for n in range(10):
    print("F_{} = {}".format(n, fibo(n)))
```

    Test de fibo() non mémoisée :
    F_0 = 1
    F_1 = 1
    F_2 = 2
    F_3 = 3
    F_4 = 5
    F_5 = 8
    F_6 = 13
    F_7 = 21
    F_8 = 34
    F_9 = 55

```python
# version plus rapide !
@memo
def fibo(n):
    if n <= 1: return 1
    else: return fibo(n-1) + fibo(n-2)

print("Test de fibo() mémoisée (plus rapide) :")
for n in range(10):
    print("F_{} = {}".format(n, fibo(n)))
```

    Test de fibo() mémoisée (plus rapide) :
    F_0 = 1
    F_1 = 1
    F_2 = 2
    F_3 = 3
    F_4 = 5
    F_5 = 8
    F_6 = 13
    F_7 = 21
    F_8 = 34
    F_9 = 55

```python
@memo
def factorielle(n):
    if n <= 0: return 0
    elif n == 1: return 1
    else: return n * factorielle(n-1)

print("Test de factorielle() mémoisée :")
for n in range(10):
    print("{}! = {}".format(n, factorielle(n)))
```

    Test de factorielle() mémoisée :
    0! = 0
    1! = 1
    2! = 2
    3! = 6
    4! = 24
    5! = 120
    6! = 720
    7! = 5040
    8! = 40320
    9! = 362880

## Conclusion

C'est bon pour aujourd'hui !