# Tutorial 1: EasyVVUQ

The Role of Uncertainty in Mathematical Modelling of Pandemics - Session 2: Wider Applications of VVUQ Methods - 9th February 2022

* Forward uncertainty propagation: computing the output distribution of a computational model, given *assumed* probability density functions for the input parameters of the model (see image below).

* [EasyVVUQ](https://github.com/UCL-CCS/EasyVVUQ) is VECMA's forward uncertainty propagation toolkit.

Author: Wouter Edeling, CWI Scientific Computing Group (Wouter.Edeling@CWI.nl)

## Installation

* EasyVVUQ: via git clone or by eceuting the command below

You will also need seaborn installed for some of the plots. Uncomment the install line below if need be.

```python
!pip install easyvvuq
#!pip install seaborn
```

    Defaulting to user installation because normal site-packages is not writeable
    Requirement already satisfied: easyvvuq in /home/wouter/.local/lib/python3.8/site-packages/easyvvuq-0.7.3.1+75.g8a46b49.dirty-py3.8.egg (0.7.3.1+75.g8a46b49.dirty)
    Requirement already satisfied: numpy in /home/wouter/.local/lib/python3.8/site-packages (from easyvvuq) (1.19.0)
    Requirement already satisfied: pandas in /home/wouter/.local/lib/python3.8/site-packages (from easyvvuq) (1.2.1)
    Requirement already satisfied: scipy in /home/wouter/.local/lib/python3.8/site-packages (from easyvvuq) (1.5.0)
    Requirement already satisfied: wheel in /home/wouter/.local/lib/python3.8/site-packages (from easyvvuq) (0.35.1)
    Requirement already satisfied: chaospy==3.3.6 in /home/wouter/.local/lib/python3.8/site-packages (from easyvvuq) (3.3.6)
    Requirement already satisfied: SALib in /home/wouter/.local/lib/python3.8/site-packages (from easyvvuq) (1.3.12)
    Requirement already satisfied: pytest in /home/wouter/.local/lib/python3.8/site-packages (from easyvvuq) (6.2.2)
    Requirement already satisfied: pytest-pep8 in /usr/local/lib/python3.8/dist-packages/pytest_pep8-1.0.6-py3.8.egg (from easyvvuq) (1.0.6)
    Requirement already satisfied: SQLAlchemy in /usr/lib/python3/dist-packages (from easyvvuq) (1.3.12)
    Requirement already satisfied: jsonpickle in /home/wouter/.local/lib/python3.8/site-packages (from easyvvuq) (1.5.1)
    Requirement already satisfied: cerberus in /home/wouter/.local/lib/python3.8/site-packages (from easyvvuq) (1.3.2)
    Requirement already satisfied: dask[complete] in /home/wouter/.local/lib/python3.8/site-packages (from easyvvuq) (2021.1.1)
    Requirement already satisfied: dask_jobqueue in /usr/local/lib/python3.8/dist-packages/dask_jobqueue-0.7.2-py3.8.egg (from easyvvuq) (0.7.2)
    Requirement already satisfied: cloudpickle in /home/wouter/.local/lib/python3.8/site-packages/cloudpickle-1.6.0-py3.8.egg (from easyvvuq) (1.6.0)
    Requirement already satisfied: scikit-learn in /usr/local/lib/python3.8/dist-packages/scikit_learn-0.23.2-py3.8-linux-x86_64.egg (from easyvvuq) (0.23.2)
    Requirement already satisfied: jinja2 in /usr/local/lib/python3.8/dist-packages/Jinja2-3.0.0a1-py3.8.egg (from easyvvuq) (3.0.0a1)
    Requirement already satisfied: kubernetes in /usr/local/lib/python3.8/dist-packages/kubernetes-17.14.0a1-py3.8.egg (from easyvvuq) (17.14.0a1)
    Requirement already satisfied: autopep8 in /home/wouter/.local/lib/python3.8/site-packages (from easyvvuq) (1.5.4)
    Requirement already satisfied: numpoly==1.0.5 in /home/wouter/.local/lib/python3.8/site-packages (from chaospy==3.3.6->easyvvuq) (1.0.5)
    Requirement already satisfied: six in /usr/lib/python3/dist-packages (from numpoly==1.0.5->chaospy==3.3.6->easyvvuq) (1.14.0)
    Requirement already satisfied: pycodestyle>=2.6.0 in /home/wouter/.local/lib/python3.8/site-packages (from autopep8->easyvvuq) (2.6.0)
    Requirement already satisfied: toml in /home/wouter/.local/lib/python3.8/site-packages (from autopep8->easyvvuq) (0.10.1)
    Requirement already satisfied: setuptools in /home/wouter/.local/lib/python3.8/site-packages (from cerberus->easyvvuq) (50.3.0)
    Requirement already satisfied: distributed>=2.19 in /home/wouter/.local/lib/python3.8/site-packages/distributed-2021.1.1-py3.8.egg (from dask_jobqueue->easyvvuq) (2021.1.1)
    Requirement already satisfied: pyyaml in /usr/lib/python3/dist-packages (from dask[complete]->easyvvuq) (5.3.1)
    Requirement already satisfied: click>=6.6 in /usr/lib/python3/dist-packages (from distributed>=2.19->dask_jobqueue->easyvvuq) (7.0)
    Requirement already satisfied: msgpack>=0.6.0 in /home/wouter/.local/lib/python3.8/site-packages (from distributed>=2.19->dask_jobqueue->easyvvuq) (1.0.0)
    Requirement already satisfied: psutil>=5.0 in /home/wouter/.local/lib/python3.8/site-packages/psutil-5.8.0-py3.8-linux-x86_64.egg (from distributed>=2.19->dask_jobqueue->easyvvuq) (5.8.0)
    Requirement already satisfied: sortedcontainers!=2.0.0,!=2.0.1 in /home/wouter/.local/lib/python3.8/site-packages/sortedcontainers-2.3.0-py3.8.egg (from distributed>=2.19->dask_jobqueue->easyvvuq) (2.3.0)
    Requirement already satisfied: tblib>=1.6.0 in /home/wouter/.local/lib/python3.8/site-packages/tblib-1.7.0-py3.8.egg (from distributed>=2.19->dask_jobqueue->easyvvuq) (1.7.0)
    Requirement already satisfied: toolz>=0.8.2 in /home/wouter/.local/lib/python3.8/site-packages/toolz-0.11.1-py3.8.egg (from distributed>=2.19->dask_jobqueue->easyvvuq) (0.11.1)
    Requirement already satisfied: tornado>=6.0.3 in /home/wouter/.local/lib/python3.8/site-packages/tornado-6.1-py3.8-linux-x86_64.egg (from distributed>=2.19->dask_jobqueue->easyvvuq) (6.1)
    Requirement already satisfied: zict>=0.1.3 in /home/wouter/.local/lib/python3.8/site-packages/zict-2.0.0-py3.8.egg (from distributed>=2.19->dask_jobqueue->easyvvuq) (2.0.0)
    Requirement already satisfied: heapdict in /home/wouter/.local/lib/python3.8/site-packages/HeapDict-1.0.1-py3.8.egg (from zict>=0.1.3->distributed>=2.19->dask_jobqueue->easyvvuq) (1.0.1)
    Requirement already satisfied: partd>=0.3.10 in /home/wouter/.local/lib/python3.8/site-packages/partd-1.1.0-py3.8.egg (from dask[complete]->easyvvuq) (1.1.0)
    Requirement already satisfied: fsspec>=0.6.0 in /home/wouter/.local/lib/python3.8/site-packages/fsspec-0.8.5-py3.8.egg (from dask[complete]->easyvvuq) (0.8.5)
    Requirement already satisfied: bokeh!=2.0.0,>=1.0.0 in /home/wouter/.local/lib/python3.8/site-packages/bokeh-2.3.0.dev12-py3.8.egg (from dask[complete]->easyvvuq) (2.3.0.dev12)
    Requirement already satisfied: python-dateutil>=2.1 in /usr/lib/python3/dist-packages (from bokeh!=2.0.0,>=1.0.0->dask[complete]->easyvvuq) (2.7.3)
    Requirement already satisfied: pillow>=7.1.0 in /home/wouter/.local/lib/python3.8/site-packages/Pillow-8.1.0-py3.8-linux-x86_64.egg (from bokeh!=2.0.0,>=1.0.0->dask[complete]->easyvvuq) (8.1.0)
    Requirement already satisfied: packaging>=16.8 in /home/wouter/.local/lib/python3.8/site-packages (from bokeh!=2.0.0,>=1.0.0->dask[complete]->easyvvuq) (20.9)
    Requirement already satisfied: typing_extensions>=3.7.4 in /home/wouter/.local/lib/python3.8/site-packages (from bokeh!=2.0.0,>=1.0.0->dask[complete]->easyvvuq) (3.7.4.3)
    Requirement already satisfied: MarkupSafe>=1.1 in /usr/lib/python3/dist-packages (from jinja2->easyvvuq) (1.1.0)
    Requirement already satisfied: pyparsing>=2.0.2 in /home/wouter/.local/lib/python3.8/site-packages (from packaging>=16.8->bokeh!=2.0.0,>=1.0.0->dask[complete]->easyvvuq) (2.4.7)
    Requirement already satisfied: pytz>=2017.3 in /usr/lib/python3/dist-packages (from pandas->easyvvuq) (2019.3)
    Requirement already satisfied: locket in /home/wouter/.local/lib/python3.8/site-packages/locket-0.2.1-py3.8.egg (from partd>=0.3.10->dask[complete]->easyvvuq) (0.2.1)
    Requirement already satisfied: certifi>=14.05.14 in /home/wouter/.local/lib/python3.8/site-packages (from kubernetes->easyvvuq) (2020.6.20)
    Requirement already satisfied: google-auth>=1.0.1 in /usr/lib/python3/dist-packages (from kubernetes->easyvvuq) (1.5.1)
    Requirement already satisfied: requests in /usr/lib/python3/dist-packages (from kubernetes->easyvvuq) (2.22.0)
    Requirement already satisfied: requests-oauthlib in /home/wouter/.local/lib/python3.8/site-packages/requests_oauthlib-1.3.0-py3.8.egg (from kubernetes->easyvvuq) (1.3.0)
    Requirement already satisfied: urllib3>=1.24.2 in /usr/lib/python3/dist-packages (from kubernetes->easyvvuq) (1.25.8)
    Requirement already satisfied: websocket-client!=0.40.0,!=0.41.*,!=0.42.*,>=0.32.0 in /home/wouter/.local/lib/python3.8/site-packages/websocket_client-0.57.0-py3.8.egg (from kubernetes->easyvvuq) (0.57.0)
    Requirement already satisfied: attrs>=19.2.0 in /home/wouter/.local/lib/python3.8/site-packages (from pytest->easyvvuq) (20.3.0)
    Requirement already satisfied: pluggy<1.0.0a1,>=0.12 in /home/wouter/.local/lib/python3.8/site-packages (from pytest->easyvvuq) (0.13.1)
    Requirement already satisfied: iniconfig in /home/wouter/.local/lib/python3.8/site-packages (from pytest->easyvvuq) (1.1.1)
    Requirement already satisfied: py>=1.8.2 in /home/wouter/.local/lib/python3.8/site-packages (from pytest->easyvvuq) (1.10.0)
    Requirement already satisfied: pep8>=1.3 in /home/wouter/.local/lib/python3.8/site-packages/pep8-1.7.1-py3.8.egg (from pytest-pep8->easyvvuq) (1.7.1)
    Requirement already satisfied: pytest-cache in /home/wouter/.local/lib/python3.8/site-packages/pytest_cache-1.0-py3.8.egg (from pytest-pep8->easyvvuq) (1.0)
    Requirement already satisfied: execnet>=1.1.dev1 in /home/wouter/.local/lib/python3.8/site-packages/execnet-1.8.0-py3.8.egg (from pytest-cache->pytest-pep8->easyvvuq) (1.8.0)
    Requirement already satisfied: apipkg>=1.4 in /home/wouter/.local/lib/python3.8/site-packages/apipkg-1.5-py3.8.egg (from execnet>=1.1.dev1->pytest-cache->pytest-pep8->easyvvuq) (1.5)
    Requirement already satisfied: oauthlib>=3.0.0 in /usr/lib/python3/dist-packages (from requests-oauthlib->kubernetes->easyvvuq) (3.1.0)
    Requirement already satisfied: matplotlib in /home/wouter/.local/lib/python3.8/site-packages (from SALib->easyvvuq) (3.3.2)
    Requirement already satisfied: cycler>=0.10 in /home/wouter/.local/lib/python3.8/site-packages (from matplotlib->SALib->easyvvuq) (0.10.0)
    Requirement already satisfied: kiwisolver>=1.0.1 in /home/wouter/.local/lib/python3.8/site-packages (from matplotlib->SALib->easyvvuq) (1.2.0)
    Requirement already satisfied: joblib>=0.11 in /usr/local/lib/python3.8/dist-packages/joblib-0.16.0-py3.8.egg (from scikit-learn->easyvvuq) (0.16.0)
    Requirement already satisfied: threadpoolctl>=2.0.0 in /usr/local/lib/python3.8/dist-packages/threadpoolctl-2.1.0-py3.8.egg (from scikit-learn->easyvvuq) (2.1.0)
    WARNING: You are using pip version 21.0.1; however, version 22.0.2 is available.
    You should consider upgrading via the '/usr/bin/python3 -m pip install --upgrade pip' command.

## Sparse-grid forward UQ in theory

### Making a interpolation-based surrogate with 1 uncertain input parameter

Let $f^{(1)}(x)$ be a one-dimensional function. We aim to create an interpolation of $f^{(1)}$, denoted by

\begin{align}
 I^{(l)}f^{(1)}(x) = \sum_{i=1}^{m_l} f(x_{i}^{(l)})a^{(l)}_i(x).
 \label{eq:sc1d}
\end{align}

Here, $l$ is the so-called *level* of the interpolant. It is an index which links to a **one-dimensional** set of collocation points $\{x^{l}_i\}$, for $i=1,\cdots, m_l$. We can select these points from different families, typically they'll be the abscissas of some quadrature rule. For instance, a Clenshaw-Curtis (CC) rule can generate different collocation points in $[0,1]$ such that

* Level 1: $x^{(1)}_i \in \{0.5\}$,
* Level 2: $x^{(2)}_i \in \{0.0, 0.5, 1.0\}$,
* Level 3: $x^{(3)}_i \in \{0.0, 0.146, 0.5, 0.854, 1.0\}$.

Note that here, the higher level collocation point sets include all points from previous levels. When this is the case, we say that the quadrature rule is **nested**. This leads to efficient sampling plans in higher dimensions, but nestedness is not a strict requirement for sparse grid interpolation or integration. This does (often) lead to 1D quadrature rules which increase exponentially, such that the number of points is given by

\begin{align}
 m_l =
 \begin{cases}
  2^{l-1} + 1 & l > 1 \\
  1 & l = 1
 \end{cases}
\end{align}

Finally, the $a^{(l)}_i(x)$ are the basis functions used for interpolation. In the case of the **Stochastic Collocation** (SC) method, these are often (although not necessarily) the Lagrange interpolation polynomials, given by

\begin{align}
a^{(l)}_i(x) = \prod_{\substack{1\leq j \leq m_l \\ j \neq i}} \frac{x - x_j}{x_i - x_j}
\end{align}

A property of the Lagrange polynomial associated with the i-th collocation point is that $a^{(l)}_i(x_i) = 1$ at this point, and $a^{(l)}_i(x_j) = 0$ at all other collocation points $x_j$. The interpolation $I^{(l)}f^{(1)}$ will therefore exactly reproduce the code outputs $f^{(1)}(x_i)$ at the collocation points $x_i$.

### Refinement

Let us define the following difference formulas for interpolation in 1D:

\begin{align}
 \Delta^{(l)} f^{(1)} := I^{(l)}f^{(1)} - I^{(l-1)}f^{(1)}\quad\mathrm{where}\quad
 I^{(0)}f^{(1)}:=0.
 \label{eq:Delta1d}
\end{align}

That is, $ \Delta^{(l)} f^{(1)}$ is just the difference between the interpolations at successive levels. These difference formulas are often used in both sparse-grid interpolation (and quadrature), see the figure below.

When interpolating a level $l-1$ interpolant using a level $l$ interpolating we retrieve the former, i.e.

\begin{align}
 I^{(l)}\left(I^{(l-1)}f^{(l)}\right) = I^{(l-1)}f^{(l)}.
\end{align}

Thus, a lower level interpolant can be exactly interpolated by a higher level interpolant (which is true for both nested and non-nested collocation points), see Figure below:

*The blue solid line is the exact 1D function $f^{(1)}$. The red striped line is a (non-nested) level $l$ interpolant. The green striped line is a lower-level $l-1$ counterpart, constructed from function evaluations at  the green star symbols.. If we evaluate the level $l-1$ interpolant at the level $l$ collocation, we obtain the blue squares. Clearly, if these function values are used to construct a level $l$ interpolant of $I^{(l-1)}f^{(1)}$, which would be $I^{(l)}\left(I^{(l-1)}f^{(1)}\right)$, we just retrieve $I^{(l-1)}f^{(1)}$.*

We can therefore write the difference formula as

\begin{align}
 \Delta^{(l)}f^{(1)} = I^{(l)}f^{(1)} - I^{(l-1)}f^{(1)} =  I^{(l)}f^{(1)} - I^{(l)}\left(I^{(l-1)}f^{(1)}\right) = \sum_{i = 1}^{m_l}f(x^{(l)}_i)a^{(l)}_i - \sum_{i=1}^{m_l}I^{(l-1)}f^{(1)}(x^{(l)}_i)a^{(l)}_i = \nonumber\\
 \sum_{i=1}^{m_l}\left[f(x^{(l)}_i) - I^{(l-1)}f^{(1)}(x^{(l)}_i)\right]a^{(l)}_i = \sum_{i=1}^{m_l}w^{(l)}_ia^{(l)}_i.
 \label{eq:diff0}
\end{align}

Here, $w^{(l)}_i:=f(x^{(l)}_i) - I^{(l-1)}f^{(1)}(x^{(l)}_i)$ is the **hierarchical surplus**, defined as the difference between the code output at a collocation point $x^{(l)}_i$ at level $l$, minus the level $l-1$ polynomial approximation of the code output at the same location. This can be thought of as a local measure of the accuracy of the interpolation. Furthermore, we can also write the interpolation $I^{(l)}f^{(1)}(x)$ in terms of the difference formulas, in which case the $a^{l}_i$ basis function will form a hierarchical basis as

\begin{align}
 I^{(L)}f^{(1)} = \sum_{l=1}^L \Delta^{(l)}f^{(1)} = \sum_{l=1}^L\sum_{i=1}^{m_l}w^{(l)}_i a^{(l)}_i
 \label{eq:diff1}
\end{align}

Hence, to obtain a level $L$ interpolant, we can just create a telescopic sum of the difference formulas, which is the first equality above. The second equality is obtained by simply plugging in the formula for $\Delta^{(l)}f^{(1)}$, and it shows that the $a^{(l)}_i$ form a hierarchical basis due to summation over $l$, which increases the number collocation point $m_l$ at every new level. This is also sketched in the Figure below, which assumes linear basis functions for simplicity.

*Top left is a standard linear (finite-element) basis, with below it a corresponding linear interpolant. Top right displays a linear hierarchical basis. Below it we show a series of 3 hierarchical interpolants ($L=1,2$ and $3$) and the hierarchical surplus coefficients $w^{(l)}_i$ used in their construction. Figure recreated from [3]*

**Why should we care?**

The formula above is the same as $I^{(l)}f^{(1)}$ in the beginning, as both are 1D interpolation formulas, only written differently, so why should we complicate things with a hierarchical notation?

One advantage of adopting the hierarchical notation is that it naturally allows for **refinement**, i.e. adding more points to an existing sampling plan, by adding another level. If we select a nested quadrature rule, adding another level means we only have to evaluate the (expensive) code at a relatively small number of *new* points, the other points are quaranteed to be present in one or more of the previous levels, see the CC example at the beginning of this section.

In the case more than one uncertain input, sampling plans are create using **tensor products of 1D collocation points**. Here, **a hierarchical construction leads to a sparse sampling plan**, with much less code evaluations compared to "standard" sampling plans.

## Sparse grid UQ on an HIV model

Here we will look at a HIV model [1], taken from the [Active Subspace data set repository](https://github.com/paulcon/as-data-sets). It models the T-cell count during different stages of the disease, and has 27 input parameters spread out over 7 coupled ODEs.

### HIV model

From [here](https://github.com/paulcon/as-data-sets/blob/master/HIV/HIV.ipynb) we find the following description of the model, see also [4]. The 7 coupled ordinary differential equations are given by:

$$
\begin{align}
\frac{dT}{dt} &= s_1 + \frac{p_1}{C_1+V}TV - \delta_1T - (K_1V + K_2M_I)T,\\
\frac{dT_I}{dt} &= \psi(K_1V + K_2M_I)T + \alpha_1T_L-\delta_2T_I-K_3T_ICTL,\\
\frac{dT_L}{dt} &= (1-\psi)(K_1V+K_2M_I)T-\alpha_1T_L-\delta_3T_L,\\
\frac{dM}{dt} &= s_2+K_4MV-K_5MV-\delta_4M,\\
\frac{dM_I}{dt} &= K_5MV-\delta_5M_I-K_6M_ICTL,\\
\frac{dCTL}{dt} &= s_3 + (K_7T_I+K_8M_I)CTL-\delta_6CTL,\\
\frac{dV}{dt} &= K_9T_I+K_{10}M_I-K_{11}TV-(K_{12}+K_{13})MV-\delta_7V,
\end{align}
$$

where $T(t)$ is the CD4$^+$ T-cell population, $T_I$ is the actively infected T-cell population, $T_L$ represents latently-infected T-cells, $M$ is macrophages, $M_I$ is infected macrophages, $CTL$ is cytotoxic lymphocytes, and $V$ is virions. The model's 27 parameters are summarized in the table below.

Parameter|Nominal Value|Distribution (U(min, max))
:-------:|:-----------:|:-------------:
$s_1$|  10 |  U(9.75, 10.25)
$s_2$|   .15 |  U(.14625, .15375)
$s_3$|   5  | U(4.875, 5.125)
$p_1$|   .2 |  U(.195, .205)
$C_1$|   55.6  | U(54.21, 56.99)
$K_1$|   3.87e-3 |  U(3.77325e-3, 3.96675e-3)
$K_2$|   1e-6  | U(.975e-6, 1.025e-6)
$K_3$|   4.5e-4 |  U(4.3875e-4, 4.6125e-4)
$K_4$|   7.45e-4 |  U(7.26375e-4, 7.63625e-4)
$K_5$|   5.22e-4 |  U(5.0895e-4, 5.3505e-4)
$K_6$|   3e-6  | U(2.925e-6, 3.075e-6)
$K_7$|   3.3e-4  | U(3.2175e-4, 3.3825e-4)
$K_8$|   6e-9  | U(5.85e-9, 6.15e-9)
$K_9$|   .537 |  U(.523575, .550425)
$K_{10}$|   .285 |  U(.277875, .292125)
$K_{11}$|   7.79e-6 |  U(7.59525e-6, 7.98475e-6)
$K_{12}$|   1e-6  | U(.975e-6, 1.025e-6)
$K_{13}$|   4e-5   |U(3.9e-5, 4.1e-5)
$\delta_1$|   .01 |  U(.00975, .01025)
$\delta_2$|   .28  | U(.273, .287)
$\delta_3$|   .05  | U(.04875, .05125)
$\delta_4$|   .005  | U(.004875, .005125)
$\delta_5$|   .005 |  U(.004875, .005125)
$\delta_6$|   .015 |  U(.014625, .015375)
$\delta_7$|   2.39 |  U(2.33025, 2.44975)
$\alpha_1$|   3e-4 |  U(2.925e-4, 3.075e-4)
$\psi$|   .97 |  U(.94575, .99425)

The limits on the uniform distributions are 2.5% above and below the nominal values.

```python
import os
import numpy as np
import easyvvuq as uq
import chaospy as cp
import matplotlib.pyplot as plt
import seaborn as sns
from easyvvuq.actions import CreateRunDirectory, Encode, Decode, ExecuteLocal, Actions
```

### Setting up an EasyVVUQ campaign

We first set up the `params` dictionary, in which we specify the name, type and default value of each input:

```python
# default values
nominal = np.array([10, .15, 5, .2, 55.6, 3.87e-3, 1e-6, 4.5e-4, 7.45e-4, 5.22e-4, 3e-6,\
    3.3e-4, 6e-9, .537, .285, 7.79e-6, 1e-6, 4e-5, .01, .28, .05, .005, .005, .015, 2.39,\
    3e-4, .97])

# parameter names
param_names = ['s_1', 's_2', 's_3', 'p_1', 'C_1', 'K_1', 'K_2', 'K_3', 'K_4', 'K_5', 'K_6',
               'K_7', 'K_8', 'K_9', 'K_10', 'K_11', 'K_12', 'K_13', 'delta_1', 'delta_2', 'delta_3',
               'delta_4', 'delta_5', 'delta_6', 'delta_7', 'alpha_1', 'psi']
# params dict
params = {}
for idx, name in enumerate(param_names):
    params[name] = {'type': 'float', 'default': nominal[idx]}

params
```

    {'s_1': {'type': 'float', 'default': 10.0},
     's_2': {'type': 'float', 'default': 0.15},
     's_3': {'type': 'float', 'default': 5.0},
     'p_1': {'type': 'float', 'default': 0.2},
     'C_1': {'type': 'float', 'default': 55.6},
     'K_1': {'type': 'float', 'default': 0.00387},
     'K_2': {'type': 'float', 'default': 1e-06},
     'K_3': {'type': 'float', 'default': 0.00045},
     'K_4': {'type': 'float', 'default': 0.000745},
     'K_5': {'type': 'float', 'default': 0.000522},
     'K_6': {'type': 'float', 'default': 3e-06},
     'K_7': {'type': 'float', 'default': 0.00033},
     'K_8': {'type': 'float', 'default': 6e-09},
     'K_9': {'type': 'float', 'default': 0.537},
     'K_10': {'type': 'float', 'default': 0.285},
     'K_11': {'type': 'float', 'default': 7.79e-06},
     'K_12': {'type': 'float', 'default': 1e-06},
     'K_13': {'type': 'float', 'default': 4e-05},
     'delta_1': {'type': 'float', 'default': 0.01},
     'delta_2': {'type': 'float', 'default': 0.28},
     'delta_3': {'type': 'float', 'default': 0.05},
     'delta_4': {'type': 'float', 'default': 0.005},
     'delta_5': {'type': 'float', 'default': 0.005},
     'delta_6': {'type': 'float', 'default': 0.015},
     'delta_7': {'type': 'float', 'default': 2.39},
     'alpha_1': {'type': 'float', 'default': 0.0003},
     'psi': {'type': 'float', 'default': 0.97}}

Next we'll set up the **encoder**, which will create the input files for the HIV model using an input template. In this case the input file is just a comma-separated file of values. To create an EasyVVUQ input template each value is replaced by `$param_name`:

`$s_1,$s_2,$s_3,$p_1,$C_1,$K_1,$K_2,$K_3,$K_4,$K_5,$K_6,$K_7,$K_8,$K_9,$K_10,$K_11,$K_12,$K_13,$delta_1,$delta_2,$delta_3,$delta_4,$delta_5,$delta_6,$delta_7,$alpha_1,$psi`

The encoder will swap out the `$param_name` flags for values drawn from the specified input distribution.

```python
# input file encoder
encoder = uq.encoders.GenericEncoder(template_fname='HIV.template', delimiter='$', target_filename='input.csv')
```

The HIV model writes a CSV file containing the predicted T-cell count. The **decoder** will read this file and store its contents within the EasyVVUQ database.

```python
# Quantity of Interest, also the column name of the output CSV file
QOI = 'T_cell_count'
# CSV output file decoder
decoder = uq.decoders.SimpleCSV(target_filename='output.csv', output_columns=[QOI])
```

We'll run the HIV ensemble locally. However, in many cases of practical interest the model will be too expensive for local execution. In this case the VECMA tools [QCG-PilotJob](https://github.com/vecma-project/QCG-PilotJob) or [FabSim3](https://github.com/djgroen/FabSim3) can be used in combination with EasyVVUQ to submit the ensemble to HPC resources.

```python
# local execution of HIV_model.py
execute = ExecuteLocal('{}/HIV_model.py'.format(os.getcwd()))
```

Now we are combine all actions we want to execute into an `Actions` object.

```python
# location where the run directories are stored
WORK_DIR = '/tmp'
# actions to be undertaken: make rundirs, encode input files, execute local model ensemble, decode output files
actions = Actions(CreateRunDirectory(root=WORK_DIR, flatten=True), Encode(encoder), execute,
                  Decode(decoder))
```

The central object in the UQ analysis is a so-called Campaign. This is created as:

```python
campaign = uq.Campaign(name='HIV_SC', params=params, actions=actions)
```

### Input specification

Next we specify a probability density function for each input parameter in the `vary` dict. Each input in the vary dict will be considered as random, and therefore part of the UQ campaign. We could select only a subset of the parameters defined in the `params` dict above. The remaining inputs will then be fixed to their default values ( also defined in the `params` dict). Here we'll just include 2 inputs to visualize the full sampling plan, and specify a uniform input distribution with bounds at $\pm 2.5 \%$ from their default value:

```python
vary = {}
#Lower and upper parameter limits
xl = .975*nominal; xu = 1.025*nominal
included_params = ['K_4', 'delta_7']
for name in included_params:
    # input distributions are specified using Chaospy
    idx = param_names.index(name)
    vary[name] = cp.Uniform(xl[idx], xu[idx])
vary
```

    {'K_4': Uniform(lower=0.000726375, upper=0.000763625),
     'delta_7': Uniform(lower=2.33025, upper=2.44975)}

### Sampler
To generate the sampling plan we select the (sparse) Stochastic Collocation sampler. The `polynomial_order` of the sampler object should be interpreted as the level (defined above) in the case of `sparse=True`. Furthermore;

* `quadrature_rule='C'`: select the Clenshaw Curtis (CC) quadrature rule
* `growth=True`: selects an exponential growth rule, making CC *nested*. This is *not* possible for every quadrature rule, see [Chaospy documentation](https://chaospy.readthedocs.io/en/master/reference/quadrature/index.html).

```python
sampler = uq.sampling.SCSampler(vary=vary, polynomial_order=1, sparse = True, quadrature_rule='C',
                                growth=True, dimension_adaptive=False)
campaign.set_sampler(sampler)
```

Setting `polynomial_order=1` will create a level 1 sparse grid, with just a single point:

```python
def plot_sampling_plan():
    """
    Plot the sampling plan of the (first) two input dimensions
    """
    fig = plt.figure()
    ax = fig.add_subplot(111, xlabel='x_1', ylabel='x_2')
    # the xi_d array contains the N x d sampling points, with N being the number of points and d the number of inputs
    ax.plot(sampler.xi_d[:,0], sampler.xi_d[:, 1], 'ro')
    # print the number of points to screen
    print("Number of sampling points = %d" % sampler.n_samples)
```

```python
plot_sampling_plan()
```

We can refine the grid to the next level via `next_level_sparse_grid`, only evaluating the code at the *new* sampling points. We do this a number of times, plotting the 2D sampling plan each time.

```python
n_adaptations = 6
fig_means = plt.figure()
for i in range(n_adaptations):
    sampler.next_level_sparse_grid()
    plot_sampling_plan()
    # this command executes the actions
    campaign.execute().collate(progress_bar=True)
```

## Dimension-adaptive sparse grids

The sparse grids shown above are **isotropic**, in the sense that both inputs are treated the same. While isotropic sparse grids contains less points compared to the standard SC method, they will not scale very well when we have 27 inputs.

In this case we can opt to apply the dimension-adaptive version of the SC sampler, in order to create an **anisotropic** sparse grid. This process is iterative in nature, usually staring from a single point. Unlike the isotropic case, only a certain (combination of) parameters will get refined.

At each iteration, the code is evaluated at certain (so-called *admissible*) refinements, the `x` symbols in the cartoon below. Refinement means adding another tensor product where the quadrature order of one or more inputs is increased by one. Only one of the admissible refinements will get accepted, creating an anisotropic sampling plan.

To determine which of the admissible refinement gets accepted, an error measure is computed for every refinement. The one with the highest error is accepted. Here we will use *hierarchical surplus*, defined above. For more information see [2,3].

We will create a new campaign, the `params` dict and the `actions` do no need to redefined.

```python
campaign = uq.Campaign(name='HIV_SC_adaptive', params=params, actions=actions)
```

We do redefine `vary`, as we will include all 27 inputs in the uncertainty analysis this time.

```python
vary = {}
#Lower and upper parameter limits
xl = .975*nominal; xu = 1.025*nominal
for i in range(27):
    vary[param_names[i]] = cp.Uniform(xl[i], xu[i])
```

Some changes must be made in the sampler as well, as now we set `dimension_adaptive = True`.

```python
sampler = uq.sampling.SCSampler(vary=vary, polynomial_order=1, quadrature_rule='C', sparse=True,
                                growth=True, dimension_adaptive=True)
campaign.set_sampler(sampler)
```

Run the first ensemble, which consists of just a single sample:

```python
campaign.execute().collate(progress_bar=True)

```

    100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00,  1.61it/s]

To analyse the results (and execute the dimension adaptivity), we need an `SCAnalysis` object:

```python
analysis = uq.analysis.SCAnalysis(sampler=sampler, qoi_cols=[QOI])
# perform analysis (basically estimates moments, Sobol analysis, and updates internal state of analysis)
campaign.apply_analysis(analysis)
```

    /home/wouter/.local/lib/python3.7/site-packages/easyvvuq-1.1.1-py3.7.egg/easyvvuq/analysis/sc_analysis.py:1097: RuntimeWarning: invalid value encountered in true_divide
      S_u[u] = D_u[u] / D

Now we'll refine the grid several times in an anisotropic fashion. Here

* `look_ahead`: determines the new admissible candidate refinements.
* `campaign.get_collation_result()`: get the data frame with all code samples.
* `adapt_dimension`: compute the hierarchical surplus at all candidate refinements, and accept the one with the highest surplus.

```python
number_of_adaptations = 15
for i in range(number_of_adaptations):
    # compute candidate refinements
    sampler.look_ahead(analysis.l_norm)
    # run ensemble (at new locations only)
    campaign.execute().collate(progress_bar=True)
    # get data frame
    data_frame = campaign.get_collation_result()
    # adapt the sampling plan
    analysis.adapt_dimension(QOI, data_frame)
    # we must apply the analysis to update its internal state
    campaign.apply_analysis(analysis)
```

    100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 54/54 [00:06<00:00,  8.72it/s]
    100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00,  3.23it/s]
    100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 6/6 [00:01<00:00,  5.78it/s]
    100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 10/10 [00:01<00:00,  6.65it/s]
    0it [00:00, ?it/s]
    0it [00:00, ?it/s]
    100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:01<00:00,  6.42it/s]
    0it [00:00, ?it/s]
    100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 14/14 [00:02<00:00,  6.87it/s]
    100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 18/18 [00:02<00:00,  7.82it/s]
    0it [00:00, ?it/s]
    100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:01<00:00,  6.20it/s]
    0it [00:00, ?it/s]
    0it [00:00, ?it/s]
    100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 8/8 [00:01<00:00,  6.27it/s]

