# Joint TV for multi-contrast MR
This demonstration shows how to do a synergistic reconstruction of two MR images with different contrast. Both MR images show the same underlying anatomy but of course with different contrast. In order to make use of this similarity a joint total variation (TV) operator is used as a regularisation in an iterative image reconstruction approach.

This demo is a jupyter notebook, i.e. intended to be run step by step.
You could export it as a Python file and run it one go, but that might
make little sense as the figures are not labelled.

Author: Christoph Kolbitsch, Evangelos Papoutsellis, Edoardo Pasca
First version: 16th of June 2021

CCP PETMR Synergistic Image Reconstruction Framework (SIRF).
Copyright 2021 Rutherford Appleton Laboratory STFC.
Copyright 2021 Physikalisch-Technische Bundesanstalt.

This is software developed for the Collaborative Computational
Project in Positron Emission Tomography and Magnetic Resonance imaging
(http://www.ccppetmr.ac.uk/).

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

# Initial set-up

```python
# Make sure figures appears inline and animations works
%matplotlib notebook
```

```python
# Make sure everything is installed that we need
!pip install brainweb nibabel --user
```

```python
# Initial imports etc
import numpy
from numpy.linalg import norm
import matplotlib.pyplot as plt
import random

import os
import sys
import shutil
import brainweb
from tqdm.auto import tqdm

# Import SIRF functionality
import notebook_setup
import sirf.Gadgetron as mr
from sirf_exercises import exercises_data_path

# Import CIL functionality
from cil.framework import  AcquisitionGeometry, BlockDataContainer, BlockGeometry, ImageGeometry
from cil.optimisation.functions import Function, OperatorCompositionFunction, SmoothMixedL21Norm, L1Norm, L2NormSquared, BlockFunction, MixedL21Norm, IndicatorBox, TotalVariation, LeastSquares, ZeroFunction
from cil.optimisation.operators import GradientOperator, BlockOperator, ZeroOperator, CompositionOperator, LinearOperator, FiniteDifferenceOperator
from cil.optimisation.algorithms import PDHG, FISTA, GD
from cil.plugins.ccpi_regularisation.functions import FGP_TV
```

# Utilities

```python
# First define some handy function definitions
# To make subsequent code cleaner, we have a few functions here. You can ignore
# ignore them when you first see this demo.

def plot_2d_image(idx,vol,title,clims=None,cmap="viridis"):
    """Customized version of subplot to plot 2D image"""
    plt.subplot(*idx)
    plt.imshow(vol,cmap=cmap)
    if not clims is None:
        plt.clim(clims)
    plt.colorbar()
    plt.title(title)
    plt.axis("off")

def crop_and_fill(templ_im, vol):
    """Crop volumetric image data and replace image content in template image object"""
    # Get size of template image and crop
    idim_orig = templ_im.as_array().shape
    idim = (1,)*(3-len(idim_orig)) + idim_orig
    offset = (numpy.array(vol.shape) - numpy.array(idim)) // 2
    vol = vol[offset[0]:offset[0]+idim[0], offset[1]:offset[1]+idim[1], offset[2]:offset[2]+idim[2]]

    # Make a copy of the template to ensure we do not overwrite it
    templ_im_out = templ_im.copy()

    # Fill image content
    templ_im_out.fill(numpy.reshape(vol, idim_orig))
    return(templ_im_out)

# This functions creates a regular (pattern='regular') or random (pattern='random') undersampled k-space data
# with an undersampling factor us_factor and num_ctr_lines fully sampled k-space lines in the k-space centre.
# For more information on this function please see the notebook f_create_undersampled_kspace
def create_undersampled_kspace(acq_orig, us_factor, num_ctr_lines, pattern='regular'):
    """Create a regular (pattern='regular') or random (pattern='random') undersampled k-space data"""

    # Get ky indices
    ky_index = acq_orig.parameter_info('kspace_encode_step_1')

    # K-space centre in the middle of ky_index
    ky0_index = len(ky_index)//2

    # Fully sampled k-space centre
    ky_index_subset =  numpy.arange(ky0_index-num_ctr_lines//2, ky0_index+num_ctr_lines//2)

    if pattern == 'regular':
        ky_index_outside = numpy.arange(start=0, stop=len(ky_index), step=us_factor)

    elif pattern == 'random':
        ky_index_outside = numpy.asarray(random.sample(list(ky_index), len(ky_index)//us_factor))

    else:
        raise ValueError('pattern should be "random" or "linear"')

    # Combine fully sampled centre and outer undersampled region
    ky_index_subset = numpy.concatenate((ky_index_subset, ky_index_outside), axis=0)

    # Ensure k-space points are note repeated
    ky_index_subset = numpy.unique(ky_index_subset)

    # Create new k-space data
    acq_new = preprocessed_data.new_acquisition_data(empty=True)

    # Select raw data
    for jnd in range(len(ky_index_subset)):
        cacq = preprocessed_data.acquisition(ky_index_subset[jnd])
        acq_new.append_acquisition(cacq)

    acq_new.sort()

    return(acq_new)
```

### Joint TV reconstruction of two MR images

Assume we want to reconstruct two MR images $u$ and $v$ and utilse the similarity between both images using a joint TV ($JTV$) operator we can formulate the reconstruction problem as:

$$
\begin{equation}
(u^{*}, v^{*}) \in \underset{u,v}{\operatorname{argmin}} \frac{1}{2} \| A_{1} u - g\|^{2}_{2} +  \frac{1}{2} \| A_{2} v - h\|^{2}_{2} + \alpha\,\mathrm{JTV}_{\eta, \lambda}(u, v)
\end{equation}
$$

* $JTV_{\eta, \lambda}(u, v) = \sum \sqrt{ \lambda|\nabla u|^{2} + (1-\lambda)|\nabla v|^{2} + \eta^{2}}$
* $A_{1}$, $A_{2}$: __MR__ `AcquisitionModel`
* $g_{1}$, $g_{2}$: __MR__ `AcquisitionData`

### Solving this problem

In order to solve the above minimization problem, we will use an alternating minimisation approach, where one variable is fixed and we solve wrt to the other variable:

$$
\begin{align*}
u^{k+1} & = \underset{u}{\operatorname{argmin}} \frac{1}{2} \| A_{1} u - g\|^{2}_{2} + \alpha_{1}\,\mathrm{JTV}_{\eta, \lambda}(u, v^{k}) \quad \text{subproblem 1}\\
v^{k+1} & = \underset{v}{\operatorname{argmin}} \frac{1}{2} \| A_{2} v - h\|^{2}_{2} + \alpha_{2}\,\mathrm{JTV}_{\eta, 1-\lambda}(u^{k+1}, v) \quad \text{subproblem 2}\\
\end{align*}$$

We are going to use a gradient descent approach to solve each of these subproblems alternatingly.

The *regularisation parameter* `alpha` should be different for each subproblem. But not to worry at this stage. Maybe we should use $\alpha_{1}, \alpha_{2}$ in front of the two JTVs and a $\lambda$, $1-\lambda$ for the first JTV and $1-\lambda$, $\lambda$, for the second JTV with $0<\lambda<1$.

This notebook builds on several other notebooks and hence certain steps will be carried out with minimal documentation. If you want more explainations, then we would like to ask you to refer to the corresponding notebooks which are mentioned in the following list. The steps we are going to carry out are

  - (A) Get a T1 and T2 map from brainweb which we are going to use as ground truth $u_{gt}$ and $v_{gt}$ for our reconstruction (further information: `introduction` notebook)

  - (B) Create __MR__ `AcquisitionModel` $A_{1}$ and $A_{2}$ and simulate undersampled __MR__ `AcquisitionData` $g_{1}$ and $g_{2}$ (further information: `acquisition_model_mr_pet_ct` notebook)

  - (C) Set up the joint TV reconstruction problem

  - (D) Solve the joint TV reconstruction problem (further information on gradient descent: `gradient_descent_mr_pet_ct` notebook)

# (A) Get brainweb data

We will download and use data from the brainweb.

```python
fname, url= sorted(brainweb.utils.LINKS.items())[0]
files = brainweb.get_file(fname, url, ".")
data = brainweb.load_file(fname)

brainweb.seed(1337)
```

```python
for f in tqdm([fname], desc="mMR ground truths", unit="subject"):
    vol = brainweb.get_mmr_fromfile(f, petNoise=1, t1Noise=0.75, t2Noise=0.75, petSigma=1, t1Sigma=1, t2Sigma=1)
```

```python
T2_arr  = vol['T2']
T1_arr   = vol['T1']

# Normalise image data
T2_arr /= numpy.max(T2_arr)
T1_arr /= numpy.max(T1_arr)
```

```python
# Display it
plt.figure();
slice_show = T1_arr.shape[0]//2
plot_2d_image([1,2,1], T1_arr[slice_show, :, :], 'T1', cmap="Greys_r")
plot_2d_image([1,2,2], T2_arr[slice_show, :, :], 'T2', cmap="Greys_r")
```

Ok, we got to two images with T1 and T2 contrast BUT they brain looks a bit small. Spoiler alert: We are going to reconstruct MR images with a FOV 256 x 256 voxels. As the above image covers 344 x 344 voxels, the brain would only cover a small part of our MR FOV. In order to ensure the brain fits well into our MR FOV, we are going to scale the images.

In order to do this we are going to use an image `rescale` from the skimage package and simply rescale the image by a factor 2 and then crop it. To speed things up, we are going to already select a single slice because also our MR scan is going to be 2D.

```python
from skimage.transform import rescale

# Select central slice
central_slice = T1_arr.shape[0]//2
T1_arr = T1_arr[central_slice, :, :]
T2_arr = T2_arr[central_slice, :, :]

# Rescale by a factor 2.0
T1_arr = rescale(T1_arr, 2,0)
T2_arr = rescale(T2_arr, 2.0)

# Select a central ROI with 256 x 256
# We could also skip this because it is automaticall done by crop_and_fill()
# but we would like to check if we did the right thing
idim = [256, 256]
offset = (numpy.array(T1_arr.shape) - numpy.array(idim)) // 2
T1_arr = T1_arr[offset[0]:offset[0]+idim[0], offset[1]:offset[1]+idim[1]]
T2_arr = T2_arr[offset[0]:offset[0]+idim[0], offset[1]:offset[1]+idim[1]]

# Now we make sure our image is of shape (1, 256, 256) again because in __SIRF__ even 2D images
# are expected to have 3 dimensions.
T1_arr = T1_arr[numpy.newaxis,...]
T2_arr = T2_arr[numpy.newaxis,...]
```

```python
# Display it
plt.figure();
slice_show = T1_arr.shape[0]//2
plot_2d_image([1,2,1], T1_arr[slice_show, :, :], 'T1', cmap="Greys_r")
plot_2d_image([1,2,2], T2_arr[slice_show, :, :], 'T2', cmap="Greys_r")
```

Now, that looks better. Now we have got images we can use for our MR simulation.

# (B) Simulate undersampled MR AcquisitionData

```python
# Create MR AcquisitionData
mr_acq = mr.AcquisitionData(exercises_data_path('MR', 'PTB_ACRPhantom_GRAPPA')
                            + '/ptb_resolutionphantom_fully_ismrmrd.h5' )
```

```python
# Calculate CSM
preprocessed_data = mr.preprocess_acquisition_data(mr_acq)

csm = mr.CoilSensitivityData()
csm.smoothness = 200
csm.calculate(preprocessed_data)
```

```python
# Calculate image template
recon = mr.FullySampledReconstructor()
recon.set_input(preprocessed_data)
recon.process()
im_mr = recon.get_output()
```

```python
# Display the coil maps
plt.figure();
csm_arr = numpy.abs(csm.as_array())

plot_2d_image([1,2,1], csm_arr[0, 0, :, :], 'Coil 0', cmap="Greys_r")
plot_2d_image([1,2,2], csm_arr[2, 0, :, :], 'Coil 2', cmap="Greys_r")
```

We want to use these coilmaps to simulate our MR raw data. Nevertheless, they are obtained from a phantom scan which unfortunately has got some signal voids inside. If we used these coil maps directly, then these signal voids would cause artefacts. We are therefore going to interpolate the coil maps first.

We are going to calculate a mask from the `ImageData` `im_mr`:

```python
im_mr_arr = numpy.squeeze(numpy.abs(im_mr.as_array()))
im_mr_arr /= numpy.max(im_mr_arr)

mask = numpy.zeros_like(im_mr_arr)
mask[im_mr_arr > 0.2] = 1

plt.figure();
plot_2d_image([1,1,1], mask, 'Mask', cmap="Greys_r")
```

Now we are going to interpolate between the values defined by the mask:

```python
from scipy.interpolate import griddata

# Target grid for a square image
xi = yi = numpy.arange(0, im_mr_arr.shape[0])
xi, yi = numpy.meshgrid(xi, yi)

# Define grid points in mask
idx = numpy.where(mask == 1)
x = xi[idx[0], idx[1]]
y = yi[idx[0], idx[1]]

# Go through each coil and interpolate linearly
csm_arr = csm.as_array()
for cnd in range(csm_arr.shape[0]):
    cdat = csm_arr[cnd, 0, idx[0], idx[1]]
    cdat_intp = griddata((x,y), cdat, (xi,yi), method='linear')
    csm_arr[cnd, 0, :, :] = cdat_intp

# No extrapolation was done by griddate and we will set these values to 0
csm_arr[numpy.isnan(csm_arr)] = 0
```

```python
# Display the coil maps
plt.figure();

plot_2d_image([1,2,1], numpy.abs(csm_arr[0, 0, :, :]), 'Coil 0', cmap="Greys_r")
plot_2d_image([1,2,2], numpy.abs(csm_arr[2, 0, :, :]), 'Coil 2', cmap="Greys_r")
```

This is not the world's best interpolation but it will do for the moment. Let's replace the data in the coils maps with the new interpolation

```python
csm.fill(csm_arr);
```

Next we are going to create the two __MR__ `AcquisitionModel` $A_{1}$ and $A_{2}$

```python
# Create undersampled acquisition data
us_factor = 2
num_ctr_lines = 30
pattern = 'random'
acq_us = create_undersampled_kspace(preprocessed_data, us_factor, num_ctr_lines, pattern)

# Create two MR acquisition models
A1 = mr.AcquisitionModel(acq_us, im_mr)
A1.set_coil_sensitivity_maps(csm)

A2 = mr.AcquisitionModel(acq_us, im_mr)
A2.set_coil_sensitivity_maps(csm)
```

and simulate undersampled __MR__ `AcquisitionData` $g_{1}$ and $g_{2}$

```python
# MR
u_gt = crop_and_fill(im_mr, T1_arr)
g1 = A1.forward(u_gt)

v_gt = crop_and_fill(im_mr, T2_arr)
g2 = A2.forward(v_gt)
```

Lastly we are going to add some noise

```python
g1_arr = g1.as_array()
g1_max = numpy.max(numpy.abs(g1_arr))
g1_arr += (numpy.random.random(g1_arr.shape) - 0.5 + 1j*(numpy.random.random(g1_arr.shape) - 0.5)) * g1_max * 0.01
g1.fill(g1_arr)

g2_arr = g2.as_array()
g2_max = numpy.max(numpy.abs(g2_arr))
g2_arr += (numpy.random.random(g2_arr.shape) - 0.5 + 1j*(numpy.random.random(g2_arr.shape) - 0.5)) * g2_max * 0.01
g2.fill(g2_arr)
```

Just to check we are going to apply the backward/adjoint operation to do a simply image reconstruction.

```python
# Simple reconstruction
u_simple = A1.backward(g1)
v_simple = A2.backward(g2)
```

```python
# Display it
plt.figure();
plot_2d_image([1,2,1], numpy.abs(u_simple.as_array())[0, :, :], '$u_{simple}$', cmap="Greys_r")
plot_2d_image([1,2,2], numpy.abs(v_simple.as_array())[0, :, :], '$v_{simple}$', cmap="Greys_r")
```

These images look quite poor compared to the ground truth input images, because they are reconstructed from an undersampled k-space. In addition, you can see a strange "structure" going through the centre of the brain. This has something to do with the coil maps. As mentioned above, our coil maps have these two "holes" in the centre and this creates this artefacts. Nevertheless, this is not going to be a problem for our reconstruction as we will see later on.

# (C) Set up the joint TV reconstruction problem

So far we have used mainly __SIRF__ functionality, now we are going to use __CIL__ in order to set up the reconstruction problem and then solve it. In order to be able to reconstruct both $u$ and $v$ at the same time, we will make use of `BlockDataContainer`. In the following we will define an operator which allows us to project a $(u,v)$ `BlockDataContainer` object into either $u$ or $v$. In literature, this operator is called **[Projection Map (or Canonical Projection)](https://proofwiki.org/wiki/Definition:Projection_(Mapping_Theory))** and is defined as:

$$ \pi_{i}: X_{1}\times\cdots\times X_{n}\rightarrow X_{i}$$

with

$$\pi_{i}(x_{0},\dots,x_{i},\dots,x_{n}) = x_{i},$$

mapping an element $x$ from a Cartesian Product $X =\prod_{k=1}^{n}X_{k}$ to the corresponding element $x_{i}$ determined by the index $i$.

```python
class ProjectionMap(LinearOperator):

    def __init__(self, domain_geometry, index, range_geometry=None):

        self.index = index
        if range_geometry is None:
            range_geometry = domain_geometry.geometries[self.index]

        super(ProjectionMap, self).__init__(domain_geometry=domain_geometry,
                                           range_geometry=range_geometry)

    def direct(self,x,out=None):

        if out is None:
            return x[self.index]
        else:
            out.fill(x[self.index])

    def adjoint(self,x, out=None):

        if out is None:
            tmp = self.domain_geometry().allocate()
            tmp[self.index].fill(x)
            return tmp
        else:
            out[self.index].fill(x)

```

In the following we define the `SmoothJointTV` class. Our plan is to use the Gradient descent (`GD`) algorithm to solve the above problems. This implements the `__call__` method required to monitor the objective value and the `gradient` method that evaluates the gradient of `JTV`.

For the two subproblems, the first variations with respect to $u$ and $v$ variables are:

$$
\begin{equation}
\begin{aligned}
& A_{1}^{T}*(A_{1}u - g_{1}) - \alpha_{1} \mathrm{div}\bigg( \frac{\nabla u}{|\nabla(u, v)|_{2,\eta,\lambda}}\bigg)\\
& A_{2}^{T}*(A_{2}v - g_{2}) - \alpha_{2} \mathrm{div}\bigg( \frac{\nabla v}{|\nabla(u, v)|_{2,\eta,1-\lambda}}\bigg)
\end{aligned}
\end{equation}
$$

where $$|\nabla(u, v)|_{2,\eta,\lambda} = \sqrt{ \lambda|\nabla u|^{2} + (1-\lambda)|\nabla v|^{2} + \eta^{2}}.$$

```python
class SmoothJointTV(Function):

    def __init__(self, eta, axis, lambda_par):

        r'''
        :param eta: smoothing parameter making SmoothJointTV differentiable
        '''

        super(SmoothJointTV, self).__init__(L=8)

        # smoothing parameter
        self.eta = eta

        # GradientOperator
        FDy = FiniteDifferenceOperator(u_simple, direction=1)
        FDx = FiniteDifferenceOperator(u_simple, direction=2)
        self.grad = BlockOperator(FDy, FDx)

        # Which variable to differentiate
        self.axis = axis

        if self.eta==0:
            raise ValueError('Need positive value for eta')

        self.lambda_par=lambda_par

    def __call__(self, x):

        r""" x is BlockDataContainer that contains (u,v). Actually x is a BlockDataContainer that contains 2 BDC.
        """
        if not isinstance(x, BlockDataContainer):
            raise ValueError('__call__ expected BlockDataContainer, got {}'.format(type(x)))

        tmp = numpy.abs((self.lambda_par*self.grad.direct(x[0]).pnorm(2).power(2) + (1-self.lambda_par)*self.grad.direct(x[1]).pnorm(2).power(2)+\
              self.eta**2).sqrt().sum())

        return tmp

    def gradient(self, x, out=None):

        denom = (self.lambda_par*self.grad.direct(x[0]).pnorm(2).power(2) + (1-self.lambda_par)*self.grad.direct(x[1]).pnorm(2).power(2)+\
              self.eta**2).sqrt()

        if self.axis==0:
            num = self.lambda_par*self.grad.direct(x[0])
        else:
            num = (1-self.lambda_par)*self.grad.direct(x[1])

        if out is None:
            tmp = self.grad.range.allocate()
            tmp[self.axis].fill(self.grad.adjoint(num.divide(denom)))
            return tmp
        else:
            self.grad.adjoint(num.divide(denom), out=out[self.axis])
```

Now we are going to put everything together and define our two objective functions which solve the two subproblems which we defined at the beginning

```python
alpha1 = 0.05
alpha2 = 0.05
lambda_par = 0.5
eta = 1e-12

# BlockGeometry for the two modalities
bg = BlockGeometry(u_simple, v_simple)

# Projection map, depending on the unkwown variable
L1 = ProjectionMap(bg, index=0)
L2 = ProjectionMap(bg, index=1)

# Fidelity terms based on the acqusition data
f1 = 0.5*L2NormSquared(b=g1)
f2 = 0.5*L2NormSquared(b=g2)

# JTV for each of the subproblems
JTV1 = alpha1*SmoothJointTV(eta=eta, axis=0, lambda_par = lambda_par )
JTV2 = alpha2*SmoothJointTV(eta=eta, axis=1, lambda_par = 1-lambda_par)

# Compose the two objective functions
objective1 = OperatorCompositionFunction(f1, CompositionOperator(A1, L1)) + JTV1
objective2 = OperatorCompositionFunction(f2, CompositionOperator(A2, L2)) + JTV2
```

# (D) Solve the joint TV reconstruction problem

```python
# We start with zero-filled images
x0 = bg.allocate(0.0)

# We use a fixed step-size for the gradient descent approach
step_size = 0.1

# We are also going to log the value of the objective functions
obj1_val_it = []
obj2_val_it = []

for i in range(10):

    gd1 = GD(x0, objective1, step_size=step_size, \
          max_iteration = 4, update_objective_interval = 1)
    gd1.run(verbose=1)

    # We skip the first one because it gets repeated
    obj1_val_it.extend(gd1.objective[1:])

    # Here we are going to do a little "trick" in order to better see, when each subproblem is optimised, we
    # are going to append NaNs to the objective function which is currently not optimised. The NaNs will not
    # show up in the final plot and hence we can nicely see each subproblem.
    obj2_val_it.extend(numpy.ones_like(gd1.objective[1:])*numpy.nan)

    gd2 = GD(gd1.solution, objective2, step_size=step_size, \
          max_iteration = 4, update_objective_interval = 1)
    gd2.run(verbose=1)

    obj2_val_it.extend(gd2.objective[1:])
    obj1_val_it.extend(numpy.ones_like(gd2.objective[1:])*numpy.nan)

    x0.fill(gd2.solution)

    print('* * * * * * Outer Iteration ', i, ' * * * * * *\n')
```

Finally we can look at the images $u_{jtv}$ and $v_{jtv}$ and compare them to the simple reconstruction $u_{simple}$ and $v_{simple}$ and the original ground truth images.

```python
u_jtv = numpy.squeeze(numpy.abs(x0[0].as_array()))
v_jtv = numpy.squeeze(numpy.abs(x0[1].as_array()))

plt.figure()
plot_2d_image([2,3,1], numpy.squeeze(numpy.abs(u_simple.as_array()[0, :, :])), '$u_{simple}$', cmap="Greys_r")
plot_2d_image([2,3,2], u_jtv, '$u_{JTV}$', cmap="Greys_r")
plot_2d_image([2,3,3], numpy.squeeze(numpy.abs(u_gt.as_array()[0, :, :])), '$u_{gt}$', cmap="Greys_r")

plot_2d_image([2,3,4], numpy.squeeze(numpy.abs(v_simple.as_array()[0, :, :])), '$v_{simple}$', cmap="Greys_r")
plot_2d_image([2,3,5], v_jtv, '$v_{JTV}$', cmap="Greys_r")
plot_2d_image([2,3,6], numpy.squeeze(numpy.abs(v_gt.as_array()[0, :, :])), '$v_{gt}$', cmap="Greys_r")
```

And let's look at the objective functions

```python
plt.figure()
plt.plot(obj1_val_it, 'o-', label='subproblem 1')
plt.plot(obj2_val_it, '+-', label='subproblem 2')
plt.xlabel('Number of iterations')
plt.ylabel('Value of objective function')
plt.title('Objective functions')
plt.legend()

# Logarithmic y-axis
plt.yscale('log')
```

# Next steps

The above is a good demonstration for a synergistic image reconstruction of two different images. The following gives a few suggestions of what to do next and also how to extend this notebook to other applications.

## Number of iterations
In our problem we have several regularisation parameters such as $\alpha_{1}$, $\alpha_{2}$ and $\lambda$. In addition, the number of inner iterations for each subproblem (currently set to 3) and the number of outer iterations (currently set to 10) also determine the final solution. Of course, for infinite number of total iterations it shouldn't matter but usually we don't have that much time.

__TODO__: Change the number of iterations and see what happens to the objective functions. For a given number of total iterations, do you think it is better to have a high number of inner or high number of outer iterations? Why? Does this also depend on the undersampling factor?

## Spatial misalignment
In the above example we simulated our data such that there is a perfect spatial match between $u$ and $v$. For real world applications this usually cannot be assumed.

__TODO__: Add spatial misalignment between $u$ and $v$. This can be achieved e.g. by calling `numpy.roll` on `T2_arr` before calling `v_gt = crop_and_fill(im_mr, T2_arr)`. What is the effect on the reconstructed images? For a more "advanced" misalignment, have a look at notebook `BrainWeb`.

__TODO__: One way to minimize spatial misalignment is to use image registration to ensure both $u$ and $v$ are well aligned. In the notebook `sirf_registration` you find information about how to register two images and also how to resample one image based on the spatial transformation estimated from the registration. Try to use this to correct for the misalignment you introduced above. For a real world example, at which point in the code would you have to carry out the registration+resampling? (some more information can also be found at the end of notebook `de_Pierro_MAPEM`)

## Pathologies
The images $u$ and $v$ show the same anatomy, just with a different contrast. Clinically more useful are of course images which show complementary image information.

__TODO__: Add a pathology to either $u$ and $v$ and see how this effects the reconstruction. For something more advanced, have a loot at the notebook `BrainWeb`.

## Single anatomical prior
So far we have alternated between two reconstruction problems. Another option is to do a single regularised reconstruction and simply use a previously reconstructed image for regularisation.

__TODO__: Adapt the above code such that $u$ is reconstructed first without regularisation and is then used for a regularised reconstruction of $v$ without any further updates of $u$.

## Complementary k-space trajectories
We used the same k-space trajectory for $u$ and $v$. This is of course not ideal for such an optimisation, because the same k-space trajectory also means the same pattern of undersampling artefacts. Of course the artefacts in each image will be different because of the different image content but it still would be better if $u$ and $v$ were acquired with different k-space trajectories.

__TODO__: Create two different k-space trajectories and compare the results to a reconstruction using the same k-space trajectories.

__TODO__: Try different undersampling factors and compare results for _regular_ and _random_ undersampling patterns.

## Other regularisation options
In this example we used a TV-based regularisation, but of course other regularisers could also be used, such as directional TV.

__TODO__: Have a look at the __CIL__ notebook `02_Dynamic_CT` and adapt the `SmoothJointTV` class above to use directional TV.


# Energy storage convex loss model

Objective: explore the *possibilities* to have convex loss models (i.e. $P_{losses}(...)$ below):

$$E_b(k+1) = E_b(k) + (P_b(k) - P_{losses}(P_b, E_b)) \Delta_t$$

Reminder: to preserve convexity, we need to replace the equality constraint with an **inequality**:

$$ E_b(k+1) ≤ ...$$

Next step: explore the interest of such models in some ESS optimization use cases:

- arbitrage
    - with optimal power split between two batteries with different losses curves ?

Also, maybe the same reasoning applies to model battery aging?

$$A(k+1) = A(k) + f(P,E)$$

PH, May 2020

```python
import sympy
from sympy import symbols, Function, Matrix, simplify
```

```python
sympy.init_printing()
```

```python
e = symbols('e', positive=True) # we are interested in the positive domain e = SoE - SoE_singular (below)
p = symbols('p')

Re = Function('R')(e)
Rep = Function('R')(e,p)
```

Losses: Joule model, dependent on SoE

```python
losses = Re*p**2
losses
```

Gradient

```python
x = [p, e]
grad = [losses.diff(xi) for xi in x]
grad
```

Hessian

```python
H = [[losses.diff(xi).diff(xj) for xi in x] for xj in x]
H = Matrix(H)
H
```

Eigen values

```python
λ1,λ2 =  H.eigenvals().keys()
λ1
```

```python
λ2
```

For the positivity, the first eigen value (λ1) may be critical

**TODO**: solve $λ ≥ 0$ for the general function R(e)

## Try with an affine R(e) function

```python
r0, r1 = symbols('r_0 r_1')
Rlin = r0 + r1*e
Rlin
```

```python
λ1_lin = simplify(λ1.subs(Re, Rlin))
λ1_lin
```

```python
simplify(H.subs(Re, Rlin))
```

```python
simplify(H.subs(Re, Rlin)).eigenvals()
```

## Try with a negative power R(e) function

$$R(e) = \frac{1}{e^a}$$

Conclusion as of May 14 2020 (to be prooved with a better pen and paper analysis of the eigenvalue):
- convex function for $a \in [0,1]$
- *quasi*-convex but not convex for $a \in [1,2]$ (cf. §3.4 in Boyd 2004 book)
- not even quasi convex for $a≥2$ (sublevel sets not convex)

Defintion:

A function f : R n → R is called quasiconvex (or unimodal ) if its domain and all its *sublevel sets*

$$ S_α = \{x ∈ \mathbf{dom} f \;| \; f(x) ≤ α \},$$

for α ∈ R, are convex.

```python
a = symbols('a')
Rpow = 1/e**a
Rpow
```

Hessian with the power R(e) function

```python
Hpow = simplify(H.subs(Re, Rpow))
Hpow
```

```python
λ1pow,_ = Hpow.eigenvals().keys()
λ1pow
```

Subcase: $a=1$:

$$λ = 0$$

although sympy doesn't find it (unless using assumption $p≥0$)

```python
simplify(
    (λ1pow * e**3).subs(a,1)
)
```

Subcase: $a=2$

```python
simplify(
    (λ1pow*e**4).subs(a,2)
)
```

Numerical value:

ccl: for $a=2$, λ is **always negative** (cf Xournal++ doc). Also, see numerical value at point (0.5, 0.5) below

TODO: prove that λ≥0 for a≤1 and λ≤0 for a≥1

```python
λ1pow.subs({
    e:0.5,
    p:0.5,
    a:1.5,
})
```

## Plot

```python
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
```

```python
%matplotlib inline
```

### Contour and 3D plots

```python
def losses_contour(a):
    n = 100
    x = np.linspace(1e-3, 1, n) # e
    y = np.linspace(-1, 1, n) # p

    X, Y = np.meshgrid(x, y)

    Z = Y**2/X**a

    fig = plt.figure(figsize=(8,4))
    ax1 = fig.add_subplot(121)
    ax2 = fig.add_subplot(122, projection='3d')

    levels = [0.125,0.25, 0.5,1, 2]
    CS = ax1.contour(X, Y, Z, levels)
    ax1.clabel(CS, inline=1, fontsize=10)
    ax1.grid()

    x = np.linspace(1e-3, 1, n) # e
    y = np.linspace(-1, 1, n) # p

    X, Y = np.meshgrid(x, y)

    Z = Y**2/X**a
    Z[Z>2.1] = np.nan

    ax2.plot_wireframe(X, Y, Z, rstride=10, cstride=10)
    ax2.set_zlim(0,2)

    ax1.set(
        title = f'losses contours, a={a}',
        xlabel = 'SoE',
        ylabel = 'Power',
    )

    ax2.set(
        title = f'losses, a={a}',
        xlabel = 'SoE',
        ylabel = 'Power',
    )

    fig.tight_layout()

losses_contour(0.5)
plt.savefig('losses_3D_0.5.png', dpi=200)
losses_contour(1.0)
plt.savefig('losses_3D_1.0.png', dpi=200)
losses_contour(1.5)
plt.savefig('losses_3D_1.5.png', dpi=200)
losses_contour(2.0)
plt.savefig('losses_3D_2.0.png', dpi=200)
```

Non convex sublevel sets: $a > 2$

```python
losses_contour(3.0)
plt.savefig('losses_3D_3.0.png', dpi=200)
```

Interactive plot

```python
%matplotlib qt5
```

```python
losses_contour(2)
```

```python
%matplotlib inline
```

### 2D cuts on lines

because of the property that a convex function evaluated on a line is a 1D convex function

```python
def plot_line_cut(a, ang, e0=0.5, p0=0.5, r = 0.45):

    fig, (ax1, ax2) = plt.subplots(1,2, figsize=(8,4))

    n = 100
    x = np.linspace(1e-3, 1, n) # e
    y = np.linspace(-1, 1, n) # p

    X, Y = np.meshgrid(x, y)

    Z = Y**2/X**a

    levels = [0.125,0.25, 0.5,1, 2]
    CS = ax1.contour(X, Y, Z, levels)
    ax1.clabel(CS, inline=1, fontsize=10)

    t = np.linspace(-r, r, 300)
    x = e0 + t*np.cos(ang)
    y = p0 + t*np.sin(ang)

    l = y**2/x**a
    ax1.plot(x,y, 'k:')
    ax1.plot(e0,p0, 'ko')

    ax2.plot(t, l)
    ax2.plot(0, p0**2/e0**a, 'ko')

    ax1.set(
        title = f'losses contours, a={a}',
        xlabel = 'SoE',
        ylabel = 'Power',
    )
    ax1.grid()

    ax2.set(
        title = f'losses cut, a={a}',
        xlabel = 't',
    )
    ax2.grid()

    fig.tight_layout()
```

```python
plot_line_cut(0.5, 0.8)
plt.savefig('losses_cut_0.5.png', dpi=200)
```

```python
plot_line_cut(1.0, 0.8)
plt.savefig('losses_cut_1.0.png', dpi=200)
```

Concavity for $a=1.5$

```python
plot_line_cut(1.5, 0.8)
plt.savefig('losses_cut_1.5.png', dpi=200)
```

Non convexity for $a=2$ (although sublevel sets are convex)

```python
plot_line_cut(2, 0.9)
plt.savefig('losses_cut_2.0.png', dpi=200)
```

```python

```


# Bootstrap utilities

```python
import numpy as np
import pandas as pd
df = pd.read_csv('sample_data.csv')

import bebi103

import bokeh.io
bokeh.io.output_notebook()
```

    Loading BokehJS ...

The bebi103 package contains utilities for nonparametric bootstrap calculations. These fall into three categories.

1. Bootstrap replicates of a single scalar statistic from a one-dimensional data set.
2. Pairs bootstrap replicates of a single scalar statistic.
3. Permutation replicates drawn from two one-dimensional data sets.

Additionally, it contains `draw_bs_reps_mle()`, which is used for drawing bootstrap replicates of a maximum likelihood estimator.

## Random number generation

Bootstrap method relies on random number generation. [Numpy's documentation](https://numpy.org/doc/stable/reference/random/index.html) suggests instantiating a `Generator` instance and using that to draw random numbers, for example as

```python
import numpy as np

# Instantiate Generator
rg = np.random.default_rng()

# Draw Normally distributed random numbers
rg.normal(loc=2.5, scale=1.0, size=100)
```

By default, `np.random.default_rng()` gives a PCG-64 random number generator.

In legacy uses of Numpy, random numbers were drawn using the Mersenne Twister algorithm. This functionality is still available, as

```python
import numpy as np

# Draw Normally distributed random numbers
np.random.normal(loc=2.5, scale=1.0, size=100)
```

Numba currently (v. 0.50) has support for the legacy Numpy implementation using the Mersenne Twister algorithm. Because much of the functions using random number generation in the bebi103 package use Numba to get speed boosts by just-in-time compilation, all functions in the `bebi103.bootstrap` module except `bebi103.bootstrap.draw_bs_reps_mle()` all use the legacy Numpy random number generation. The MLE bootstrap function does not because it allows for parallel computation, and seeding the RNG would result in all threads giving the same values.

### Seeding the RNG

For testing, debugging, and pedagogical reproducibility purposes, it is sometimes valuable to seed the random number generator. To do so, you need to seed the random number generator *both* in vanilla Numpy and with a Numba'd function. This is accomplished using the `bebi103.bootstrap.seed_rng()` function.

```python
bebi103.bootstrap.seed_rng(3252)
```

The results of `bebi103.bootstrap.draw_bs_reps_mle()` are unaffected by this seeding.

## Nonparametric resampling methods

We begin with the available nonparametric bootstrap and permutation methods.

### Boostrap replicates from one-dimensional data

A bootstrap replicate is obtained by resampling a data set with replacement and then computing the prescribed summary statistic. The summary statistic need not be scalar, scalar summary statistics are a common use case. Drawing the bootstrap replicates from a set of scalar measurements is achieved using the `bebi103.bootstrap.draw_bs_reps()` function. It takes two positional arguments.

1. The data set to resample.
2. The function used to compute the summary statistic.

Additionally, it takes two keyword arguments.

1. `size`, the number of bootstrap replicates to generate.
2. `args`, a tuple containing any other parameters that need to be passed into the function used to compute the summary statistic.

Below, I draw 10,000 replicates of the median.

```python
bs_reps = bebi103.bootstrap.draw_bs_reps(df['x'], np.median, size=10000)
```

To compute confidence intervals from the replicates, we take percentiles. For example, to compute a 95% confidence interval, we compute the 2.5th and 97.5th percentiles.

```python
conf_int = np.percentile(bs_reps, [2.5, 97.5])
```

We just computed the confidence interval for the *x* values of the data set ignoring the trials. We can automate this process to compute confidence intervals for each of the three trials, storing them in a list of dictionaries along with the trial numbers and point estimates of the median.

```python
summaries = []

for label, group in df.groupby("trial"):
    label = str(label)
    estimate = np.median(group["x"])

    bs_reps = bebi103.bootstrap.draw_bs_reps(group["x"], np.median, size=10000)
    conf_int = np.percentile(bs_reps, [2.5, 97.5])

    summaries.append(dict(label=label, estimate=estimate, conf_int=conf_int))
```

To make sure we understand the entries in `summaries`, a list of dictionaries, let's take a look.

```python
summaries
```

    [{'label': '1',
      'estimate': 3.5801108355411304,
      'conf_int': array([2.51454132, 4.30516588])},
     {'label': '2',
      'estimate': 2.0008366741479433,
      'conf_int': array([1.40245352, 3.65771395])},
     {'label': '3',
      'estimate': 1.0906343409350114,
      'conf_int': array([0.43744037, 2.90887073])}]

When the summary statistics are stored in this format, we can use the `bebi103.viz.confints()` function to make a plot.

```python
bokeh.io.show(bebi103.viz.confints(summaries, x_axis_label='x', y_axis_label='trial'))
```

### Pairs bootstrap

We can also perform pairs bootstrap calculations in which pairs of data from two one-dimensional arrays of the same length are drawn. For example, we may with to draw pairs bootstrap replicates of the Pearson correlation coefficient for the $x$ and $y$ data in the data set. Conveniently, the `bebi103.bootstrap.pearson_r()` function computes the correlation coefficient. We use the `bebi103.bootstrap.draw_bs_reps_pairs()` to do the calculation. The API is similar to the univariate case, except now we supply three positional arguments, the *x*-data, the *y*-data, and the function to compute the summary statistic from the two.

```python
summaries = []

for label, group in df.groupby("trial"):
    label = str(label)
    estimate = bebi103.bootstrap.pearson_r(group["x"], group["y"])

    bs_reps = bebi103.bootstrap.draw_bs_reps_pairs(
        group["x"], group["y"], bebi103.bootstrap.pearson_r, size=10000
    )
    conf_int = np.percentile(bs_reps, [2.5, 97.5])

    summaries.append(dict(label=label, estimate=estimate, conf_int=conf_int))

bokeh.io.show(bebi103.viz.confints(summaries, x_axis_label="ρ", y_axis_label="trial"))
```

### Permutation replicates

When testing the hypothesis that two one-dimensional data sets come from identical distribution, we may use a permutation test. If data set *x* has *m* measurements and data set *y* has *n* measurements, a permutation sample is obtained by concatenating the two data sets, randomly scrambling the order of the concatenated data set, and then assigning the first *m* entries in the concatenated/scrambled data set to be "*x*" and the remaining *n* entries to be "*y*." A summary statistic is then computed from the permutation sample to get a permutation replicate. This is done over and over again in order to get many permutation replicates to ultimately compute a p-value.

The `bebi103.bootstrap.draw_perm_reps()` function generates permutation replicates. The API is similar to generating pairs bootstrap with three positional arguments, the *x*-data, the *y*-data, and the function to compute the summary statistic from the two. Here, we demonstrate a permutation test on the values of measurement *x* for trials 1 and 2. We will use the Studentized difference of means as the test statistic, which is conveniently available in the `bebi103.bootstrap.studentized_diff_of_means()` function.

```python
# Extract data sets
x1 = df.loc[df["trial"] == 1, "x"]
x2 = df.loc[df["trial"] == 2, "x"]

# Compute test statistic from original data
diff_means_studentized = bebi103.bootstrap.studentized_diff_of_means(x1, x2)

# Draw permutation reps
perm_reps = bebi103.bootstrap.draw_perm_reps(
    x1, x2, bebi103.bootstrap.studentized_diff_of_means, size=10000
)

# Compute p-value
np.sum(perm_reps >= diff_means_studentized) / 10000
```

    0.0287

In this case, we got *p* = 0.03.

## Parametric methods

In parametric modeling, we have an explicit *generative model* in mind that generated the data. The generative model has a set of parameters, and a central task is to estimate those parameters. The **maximum likelihood estimate** (MLE) of the parameters consists of the parameter values that maximize the [likelihood function](https://en.wikipedia.org/wiki/Likelihood_function).

The bebi103 package allows for drawing bootstrap replicates of maximum likelihood estimates (MLEs). This is accomplished using the `bebi103.bootstrap.draw_bs_reps_mle()` function. The function does the following.

1. Compute the MLE of the parameters of the generative model from the measured data.
2. Use the generative model parametrized by the MLE to generate a new data set. This new data set is referred to as a bootstrap sample.
3. Compute the MLE from the bootstrap sample and store it. This value of the MLE from a bootstrap sample is referred to as a bootstrap replicate.
4. Repeat steps 2 and 3 until the desired number of bootstrap replicates is acquired.

This function is quite general, requiring the user to specify a function to compute the MLE, a function to draw a bootstrap sample, and the data themselves.

### MLE for a univariate data set

To begin, we will consider a model in which all of the $x$-data from from a Normal distribution. That is, our generative model is

\begin{align}
x \sim \text{Norm}(\mu, \sigma),
\end{align}

with the two parameters we seek to estimate from the data being the location parameter $\mu$ and the scale parameter $\sigma$. Conveniently, the MLEs for the location and scale parameters for a Normal model are the plug-in estimates for the mean and standard deviation.

First, we define the function to compute the MLE. The function must the data set as its first argument, and may have other arguments if necessary.

```python
def mle_fun(x):
    """Given a data set, compute MLE (plug-in) estimates for the
    mean and variance."""
    return np.array([np.mean(x), np.std(x)])
```

Next, we need a function that generates a new data set from the MLE. This function must have the parameters as a Numpy array (in this case an array of the mean and standard deviation) as its first argument. Additional arguments, if any, follow. The last two arguments must be `size` and `rg`, which are respectively the number of new data to generate and the random number generator to use.

```python
def gen_fun(params, size, rg):
    """Generate a new data set"""
    return rg.normal(*params, size=size)
```

We can now draw bootstrap replicates of the MLE.

```python
bs_reps = bebi103.bootstrap.draw_bs_reps_mle(mle_fun, gen_fun, df['x'], size=10000)
```

The output is a Numpy array where each row is a parametric bootstrap replicate of the MLE. Each column corresponds to on of the variables, the first being $\mu$ and the second being $\sigma$. We can compute 95% confidence intervals for the two.

```python
np.percentile(bs_reps, [2.5, 97.5], axis=0)
```

    array([[2.08490171, 1.74424982],
           [3.12677947, 2.48335369]])

The first column is the confidence interval for $\mu$ and the second is the confidence interval for $\sigma$.

We should, of course, also compute the MLE itself (not just its confidence interval!).

```python
params_mle = mle_fun(df['x'])

print("μ MLE: {0:.2f}\nσ MLE: {1:.2f}".format(*params_mle))
```

    μ MLE: 2.61
    σ MLE: 2.13

### Graphical display of a confidence region

While the above calculation of confidence *intervals* are useful for each parameter, we may wish to understand how the MLE of $\mu$ and that of $\sigma$ might depend on each other. E.g., we might wonder if we have a higher estimate for $\mu$, do we also have a higher estimate for $\sigma$?

To address this question, we can plot all of the bootstrap replicates. From those, we can draw a contour around a 95% confidence *region*, which contains 95% of the bootstrap samples. This is achieved with a corner plot, which also includes histograms of the bootstrap replicates for each parameter individually. This functionality is given in the `bebi103.viz.corner()` function.

```python
bokeh.io.show(
    bebi103.viz.corner(
        bs_reps,
        labels=["µ", "σ"],
        show_contours=True,
        levels=[0.95],
        max_plotted=1000,
    )
)
```

Note that we have used the `max_plotted=1000` kwarg to only plot 1,000 of the 10,000 samples. This is to keep the file size down in the rendering of this notebook in the hosted documentation.

### Graphical model comparison of univariate data sets

The generative model emits data. If we parametrize the generative model with the MLE, we can use the generative model to draw new data sets. We can compare these *predicted* data set the experimental data set. For this purpose, graphical comparisons are useful.

In order to construct the graphical model comparisons, we need to generate predicted data sets as samples out of the generative model parametrized by the MLE. Each data set has the same number of "observations" as the observed data set. We will generate 10,000 such data sets.

```python
rg = np.random.default_rng(seed=3252)
samples = np.array([gen_fun(params_mle, len(df), rg) for _ in range(10000)])
```

#### Q-Q plots

Q-Q plots are an effective means of model comparison. They are constructed as follows. Say we have *n* data points. We draw *n* samples out of the generative model and sort them. We then plot the sorted generated data against the sorted measured data points. Plotted points falling on a straight line of slope one and passing through the origin is indicative of agreement between the model and the true process that generated the data.

We can repeat this over and over again, can can compute percentiles of the Q-Q plots to give a confidence interval of the plots of sorted generated data versus sorted measured data. If the confidence interval captures the line of unit slope and zero intercept, the data are commensurate with the model.

We have the generated data sets, stored as `samples`. Along with the measured data (in this case, fabricated "measured" data for illustrative purposes), we can generated a Q-Q plot using `bebi103.viz.qqplot()`.

```python
bokeh.io.show(bebi103.viz.qqplot(data=df["x"], samples=samples))
```

By default, the middle 95% of samples are displayed as the shaded region.

#### Predictive ECDFs

To compare data sets that would be generated from the model and the observed data set, we plot percentiles of the ECDFs of the generated data sets with the ECDF of the data overlaid using the `bebi103.viz.predictive_ecdf()` function.

```python
p = bebi103.viz.predictive_ecdf(data=df['x'], samples=samples)
bokeh.io.show(p)
```

The orange curve is the ECDF of the data. The dark blue curve in the center is the median ECDF of the samples. By default, the coloring shows the middle 68th and middle 95th percentiles of the ECDFs from samples out of the generative model.

Predictive ECDFs are often easier to look at as differences from the median. We can consider two different differences. First, we can take each value of the ECDF and look at how different the x-values are from the median. Second, we can consider each value of *x* and look at how different the value of the ECDF is. In the first case, we are looking at an inverse ECDF (IECDF). We can look at either different plot using the `diff` kwarg.

```python
p_iecdf = bebi103.viz.predictive_ecdf(
    data=df["x"], samples=samples, diff="iecdf", frame_height=175
)

p_ecdf = bebi103.viz.predictive_ecdf(
    data=df["x"], samples=samples, diff="ecdf", frame_height=175
)

bokeh.io.show(
    bokeh.layouts.column([p_iecdf, bokeh.layouts.Spacer(height=30), p_ecdf])
)
```

### MLE for a multivariate data set

The MLE analysis generalizes to multivariate data. As a simple example, we will consider a bivariate example where $y$ is a linear function of $x$.

\begin{align}
y_i \sim \text{Norm}(a_0 + a_1 x_i, \sigma) \;\;\forall i.
\end{align}

The MLE for $a_0$, and $a_1$ are given by the slope and intercept from a least squares calculation, and the MLE for $\sigma$ is given by

\begin{align}
\sigma_\mathrm{MLE} = \sqrt{\text{RSS}/n},
\end{align}

where RSS is the residual sum of squares. We can write `mle_fun()` to return the MLEs for the three parameters.

```python
def mle_fun(y, x):
    """Given x, y data and a linear model with homoscedastic error,
    compute MLE for slope, intercept, and standard deviation."""
    # Obtain MLEs
    a1, a0 = np.polyfit(x, y, deg=1)
    sigma = np.sqrt(np.sum((a0 + a1 * x - y)**2) / len(x))

    return a0, a1, sigma
```

Note that we have identified the dependent variable *y* as the "data," and the independent variable *x* as a "parameter."

We can use this function to get the MLEs.

```python
x = df["x"].values
y = df["y"].values

mle_params = mle_fun(y, x)
```

To obtain bootstrap replicates of the MLE using `bebi103.bootstrap.draw_bs_reps_mle()`, we need to specify a function to generate data from the model. As with the MLE function, we need to pass an additional parameter here, `x`, the values of the dependent variable.

```python
def gen_fun(params, x, size, rg):
    """Generate a new data set"""
    a0, a1, sigma = params

    return rg.normal(a0 + a1 * x, sigma)
```

We can now draw our bootstrap replicates and compute confidence intervals for the MLEs. We use the `mle_args` and `gen_args` keyword arguments to specify the `x` must be passed as an argument to `mle_fun()` and `gen_fun()`. In this case, we will use the `n_jobs` kwargs to enable parallel acquisition of the bootstrap samples.

```python
# Get reps
bs_reps = bebi103.bootstrap.draw_bs_reps_mle(
    mle_fun, gen_fun, y, mle_args=(x,), gen_args=(x,), size=10000, n_jobs=2
)

# Confidence intervals
np.percentile(bs_reps, [2.5, 97.5], axis=0)
```

    array([[4.15616345, 0.71864661, 1.9791932 ],
           [6.09647939, 1.29010591, 2.8266433 ]])

The first column is the confidence interval for $a_0$, the second for $a_1$, and the third for $\sigma$.

#### Graphical display of confidence region

We can again use the `bebi103.viz.corner()` function to display the confidence regions. This time, we will plot the samples for the individual parameters with an ECDF.

```python
bokeh.io.show(
    bebi103.viz.corner(
        bs_reps,
        labels=["a₀", "a₁", "σ"],
        show_contours=True,
        levels=[0.95],
        plot_ecdf=True,
        max_plotted=1000,
    )
)
```

The corner plot reveals in this case that the MLEs for $a_0$ and $a_1$ are correlated, as we might expect.

#### Predictive regression plots

It is traditional to display a plot of the curve parametrized by the MLEs. Such a plot might look like this:

```python
a0, a1, sigma = mle_params

p = bokeh.plotting.figure(frame_height=200, frame_width=200)
p.circle(x, y)
p.line(x, a0 + a1 * x, color='tomato')

bokeh.io.show(p)
```

However, this kind of plot does not display what might be predicted from the generative model, which has variability in measured beyond the (in this case linear) theoretical curve. It is more instructive to make a predictive regression plot. This is generated by drawing samples out of the generative model, and then plotting percentiles of the samples. This is accomplished with the `bebi103.viz.predictive_regression()` function.

It expects the data to come as a two-column Numpy array, so we need to create one.

```python
xy = df[['x', 'y']].values
```

We also need to specify for which x-values we want the samples. We can choose arbitrary x-values, so we will choose 200 point spanning the range of the data set in order to get smooth curves.

```python
samples_x = np.linspace(-3, 10, 200)
```

Finally, we can draw sample (we'll do 10,000 of them), and make the plot.

```python
samples = np.array(
    [gen_fun(mle_params, samples_x, size=1, rg=rg) for _ in range(5000)]
)

bokeh.io.show(
    bebi103.viz.predictive_regression(
        samples, samples_x, data=xy, x_range=[-3, 10],
    )
)
```

By default, the middle 68% and 95% of samples are shown in the shaded blue regions, and the dark blue line is the median of the samples.

If we would like to compare the difference between compared to the median, we can use the `diff=True` kwarg. However, for this kind of plot, the x-values for which the samples were generated have to match those of the data. This is a necessary because the x-values can in general be floats and it is not entirely ambiguous which samples correspond to which x-values otherwise.

```python
samples = np.array(
    [gen_fun(mle_params, x, size=1, rg=rg) for _ in range(5000)]
)

bokeh.io.show(
    bebi103.viz.predictive_regression(
        samples, x, data=xy, x_range=[x.min(), x.max()], diff=True
    )
)
```


```python
%matplotlib notebook
%matplotlib inline

import matplotlib
import numpy as np
import matplotlib.pyplot as plt
```

# Time Value of Money

The **time value of money** means that a person places a higher value on a dollar in one point in time than on the same dollar at a different (usually later) point in time.

- Are you willing to give me $\$100$ today if I give you $\$100$ in one year?
- Why or why not?
- **Risk:** Can you trust that you will get the $\$100$ back?
- **Availability of Capital:** What if you don’t have the $\$100$ to give me?

## Learning Objectives

At the end of this lesson, you will be equipped to:

- Recognize the relationship between time and money
- Explain discounting, interest, and growth of money
- Calculate present and future values of money, annuities, and bonds
- Apply these calculations to appropriate situations

## Notation

In mathematically describing the time value of money, we'll use the following notation.

$$P=\mbox{present value } (\$)$$
$$F=\mbox{future value } (\$)$$
$$N=\mbox{number of periods of investment } (\mbox{period})$$
$$i=\mbox{interest rate per period } (\mbox{period}^{-1}) $$

## Growth With Simple Interest

In the simplest case, the future value of money grows linearly with the number of periods invested.

$$F=P(1+iN)$$

Let's create a python function that calculates this. It will accept 3 variables as inputs, $P$, $i$, and $N$. It will return a single value, $F_{simple}$, the future value in the case of simple interest.

```python
def F_simple(P, i, N):
    '''Growth with simple interest'''
    return P*(1+i*N)
```

```python
# example
P=102  # You give me $100
i=0.10 # at 10 percent interest per period
N=10   # it takes me 10 periods to pay it back
```

```python
print("You give me $"+str(P)+" today, but after " + str(N) +
      " years at an interest rate of " + str(i) + ", I will owe you:")
print(F_simple(P, i, N))
```

    You give me $102 today, but after 10 years at an interest rate of 0.1, I will owe you:
    204.0

### Question: What kind of function is the simple growth equation?

Recall the equation for F(P, i, N) above. With this simple interest growth type, the future value (the amount owed, in this case) is what kind of function of time (N)?

- linear
- quadratic
- logrithmic
- exponential

```python
# The magic below shows the plot interactively when it's created
%matplotlib notebook
# we need the numerical python library
import numpy as np
# we'll use matplotlib to plot the function
import matplotlib
# we'll call the useful parts of matplotlib "plt"
import matplotlib.pyplot as plt
# time is an array of values between 0 and 100 years in increments of 1 year
time = np.arange(0, 101, 1)
# label the x axis
plt.xlabel('Time (y)')
# label the y axis
plt.ylabel('How Much I Owe ($)')
# title the plot
title = "Amount Owed Over Time on $" + str(P) + " at " + str(i*100) + "% Interest"
plt.title(title)
# attach a data series to the plot. Use blue dots with lines.
plt.plot(time, F_simple(P,i,time), 'bo-', label="simple")
# create a legend and put it in the top left
plt.legend(loc=2)
# Display the plot (not actually necessary with %matplotlib notebook, but needed in scripts)
plt.show()
```

    <IPython.core.display.Javascript object>

### Question: What if the interest rate were doubled?

After 10 years at 10% interest, I would pay you \$200 to make us square. Within this amount, I am paying \$100 to return the original prinicipal. The other \$100 is pure interest.

If the interest rate were doubled (20%), how much _interest_ would I pay you in 10 years?

```python
%matplotlib notebook
time = np.arange(0, 11, 1) # plot time between 0 and 10 years in increments of 1 year
rates = np.arange(0.0, 0.11, 0.01) # plot interest rates between 0 and 10% in increments of 1%
plt.xlabel('Interest Rate')
plt.ylabel('How Much I Owe ($)')
title = 'Amount Owed (P=$' + str(P) + ')'
plt.title(title)
for rate in rates:
    plt.plot(time, F_simple(P,rate,time), label=rate)
plt.legend(loc=2)
plt.show()
```

    <IPython.core.display.Javascript object>

## Growth With Compounding Interest

In a compounding interest scenario, the future value of money is _compounded_ at each period. That is, after each period (e.g. each year) the total amount owed is note and then interest is applied to that amount. After the first period, the borrower is racking up additional interest on the interest that they owe. The equation is like this:

$$F=P(1+i)^N$$

### Question: What kind of equation is the compounding interest equation?
For the compounding interest form of F(P, i, N) in the equation above, what kind of relationship does the future value of money (F) have with time (N):

- quadratic
- linear
- logarithmic
- exponential

```python
def F_compound(P, i, N):
    '''Growth with compounding interest'''
    return P*pow((1+i),N)
```

```python
print("You give me $"+str(P)+" today, but after " + str(N) +
      " years at an interest rate of " + str(i) + ", I will owe you:")
print(F_compound(P, i, N))
```

    You give me $102 today, but after 10 years at an interest rate of 0.1, I will owe you:
    264.5617309302002

```python
# Plot it as a function of interest rate
plt.clf()
time = np.arange(0, 11, 1)
plt.xlabel('Time')
plt.ylabel('How Much I Owe ($)')
plt.title('Amount Owed As a Function of Time')
plt.plot(time, F_simple(P,i,time), 'bo-', label="simple")
plt.plot(time, F_compound(P,i,time), 'go-', label="compound")
plt.legend(loc=2)
plt.show()
```

```python
# Plot it for different interest rates
plt.clf()
time = np.arange(0, 11, 1)
rates = np.arange(0.0, 0.11, 0.01)
plt.xlabel('Time (y)')
plt.ylabel('How Much I Owe ($)')
plt.title('Amount Owed As a Function of Time')
for rate in rates:
    plt.plot(time, F_compound(P,rate,time), label=rate)
plt.legend(loc=2)
plt.show()
```

## Discounting

Our examples have shown the growth in an investment due to interest earned.
Compounding is a way to move money from the present to the future.
What about going the other way in time, that is, **what is the value of the
promise of some future amount worth today?** The answer is provided by the
_inverse_ of compounding, **discounting**.

$$F=\mbox{Future value}$$
$$P=\mbox{Present value}$$
$$N=\mbox{Number of periods}$$
$$i=\mbox{Investment rate per period}$$

Recall that F, as a function of P, is :

$$F = P(1+i)^N$$

Solving this for P, as a function of F, we get:

$$P = \frac{F}{(1+i)^N}$$

```python
def P_compound(F, i, N):
    """The present value, as a function of future value, for compounding interest"""
    return F/pow((1+i),N)
```

```python
# example
F=100  # I promise you $100
i=0.10 # at 10 percent investment per period
N=10   # but you have to wait 10 periods before I give it to you

print("If you invest " +
      str(P_compound(F, i, N)) + " today at an investment rate of " + str(i*100) +
      "%, it will be worth $" + str(F) +
      " in " + str(N) + " years.")

```

    If you invest 38.55432894295314 today at an investment rate of 10.0%, it will be worth $100 in 10 years.

```python
# Exercise... Show that P_compound is truly the inverse of F_compound
i = 0.1
N = 10
test_values = [10, 100, 1000]

for f_test in test_values:
    P = P_compound(f_test,i,N)
    obs = F_compound(P, i, N)
    assert(obs == f_test)
    print(obs," = ",f_test)
```

    10.0  =  10
    100.0  =  100
    1000.0  =  1000

# Inflation

Inflation is the tendency for prices to continuously rise. Conversely, it is the continuous fall in the value of currency (like the dollar).

**Example:** What has happened to the value of the dollar since you entered college (4 yrs)? To answer this, we need inflation rates for 2013‐2016:

 - 2013: 1.5%
 - 2014: 1.6%
 - 2015: 0.1%
 - 2016: 1.0%

$$F = \mbox{buying power today}$$
$$P = \mbox{one dollar in your first year of college}$$
$$i_n = \mbox{inflation rate in year n.}$$
$$F_{n} = P_n(1 +i_{n})$$
$$F_{n} = P_{n+1}$$

$$F_{2016} = P_{2016}(1 + i_{2016})$$
$$F_{2015} = P_{2015}(1 + i_{2015})$$
$$F_{2014} = P_{2014}(1 + i_{2014})$$
$$F_{2013} = P_{2013}(1 + i_{2013})$$

Combined:

$$F_{2016} = P_{2013}(1 + i_{2013})(1 + i_{2014})(1 + i_{2015})(1 + i_{2016})$$

```python
def F_infl(P, infl):
    """The future value of money, due to one year of inflation at rate infl"""
    return P*(1+infl)

import collections
year_infl = collections.OrderedDict()
#year_infl[2011] = 0.03
#year_infl[2012] = 0.017
year_infl[2013] = 0.015
year_infl[2014] = 0.016
year_infl[2015] = 0.001
year_infl[2016] = 0.01

P = {2013:1}
F = {}
for year, infl in year_infl.items():
    P[year+1] = F_infl(P[year], infl)
    F[year] = P[year+1]

print(F[2016])

```

    1.0425939523999999

### Inflation adjusted interest rate

To adjust your interest rate for the existence of inflation,

$$i^* = \frac{(1+i)}{(1+r)} - 1$$
$$i^* = \mbox{interest rate adjusted for inflation}$$
$$r = \mbox{rate of inflation}$$
$$i = \mbox{nominal interest rate}$$

## MARR – minimum acceptable rate of return

- You know F and N and are asking for a value of P. But what is the value of i? As an investor you must specify the rate of return that you require.
- We call the interest rate in the computation the minimal acceptable rate of return, MARR

MARR depends on many things; how much capital is available, how much is needed, etc.
- For an individual, it should be at least what could be earned elsewhere.
- For someone with debt, it should be at least the interest charged by the lender.

Let’s say you are a student with a credit card debt charging 1.5% per month on the outstanding balance. That is roughly 18% per year. Your friend Feynman wants to borrow some money. Feynman will give you $100 in one year. How much should you be willing to loan him?

```python
F=100
N=1
i = 0.18
P = P_compound(F, i, N)
print(P)
```

    84.74576271186442

If your MARR is greater than 18%, the amount you should be willing to give should be less than P. If your MARR is less Than 18%, you could give more.

### Exercise: Formulate an equation for $i_{MARR}$
To get $i_{MARR}$ as a function of P and F, you must solve $F_{compound}$ for i.

In the following markdown cell, replace the question marks with the appropriate equation:

$$i_{MARR} = \left(\frac{F}{P}\right)^\frac{1}{n}-1$$

### Excercise: Write the $i_{MARR}$ equation as a python function

In the following python cell, fill in the function:

```python
def i_MARR(F, P, N):
    """The $i_{MARR}$ as a function of future value, present value,
    and period, for compounding interest
    """
    return pow(F/P, 1/N) - 1

```

## What about borrowing money?

Sometimes, the rate at which we make payments is different than the period over which the rate is quoted and compounded. For a car loan, for example, the compounding period is annual, but you make monthly payments for some $m$ months.

$$F = \frac{m}{12}\left(\frac{Pi}{1-(1+i/12)^{-m}}\right)$$

```python
def F_monthly(P, i, m):
    F = (m/12)*(P*i)/(1 - pow(1+i/12,-m))
    print("Total: ",F)
    print("Monthly payment: ", F/m)
    print("Interest paid: ", F-P)
    return F

```

### Example : Car Loan

- Let’s use an example of a $20,000 loan so you can buy a car.
- The current interest rate is 4% on a 60 month loan.

```python
# How much do you pay per month?
# Over the course of the 60 months, how much interest have you paid?

m=60
i=0.04
P=20000.0

F = F_monthly(P, i, m)

```

    Total:  22099.826466319162
    Monthly payment:  368.3304411053194
    Interest paid:  2099.826466319162

### Example : Home Loan

Now lets look longer term. It's time to buy a house. The average price of a home sold in the US in June 2014 was $331,000$. Let's say you get a 15 year fixed rate mortgage at 3.25%.

```python
N=15
m=15*12
i=0.0325
P=331000.

F = F_monthly(P, i, m)
```

    Total:  418650.05252246873
    Monthly payment:  2325.833625124826
    Interest paid:  87650.05252246873

```python
N=15
m=15*12
i=0.031
P=370000.

F = F_monthly(P, i, m)
```

    Total:  463137.2410848306
    Monthly payment:  2572.9846726935034
    Interest paid:  93137.2410848306

```python
# what about at 6.5 percent?
i=0.065

F = F_monthly(P, i, m)

```

    Total:  580157.5052880454
    Monthly payment:  3223.097251600252
    Interest paid:  210157.5052880454

```python
# Same house, $331,000, 30 yr fixed rate 4.25%
N=30
m=N*12
i=0.0425

F = F_monthly(P, i, m)
```

    Total:  655263.9349178623
    Monthly payment:  1820.177596994062
    Interest paid:  285263.9349178623

```python
# How about at 6.5% interest?
i=0.065

F = F_monthly(P, i, m)
```

    Total:  841914.6072926298
    Monthly payment:  2338.6516869239717
    Interest paid:  471914.6072926298

### Exercise: Paying down the principal

Okay, so, those big banks don't need all your money. It would be great to take their nice low interest rate and pay off the principal as quickly as possible so you aren't paying interest on top of quite so much interest. In buying houses, sometimes a bank won't even charge a fee for paying off your loan faster than intended. Can you derive a formula for how much this kind of behavior will save you? In the case of the mortgate above, let's assume you pay an extra \$50 a month, every month. How much faster could you pay off your loan? How much money do you save?

```python
# do your calculations here

```

## Annuities

Annuities: a series of equal dollar payments coming at the end of a certain time period for a specified number of time periods (n). Examples include mortgages, life insurance benefits, lottery payments, retirement payments.

### Compound annuities

Compound Annuities: depositing an equal sum of money at the end of each time period for a certain number of periods and allowing the money to grow. An example might be having $\$50$ taken out of each paycheck and put in a Christmas account earning a $9\%$ Annual Percentage Rate.

### Future value of an annuity (FVA) equation

This equation is used to determine the future value of a stream of deposits/payments (PMT) invested at a specific interest rate (i), for a specific number of periods (n). For example, the future value of your 401k contributions can be calculated using this equation:

$$
\begin{align}
FVA &= PMT\left(\frac{(1 + i)^n – 1}{i}\right)\\
FVA &= \mbox{the future value, in today’s dollars, of a sum of
money}\\
PMT &= \mbox{the payment made at the end of each time period}\\
i &= \mbox{interest rate}\\
n &= \mbox{number of periods}
\end{align}
$$

```python
def fva(pmt, i, n):
    """the future value, in today’s dollars, of a sum of money
    :param pmt: the payment made at the end of each time period
    :param i: interest rate
    :param n: number of periods
    """
    num = pmt*(pow((1+i),n) - 1)
    return num/i
```

### Exercise
Assuming a $2000 annual contribution with a 9% rate of return, how much will an IRA be worth in 30 years?

$$
\begin{align}
FVA &= PMT\left(\frac{(1 + i)^n – 1}{i}\right)\\
\end{align}
$$

```python
# Assuming a $2000 annual contribution
pmt = 2000
# with a 9% rate of return,
i=0.09
# how much will an IRA be worth in 30 years?
n=30
ira_val = fva(pmt, i, n)
print(ira_val)

```

    272615.07709180616

### Exercise : Anna's savings
Each month, Anna deposits her paycheck ($5,000) in an account offering a monthly interest rate of 6%. How much will Anna have in her account at the end of 1 year?

$$
\begin{align}
FVA &= PMT\left(\frac{(1 + i)^n – 1}{i}\right)\\
\end{align}
$$

```python
# Assuming a $5000 monthly contribution
pmt=5000

# with a 6% monthly rate of return,
i = 0.06

# how much will her account have in it in 1 year?
n=12

ira_val = fva(pmt, i, n)
print(ira_val)
```

    84349.70598629594

### Exercise : Startup savings

- If a garage startup deposits $1,200 each year into a savings account earning an Annual Rate of return of 2% for 15 years, how much will the startup have at the end of the 15 years?

$$
\begin{align}
FVA &= PMT\left(\frac{(1 + i)^n – 1}{i}\right)\\
\end{align}
$$

```python
# Assuming a $1200 yearly contribution
pmt = 1200

# with a 2% annual rate of return,
i = 0.02

# how much will the account have in it in 15 years?
n =15

ira_val = fva(pmt, i, n)
print(ira_val)
```

    20752.100299447793

- How much will the startup have if it deposits $1,200 each **month**?

$$
\begin{align}
FVA &= PMT\left(\frac{(1 + i)^n – 1}{i}\right)\\
\end{align}
$$

```python
# Assuming a $1200 monthly contribution
pmt = 1200
# with a 2% annual rate of return,
i = 0.02/12
# how much will the account have in it in 15 years?
n=15*12

ira_val = fva(pmt, i, n)
print(ira_val)
```

    251655.66680604088

- How much will the garage have if they **earn 2% interest monthly**?

```python
# Assuming a $1200 monthly contribution
pmt=1200
# with a 2% monthly rate of return,
i = 0.02
# how much will the account have in it in 15 years?
n=15*12

ira_val = fva(pmt, i, n)
print(ira_val)
```

    2059249.8814193557

This example demonstrates why you should exercise extreme caution with these equations to make double sure your time frames are consistent.

- If the payment is a monthly payment; then the compounding rate of return has to be a monthly rate of return.
- Example: A 15% ANNUAL rate of return is equal to a monthly rate of return of 1.25% (because 15/12 = 1.25)

### Present value of an annuity (PVA) equation

This equation is used to determine the present value of a future stream of payments, such as your pension fund or insurance benefits.

$$
\begin{align}
PVA &=PMT\frac{1–(1+i)^{-n}}{i}\\
PVA &= \mbox{the present value, in today’s dollars, of a sum of money}\\
PMT &= \mbox{the payment to be made at the end of each time period}\\
i &= \mbox{interest rate}\\
n &= \mbox{number of periods}
\end{align}
$$

```python
def pva(pmt, i, n):
    """the present value, in today’s dollars, of a sum of money
    :param PMT: the payment to be made at the end of each time period
    :param i: interest rate
    :param n: number of periods
    """
    num =1 - pow((1+i),-n)
    return (pmt/i)*num
```

### Exercise: Retirement Payout
What is the present value of 25 annual payments of $50,000 offered to a retiring employee, assuming a 5% annual discount rate? (PVA is the only unknown)

```python
pva(5e5, 0.05, 25)
```

    7046972.283022381

**Warning!!**
Don't use the wrong equation! The difference between future value and present value is massive. The same inputs to the fva equation is a much larger number!

```python
fva(5e5, 0.05, 25)
```

    23863549.40899388

We can solve the PVA equation to give a solution for the payment itself in terms of the present value.

$$
\begin{align}
PMT &=\frac{1}{PVA}\frac{1–(1+i)^{-n}}{i}
\end{align}
$$

With this equation, for example, one can determine what size payment one needs to make. What are the annual payments to repay $\$6,000$ at $5\%$ APR interest? (the payment is the unknown)

```python
def pmt(pva, i, n):
    return pva*i/((1-pow((1+i), -n)))
```

```python
pva = 6000
i = 0.05
n = 4
print(pmt(pva,i,n))
```

    1692.0709956207754

## Bonds

A bond is a type of security instrument used to raise capital by an issuing party (an issuer).

They have the following characteristics :
- A **principal** amount to be repaid on a specific date in the future.
- The principal amount is also known as the face value or par value
- The payment date is known as the maturity date
- Many bonds have regular coupon payments which are paid-out annually, semi-annually or quarterly.
- The coupon rate is the interest rate used to calculate the coupon payment and is a percentage of the principal amount
- Coupon Payment = Coupon Rate x Principal
- A bond is a **legal debt obligation**. Failure to make payments as required can result in legal recourse by the holders of the bonds.
- A bond may be **callable** by the issuer. That is, after some specified amount of time or some specified event, the issuer can purchase the bonds back from the market. Typically the issuer will have to pay some type of penalty for this early re-call.
    -
- **Issuer:** the entity that has promised payment
  - The issuance of bonds is known as a **bond offering**
  - The issuer may offer thousands or millions of bonds in the single offering.
- **Holder** - the party who currently has possession of the bond
  - The holding party receives the payments from the issuer
  - Often, the holding party can freely sell the bond to a third-party and all rights will transfer
- Effectively, a bond is a loan.
- When issued, some bonds simply sell for their face value.
- In this case,the company would convey the bond to the buyer in exchange for receiving $\$1,000$.
- Assuming the buyer (now the holder) holds the bond to maturity, they will receive 5 annual payments of $\$50$ and a final payment of $\$1,000$ after 5 years.
- In other words, the buyer receives annual interest payments and finally the return of his principal.

### Bonds: example

A company wants to raise money for a new project and decides to do so by issuing bonds
- Principal is $\$1,000$
- Maturity Date is 5 years from issuance
- Coupon Rate is 5%
- Coupon Payments are made annually at end of year
- Coupon Payment is (Principal x Coupon Rate) = $1000 \times 5\% = \$50$

```python
def coupon_pmt(principal, coupon_rate):
    return principal*coupon_rate
```

```python

from pandas import DataFrame as df

def payout(principal, mat_date, coupon_rate, year):
    pay = 0
    if 0 < year <= mat_date:
        pay = coupon_pmt(principal, coupon_rate)
        if year == mat_date:
            pay += principal
    return pay

def payouts(principal, mat_date, coupon_rate):
    y = 0
    pay = np.zeros((mat_date+1, 1))
    while y <= mat_date :
        pay[y] = payout(principal, mat_date, coupon_rate, y)
        y+=1
    return pay

i = 0.05
coupon_rate = 0.05
principal = 1000
mat_date = 5
pay = payouts(1000, 5, 0.05)
print(pay)
```

    [[    0.]
     [   50.]
     [   50.]
     [   50.]
     [   50.]
     [ 1050.]]

### Bonds: Valuation
- Some bonds are issued at a price equal to their face value.
- Many bonds, however, are issued at a price higher or lower than their face value.
- Ultimately, the market (i.e. supply and demand) determines a bond’s price. Sometimes it is willing to pay more than face value, other times less.

**Discussion** Why? What drives this? Can you explain this with what we've learned already?

## Bonds: Present Value Interest Factor

The Present Value Interest Factor reflects the impact of the time value of money on this investment. Money now is worth more than money later.

$$
PVIF = (1 + i)^{-n}
$$

The Present Value (P), is then:

$$
P = F(1+i)^{-n}
$$

Look familiar? This is just the discounting equation we have already seen.

```python
import pandas as pd
bv = pd.DataFrame() # This is a data structure to hold our bond valuation information

i = 0.05
coupon_rate = 0.05
principal = 1000
mat_date = 5

bv['year'] = range(0, mat_date+1)
bv['payout'] = [payout(principal, mat_date, coupon_rate, y) for y in bv['year']]
print(bv)
```

       year  payout
    0     0     0.0
    1     1    50.0
    2     2    50.0
    3     3    50.0
    4     4    50.0
    5     5  1050.0

```python
bv['pvif'] = [pow(1+i, -y) for y in bv['year']]
print(bv)
```

       year  payout      pvif
    0     0     0.0  1.000000
    1     1    50.0  0.952381
    2     2    50.0  0.907029
    3     3    50.0  0.863838
    4     4    50.0  0.822702
    5     5  1050.0  0.783526

```python
bv['p'] = bv['payout']*bv['pvif']
print(bv)
```

       year  payout      pvif           p
    0     0     0.0  1.000000    0.000000
    1     1    50.0  0.952381   47.619048
    2     2    50.0  0.907029   45.351474
    3     3    50.0  0.863838   43.191880
    4     4    50.0  0.822702   41.135124
    5     5  1050.0  0.783526  822.702475

```python
bv['p'].cumsum()
```

    0       0.000000
    1      47.619048
    2      92.970522
    3     136.162401
    4     177.297525
    5    1000.000000
    Name: p, dtype: float64

```python
plt.clf()
bv[['payout','p']].cumsum().plot()

```

    <IPython.core.display.Javascript object>

    <matplotlib.axes._subplots.AxesSubplot at 0x10cbd40f0>

### Bonds: Valuation Conclusion
Based on the Time Value of Money, then, we should be willing to pay $\$1,000$ to receive five annual payments of $\$50$ and a final payment of $\$1,000$ after five years, assuming an interest rate of $5\%$. When the bond valuation is equal to the face value, we say the bond is **“selling at _par_ value.”**

### Exercise: Question the Assumptions

What if the interest rate assumption was wrong? How does a higher rate impact the attractiveness of investing in this bond? What about a lower rate?

Keeping everything else the same, change the interest rate, i, and re-analyze the valuation of the bond. At an interest rate of 7%, should you still buy it at a face value of $\$1000$?

What about a 4% interest rate?

```python
i = 0.04
bv['pvif'] = [pow(1+i, -y) for y in bv['year']]
bv['p'] = bv['payout']*bv['pvif']
print(bv['p'].cumsum())
```

    0       0.000000
    1      48.076923
    2      94.304734
    3     138.754552
    4     181.494761
    5    1044.518223
    Name: p, dtype: float64

When the bond valuation is below the face value we say the bond is “selling at discount to par value.”

When the bond valuation is below the face value we say the bond is “selling at premium to par value.”

### A note about Interest Rates

I keep using the term, “interest rate” and it appears to mean different things depending on the use. Unfortunately, there are many terms for the same concept. And there are many concepts that use the same name.
- Back in our bond calculations, the market interest rate which was the denominator of our TVM analysis is also known as the “yield to maturity” (YTM) or simply “yield”.
- People will also use the term “discount rate” or “discount factor” because we are discounting our future cash flows back to the present at this rate

To make it even more confusing, there are many different interest rates in an economy
- The interest rate the government is charged to borrow money is lower than the interest rate I am charged on my credit card
- Companies with good investment opportunities and lots of cash have lower interest rates for their corporate debt than companies few growth opportunities and no cash.
- Interest rates for the exact same security will change over time
- Interest rates of identical securities with different times to maturity will be different
- But in all these cases, the interest rate will increase when a given risk increases and decrease when a given risk decreases.

The logic is simple:
- In the face of multiple investment or lending opportunities, if we are not compensated for additional risk we will always put our money in the least risky opportunity.
- We need to be induced to invest or lend to the riskier situation by the promise of higher returns.

### The Fed.

The growth of money, being tied to so closely to the market and to the rate at which banks can get loans, is strongly affected by the Federal Reserve. [Try being the Fed](http://sffed-education.org/chairthefed/). Can you keep us on an even keel?

## Additional info on Bond pricing, rates paid by utilities to print, handle, etc.

Bonds aren't sold out of thin air. Importantly, the price of the bond should also reflect the sometimes small, sometimes appreciable, expenditures required to print and manage the bond sales. Contemplate the impact of selling many small bonds vs. a few large bonds. How might this choice impact those expenditures?

## Key Lessons

We have covered the following key concepts in this notebook:

- \$x today is more valuable than \$x tomorrow (time value of money)
- The future value with simple interest is $F_{simple} =  P(1+iN)$
- The future value with compounding interest is $F_{compound} = P(1+i)^N$
- The principal P required to acheive a certain F is $P_{compound} = \frac{F}{(1+i)^N}$
- Inflation is a direct result of the time value of money and can be derived from $F_{simple}$
- Inflation adjusted interest rate is $i^* = \frac{(1+i)}{(1+r)} - 1$ where r is inflation rate, i is nominal interest rate.
- $i_{MARR}$ is the lowest interest rate $i$ one is willing to get for investing their money, given known $F$ and $P$.
- Amortized payments (like monthly car payments) make this whole thing much more complex.
- Annuities are repeated payments, and compound annuities receive compounded interest.
- The future value of a compound annuity in today's dollars is $FVA = PMT\frac{(1+i)^n-1}{i}$
- Solving for the payment can give an idea of the real value: $PMT = FVA\frac{i}{(1+i)^n-1}$
- The present value of a compound annuity in today's dollars is $PVA = PMT\frac{1 - (1+i)^{-n}}{i}$
- Solving for the payment can give an idea of obligations: $PMT = PVA\frac{i}{1 - (1+i)^{-n}}$
- The book's notation for payments for the present value of an annuity is : $\frac{F}{N} = \left[ \frac{i(1+i)^n}{(1+i)^n - 1} \right]P$
- With rearrangement, that is the same as: $\frac{F}{N} = \left[ \frac{i}{1 - (1+i)^{-n}} \right]P$.

## Next Topic

Over the next week, we'll be talking about :

- The Levelized Cost of Electricity

```python

```


<h1>Dense Coding
</h1>

```
%load_ext sympyprinting
```

```
from sympy import sqrt, symbols, Rational
from sympy import expand, Eq, Symbol, simplify, exp, sin
from sympy.physics.quantum import *
from sympy.physics.quantum.qubit import *
from sympy.physics.quantum.gate import *
from sympy.physics.quantum.grover import *
from sympy.physics.quantum.qft import QFT, IQFT, Fourier
from sympy.physics.quantum.circuitplot import circuit_plot
```

```
psi = Qubit('00')/sqrt(2) + Qubit('11')/sqrt(2); psi

```

```
circuits = [H(1)*CNOT(1,0), H(1)*CNOT(1,0)*X(1), H(1)*CNOT(1,0)*Z(1), H(1)*CNOT(1,0)*Z(1)*X(1)]
```

```
for circuit in circuits:
    circuit_plot(circuit, nqubits=2)
    display(Eq(circuit*psi,qapply(circuit*psi)))
```


# Learning from data: Error propagation and nuisance parameters

**Christian Forssén**, Department of Physics, Chalmers University of Technology, Sweden

Date: **Sep 24, 2019**

Copyright 2018-2019, Christian Forssén. Released under CC Attribution-NonCommercial 4.0 license

# Why Bayes is Better

### Quotes from one pioneering and one renaissance Bayesian authority

Laplace:
> *"Probability theory is nothing but common sense reduced to calculation."*

Sivia
> *"Bayesian inference probabilities are a measure of our state of knowledge about nature, not a measure of nature itself."*

## Advantages of the Bayesian approach
1. Provides an elegantly simple and rational approach for answering, in an optimal way, any scientific question for a given state of information. This contrasts to the recipe or cookbook approach of conventional statistical analysis. The procedure is well-defined:

  * Clearly state your question and prior information.

  * Apply the sum and product rules. The starting point is always Bayes’ theorem.

2. For some problems, a Bayesian analysis may simply lead to a familiar statistic. Even in this situation it often provides a powerful new insight concerning the interpretation of the statistic.

3. Incorporates relevant prior (e.g., known signal model or known theory model expansion) information through Bayes’ theorem. This is one of the great strengths of Bayesian analysis.

  * For data with a small signal-to-noise ratio, a Bayesian analysis can frequently yield many orders of magnitude improvement in model parameter estimation, through the incorporation of relevant prior information about the signal model.

4. Provides a way of eliminating nuisance parameters through marginalization. For some problems, the marginalization can be performed analytically, permitting certain calculations to become computationally tractable.

5. Provides a way for incorporating the effects of systematic errors arising from both the measurement operation and theoretical model predictions.

6. Calculates probability of hypothesis directly: $p(H_i|D, I)$.

7. Provides a more powerful way of assessing competing theories at the forefront of science by automatically quantifying Occam’s razor.

The Bayesian quantitative Occam’s razor can also save a lot of time that might otherwise be spent chasing noise artifacts that masquerade as possible detections of real phenomena.

### Occam's razor

Occam’s razor is a principle attributed to the medieval philosopher William of Occam (or Ockham). The principle states that one should not make more assumptions than the minimum needed. It underlies all scientific modeling and theory building. It cautions us to choose from a set of otherwise equivalent models of a given phenomenon the simplest one. In any given model, Occam’s razor helps us to "shave off" those variables that are not really needed to explain the phenomenon. It was previously thought to be only a qualitative principle.

Did the Leprechaun drink your wine, or is there a simpler explanation?

## Nuisance parameters (I)
See demonstration notebook: A Bayesian Billiard game

## Nuisance parameters (II): marginal distributions
Assume that we have a model with two parameters, $\theta0,\theta_1$, although only one of them (say $\theta_1$) is of physical relevance (the other one is a nuisance parameter). Through a Bayesian data analysis we have the joint, posterior pdf

$$
p(\theta_0, \theta_1 | D, I).
$$

The marginal posterior pdf $p(\theta_1 | D, I)$ is obtained via marginalization

$$
p(\theta_1 | D, I) = \int p(\theta_0, \theta_1 | D, I) d\theta_0.
$$

Assume that we have $N$ samples from the joint pdf. This might be the Markov Chain from an MCMC sampler: $\left\{ (\theta_0, \theta_1)_i \right\}_{i=0}^{N-1}$. Then the marginal distribution of $\theta_1$ will be given by the same chain by simply ignoring the $\theta_0$ column, i.e., $\left\{ \theta_{1,i} \right\}_{i=0}^{N-1}$.

See the interactive demos created by Chi Feng for an illustration of this: [The Markov-chain Monte Carlo Interactive Gallery](https://chi-feng.github.io/mcmc-demo/).

## Error propagation (I): marginalization
The Bayesian approach offers a straight-forward approach for dealing with (known) systematic uncertainties; namely by marginalization. Let us demonstrate this with an example

**Inferring galactic distances with an imprecise knowledge of the Hubble constant**
The Hubble constant acts as a galactic ruler as it is used to measure astronomical distances according to $v = H_0 x$. An error in this ruler will therefore correspond to a systematic uncertainty in such measurements.

Here we use marginalization to obtain the desired posterior pdf $p(x|D,I)$ from the joint distribution of $p(x,H_0|D,I)$

$$
\begin{equation}
p(x|D,I) = \int_{-\infty}^\infty dH_0 p(x,H_0|D,I).
\label{eq:marginalization} \tag{1}
\end{equation}
$$

Using Bayes' rule: $p(x,H_0|D,I) \propto p(D|x,H_0,I) p(x,H_0|I)$, the product rule: $p(x,H_0|I) = p(H_0|x,I)p(x|I)$, and the fact that $H_0$ is independent of $x$: $p(H_0|x,I) = p(H_0|I)$, we find that

$$
p(x|D,I) \propto p(x|I) \int dH_0 p(H_0|I) p(D|x,H_0,I),
$$

which means that we have expressed the quantity that we want (the posterior of $x$) in terms of quantities that we know.

Assume that the pdf $p(H_0 | I)$ is known via its $N$ samples $\{H_{i}\}_{i=0}^{N-1}$ generated by the MCMC sampler.

This means that we can approximate

$$
p(x |D,I) \propto \int dH_0 p(H_0|I) p(D|x,H_0,I) \approx \frac{1}{N} \sum_{i=1}^N p(D | x, H_i, I)
$$

where we have used a uniform prior for the distance $p(x|I) \propto 1$.

## Error propagation (II): changing variables and prior information
(Based on Sivia, ch 3.6.)

Assume that we have measured parameter $X = 10 \pm 3$ and $Y=7 \pm 2$; what can we say about the difference $X-Y$ or the raio $X/Y$, or the sum of their squares $X^2+Y^2$, etc? In essence, the problem is nothing more than an exercise in the change of variables: given the joint pdf $p(X,Y|I)$, where the information $I$ might include the data if the pdf is a posterior from a data analysis, we need the corresponding pdf $p(Z|I)$, where $Z=X-Y$, or $Z=X/Y$, or whatever as appropriate.

Let us start with a single variable $X$ and a function $Y=f(X)$. How is $p(X|I)$ related to $p(Y|I)$?

Consider a point $X^*$ and a small interval $\delta X$ around it. The probability that $X$ lies within that interval can be written

$$
p \left( X^* - \frac{\delta X}{2} \le X < X^* + \frac{\delta X}{2} \big| I \right)
\approx p(X=X^*|I) \delta X.
$$

Assume now that the function $f$ will map the point $X=X^*$ uniquely onto $Y=Y^*=f(X^*)$. Then there must be an interval $\delta Y$ around $Y^*$ so that the probability is conserved

$$
p(X=X^*|I) \delta X = p(Y=Y^*|I) \delta Y.
$$

In the limit of infinitesimally small intervals, and with the realization that this should be true for any point $X$, we obtain the relationship

$$
\begin{equation}
p(X|I) = p(Y=Y|I) \left| \frac{dY}{dX} \right|,
\label{eq:transformation} \tag{2}
\end{equation}
$$

where the term on the far right is called the *Jacobian*.

The generalization to several variables, relating the pdf for $M$ variables $\{ X_j \}$ in terms of the same number of quantities $\{ Y_j \}$ related to them, is

$$
\begin{equation}
p(\{X_j\}|I) = p(\{Y_j\}|I) \left| \frac{\partial (Y_1, Y_2, \ldots, Y_M)}{\partial (X_1, X_2, \ldots, X_M)} \right|,
\label{eq:multivariate-transformation} \tag{3}
\end{equation}
$$

where the multivariate Jacobian is given by the determinant of the $M \times M$ matrix of partial derivatives $\partial Y_i / \partial X_j$.

**Summary.**

We have now seen the basic ingredients required for the propagation of errors: it either involves a transformation in the sense of Eq. ([3](#eq:multivariate-transformation)) or an integration as in Eq. ([1](#eq:marginalization)).

### A useful short cut

For practical purposes, we are often satisfied to approximate pdfs with Gaussians. Within such limits there is an easier method that is often used for error propagation. Note, however, that there are instances when this method fails miserably as will be shown in the example further down.

Suppose that we have summarized the pdfs $p(X|I)$ and $p(Y|I)$ as two Gaussians with mean and standard deviation $x_0, \sigma_x$ and $y_0, \sigma_y$, respectively. Assume further that these two variables are not correlated, i.e., $p(X,Y|I) = p(X|I) p(Y|I)$.

Suppose now that we are interested in $Z=X-Y$. Intuitively, we might guess that the best estimate $z_0 = x_0 - y_0$, but the error bar $\sigma_z$ requires some more thought. Differentiate the relation

$$
\delta Z = \delta X - \delta Y.
$$

Square both sides and integrate to get the expectation value

$$
\langle \delta Z^2 \rangle = \langle \delta X^2 + \delta Y^2 - 2 \delta x \delta Y \rangle = \langle \delta X^2 \rangle + \langle  \delta Y^2 \rangle - 2 \langle \delta X \delta Y \rangle,
$$

where we have employed the linear property for an integral over a sum of terms.

Since we assumed that the pdfs for $X$ and $Y$ were described by independent Gaussians we have

$$
\begin{equation}
\langle \delta X^2 \rangle = \sigma_x^2; \qquad \langle \delta Y^2 \rangle = \sigma_y^2; \qquad \langle \delta X \delta Y \rangle = 0,
\label{eq:stddev} \tag{4}
\end{equation}
$$

and we find that

$$
\sigma_z = \sqrt{ \langle \delta Z^2 \rangle } = \sqrt{ \sigma_x^2 + \sigma_y^2 }.
$$

Consider, as a second example, the ratio of two parameters $Z = X/Y$. Differentiation gives

$$
\delta Z = \frac{Y \delta X - X \delta Y}{Y^2} \quad \Leftrightarrow \quad \frac{\delta Z}{Z} = \frac{\delta X}{X} - \frac{\delta Y}{Y}.
$$

Squaring both sides and taking the expectation values, we obtain

$$
\frac{\langle \delta Z^2 \rangle}{z_0^2} = \frac{\langle \delta X^2 \rangle}{x_0^2} + \frac{\langle \delta Y^2 \rangle}{y_0^2} - 2 \frac{\langle \delta X \rangle \langle \delta ZY \rangle}{x_0 y_0},
$$

where the $X$, $Y$ and $Z$ in the denominator have been replaced by the constants $x_0$, $y_0$ and $z_0 = x_0 / y_0$ because we are interested in deviations from the peak of the pdf.

Finally, substituting the information for the pdfs of $X$ and $Y$ as summarized in Eq. ([4](#eq:stddev)) we finally obtain the propagated error for the ratio

$$
\frac{\sigma_z}{z_0} = \sqrt{ \left( \frac{\sigma_x}{x_0} \right)^2 + \left( \frac{\sigma_y}{y_0} \right)^2}.
$$

Despite its virtues, let us end our discussion of error-propagation with a salutary warning against the blind use of this nifty short cut.

### Example: Taking the square root of a number

(Example 3.6.2 in Sivia)

* Assume that the amplitude of a Bragg peak is measured with an uncertainty $A = A_0 \pm \sigma_A$ from a least-squares fit to experimental data.

* The Bragg peak amplitude is proportional to the square of a complex structure function: $A = |F|^2 \equiv f^2$.

* What is $f = f_0 \pm \sigma_f$?

Obviously, we have that $f_0 = \sqrt{A_0}$. Differentiate the relation, square and take the expectation value

$$
\langle \delta A^2 \rangle = 4 f_0^2 \langle \delta f^2 \rangle \quad
\Leftrightarrow \quad
\sigma_f = \frac{\sigma_A}{2 \sqrt{A_0}},
$$

where we have used the Gaussian approximation for the pdfs.

But what happens if the best fit gives $A_0 < 0$, which would not be impossible if we have weak and strongly overlapping peaks. The above equation obviously does not work since $f_0$ would be a complex number.

We have made two mistakes:
1. Likelihood is not posterior!

2. The Gaussian approximation around the peak does not always work.

Consider first the best fit of the signal peak. It implies that the likelihood can be approximated by

$$
p(D | A, I) \propto \exp \left[ -\frac{(A-A_0)^2}{2\sigma_A^2} \right].
$$

However, the posterior for $A$ is $p(A|D,I) \propto p(D|A,I) p(A|I)$ and we should use the fact that we know that $A \ge 0$.

We will incorporate this information through a simple step-function prior

$$
p(A|I) = \left\{
\begin{array}{ll}
\frac{1}{A_\mathrm{max}}, & 0 \le A \le A_\mathrm{max}, \\
0, & \mathrm{otherwise}.
\end{array}
\right.
$$

This implies that the posterior will be a truncated Gaussian, and its maximum will always be above zero.

This also implies that we cannot use the Gaussian approximation. Instead we will do the proper calculation using the transformation ([2](#eq:transformation))

$$
p(f|D,I) = p(A|D,I) \left| \frac{dA}{df} \right| = 2 f p(A|D,I)
$$

In the end we find the proper Bayesian error propagation given by the pdf

$$
p(f|D,I) \propto \left\{
\begin{array}{ll}
f \exp \left[ -\frac{(A-A_0)^2}{2\sigma_A^2} \right], & 0 \le f \le \sqrt{A_\mathrm{max}}, \\
0, & \mathrm{otherwise}.
\end{array}
\right.
$$

Let us visualize the difference between the Bayesian and the naive error propagation for a few scenarios.

```
def A_posterior(A,A0,sigA):
    pA = np.exp(-(A-A0)**2/(2*sigA**2))
    return pA/np.max(pA)

# Wrong analysis
def f_likelihood(f,A0,sigA):
    sigf = sigA / (2*np.sqrt(A0))
    pf = np.exp(-(f-np.sqrt(A0))**2/(2*sigf**2))
    return pf/np.max(pf)

# Correct error propagation
def f_posterior(f,A0,sigA):
    pf = f*np.exp(-(f**2-A0)**2/(2*sigA**2))
    return pf/np.max(pf)
```

```
for (A0,sigA) in [(9,1),(1,9),(-20,9)]:
    maxA = max(2*A0,3*sigA)
    A_arr = np.linspace(0.01,maxA)
    f_arr = np.sqrt(A_arr)
    fig,ax=plt.subplots(1,2,figsize=(10,4))
    ax[0].plot(A_arr,A_posterior(A_arr,A0,sigA))
    ax[1].plot(f_arr,f_posterior(f_arr,A0,sigA),label='Bayesian')
    if A0>0:
        ax[1].plot(f_arr,f_likelihood(f_arr,A0,sigA),'--',label='Naive')
    ax[0].set(xlabel='A',ylabel='p(A|D,I)')
    plt.text(0.55,0.8,f'$A={A0}$, $\sigma_A={sigA}$', transform=ax[0].transAxes,fontsize=16)
    ax[1].set(xlabel='f',ylabel='p(f|D,I)')
    ax[1].legend(loc='best')
    plt.tight_layout()
```

The left-hand panels show the posterior pdf for the amplitude of a Bragg peak in three different scenarios. The right-hand plots are the corresponding pdfs for the modulus of the structure factor $f=\sqrt{A}$. The solid lines correspond to a full bayesian error propagation, while the dashed lines are obtained with the short-cut error propagation.


```python
import pandas as pd
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
import seaborn as sns
from numpy import genfromtxt
from numpy import linalg as LA
import scipy as sp
import sympy
```

```python
input_data = pd.read_csv('a6n.csv', index_col=0)
G = nx.Graph(input_data.values)
```

### how many driver node we have?

```python
def number_of_driver_nodes(G):

    N = G.number_of_nodes()
    A = (nx.adjacency_matrix(G)).todense()  # get adjacency matrix A of G
    all_eigs = LA.eigvals(A)           # get eigenvalues of A

    lambda_i = list(set(np.round(all_eigs,8)))
    #lambda_i = list(set(all_eigs))

    driver_nodes_num = -1
    lambda_m = -1
    IN = np.eye(N)
    n = len(lambda_i)
    miu_lambda =np.zeros(n)

    for i in range(0,n):

        miu_lambda[i] = N - LA.matrix_rank(lambda_i[i] * IN - A, tol=1E-6)
        if miu_lambda[i] > driver_nodes_num:
            driver_nodes_num = miu_lambda[i]
            lambda_m = lambda_i[i]

    return (driver_nodes_num, lambda_m)

```

### Which node is a driver node?

```python
def get_driver_nodes(G):

    N = G.number_of_nodes()
    A = (nx.adjacency_matrix(G)).todense()  # get adjacency matrix A of G
    all_eigs = LA.eigvals(A)           # get eigenvalues of A

    lambda_i = list(set(np.round(all_eigs,8)))
    #lambda_i = list(set(all_eigs))

    driver_nodes_num = -1
    lambda_m = -1
    IN = np.eye(N)
    n = len(lambda_i)
    miu_lambda =np.zeros(n)

    for i in range(0,n):

        miu_lambda[i] = N - LA.matrix_rank(lambda_i[i] * IN - A, tol=1E-8)
        if miu_lambda[i] > driver_nodes_num:
            driver_nodes_num = miu_lambda[i]
            lambda_m = lambda_i[i]

    middle_matrix = lambda_m * np.eye(N) - A   # get the middle matrix A - \lambda * I_N
    middle_matrix = np.round(middle_matrix, 8)
    reduced_matrix,pivot_array1=sympy.Matrix(middle_matrix).rref()

    reduced_matrix_array = np.array(reduced_matrix).astype(np.float64)
    reduced_matrix_array_transpose=np.matrix.transpose(reduced_matrix_array)
    _, pivot_array2 = sympy.Matrix(reduced_matrix_array_transpose).T.rref()
    all_nodes = G.nodes()
    pivot_nodes = [all_nodes[i] for i in pivot_array2]
    t=0
    N_d = N-len(pivot_array2)
    driver_nodes = np.zeros(N_d)
    for i in all_nodes:
        if i in pivot_array2:
            pass
        else:
            driver_nodes[t] = i
            t = t+1

    return (driver_nodes_num, driver_nodes)

```

## Example

```python
mat = np.array(
    [[-1.0, 1.0, 1.0,1.0,1.0,1.0],
      [1.0, 0.0, 0.0,0.0,0.0, 0.0],
      [1.0, 0.0, 0.0,0.0,0.0 ,0.0],
      [1.0, 0.0, 0.0,-1.0,0.0,0.0],
      [1.0, 0.0, 0.0,0.0,-1.0,1.0],
      [1.0, 0.0, 0.0,0.0,1.0,-1.0]
    ])
G2 = nx.Graph(mat)
```

```python
input_data = pd.read_csv('Book2.csv', index_col=0)
G3 = nx.Graph(input_data.values)
```

```python
number_of_driver_nodes(G3)
```

    (2.0, 0.0)

```python
get_driver_nodes(G3)
```

    (2.0, array([2., 5.]))

```python

```

```python

```