This command retrieves the result after all refinements:

```python
results = campaign.get_last_analysis()
```

The `adaptation_table` shows a plot of that visualizes which inputs got refined. Again, each refinement is another tensor product of 1D quadrature points, where the quadrature order of at least 1 input is increased. Note that the algorithm will focus on a subset of inputs.

```python
analysis.adaptation_table()
```

Here we will compute confidence intervals:

```python
def get_confidence_intervals(samples, conf=0.9):
    """
    Compute the confidence intervals given an array of samples

    Parameters
    ----------
    samples : array
        Samples on which to compute the intervals.
    conf : float, optional, must be in [0, 1].
        The confidence interval percentage. The default is 0.9.

    Returns
    -------
    lower : array
        The lower confidence bound..
    upper : array
        The upper confidence bound.

    """

    # ake sure conf is in [0, 1]
    if conf < 0.0 or conf > 1.0:
        print('conf must be specified within [0, 1]')
        return

    # lower bound = alpha, upper bound = 1 - alpha
    alpha = 0.5 * (1.0 - conf)

    # arrays for lower and upper bound of the interval
    n_samples = samples.shape[0]
    N_qoi = samples.shape[1]
    lower = np.zeros(N_qoi)
    upper = np.zeros(N_qoi)

    # the probabilities of the ecdf
    prob = np.linspace(0, 1, n_samples)
    # the closest locations in prob that correspond to the interval bounds
    idx0 = np.where(prob <= alpha)[0][-1]
    idx1 = np.where(prob <= 1.0 - alpha)[0][-1]

    # for every location of qoi compute the ecdf-based confidence interval
    for i in range(N_qoi):
        # the sorted surrogate samples at the current location
        samples_sorted = np.sort(samples[:, i])
        # the corresponding confidence interval
        lower[i] = samples_sorted[idx0]
        upper[i] = samples_sorted[idx1]

    return lower, upper
```

```python
from matplotlib import gridspec

x = range(analysis.N_qoi)

code_samples = analysis.get_sample_array(QOI)
n_samples = code_samples.shape[0]

#confidence bounds
lower1, upper1 = get_confidence_intervals(code_samples, conf=0.63)
lower2, upper2 = get_confidence_intervals(code_samples, conf=0.95)

fig = plt.figure(figsize=(10,5))
spec = gridspec.GridSpec(ncols=2, nrows=1,
                          width_ratios=[3, 1])

ax1 = fig.add_subplot(spec[0])
ax2 = fig.add_subplot(spec[1], sharey=ax1)
ax2.get_xaxis().set_ticks([])
fig.subplots_adjust(wspace=0)
plt.setp(ax2.get_yticklabels(), visible=False)

ax1.fill_between(x, lower2, upper2, color='#aa99cc', label='95% CI', alpha=0.5)
ax1.fill_between(x, lower1, upper1, color='#aa99cc', label='68% CI')

mean = results.describe(QOI, 'mean')
ax1.plot(x, mean, label='Mean')

ax1.legend(loc=0, frameon=False)

ax1.set_xlabel('Days')
ax1.set_ylabel('T cell count')
ax2.axis('off')
sns.despine(top=True)
ax2.hist(code_samples[:, -1], orientation='horizontal', color='#aa99cc')

plt.tight_layout()
```

Finally, we will create a plot with all first-order Sobol indices `S_i`, which shows the fraction of the output variance (vs time here), that each input variable is responsible for *by itself*. To also estimate higher-order effects (the fraction of variance obtained by simultaneously varying 2 or more inputs together), we sum all first-order indices. Where this sum is close to one, there is (almost) no higher-order effect.

```python
from itertools import cycle

# color = cycle(['b', 'r', 'g', 'm', 'c', 'k'])
marker = cycle(['o', 'v', '^', '<', '>', 's', '*', 'p', 'd', 'P', 'X',
                '1', '2', '3', '4', 'x', 'D', '|', '_'])
skip = 100
x = range(0, analysis.N_qoi, skip)

fig = plt.figure(figsize=[10, 5])
ax = fig.add_subplot(121, title=r'First-order Sobol indices',
                      xlabel="days", ylim=[0,1])
ax.set_ylabel(r'$S_i$', fontsize=14)
sobols_first = results.sobols_first(QOI)

first_order_contribution = 0

for param in sobols_first.keys():
    ax.plot(x, sobols_first[param][0:-1:skip], label=param, marker=next(marker))
    first_order_contribution += sobols_first[param][0:-1:skip]

ax.plot(x, first_order_contribution, 'b*', label=r'First-order contribution all 27 parameters')

# place legend on empty axes on the right
handles, labels = ax.get_legend_handles_labels()    # ax1 legend
ax2 = fig.add_subplot(122)                          # ax2
ax2.axis('off')                                     # make ax2 empty
ax2.legend(handles, labels, ncol=2)                 # place ax1 legend on ax1
plt.tight_layout()
```

## References

[1] T. Loudon and S. Pankavich. _Mathematical Analysis and Dynamic Active Subspaces for a Long term model of HIV_. arXiv:1604.04588, 2016

[2] Edeling, W., Arabnejad, H., Sinclair, R., Suleimenova, D., Gopalakrishnan, K., Bosak, B., ... & Coveney, P. V. (2021). The impact of uncertainty on predictions of the CovidSim epidemiological code. Nature Computational Science, 1(2), 128-135.

[3] Ma, X., & Zabaras, N. (2009). An adaptive hierarchical sparse grid collocation algorithm for the solution of stochastic differential equations. Journal of Computational Physics, 228(8), 3084-3113.


# Hello, world!

This is our hello world notebook!

```
print("Hello world!")
```

    Hello world!

Here we continue with some formula

\begin{align}
f(x) = \sin(\pi x),
\end{align}

which we would like to plot

```
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
```

```
x = np.linspace(0., 2.)
plt.plot(x, np.sin(np.pi * x))
plt.show()
```

This is the end of the file.


```python
# This cell is for the Google Colaboratory
# https://stackoverflow.com/a/63519730
if 'google.colab' in str(get_ipython()):
  # https://colab.research.google.com/notebooks/io.ipynb
  import google.colab.drive as gcdrive
  # may need to visit a link for the Google Colab authorization code
  gcdrive.mount("/content/drive/")
  import sys
  sys.path.insert(0,"/content/drive/My Drive/Colab Notebooks/nmisp/10_root_finding")

```

```python
# 그래프, 수학 기능 추가
# Add graph and math features
import pylab as py

```

# 순차법
Sequential Method

10의 제곱근을 구한다고 생각해 보자.
Let's try to find the square root of 10.

$$
x=\sqrt{10} = 10 ^ \frac{1}{2} = 10 ^ {0.5}
$$

계산기라면 이런 식으로 구할 수 있을 것이다.
With a calcuator, an engineer can find it as follows.

```python
print('sqrt(10) =', 10 ** 0.5)

```

조금 다른 방식으로 해 보자.
Let's try a different way.

양변을 제곱해 보자.
Let's square both sides.

$$
\begin{align}
x &= 10 ^ {0.5} \\
x^2 &= \left(10 ^ {0.5}\right)^2 \\
x^2 &= 10
\end{align}
$$

이 관계를 그래프로 표현해 보면 다음과 같을 것이다.
An engineer can visualize this relationship as follows.

```python
# x 의 범위와 간격을 지정
# Specify range and interval of x
x = py.arange(-5, 5+0.1, 0.2)

```

위 `py.arange(-5, 5+0.1, 0.2)`은 -5 부터 5까지의 배열을 0.2 간격으로 생성한다.

The `py.arange(-5, 5+0.1, 0.2)` above would generate an array starting from -5 to 5 with interval of 0.2.

`x`의 행과 열의 갯수:

Number of rows and columns of `x`:

```python
x.shape

```

`(50, 0)` 이라면 1차원 배열이고 길이는 50 이다.

`(50, 0)` means it is a 1-D array with length of 50.

`x`의 첫 항목
The first item of the `x`

```python
x[0]

```

처음 다섯 항목

First five items of `x`

```python
x[:5]

```

마지막 다섯 항목

Last five items of `x`

```python
x[-5:]

```

마지막 항목

Last item

```python
x[-1]

```

위 각 `x` 점에 대해 위 등식을 표시해 보자.

Let's represent the equation above for each point of `x`.

```python
# y = x^2
py.plot(x, x**2, 'k.')
# y = 10
py.plot(x, 10*py.ones_like(x), 'r.')
# 모눈 표시
# Indicate grid
py.grid()

```

`py.ones_like(x)` 는 `x`와 행, 열의 크기가 같은 배열을 만들고 1로 채운다.

`py.ones_like(x)` would create an array with same number of rows and columns and fill it with ones.

한번 해당 함수를 사용해 보자.

Let's try using the function.

```python
x.shape

```

```python
n = py.ones_like(x)
n.shape

```

첫번째 그리고 마지막 5 항목

First and last items

```python
n[:5], n[-5:]

```

최소값과 최대값

Minimum and maximum values

```python
n.min(), n.max()

```

`py.zeros_like(x)` 는 `x`와 행, 열의 크기가 같은 배열을 만들고 0으로 채운다.

`py.zeros_like(x)` would create an array with same number of rows and columns and fill it with zeros.

```python
z = py.zeros_like(x)
z.shape

```

```python
z[:5], z[-5:]

```

이제 양변에서 10을 빼 보자.

Now let's subtract 10 from both sides.

$$
\begin{align}
x^2-10 &= 10-10\\
x^2-10 &= 0
\end{align}
$$

이 관계도 그려보자.
Let's plot this, too.

```python
# x 의 범위와 간격을 지정
# Specify range and interval of x
x = py.arange(-5, 5, 0.2)
# y = x^2
py.plot(x, x**2 - 10, 'k.')
# y = 0
py.plot(x, py.zeros_like(x), 'r.')
# 모눈 표시
# Indicate grid
py.grid()

```

위 방정식을 만족하는 $x$ 는 10의 제곱근일 것이다.
$x$ satisfying the equation above would be the square root of 10.

이러한 $x$를 위 방정식의 **근** 이라고 부른다.
We call such $x$ a **root** of the equation above.

그리고 그러한 x는 아래와 같은 함수를 0으로 만들 것이다.
And such $x$ would make following function zero.

$$
f(x) = x^2 - 10=0
$$

```python
# 이 함수를 0으로 만드는 x 를 찾고자 함
# Intend to find x making the function below zero
def f(x):
    return x ** 2 - 10

```

컴퓨터의 연산능력을 이용하여 근을 찾아 보도록 하자.
Let's try to find the root using the computer's computation capability.

여러 가능한 방법 가운데 하나는 $x$를 어떤 *초기값*으로부터 시작하여 일정 *간격* 으로 증가시키면서 $f(x)=x^2-10$을 계산해 보는 것이다.

One of the possible ways is to evaluate $f(x)=x^2-10$ starting from an *initial value* of $x$, increasing by a constant *interval*.

그렇게 반복하다가 $\left|f(x)\right|=\left|x^2-10\right|$ 이 어떤 *만족스러운 수준* 이하가 되면, 그 때의 $x$ 값이 $\sqrt{10}$의 *근사값*이 될 것이다.

During the iteration, the $x$ making $\left|f(x)\right|=\left|x^2-10\right|$ below a certain *satisfactory level* would be the *approximation* of $\sqrt{10}.$

$$
\left(\left|x^2-10\right|<\epsilon \right) \equiv \left(\left|x^2-10\right|\approx 0 \right)
$$

$$
\left(\left|f(x)\right|<\epsilon \right) \equiv \left(\left|f(x)\right|\approx 0 \right)
$$

여기서 $\epsilon$ 값의 의미는, 예를 들어 $\left|y\right| < \epsilon$ 이면 $y \approx 0$ 이라고 보는 것이다.

Here, $\epsilon$ means that if $\left|y\right| < \epsilon$ then $y \approx 0$.

## 왜 $\epsilon$ 이 필요한가?
Why do we need $\epsilon$?

위의 그래프의 일부를 확대해보자.
Let's zoom into the plot above.

```python
# x 의 범위와 간격을 지정
# Specify range and interval of x
x = py.arange(2.8, 3.5, 0.2)
# y = f(x) = x * x - 10
py.plot(x, f(x), 'ko')
# y = 0
py.plot(x, py.zeros_like(x), 'ro')
# x 축 이름표
# x axis label
py.xlabel('x')
# 모눈 표시
# Indicate grid
py.grid()

```

위 그림에서 $|x^2-10|$이 $x=3.0$과 $x=3.2$ 사이 어딘가에서 0이 될 것임을 짐작할 수 있다.

We can see that $|x^2-10|$ will be zero somewhere between $x=3.0$ and $x=3.2$.

그 값을 더 정확히 알고 싶다면 점을 그리는 간격을 줄이면 될 것이다.

To find more precise value, we can reduce the interval of the dots.

```python
# x 의 범위와 간격을 지정
# Specify range and interval of x
x = py.arange(2.8, 3.5, 0.05)
# y = f(x) = x * x - 10
py.plot(x, f(x), 'ko')
# y = 0
py.plot(x, py.zeros_like(x), 'ro')
# x 축 이름표
# x axis label
py.xlabel('x')
# 모눈 표시
# Indicate grid
py.grid()

```

사람의 경우는 그 간격을 무한히 줄여서 점이 **연속적**이 되는 것을 생각할 수 있지만, 컴퓨터는 **이산적**인 그래프까지만 그릴 수 있다.

A person may think infinitely small interval so that the dots become **continuous**, however, computer plots can only be **discrete**.

컴퓨터는 **정밀도**라는 어떤 수준 이하로 점을 그리는 간격을 줄일 수 없기 때문이다.
Because computers are unable to make the interval of the dots smaller than a certain level called **precision**.

사람은 $x^2-10$ 을 0으로 만들 수 있는 $x$ 를 생각할 수 있지만, 컴퓨터가 찾아 낼 수 있는 최선의 $\sqrt{10}$의 근사값으로도 $x^2-10$ 이 정확히 0이 되지 않을 수 있는 것이다.

While a person may think about $x$ making $x^2-10$ exact zero, even computer's best approximation of $\sqrt{10}$ may not exactly make $x^2-10$ zero.

그래서 컴퓨터의 계산 결과 절대값이 **$\epsilon$** 값보다 작으면, 그 결과는 0과 같은 것으로 생각할 것이다.

Hence, if a computation result has absolute value smaller then **$\epsilon$**, we would regard that the result is the same as zero.

```python
import plot_root_finding

x_start = 2.8
x_end = 3.5
x_interval = 0.05

plot_root_finding.plot(f, x_start, x_end, x_interval);

```

## 순차법 구현
Implementing Sequantial Method

아래 python 프로그램은 순차법으로 $\sqrt{10}$을 계산한다
Following python script calculates $\sqrt{10}$ using sequential method.

### 실행을 위한 준비
Prepare to run

입실론 값 : `y_i` 의 절대값이 이 값 보다 작으면 `y_i == 0`인 것으로 봄

epsilon : if absolute value of `y_i` is smaller than this, we would think that `y_i == 0`.

```python
epsilon = 1e-3

```

`sqrt_10` 의 초기값
Initial value for `sqrt_10`

```python
sqrt_10 = 'Not Found'

```

`x_i` 의 초기값
Initial value of `x_i`

```python
x_init = 0

```

`x_i` 의 마지막 값
Final value of `x_i`

```python
x_final = 4

```

`i` 번째 `x` 값과 `i+1` 번째 `x` 값 사이의 간격
The interval between `i`'th and `(i+1)`'st `x`s

```python
x_interval = 1e-5

```

일련의 `x_i` 값을 미리 준비
Prepare a sequence of `x_i` values in advance

```python
x_array = py.arange(x_init, x_final+x_interval*0.5, x_interval)

```

몇 번 반복했는지 측정하는 변수 선언

Let's declare a variable to count the number of iterations

```python
counter = 0

```

### 순차법을 위한 `for` 반복문
The `for` loop for the sequential method

```python
# x_i 에 관한 반복문
# x_i loop
for x_i in x_array:

    # 근을 찾고자 하는 함수를 계산
    # Evaluate the function whose root we are looking for

    y_i = x_i ** 2 - 10

    # 한번 반복할 때 마다 1씩 증가
    # Increase counter variable by one
    counter += 1

    # 절대값이 입실론 값 보다 작은지 확인
    # Check if absolute value is smaller than epsilon

    if abs(y_i) < epsilon:

        # 위 조건문이 참이면 근을 찾은 것임
        # if true, we found the root

        sqrt_10 = x_i
        break

```

아래 셀은 `isinstance(sqrt_10, float)` 함수의 결과값이 참인지 확인한다.  거짓이라면 `sqrt_10` 값을 화면에 표시하고 실행을 중단시킬 것이다.

Following cell tests whether the returned result of `isinstance(sqrt_10, float)` is `True`.  If not, it will present the value of `sqrt_10` and stop execution by raising an exception.

```python
assert isinstance(sqrt_10, float), sqrt_10

```

### 후처리
Postprocessing

순차법으로 찾은 10의 제곱근
The square root of 10 that we just found using the sequential method

```python
print('sqrt_10 =', sqrt_10)

```

위 결과를 제곱해보자
What is the square of the value above?

```python
print('sqrt_10**2 =', sqrt_10**2)

```

아래 연산의 결과는 0인가?
Is the result of the following calculation zero?

```python
print('sqrt_10 ** 2 - 10 =', sqrt_10 ** 2 - 10)

```

위 결과의 의미는 무엇인가?
What would be the result above?

반복 횟수는 몇 번이었는가?
Number of iterations?

```python
print('counter =', counter)

```

`epsilon`, `x_init`, `x_interval` 등 매개 변수를 바꾸어 보면서 결과가 어떻게 달라지는지 확인 해 보라.

See how the results change as you change parameters such as `epsilon`, `x_init`, and `x_interval`.

## 순차법을 함수로 구현
Implementing Sequantial Method in a Function

위 프로그램으로 어떤 수의 제곱근을 구할 수 있었다. 다른 함수의 근을 구하기에 더 편리한 형태로 바꾸면 더 좋을 것이다.

We could the sqare root of a number.  It would be even better modify so that we can easily find roots of other functions.

```python
def sequential(f, x_init, x_interval, epsilon, x_final, b_print_counter=True):
    # result 의 초기값
    # Initial value for sqrt_10
    result = 'Not Found'

    # 일련의 x_i 값을 미리 준비한다
    # Prepare a series of x_i values in advance
    x_array = py.arange(x_init, x_final+x_interval*0.5, x_interval)

    # 몇번 반복했는지 측정하는 변수를 선언
    # Declare a variable to count the number of iterations
    counter = 0

    # x_i 에 관한 반복문
    # x_i loop
    for x_i in x_array:
        # x = x_i 에서의 함수 f(x)값을 계산하여 y_i에 저장
        # Evaluate the function f(x) at x = x_i and store in y_i
        y_i = f(x_i)

        # counter 변수 1 증가
        # Increase the counter by one
        counter += 1

        # y_i 의 절대값이 epsilon 보다 작은지 확인
        # Check if absolute value is smaller than epsilon
        if abs(y_i) < epsilon:
            # y_i의 절대값이 epsilon 보다 작다면 근을 찾은 것임
            # if the absolute value of y_i is smaller than epsilon
            # we found the root
            result = x_i

            # 근을 찾았으므로 for 반복문을 중단함
            # As we found a root, break out of the for loop
            break

    # 반복 횟수
    # Number of iterations
    if b_print_counter:
        print('counter =', counter)

    # f(x)의 절대값을 epsilon 보다 작게 만드는 결과값을 반환
    # return the result that would make
    # the absolute value of f(x) smaller than epsilon
    return result

```

### 적용사례
Application example

#### 10의 제곱근
square root of 10

우리는 아래 함수를 0으로 만드는 $x$를 찾고자 한다.
We want to find $x$ that makes the following function zero.

```python
def find_sqrt_10(x):
    return (x ** 2) - 10

```

```python
sqrt_10_function = sequential(find_sqrt_10, x_init, x_interval, epsilon, x_final)

```

결과값
Result

```python
sqrt_10_function

```

아래 셀은 함수로 구한 결과와 이전에 구한 결과를 비교
Following cell compares results from function with the previous one.

```python
assert abs(sqrt_10_function - sqrt_10) < epsilon, f"previous = {sqrt_10}, new = {sqrt_10_function}"

```

계산 시간:
Computation time.

```python
%timeit sqrt_10_function = sequential(find_sqrt_10, x_init, x_interval, epsilon, x_final, b_print_counter=False)

```

#### 5의 제곱근
square root of 5

우리는 아래 함수를 0으로 만드는 $x$를 찾고자 한다.
We want to find $x$ that makes the following function zero.

```python
def find_sqrt_5(x):
    return (x ** 2) - 5

```

매개변수는 다음과 같다.

Parameters are as follows.

```python
# y_i 의 절대값이 이 값 보다 작으면 y_i = 0으로 본다
# If absolute value of y_i is smaller than this, we would think y_i = 0.
epsilon = 1e-3
# 순차법의 매개변수
# Parameters of the Sequantial Method
# x_i 의 초기값
# Initial value of x_i
x_init = 0
# x_i 의 마지막 값
# Final value of x_i
x_final = 4
# i 번째 x 값과 i+1 번째 x 값 사이의 간격
# The interval between i'th and (i+1)'th x's
x_interval = 1.0 / (2**12)

```

제곱근을 구하기 위해 순차법 함수는 다음과 같이 호출할 수 있다.
To find the square root of five, we can call the function of the sequential method as follows.

```python
sqrt_5 = sequential(find_sqrt_5, x_init, x_interval, epsilon, x_final)

```

순차법으로 찾은 5의 제곱근

Square root of 5 that we just found using the sequential method

```python
print('sqrt_5 =', sqrt_5)

```

아래 연산의 결과는 0인가?

Is the result of the following calculation zero?

```python
print('sqrt_5 ** 2 - 5 =', find_sqrt_5(sqrt_5))

```

$\epsilon$ 값과 비교
Check with the $\epsilon$

```python
assert abs(find_sqrt_5(sqrt_5)) < epsilon

```

계산 시간
Computatin time

```python
%timeit sqrt_5 = sequential(find_sqrt_5, x_init, x_interval, epsilon, x_final, b_print_counter=False)

```

## 연습 문제
Exercises

도전 과제 1: 다음 매개 변수값을 하나씩 바꾸어 보고 그 영향에 대한 의견을 적으시오.
Try this 1: Change one parameter value at a time and write down your opinion on its influence.

|매개변수
Parameter | 현재값
Current value | 바꾸어 본 값
New value | 영향
Influence |
|:------:|:------:|:----------:|:----------:|
|`epsilon` | `1e-3` |   ?   |   ?   |
|`x_init` | `0` |   ?   |   ?   |
|`x_interval` | `1.0 / (2**12)` |   ?   |   ?   |

도전 과제 2: $sin^2(\theta)=0.5$ 인 $\theta$(도)를 순차법으로 구해 보시오.
Try this 2: Find $\theta$(degree) satisfying $sin^2(\theta)=0.5$ using the sequential method.

도전 과제 3: 관심 있는 $f(x)=0$ 문제를 정하여 그 근을 순차법으로 구해 보시오.
Try this 3: Find a root of a $f(x)=0$ problem of your choice using the sequential method.

## Final Bell
마지막 종

메뉴의 Cell/Run All 명령을 실행시켰을 때 문서의 모든 셀이 실행되었으면 종소리를 냄
When all cells executed by Cell/Run All command of the menu, generate the bell sound.

```python
# stackoverfow.com/a/24634221
import os
os.system("printf '\a'");

```

```python

```