## RISE
## Turn your notebooks into slideshows
Use jupyter to create a slideshow

Use the space bar to advance to the next slide

### Goals
- Turn an existing Jupyter notebook into a slide show
- Use Jupyter to create a slide show - *It does not have to start out as a notebook*
- Embed executable code into slides
- Add images to slides
- Even embed youtube videos!

Shift-Space to go backward, Space to go forward

## Slide types - Slide Show Domain

- Slide - A normal slide

- Sub-Slide - A slide that is "below" the previous slide

- Fragment - A piece of a slide that appears on the existing slide

- Skip - A cell that will not appear in the slide show

- Notes - A slide that shows up in the presenter view only
  - press "t" to see the presenter notes

If you pressed "t", you will now see the presenter view.

This slide combined a slide cell and 6 fragments

This cell will NOT appear in the slide show because it is designed to be skipped.

## Cell Types - Jupyter Notebook Domain

- Code - Contains code in some language that will be executed by a kernel

```python
# Shift-Enter to execute this code
print('Jup ter and RISE - reveal.js work really well together')
print("Note - don't forget to fix this bug.")
```

    Jup ter and RISE - reveal.js work really well together
    Note - don't forget to fix this bug.

- Markdown - A cell that is formatted
  - Generally consistently with GitHub markdown syntax
  - Can contain some embedded HTML syntax

- Markdown - A cell that is formatted
  - Generally consistently with GitHub markdown syntax
  - Can contain some embedded HTML syntax
- Raw NBConvert
  - Displayed as is.
  - Not executable, though it might look like code.
  - Not formatted.
  - Used above to show how markdown looks before executing it.

## Documentation for RISE
[ReadTheDocs for RISE](https://rise.readthedocs.io/en/latest/index.html)

The docs tell you two ways to install it locally and how to use it.

```python
# press Shift-Enter to execute this code

import numpy as np
import matplotlib.pyplot as plt
plt.ion()

"Hello world"
```

    'Hello world'

```python
# of course you can show figures

def polynom(x):
    return 2 * x**2 - 20 * x + 2

X = np.linspace(-10, 10)
Y = polynom(X)
plt.plot(X, Y);
```

```python
# and everything works as usual

# an animation to illustrate
# translation by variable change
from ipywidgets import interact, FloatSlider

def parabolic(offset):
    X = np.linspace(-10, 10)
    Y = polynom(X-offset)
    # use same y scale for all offsets
    plt.gca().set_ylim([-100, 500])
    plt.plot(X, Y);

interact(parabolic,
         offset=FloatSlider(min=-10., max=10.,
                           step=0.25));
```

    interactive(children=(FloatSlider(value=0.0, description='offset', max=10.0, min=-10.0, step=0.25), Output()),…

### How do we use the notebook to set up slides?

### Step 1
- View, Cell Toolbar, Slideshow
- This should create a selection for each cell
- Two ways to view an image

```python

```

### Step 2
  - Select the slide types to meet the needs of your slide show

### Step 3
- Run the slide show to see what we have so far.
- Click on the first cell then
- Click on the Enter/Exit RISE Slideshow icon in the menu bar far right or Alt-R
- Click on space bar to advance through the slides

### This is a sub-slide
- Navigate to it with the space bar, or
- click the down arrow on the bottom left when running the show.

A key feature is that the slide show does not need to be static.
We can even add some code that is too big for a slide, and execute it.
If the code or output is too "tall" we will have a scroll bar.

## Let's display some mathematical equations

### Maxwell's Equations

\begin{align}
\nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\   \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
\nabla \cdot \vec{\mathbf{B}} & = 0
\end{align}

### The probability of getting (k) heads when flipping (n) coins is

\begin{equation*}
P(E)   = {n \choose k} p^k (1-p)^{ n-k}
\end{equation*}

```python
import random

n     = 100
heads = 0
for _ in range(n):
    if random.randint(0, 1) == 1:
        heads += 1
print(f'heads occured {heads} times.')
```

    heads occured 52 times.

```python
import random

n     = 100
heads = 0
for _ in range(n):
    if random.randint(0, 1) == 1:
        heads += 1
print(f'heads occured {heads} times.')
```

    heads occured 56 times.

```python
from IPython.lib.display import YouTubeVideo
YouTubeVideo('CUpwLhZh66A')
```

Notes - the need for speed.

Mig-28 observed in a 4G negative dive - classified.

# That's all folks!

```python

```


# 设施选址问题 Facility Location

等级：初级

## 目的和先决条件

在此示例中，我们将解决设施位置问题，即我们要建立仓库以供应一定数量的超级市场。我们将构建此问题的混合整数编程（MIP）模型，在Gurobi Python界面中实现此模型，并计算最佳解决方案。

该建模示例处于初级阶段，我们假设你了解Python，并且具有一些有关构建数学优化模型的知识。

**注意：** 你可以通过单击 [此处](https://github.com/arvinxx/gurobi-and-mathematical-modeling/archive/master.zip) 下载包含此示例和其他示例的代码。为了正确运行此 Jupyter Notebook，你必须具有 Gurobi 许可证。如果你没有，则可以**商业用户身份**申请 [试用许可证](https://www.gurobi.com/downloads/request-an-evaluation-license/)，或以**学术用户身份**下载 [免费许可证](https://www.gurobi.com/academia/academic-program-and-licenses)。

## 动机

设施选址问题的研究（也称为位置分析）是运筹学和计算几何学的一个分支，与设施的最佳布置有关，以最大程度地降低运输成本，同时考虑避免在房屋附近放置危险材料以及竞争对手设施的位置等因素。

费马-韦伯问题（Fermat-Weber Problem）是17世纪提出的最早的设施选址问题之一。

费马-韦伯问题可以描述如下：

> 给定一个平面中的三个点，找到第四个点，使得其到三个给定点的距离之和最小。

该问题可以解释为设施位置问题的一种形式，其中假设所有目的地的每距离运输成本都相同。

设施选址问题在许多行业都有应用。对于供应链管理和物流，该问题可用于寻找商店、工厂、仓库等的最佳位置。其他应用范围包括公共政策（例如在城市中安置警察）、电信（例如网络中的蜂窝塔），甚至还有粒子物理学（例如，排斥电荷之间的分离距离）。设施选址问题的另一个应用是确定天然气输送设备的位置。最后，设施选址问题可应用于聚类分析。

## 问题描述

英国的一家大型连锁超市需要为其在英格兰北部开设的一系列超市建立仓库。超级市场的位置已经确定，但是仓库的位置尚未确定。

目前已经确定了若干仓库的备选地点，但必须就建立多少个仓库以及在哪些候选地点建立仓库作出决定。

建设多个仓库是有好处的，因为这将减少卡车从仓库到超市的平均行驶距离，从而降低运输成本。但是，建一个仓库有一定的固定成本。

在这个例子中，我们的目标是找到运输成本和建造新仓库成本之间的最佳权衡。

## 解决方法

数学优化（也称为数学编程）是一种声明性方法，其中建模者制定了一个优化问题，该问题捕获了复杂决策问题的关键特征。然后 Gurobi 使用最先进的数学和计算机科学技术来解决这一类数学优化问题。

数学优化模型具有五个组成部分：

* 数据集（Sets）
* 参数(Parameters)
* 决策变量(Decision variables)
* 约束（Constraints）
* 目标函数（Objective function(s)）

我们现在提出了一个设施选址问题的 MIP 模型。

## 模型制定

### 索引和集合

$i \in I$: 超市（或客户）位置的索引和集合。

$j \in J$: 候选仓库（或设施）位置的索引和集合。

### 参数

$f_{j} \in \mathbb{R}^+$: 与建造设施 $j$ 有关的固定成本，其中 $j \in J$。

$d_{i,j} \in \mathbb{R}^+$: 设施  $j \in J$ 和 客户 $i \in I$ 的距离。

$c_{i,j} \in \mathbb{R}^+$: 候选设施 $j \in J$ 和客户 $i \in I$ 之间的运输成本。 我们假设此成本与设施与客户之间的距离成比例。
也就是说， $c_{i,j} = \alpha \cdot d_{i,j}$，其中 $\alpha$ 是每英里的运输成本，将其调整为货车在五年内的平均行程开销。

### 决策变量

$select_{j} \in \{0, 1 \}$: 如果我们在候选位置 $j \in J$ 中建立设施，则此变量等于1，否则为0；

$0 \leq assign_{i,j} \leq 1$: 这个非负连续变量决定了客户 $i\in I$ 从设施 $j \in J$ 收到的货量占总供货量的比例。

### 目标函数

- **总成本**：我们想要把开办和运营这些设施的总成本降到最低。这是建设设施的成本和运输成本的总和。这个总成本衡量了建造一个新设施的成本与五年内总运输成本之间的权衡。

\begin{equation}
\text{Max} \quad Z = \sum_{j \in J} f_{j} \cdot select_{j} + \sum_{j \in J} \sum_{i \in I} c_{i,j} \cdot assign_{i,j}
\tag{0}
\end{equation}

### 约束

- **客户需求**。对于每个客户$i \in I$，确保满足其需求。即，每个客户从每个设施收到的分配比之和必须等于1：

\begin{equation}
\sum_{j \in J} assign_{i,j} = 1 \quad \forall i \in I
\tag{1}
\end{equation}

- **运输**。 我们需要确保让已建设的设施 $j \in J$ 进行运输。

\begin{equation}
assign_{i,j} \leq select_{j} \quad \forall i \in I \quad \forall j \in J
\tag{2}
\end{equation}

## Python 实现

本案例包含 2 个超市和 9 个仓库备选地址。下表提供了各超市的坐标。

|  | 坐标 |
| --- | --- |
| 超市 1 | (0,1.5) |
| 超市 2 | (2.5,1.2) |

下表显示了候选仓库的坐标和建造仓库的固定成本（单位：百万英镑）。

|  | 坐标 | 固定成本 |
| --- | --- |  --- |
| 仓库 1 | (0,0) | 3 |
| 仓库 2 | (0,1) | 2 |
| 仓库 3 | (0,2) | 3 |
| 仓库 4 | (1,0) | 1 |
| 仓库 5 | (1,1) | 3 |
| 仓库 6 | (1,2) | 3 |
| 仓库 7 | (2,0) | 4 |
| 仓库 8 | (2,1) | 3 |
| 仓库 9 | (2,2) | 2 |

每英里的成本为一百万英镑。

## 代码实现

导入 Gurobi 的 Python 模块和相关 Python 库。然后用给定的数据初始化数据结构。

```python
# 官方测试版本 Gurobi v9.0.0 & Python 3.7.0

# 译者测试版本 Gurobi v9.1.0 & Python 3.8.6

from itertools import product
from math import sqrt

import gurobipy as gp
from gurobipy import GRB

# 参数
customers = [(0,1.5), (2.5,1.2)]
facilities = [(0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2)]
setup_cost = [3,2,3,1,3,3,4,3,2]
cost_per_mile = 1
```

### 预处理

我们定义了一个函数来确定每个设施和客户站点之间的欧式距离。此外，我们还计算了设施选址问题的MIP模型公式所需的关键参数。

```python
# 此函数确定设施和客户站点之间的欧式距离

def compute_distance(loc1, loc2):
    dx = loc1[0] - loc2[0]
    dy = loc1[1] - loc2[1]
    return sqrt(dx*dx + dy*dy)

# 计算MIP模型的关键参数

num_facilities = len(facilities)
num_customers = len(customers)
cartesian_prod = list(product(range(num_customers), range(num_facilities)))

# 计算运输成本

shipping_cost = {(c,f): cost_per_mile*compute_distance(customers[c], facilities[f]) for c, f in cartesian_prod}
```

### 模型部署

现在我们通过定义决策变量、约束条件和目标函数来确定设备选址问题的MIP模型。然后进行优化计算，Gurobi会找到建造设施的可行解，使总成本最小化。

```python
# MIP 模型

m = gp.Model('facility_location')

select = m.addVars(num_facilities, vtype=GRB.BINARY, name='Select')
assign = m.addVars(cartesian_prod, ub=1, vtype=GRB.CONTINUOUS, name='Assign')

m.addConstrs((assign[(c,f)] <= select[f] for c,f in cartesian_prod), name='Setup2ship')
m.addConstrs((gp.quicksum(assign[(c,f)] for f in range(num_facilities)) == 1 for c in range(num_customers)), name='Demand')

m.setObjective(select.prod(setup_cost)+assign.prod(shipping_cost), GRB.MINIMIZE)

m.optimize()
```

    Using license file /Users/arvinxx/gurobi.lic
    Academic license - for non-commercial use only - expires 2021-02-01
    Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (mac64)
    Thread count: 6 physical cores, 12 logical processors, using up to 12 threads
    Optimize a model with 20 rows, 27 columns and 54 nonzeros
    Model fingerprint: 0x0939f503
    Variable types: 18 continuous, 9 integer (9 binary)
    Coefficient statistics:
      Matrix range     [1e+00, 1e+00]
      Objective range  [5e-01, 4e+00]
      Bounds range     [1e+00, 1e+00]
      RHS range        [1e+00, 1e+00]
    Presolve time: 0.00s
    Presolved: 20 rows, 27 columns, 54 nonzeros
    Variable types: 18 continuous, 9 integer (9 binary)

    Root relaxation: objective 4.723713e+00, 15 iterations, 0.00 seconds

        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

    *    0     0               0       4.7237129    4.72371  0.00%     -    0s

    Explored 0 nodes (15 simplex iterations) in 0.02 seconds
    Thread count was 12 (of 12 available processors)

    Solution count 1: 4.72371

    Optimal solution found (tolerance 1.00e-04)
    Best objective 4.723712908962e+00, best bound 4.723712908962e+00, gap 0.0000%

## 分析

优化模型的结果表明，最小总成本价值为472万英镑。让我们看看达到最佳结果的解决方案。

### 仓库建设计划

该计划决定在哪个厂址的位置建立仓库。

```python
# 显示决策变量的最佳值

for facility in select.keys():
    if (abs(select[facility].x) > 1e-6):
        print(f"\n 建立仓库的地址为：{facility + 1}.")
```

     建立仓库的地址为： 4.

### 运输计划

这个计划决定了从每个工厂发送到每个客户的出货量的百分比。

```python
# 从工厂到客户的运输方案

for customer, facility in assign.keys():
    if (abs(assign[customer, facility].x) > 1e-6):
        print(f"\n 超市{customer + 1}从工厂{facility + 1}接受 {round(100*assign[customer, facility].x, 2)} % of 的需求")

```

     超市 1 从工厂 4 接受 100.0 % of 的需求

     超市 2 从工厂 4 接受 100.0 % of 的需求

##  结论

在此示例中，我们解决了一个设施选址问题，即我们要建立仓库以供应大量的超市，同时最大程度地减少建造仓库的固定总成本和从仓库到超市的可变运输总成本。我们学习了如何将问题表达为MIP模型。此外，我们还学习了如何实现 MIP 模型并使用 Gurobi 对其求解。

##  参考资料

[1] Laporte, Gilbert, Stefan Nickel, and Saldanha da Gama, Francisco. Location Science. Springer, 2015.

Copyright © 2020 Gurobi Optimization, LLC

翻译 By Arvin Xu


# Inference example for trained 2D U-Net model on BraTS.
Takes a trained model and performs inference on a few validation examples.

```python
import sys
import platform
import os

print("Python version: {}".format(sys.version))
print("{}".format(platform.platform()))
```

```python
def test_intel_tensorflow():
    """
    Check if Intel version of TensorFlow is installed
    """
    import tensorflow as tf

    print("We are using Tensorflow version {}".format(tf.__version__))

    major_version = int(tf.__version__.split(".")[0])
    if major_version >= 2:
       from tensorflow.python import _pywrap_util_port
       print("Intel-optimizations (DNNL) enabled:", _pywrap_util_port.IsMklEnabled())
    else:
       print("Intel-optimizations (DNNL) enabled:", tf.pywrap_tensorflow.IsMklEnabled())

test_intel_tensorflow()
```

```python
saved_model_dir = "./output/2d_unet_decathlon"
```

```python
# Create output directory for images
png_directory = "inference_examples"
if not os.path.exists(png_directory):
    os.makedirs(png_directory)

model_filename = os.path.join(saved_model_dir)
```

#### Define the DICE coefficient and loss function

The Sørensen–Dice coefficient is a statistic used for comparing the similarity of two samples. Given two sets, X and Y, it is defined as

\begin{equation}
dice = \frac{2|X\cap Y|}{|X|+|Y|}
\end{equation}

```python
import numpy as np

def calc_dice(target, prediction, smooth=0.0001):
    """
    Sorensen Dice coefficient
    """
    prediction = np.round(prediction)

    numerator = 2.0 * np.sum(target * prediction) + smooth
    denominator = np.sum(target) + np.sum(prediction) + smooth
    coef = numerator / denominator

    return coef

def calc_soft_dice(target, prediction, smooth=0.0001):
    """
    """
    numerator = 2.0 * np.sum(target * prediction) + smooth
    denominator = np.sum(target) + np.sum(prediction) + smooth
    coef = numerator / denominator

    return coef
```

## Inference Time!

Inferencing in this example can be done in 3 simple steps:
1. Load the data
1. Load the Keras model
1. Perform a `model.predict` on an input image (or set of images)

#### Step 1 : Load data

```python
data_path = "../data/decathlon/Task01_BrainTumour/2D_model/"
saved_model_dir = "./output/2d_unet_decathlon"

crop_dim=128  # -1 = Original resolution (240)
batch_size = 128
seed=816  # Change this to see different examples in the test dataset
```

# Challenge A: Complete the code

Fill in the dataloader parameters for the testing dataset

```python
from dataloader import DatasetGenerator

# TODO: Fill in the parameters for the dataset generator to return the `testing` data
ds_testing = DatasetGenerator(...)
```

#### Step 2 : Load the model

```python
from model import unet

from tensorflow import keras as K
model = K.models.load_model(saved_model_dir, compile=False, custom_objects=unet().custom_objects)
```

#### Step 3: Perform prediction on some images.
The prediction results will be saved in the output directory for images, which is defined by the `png_directory` variable.

```python
import matplotlib.pyplot as plt
%matplotlib inline

def plot_results(ds, idx):

    dt = ds.get_dataset().take(1).as_numpy_iterator()  # Get some examples (use different seed for different samples)

    plt.figure(figsize=(10,10))

    for img, msk in dt:

        plt.subplot(1, 3, 1)
        plt.imshow(img[idx, :, :, 0], cmap="bone", origin="lower")
        plt.title("MRI {}".format(idx), fontsize=20)

        plt.subplot(1, 3, 2)
        plt.imshow(msk[idx, :, :], cmap="bone", origin="lower")
        plt.title("Ground truth", fontsize=20)

        plt.subplot(1, 3, 3)

        # Predict using the TensorFlow model
        prediction = model.predict(img[[idx]])
        plt.imshow(prediction[0,:,:,0], cmap="bone", origin="lower")
        plt.title("Prediction\nDice = {:.4f}".format(calc_dice(msk[idx, :, :], prediction)), fontsize=20)

        plt.savefig(os.path.join(png_directory, "prediction_{}.png".format(idx)))
```

```python
plot_results(ds_testing, 11)
```

```python
plot_results(ds_testing, 17)
```

```python
plot_results(ds_testing, 23)
```

```python
plot_results(ds_testing, 56)
```

```python
plot_results(ds_testing, 89)
```

```python
plot_results(ds_testing, 101)
```

```python
plot_results(ds_testing, 119)
```

# Can we perform inference even faster? Hmm..

Let's find out. Move on the the next tutorial section.

*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. SPDX-License-Identifier: EPL-2.0*

*Copyright (c) 2019-2020 Intel Corporation*

```python

```


Table of Contents

Partie 1: Altération d'une image

Partie 2: Restauration d'image

Partie 3: Implémentation de l'ADMM pour résoudre le problème de restauration d'image

# TP3: Restauration d'image à l'aide de l'ADMM
*
Author: Pengfei MI
*
*
Date: 23/06/2017
*

```python
import numpy as np
from scipy.sparse.linalg import cg
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from time import time
import pickle
```

## Partie 1: Altération d'une image

```python
# Data manipulation

# Ouvrir des données stockées dans un fichier .pk
data = pickle.load(open('data.pk','rb'))

# Charger par exemple la matrice H
#H = data.get('H')

# Sauvegarder des données dans un fichier .pk
#pickle.dump({'obj1': obj1, 'obj2':obj2}, open('filename.pk','wb'), pickle.HIGHEST_PROTOCOL)
```

$\textbf{Manipulation 1} \quad\text{Observer l'effet du flou de bougé sur les observations.}$

```python
print data.keys()

print "Convolution matrix: ", data['H'].shape
print "Original image: ", data['original'].shape
print "Observed image: ", data['observations'].shape
print "Total variation matrix: ", data['T'].shape

plt.figure(1, figsize=(15,5))
plt.subplot(121)
plt.title("Original image")
plt.imshow(data['original'], cmap='gray')
plt.axis('off')
plt.subplot(122)
plt.title("Observed image")
plt.imshow(data['observations'], cmap='gray')
plt.axis('off')
plt.show()
```

## Partie 2: Restauration d'image

$\textbf{Préparation 1}\quad \text{Écrire le problème de restauration d'image (3) sous la forme (5). Exprimer les fonctions }f\text{ et }g\text{, le vecteur }\mathbf{z}\text{ et la matrice }\mathbf{A}\text{.}$

$\mathbf{z}_1 = \mathbf{x} \in \mathbb{R}^P, \mathbf{z}_2 = \mathbf{Tx} \in \mathbb{R}^{2P-K-L}$ et $\mathbf{z} = [\mathbf{z}_1^T, \mathbf{z}_2^T]^T$, donc on a:
$$
\begin{align}
& \mathbf{\iota}_{[0,1]^P}(\mathbf{z}_1) = \mathbf{\iota}_{[0,1]^P}(\mathbf{B}_1\mathbf{z}), \quad\text{où}\ \mathbf{B}_1 = diag(\underbrace{1,...,1}_{P}, \underbrace{0,...,0}_{2P-K-L}) \\
& \|\mathbf{z}_2\|_1 = \|\mathbf{B}_2\mathbf{z}\|_1, \quad\text{où}\ \mathbf{B}_2 = diag(\underbrace{0,...,0}_{P}, \underbrace{1,...,1}_{2P-K-L})
\end{align}
$$

Alors
$$
\begin{align}
& \underset{\mathbf{x}}{\text{min}}\ \frac{1}{2}\|\mathbf{y}-\mathbf{Hx}\|_2^2 + \beta\|\mathbf{Tx}\|_1 + \mathbf{\iota}_{[0,1]^P}(\mathbf{x}) \\
=\ & \underset{\mathbf{x}}{\text{min}}\ \frac{1}{2}\|\mathbf{y}-\mathbf{Hx}\|_2^2 + \beta\|\mathbf{z}_2\|_1 + \mathbf{\iota}_{[0,1]^P}(\mathbf{z}_1) \\
=\ & \underset{\mathbf{x}}{\text{min}}\ \frac{1}{2}\|\mathbf{y}-\mathbf{Hx}\|_2^2 + \beta\|\mathbf{B}_2\mathbf{z}\|_1 + \mathbf{\iota}_{[0,1]^P}(\mathbf{B}_1\mathbf{z}) \\
=\ & \underset{\mathbf{x}}{\text{min}}\ f(\mathbf{x}) + g(\mathbf{z})
\end{align}
$$
où $f(\mathbf{x}) = \frac{1}{2}\|\mathbf{y}-\mathbf{Hx}\|_2^2$, $g(\mathbf{z}) = \beta\|\mathbf{B}_2\mathbf{z}\|_1 + \mathbf{\iota}_{[0,1]^P}(\mathbf{B}_1\mathbf{z})$, avec $\mathbf{B}_1$ et $\mathbf{B}_2$ définies comme précédentes.

$\nabla_{\mathbf{x}} f = \mathbf{H}^T(\mathbf{Hx} - \mathbf{y})$, $\nabla_{\mathbf{x}}^2 f = \mathbf{H}^T\mathbf{H}$, donc la matrice hessienne de $f(\mathbf{x})$ est positive semi-définie, $f(\mathbf{x})$ est convexe,

$g(\mathbf{z})$ est convexe pour tout $\beta > 0$,

Donc le problème peut s'écrire sous la forme (5), $\mathbf{A} =
\begin{pmatrix}
\mathbf{I}_{P} \\
\mathbf{T}
\end{pmatrix} \in \mathbb{R}^{(3P-K-L)\ \times\ P}$.

$\textbf{Préparation 2}\quad \text{Ecrire le Lagrangien augmenté }\mathcal{L}_\rho\text{ du problème de restauration d'image (3).}$

$$\mathcal{L}_\rho(\mathbf{x}, \mathbf{z}^k, \mathbf{\gamma}^k)\ =\ \frac{1}{2}\|\mathbf{y}-\mathbf{Hx}\|_2^2 + \mathbf{\iota}_{[0,1]^P}(\mathbf{z}_1) + \beta\|\mathbf{z}_2\|_1 + \mathbf{\gamma}_1^T(\mathbf{x}-\mathbf{z}_1) + \mathbf{\gamma}_2^T(\mathbf{Tx}-\mathbf{z}_2) + \frac{\rho}{2}\|\mathbf{x}-\mathbf{z}_1\|_2^2 + \frac{\rho}{2}\|\mathbf{Tx}-\mathbf{z}_2\|_2^2$$

$\textbf{Préparation 3}\quad \text{Ecrire l'étape de minimisation de }\mathcal{L}_\rho \text{ selon la variable }\mathbf{x}\text{.}$

$$\nabla_x \mathcal{L}_\rho = \mathbf{H}^T(\mathbf{Hx-y}) + \gamma_1 + \mathbf{T}^T\gamma_2 + \rho(\mathbf{x}-\mathbf{z}_1) + \rho\mathbf{T}^T(\mathbf{Tx}-\mathbf{z}_2)$$
Soit $\nabla_x \mathcal{L}_\rho = 0$, on a:
$$\mathbf{x}^{k+1} = (\mathbf{H}^T\mathbf{H} + \rho\mathbf{I}_P + \rho\mathbf{T}^T\mathbf{T})^{-1}(\mathbf{H}^T\mathbf{y} + \rho\mathbf{z}_1^k + \rho\mathbf{T}^T\mathbf{z}_2^k - \gamma_1^k - \mathbf{T}^T\gamma_2^k)$$

$\textbf{Préparation 4}\quad \text{Ecrire l'étape de minimisation de }\mathcal{L}_\rho \text{ selon la variable }\mathbf{z}_1\text{ et }\mathbf{z}_2\text{.}$

$$
\begin{aligned}
\text{arg}\ \underset{\mathbf{z}_1}{\text{min}}\ \mathcal{L}_\rho &= \text{arg}\ \underset{\mathbf{z}_1}{\text{min}}\ \mathbf{\iota}_{[0,1]^P}(\mathbf{z}_1) + \mathbf{\gamma}_1^T(\mathbf{x}-\mathbf{z}_1) + \frac{\rho}{2}\|\mathbf{x}-\mathbf{z}_1\|_2^2 \\
&= \text{arg}\ \underset{\mathbf{z}_1 \in [0,1]^P}{\text{min}}\ \mathbf{\gamma}_1^T(\mathbf{x}-\mathbf{z}_1) + \frac{\rho}{2}\|\mathbf{x}-\mathbf{z}_1\|_2^2
\end{aligned}
$$

Soit la dérivé de la fonction objectif:
$$-\mathbf{\gamma}_1 + \rho(\mathbf{z}_1 - \mathbf{x}) = 0 \quad \Rightarrow \quad \mathbf{z}_1 = \frac{\mathbf{\gamma}_1}{\rho} + \mathbf{x}$$

En considérant la contrainte sur la domaine, pour chaque élément $\mathbf{z}_{1,n},\ 1 \leq n \leq P$, on a:
$$\mathbf{z}_{1,n}^{k+1} = \text{max}(0,\ \text{min}(1,\ \frac{\mathbf{\gamma}_{1,n}^k}{\rho} + \mathbf{x}_n^{k+1})$$

$$\text{arg}\ \underset{\mathbf{z}_2}{\text{min}}\ \mathcal{L}_\rho = \text{arg}\ \underset{\mathbf{z}_2}{\text{min}}\ \beta\|\mathbf{z}_2\|_1 + \mathbf{\gamma}_2^T(\mathbf{Tx}-\mathbf{z}_2) + \frac{\rho}{2}\|\mathbf{Tx}-\mathbf{z}_2\|_2^2$$

La fonction objectif peut s'écrit de la forme $\mathcal{L}_{\rho, \mathbf{z}_2} = h_1(\mathbf{z}_2) + h_2(\mathbf{z}_2)$, où $h_1(\mathbf{z}_2) = \beta \|\mathbf{z}_2\|_1$, $h_2(\mathbf{z}_2) = \mathbf{\gamma}_2^T(\mathbf{Tx}-\mathbf{z}_2) + \frac{\rho}{2}\|\mathbf{Tx}-\mathbf{z}_2\|_2^2$.

$h_1(\mathbf{z}_2)$ est convexe pour tout $\beta > 0$,

$\nabla_{\mathbf{z}_2} h_2 = -\mathbf{\gamma}_2 + \rho(\mathbf{z}_2 - \mathbf{Tx})$, $\nabla_{\mathbf{z}_2}^2 h_2 = \rho \mathbf{I}_{2P-K-L}$, donc $h_2(\mathbf{z}_2)$ est convexe pour tout $\rho > 0$,

Donc, on peut appliquer la méthode du gradient proximal à la fonction objectif.

$\|\nabla_{\mathbf{z}_2} h_2 (\mathbf{z}_a) - \nabla_{\mathbf{z}_2} h_2 (\mathbf{z}_b)\| = \|\rho(\mathbf{z}_a - \mathbf{z}_b)\| \leq \rho\|\mathbf{z}_a - \mathbf{z}_b\|$, donc $\nabla_{\mathbf{z}_2} h_2$ est $\rho$-Lipschitzienne, donc

$$
\begin{aligned}
\mathbf{z}_2^{k+1} &= \text{prox}_{\frac{1}{\rho}h_1} (\mathbf{z}_2^k - \frac{1}{\rho} \nabla_{\mathbf{z}_2} h_2(\mathbf{z}_2^k)) \\
&= \text{prox}_{\frac{1}{\rho}h_1} (\mathbf{T}\mathbf{x}^{k+1} + \frac{\mathbf{\gamma}_2^k}{\rho}) \\
&= \mathcal{S}_{\frac{\beta}{\rho}} (\mathbf{T}\mathbf{x}^{k+1} + \frac{\mathbf{\gamma}_2^k}{\rho})
\end{aligned}
$$

$\textbf{Préparation 5}\quad \text{Ecrire l'étape de mise à jour des variables duales } \mathbf{\gamma}_1 \text{ et } \mathbf{\gamma}_2.$

$$\mathbf{\gamma}_1^{k+1} = \mathbf{\gamma}_1^k + \rho(\mathbf{x}^{k+1} - \mathbf{z}_1^{k+1})$$
$$\mathbf{\gamma}_2^{k+1} = \mathbf{\gamma}_2^k + \rho(\mathbf{T}\mathbf{x}^{k+1} - \mathbf{z}_2^{k+1})$$

## Partie 3: Implémentation de l'ADMM pour résoudre le problème de restauration d'image

$\textbf{Manipulation 2} \quad\text{Ecrire un script python permettant de résoudre le problème à l'aide de l'ADMM.}$

```python
def ADMM(y, original, H, T, beta, rho, k_max=100, early_stop=False, tol=1e-3, return_info=False, return_iter_info=False):
    K, L = y.shape
    P = K*L
    y = y.ravel()
    original = original.ravel()
    x = np.zeros_like(y)
    z1 = x
    z2 = T.dot(x)
    gamma1 = np.zeros_like(z1)
    gamma2 = np.zeros_like(z2)
    val_L = np.zeros(k_max)
    val_biais = np.zeros(k_max)
    val_EQM = np.zeros(k_max)

    n_iter = 0
    for k in range(k_max):
        n_iter += 1
        # Update x
        x, info = cg(H.T.dot(H) + rho*np.identity(P) + rho*T.T.dot(T), H.T.dot(y) + rho*z1 + rho*T.T.dot(z2) - gamma1 - T.T.dot(gamma2))
        # Update z_1 and z_2
        z1 = gamma1/rho + x
        for i in range(len(z1)):
            z1[i] = max(0.0, min(1.0, z1[i]))
        z2 = T.dot(x) + gamma2/rho
        for i in range(len(z2)):
            if (z2[i] > beta/rho):
                z2[i] -= beta/rho
            elif (z2[i] < -beta/rho):
                z2[i] += beta/rho
            else:
                z2[i] = 0
        # Update gamma_1 and gamma_2
        gamma1 = gamma1 + rho*(x - z1)
        gamma2 = gamma2 + rho*(T.dot(x) - z2)
        val_L[k] = np.sum((H.dot(x)-y)*(H.dot(x)-y))/2. + \
                   beta*np.sum(np.fabs(z2)) + \
                   np.sum(gamma1*(x-z1)) + np.sum(gamma2*(T.dot(x)-z2)) + \
                   rho*np.sum((x-z1)*(x-z1))/2. + rho*np.sum((T.dot(x)-z2)*(T.dot(x)-z2))/2.
        val_biais[k] = np.mean(np.fabs(x-original))
        val_EQM[k] = np.mean((x-original)*(x-original))
        if (early_stop and k > 0 and val_L[k-1] - val_L[k] <= tol):
            break
        if (n_iter == k_max and val_L[k-1] > val_L[k] > tol):
            print "rho = %0.3f, beta = %0.3f, not converged whithin iteration limit." % (rho, beta)

    if return_iter_info:
        return x.reshape(K, L), val_L[:n_iter], val_biais[:n_iter], val_EQM[:n_iter], n_iter
    elif return_info:
        return x.reshape(K, L), val_L[n_iter-1], val_biais[n_iter-1], val_EQM[n_iter-1], n_iter
    else:
        return x.reshape(K, L), n_iter
```

```python
rho = 0.05
beta = 0.01
k_max = 100

t0 = time()
x_restored, k = ADMM(data['observations'], data['original'], data['H'], data['T'], beta, rho, k_max)
print "Done in %0.3fs." % (time()-t0)

plt.figure(2, figsize=(15,5))
plt.subplot(131)
plt.title("Observed image")
plt.imshow(data['observations'], cmap='gray')
plt.axis('off')
plt.subplot(132)
plt.title("Original image")
plt.imshow(data['original'], cmap='gray')
plt.axis('off')
plt.subplot(133)
plt.title("Restored image")
plt.imshow(x_restored, cmap='gray')
plt.axis('off')
plt.show()
```

$\textbf{Manipulation 3} \quad\text{Evaluer à chaque itération la valeur du Lagrangien augmenté et de l'EQM.}$

```python
t0 = time()
x_restored, L, biais, EQM, k = ADMM(data['observations'], data['original'], data['H'], data['T'], beta, rho, k_max, return_iter_info=True)
print "Done in %0.3fs." % (time()-t0)

ite_range = range(1, k_max+1)

plt.figure(3, figsize=(16,6))
plt.subplot(121)
plt.title("Value of augmented Lagrangian")
plt.plot(ite_range, L, c='r')
plt.xlabel("Number of iteration")
plt.ylabel("Value")
plt.xlim(0, 100)
plt.subplot(122)
plt.title("Value of EQM")
plt.plot(ite_range, EQM, c='b')
plt.xlabel("Number of iteration")
plt.ylabel("EQM value")
plt.xlim(0, 100)
plt.show()
```

La courbe de la valeur du Lagrangien augmenté décroît très rapidement vers un certain valeur positive, après elle devient stable.

La courbe de l'EQM décroît un peu moins rapidement que l'autre, elle devient satble autour une petite valeur positive.

De plus, nous pouvons afficher le biais intuitivement.

```python
plt.figure(4, figsize=(5,5))
plt.title("The biais between restored image and the original")
plt.imshow(x_restored - data['original'], cmap='gray')
plt.axis('off')
plt.show()
```

$\textbf{Manipulation 4} \quad\text{Analyser l'influence de } \rho \text{ et de } \beta \text{ sur la solution et sur le problème d'optimisation.}$

```python
t0 = time()

arr_rho = np.append(np.finfo('float').eps, np.linspace(0.02, 0.1, 5))
arr_beta = np.linspace(0, 0.02, 6)
arr_rho, arr_beta = np.meshgrid(arr_rho, arr_beta)

arr_biais = np.empty_like(arr_rho, dtype=np.float32)
arr_EQM = np.empty_like(arr_rho, dtype=np.float32)
arr_L = np.empty_like(arr_rho, dtype=np.float32)
arr_k = np.empty_like(arr_rho, dtype=np.float32)

plt.close('all')
plt.figure(6, figsize=(18, 18))

for i in range(6):
    for j in range(6):
        rho = arr_rho[i][j]
        beta = arr_beta[i][j]
        x_restored, L, biais, EQM, k = ADMM(data['observations'], data['original'], data['H'], data['T'], beta, rho, k_max, early_stop=True, tol=1e-4, return_info=True)
        arr_biais[i][j] = biais
        arr_EQM[i][j] = EQM
        arr_L[i][j] = L
        arr_k[i][j] = k
        plt.subplot(6, 6, i*6+j+1)
        plt.title("rho=%0.2f, beta=%0.3f" % (rho, beta))
        plt.imshow(x_restored, cmap='gray')
        plt.axis('off')
plt.show()

print "Done in %0.3fs." % (time()-t0)
```

    Sur la qualité de la reconstruction, on peut conclure que quand la valeur de $\rho$ augmente, il y a moins de points de bruit.

    Quand la valeur de $\beta$ augmente les pixels adjacents deviennent de plus en plus continuels. D'abord, l'image est plus lisible, après, il devient flou encore une fois.

```python
fig = plt.figure(6)
ax = Axes3D(fig)
ax.text2D(0.05, 0.95, "The influence of parameters rho and beta on biais", transform=ax.transAxes)
ax.plot_surface(arr_rho, arr_beta, arr_biais, cmap='rainbow')
ax.set_xlabel('rho')
ax.set_ylabel('beta')
ax.set_zlabel('biais')
plt.show()

fig = plt.figure(7)
ax = Axes3D(fig)
ax.text2D(0.05, 0.95, "The influence of parameters rho and beta on EQM", transform=ax.transAxes)
ax.plot_surface(arr_rho, arr_beta, arr_EQM, cmap='rainbow')
ax.set_xlabel('rho')
ax.set_ylabel('beta')
ax.set_zlabel('EQM')
plt.show()

fig = plt.figure(8)
ax = Axes3D(fig)
ax.text2D(0.05, 0.95, "The influence of parameters rho and beta on augmented Lagrangian value", transform=ax.transAxes)
ax.plot_surface(arr_rho, arr_beta, arr_L, cmap='rainbow')
ax.set_xlabel('rho')
ax.set_ylabel('beta')
ax.set_zlabel('value')
plt.show()

fig = plt.figure(9)
ax = Axes3D(fig)
ax.text2D(0.05, 0.95, "The influence of parameters rho and beta on convergence rate", transform=ax.transAxes)
ax.plot_surface(arr_rho, arr_beta, arr_k, cmap='rainbow')
ax.set_xlabel('rho')
ax.set_ylabel('beta')
ax.set_zlabel('Number of iterations')
plt.show()
```

Quand la valeur de $\rho$ augmente, le biais et l'EQM vont décroît, la valeur de Lagrangien augmenté va augmenter, le nombre nécessaire pour convergence va augmenter.

Quand la valeur de $\beta$ augmente, le biais, l'EQM et la valeur de Lagrangien augmenté vont augmenter, le nombre nécessaire pour convergence va décoire.

$\textbf{Facultatif}\quad\text{Ajouter un bruit additif gaussien de variance 0.1 à l'image et étudier son influence sur la reconstruction.}$

```python
rho = 0.05
beta = 0.01
k_max = 100

y = data['observations'] + 0.1*np.random.randn(64,64)

t0 = time()
x_restored, k = ADMM(y, data['original'], data['H'], data['T'], beta, rho, k_max)
print "Done in %0.3fs." % (time()-t0)

plt.figure(10, figsize=(15,5))
plt.subplot(131)
plt.title("Observed image")
plt.imshow(y, cmap='gray')
plt.axis('off')
plt.subplot(132)
plt.title("Original image")
plt.imshow(data['original'], cmap='gray')
plt.axis('off')
plt.subplot(133)
plt.title("Restored image")
plt.imshow(x_restored, cmap='gray')
plt.axis('off')
plt.show()
```

Le bruit additif va influencer la qualité de reconstruction.


```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline

pd.plotting.register_matplotlib_converters()
```

# Class 9: Introduction to Business Cycle Data

The *business cycle* is the fluctuation of many macroeconomic quantities that last for about 1.5 to 8 years. Colloquially, the term refers to the alternating periods of expansion and contraction in the macroeconomy. Business cycle fluctuations are costly because they are associated with misallocations of capital and labor. Recessions are particularly painful for workers that become unemployed and for the families of workers who become unemployed. The costs of the business cycle have driven research into understanding the cause of the cycle.

The collective set of theories to explain the cycle is called *business cycle theory* and eventually we'll study and critique two competing theoretical perspectives. However, before we approach the theory, we should first uncover some empirical facts about the business cycle that models should be able to explain.

In this lecture, we will:

1. Visualize the difference between the trend and cyclical components of GDP, consumption, investent, and hours.
2. Compute the log-deviations from trend of GDP, consumption, investent, and hours.
3. Compute summary statistics about the business cycle that models of the cycle should be able to explain.

## Data

The file `rbc_data_actual_trend.csv`, available at https://github.com/letsgoexploring/econ126/raw/master/Data/Csv/rbc_data_actual_trend.csv, contains actual and trend data for real GDP per capita, real consumption per capita, real investment per capita, real physical capital per capita, TFP, and hours per capita at quarterly frequency. The GDP, consumption, investment, and capital data are in terms of 2012 dollars. Hours is measured as an index with the value in October 2012 set to 100. All of the data are *real* quantities. That is, there are no *nominal* quantities like money or inflation or a nominal interest rate. The reason is that the first theory that we will encounter is called *real business cycle* or RBC theory and, in that theory, there is no place for nominal quantities. RBC theory seeks to explain fluctuations in real quantities as being primarily due to TFP shocks; i.e., shocks to the production function.

```python
# Read business_cycle_data_actual_trend.csv into a Pandas DataFrame with the first column set as the index and parse_dates=True
df = pd.read_csv('https://github.com/letsgoexploring/econ126/raw/master/Data/Csv/rbc_data_actual_trend.csv',index_col=0,parse_dates=True)

# Print the last five rows of the data
df.tail()
```

      gdp
      gdp_trend
      consumption
      consumption_trend
      investment
      investment_trend
      hours
      hours_trend
      capital
      capital_trend
      tfp
      tfp_trend

      2018-07-01
      72.589059
      72.376813
      49.377099
      49.228728
      12.887706
      12.752714
      105.399877
      105.120948
      124.332148
      124.420606
      23.072294
      23.038755

      2018-10-01
      72.606447
      72.672948
      49.377294
      49.441990
      12.942796
      12.825832
      105.435736
      105.356245
      124.834105
      124.896744
      23.040205
      23.068565

      2019-01-01
      73.251081
      72.970978
      49.529684
      49.656456
      13.135114
      12.899000
      105.677466
      105.590970
      125.800922
      125.376705
      23.147603
      23.098658

      2019-04-01
      73.482233
      73.270527
      49.967211
      49.872074
      12.910780
      12.972253
      105.340443
      105.825548
      126.412315
      125.859556
      23.229459
      23.128911

      2019-07-01
      73.703609
      73.571388
      50.218595
      50.088709
      12.811444
      13.045774
      105.768114
      106.060465
      126.909358
      126.344619
      23.206266
      23.159233

```python
# Construct a 2x2 grid of plots of GDP, consumption, investment, and hours. CELL PROVIDED
base_year = '2012'
fig = plt.figure(figsize=(12,8))

ax1 = fig.add_subplot(2,2,1)
ax1.plot(df.gdp,'-',lw=3,alpha = 0.7)
ax1.grid()
ax1.set_title('GDP per capita')
ax1.set_ylabel('Thousands of '+base_year+' dollars')

ax2 = fig.add_subplot(2,2,2)
ax2.plot(df.consumption,'-',lw=3,alpha = 0.7)
ax2.grid()
ax2.set_title('Consumption per capita')
ax2.set_ylabel('Thousands of '+base_year+' dollars')

ax3 = fig.add_subplot(2,2,3)
ax3.plot(df.investment,'-',lw=3,alpha = 0.7)
ax3.grid()
ax3.set_title('Investment per capita')
ax3.set_ylabel('Thousands of '+base_year+' dollars')

ax4 = fig.add_subplot(2,2,4)
ax4.plot(df.hours,'-',lw=3,alpha = 0.7)
ax4.grid()
ax4.set_title('Hours per capita')
ax4.set_ylabel('Index ('+base_year+'=100)')

fig.tight_layout()
```

## Cycles and Trends

Business cycle theory is tested against data. However, data on the business cycle data is not readily available and must be constructed. A time series $X_t$ can be decomposed into a trend component $X_t^{trend}$ and a cyclical component $X_t^{cycle}$ such that:

\begin{align}
X_t & = X_t^{trend} + X_t^{cycle}. \tag{1}
\end{align}

In equation (1), $X_t^{trend}$ is the long-run value about which $X_t$ fluctuates. $X^{cycle}_t$ is the amount by which $X_t$ excedes its trend. The process for decomposing a series into trend and cyclical components is called *filtering* and is more technical than we want to get into. We'll take for granted that such procedures exist.

Often times, it's useful to express the cyclical component of a time series as the difference between the (natural) log of the series and the log of the trend:

\begin{align}
\hat{x}_t & = \log\left(X_t\right) - \log\left(X_t^{trend}\right) \approx\frac{X_t-X_t^{trend}}{X_t^{trend}}
\end{align}

The log-deviation from trend is approximately equal to the percent deviation of the series from trend (divided by 100).

### GDP

```python
# Construct a plot of real GDP with its trend with:
#    1. Actual line: blue with lw=1, alpha=0.7, label = 'actual'
#    2. Trend line:  red with lw=3, alpha=0.7, label = 'trend'
plt.plot(df.gdp,'-',lw=1,alpha = 0.7,label='actual')
plt.plot(df.gdp_trend,'r-',lw=3,alpha = 0.7,label='trend')
plt.ylabel('Thousands of '+base_year+' dollars per person')
plt.title('GDP per capita')
plt.legend(loc='lower right',ncol=2)
plt.grid()
```

```python
# Create a new column called gdp_cycle equal to the difference between acual and trend GDP
df['gdp_cycle'] = df['gdp'] - df['gdp_trend']

# Create a new column called gdp_cycle_dev equal to the log difference between actual GDP and trend GDP:
df['gdp_cycle_dev'] = np.log(df['gdp']) - np.log(df['gdp_trend'])

# Plot the log deviation of GDP from its trend (times 100)
plt.plot(df.gdp_cycle_dev*100,'b-',lw=3,alpha = 0.7)
plt.ylabel('Percent')
plt.title('GDP: Percent deviation from trend')
plt.grid()
```

### Consumption, Investment, and Hours

```python
# Create three new columns called cons_cycle, invest_cycle, and hours_cycle equal to the cyclical components of the
# respective series
df['cons_cycle'] = df['consumption'] - df['consumption_trend']
df['invest_cycle'] = df['investment'] - df['investment_trend']
df['hours_cycle'] = df['hours'] - df['hours_trend']

# Create a new column called cons_cycle_dev, invest_cycle_dev, and hours_cycle_dev equal to the log difference between
# the actual and trend values of the respective series:
df['cons_cycle_dev'] = np.log(df['consumption']) - np.log(df['consumption_trend'])
df['invest_cycle_dev'] = np.log(df['investment']) - np.log(df['investment_trend'])
df['hours_cycle_dev'] = np.log(df['hours']) - np.log(df['hours_trend'])

```

```python
# Construct a plot of consumption with its trend
#    1. Actual line: blue with lw=1, alpha=0.7, label = 'actual'
#    2. Trend line:  red with lw=3, alpha=0.7, label = 'trend'
plt.plot(df.consumption,'-',lw=1,alpha = 0.7,label='actual')
plt.plot(df.consumption_trend,'r-',lw=3,alpha = 0.7,label='trend')
plt.ylabel('Thousands of '+base_year+' dollars per person')
plt.title('Consumption per capita')
plt.legend(loc='lower right',ncol=2)
plt.grid()
```

```python
# Plot the log deviation of consumption from its trend (times 100)
plt.plot(df.cons_cycle_dev*100,'b-',lw=3,alpha = 0.7)
plt.ylabel('Percent')
plt.title('Consumption: Percent deviation from trend')
plt.grid()
```

```python
# Construct a plot of investment with its trend
#    1. Actual line: blue with lw=1, alpha=0.7, label = 'actual'
#    2. Trend line:  red with lw=3, alpha=0.7, label = 'trend'
plt.plot(df.investment,'-',lw=1,alpha = 0.7,label='actual')
plt.plot(df.investment_trend,'r-',lw=3,alpha = 0.7,label='trend')
plt.ylabel('Thousands of '+base_year+' dollars per person')
plt.title('Investment per capita')
plt.legend(loc='lower right',ncol=2)
plt.grid()
```

```python
# Plot the log deviation of investment from its trend (times 100)
plt.plot(df.invest_cycle_dev*100,'b-',lw=3,alpha = 0.7)
plt.ylabel('Percent')
plt.title('Investment: Percent deviation from trend')
plt.grid()
```

```python
# Construct a plot of hours with its trend
#    1. Actual line: blue with lw=1, alpha=0.7, label = 'actual'
#    2. Trend line:  red with lw=3, alpha=0.7, label = 'trend'
plt.plot(df.hours,'-',lw=1,alpha = 0.7,label='actual')
plt.plot(df.hours_trend,'r-',lw=3,alpha = 0.7,label='trend')
plt.ylabel('Index: ('+base_year+'=100)')
plt.title('Hours per capita')
plt.legend(loc='lower left',ncol=2)
plt.grid()
```

```python
# Plot the log deviation of hours from its trend (times 100)
plt.plot(df.hours_cycle_dev*100,'b-',lw=3,alpha = 0.7)
plt.ylabel('Percent')
plt.title('Hours: Percent deviation from trend')
plt.grid()
```

## US Business Cycle Statistics

```python
# Create a new variable called df_cycle that is a DataFrame with columns columns gdp_cycle_dev, cons_cycle_dev,
# invest_cycle_dev, and hours_cycle_dev from df.
df_cycle = df[['gdp_cycle_dev', 'cons_cycle_dev', 'invest_cycle_dev', 'hours_cycle_dev']]

# Print the first five rows of df_cycle
print(df_cycle.head())
```

                gdp_cycle_dev  cons_cycle_dev  invest_cycle_dev  hours_cycle_dev
    1948-01-01       0.021966        0.007885          0.050324         0.027221
    1948-04-01       0.025403        0.011468          0.096154         0.023184
    1948-07-01       0.017087       -0.000568          0.109654         0.025493
    1948-10-01       0.005779       -0.007582          0.074770         0.013023
    1949-01-01      -0.020577       -0.017215         -0.103118        -0.007272

```python
# Use the DataFrame method .mean() to find the average values of the gdp_cycle_dev, cons_cycle_dev, invest_cycle_dev,
# and hours_cycle_dev columns
df_cycle.mean()
```

    gdp_cycle_dev      -1.224108e-14
    cons_cycle_dev     -3.645400e-14
    invest_cycle_dev   -3.935915e-14
    hours_cycle_dev    -1.619579e-13
    dtype: float64

```python
# Use the DataFrame method .std() to find the standard deviations of the gdp_cycle_dev, cons_cycle_dev, invest_cycle_dev,
# and hours_cycle_dev columns
df_cycle.std()
```

    gdp_cycle_dev       0.016091
    cons_cycle_dev      0.011490
    invest_cycle_dev    0.074404
    hours_cycle_dev     0.018850
    dtype: float64

```python
# Use the DataFrame method .corr() to find the coefficients of correlation among the gdp_cycle_dev, cons_cycle_dev,
# invest_cycle_dev, and hours_cycle_dev columns
df_cycle.corr()
```

      gdp_cycle_dev
      cons_cycle_dev
      invest_cycle_dev
      hours_cycle_dev

      gdp_cycle_dev
      1.000000
      0.794761
      0.847764
      0.874851

      cons_cycle_dev
      0.794761
      1.000000
      0.682279
      0.706058

      invest_cycle_dev
      0.847764
      0.682279
      1.000000
      0.790240

      hours_cycle_dev
      0.874851
      0.706058
      0.790240
      1.000000

**Questions**

1. Which quantity varies the most over the business cycle?
2. Which quantity varies the least over the business cycle?
3. Which quantity is most correlated with GDP over the business cycle?

**Answers**

1. Investment fluctuates the most over the business cycle.
2. Consumption fluctuates the least over the business cycle.
3. Hours is the quantity that is most correlated with GDP over the cycle. Since the capital stock changes slowly over time, large fluctuations due primarily to large fluctuations in employment.

```python
# Plot the cyclical components of GDP, consumption, investment, and hours (all times 100) on the same set of axes
fig = plt.figure(figsize=(12,4))
ax = fig.add_subplot(1,1,1)
(df_cycle*100).plot(ax = ax,legend=False,lw=3,alpha=0.7)
ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
ax.set_ylabel('Percent')
ax.set_title('US Business cycle data')
ax.grid()
```


```c++
#pragma cling add_include_path("../../include")
#pragma cling add_include_path("../feltor/inc") // Feltor path
#define THRUST_DEVICE_SYSTEM THRUST_DEVICE_SYSTEM_CPP
#include <iostream>
#include "dg/algorithm.h"
```

    In file included from input_line_8:2:
    In file included from ../feltor/inc/dg/algorithm.h:8:
    ../feltor/inc/dg/backend/config.h:20:9: warning: NOTE: Fast std::fma(a,b,c) not activated! Using a*b+c instead! [-W#pragma-messages]
    #pragma message( "NOTE: Fast std::fma(a,b,c) not activated! Using a*b+c instead!")
            ^
    In file included from input_line_8:2:
    In file included from ../feltor/inc/dg/algorithm.h:11:
    In file included from ../feltor/inc/dg/topology/split_and_join.h:4:
    In file included from ../feltor/inc/dg/backend/blas1_dispatch_shared.h:12:
    In file included from ../feltor/inc/dg/backend/blas1_serial.h:6:
    In file included from ../feltor/inc/dg/backend/exblas/exdot_serial.h:25:
    In file included from ../feltor/inc/dg/backend/exblas/accumulate.h:19:
    ../feltor/inc/dg/backend/exblas/config.h:31:9: warning: WARNING: Instruction set below SSE4.1! Deactivating vectorization!
          [-W#pragma-messages]
    #pragma message("WARNING: Instruction set below SSE4.1! Deactivating vectorization!")
            ^
    In file included from input_line_8:2:
    In file included from ../feltor/inc/dg/algorithm.h:11:
    In file included from ../feltor/inc/dg/topology/split_and_join.h:4:
    In file included from ../feltor/inc/dg/backend/blas1_dispatch_shared.h:12:
    In file included from ../feltor/inc/dg/backend/blas1_serial.h:6:
    In file included from ../feltor/inc/dg/backend/exblas/exdot_serial.h:25:
    ../feltor/inc/dg/backend/exblas/accumulate.h:93:43: warning: shifting a negative signed value is undefined [-Wshift-negative-value]
            carrybit = (s ? 1ll << KRX : -1ll << KRX);
                                         ~~~~ ^
    In file included from input_line_8:2:
    In file included from ../feltor/inc/dg/algorithm.h:11:
    In file included from ../feltor/inc/dg/topology/split_and_join.h:4:
    In file included from ../feltor/inc/dg/backend/blas1_dispatch_shared.h:12:
    In file included from ../feltor/inc/dg/backend/blas1_serial.h:6:
    In file included from ../feltor/inc/dg/backend/exblas/exdot_serial.h:26:
    ../feltor/inc/dg/backend/exblas/ExSUM.FPE.hpp:143:46: warning: unknown attribute 'optimize' ignored [-Wunknown-attributes]
    template<typename T, int N, typename TRAITS> UNROLL_ATTRIBUTE
                                                 ^
    ../feltor/inc/dg/backend/exblas/config.h:42:41: note: expanded from macro 'UNROLL_ATTRIBUTE'
    #define UNROLL_ATTRIBUTE __attribute__((optimize("unroll-loops")))
                                            ^
    In file included from input_line_8:2:
    In file included from ../feltor/inc/dg/algorithm.h:11:
    In file included from ../feltor/inc/dg/topology/split_and_join.h:4:
    In file included from ../feltor/inc/dg/backend/blas1_dispatch_shared.h:12:
    In file included from ../feltor/inc/dg/backend/blas1_serial.h:6:
    In file included from ../feltor/inc/dg/backend/exblas/exdot_serial.h:26:
    ../feltor/inc/dg/backend/exblas/ExSUM.FPE.hpp:189:46: warning: unknown attribute 'optimize' ignored [-Wunknown-attributes]
    template<typename T, int N, typename TRAITS> UNROLL_ATTRIBUTE
                                                 ^
    ../feltor/inc/dg/backend/exblas/config.h:42:41: note: expanded from macro 'UNROLL_ATTRIBUTE'
    #define UNROLL_ATTRIBUTE __attribute__((optimize("unroll-loops")))
                                            ^
    In file included from input_line_8:2:
    In file included from ../feltor/inc/dg/algorithm.h:11:
    In file included from ../feltor/inc/dg/topology/split_and_join.h:4:
    In file included from ../feltor/inc/dg/backend/blas1_dispatch_shared.h:12:
    In file included from ../feltor/inc/dg/backend/blas1_serial.h:6:
    In file included from ../feltor/inc/dg/backend/exblas/exdot_serial.h:26:
    ../feltor/inc/dg/backend/exblas/ExSUM.FPE.hpp:221:46: warning: unknown attribute 'optimize' ignored [-Wunknown-attributes]
    template<typename T, int N, typename TRAITS> UNROLL_ATTRIBUTE
                                                 ^
    ../feltor/inc/dg/backend/exblas/config.h:42:41: note: expanded from macro 'UNROLL_ATTRIBUTE'
    #define UNROLL_ATTRIBUTE __attribute__((optimize("unroll-loops")))
                                            ^

# Solvers in Feltor

In this chapter we will study how to solve
\begin{align}
f(x) = b
\end{align}
for both linear and nonlinear functions $f$ in Feltor.

## Overview

Feltor offers a growing range of solvers. Among others there are linear
- `dg::PCG` for preconditioned conjugate gradient
- `dg::LGMRES` for the restarted generalized minimal residual method
- `dg::BICGSTABL`
- `dg::ChebyshevIteration`

and non-linear solvers
- `dg::FixedPointSolver`
- `dg::AndersonAcceleration`

```{admonition} Matrix-free solvers
It is important to realize that Feltor's solvers are _matrix-free_ (and in consequence iterative). What this means is that the actual matrix does not need to be allocated. In fact, the matrix does not need to be known at all, in code or analytically. The only thing that is needed is the **application** of the matrix to a vector. In other words, the solver just needs a "black box" that takes a vector on one side and spits out a vector on the other side. Linearity is just a promise of the "black box" (which our solvers blindly trust).
```

These solvers have in common that they are used in some variation of
```cpp
Operator op; // some operator f to invert
Solver solver( copyable, construct_params...);
Vector x, b;
double rtol = 1e-8, atol = rtol;
solver.solve( op, x, b, solve_params...);
```
Importantly, many solvers take a preconditioner and the weights as solve parameters. The latter define the scalar product
\begin{align}
   \langle x, y \rangle := \sum_i x_i w_i y_i
\end{align}
in which in particular the error norm $|| v || = \sqrt{ \langle v, v\rangle }$ is computed.

Section [Operators](sec:operator) details the format and how to write the function $f$ in code, while [Preconditioners](sec:preconditioner) deal with the inverse operators.
```{seealso}
The [doxygen documentation](https://feltor-dev.github.io/doc/dg/html/group__invert.html) holds the complete list of solvers and details the parameters they use.
```

Furthermore, we have the **multigrid family** of solvers, which we designed as a customizable combination of the above solvers into
- `dg::nested_iterations`
- `dg::fmg_solve` (experimental).

Here, we refer to section [The multigrid solvers](sec:multigrid) for how to construct and use these solvers.
```{seealso}
The multigrid section in the [doxygen documentation](https://feltor-dev.github.io/doc/dg/html/group__multigrid.html)  holds the complete list of solvers and details the parameters they use.
```

```{admonition} Initial guess
Look at `dg::Extrapolation` to get an initial guess based on past solutions.
```

## A first example: inverting the Laplace operator

An often encountered problem is the discretization and inversion of elliptic equations, for example Poison's equation.
\begin{align}
-\Delta \phi = \rho
\end{align}
Using a local discontinuous Galerkin discretization this equation transforms to
\begin{align}
 M \vec \phi = \vec \rho
 \end{align}
where $M$ is self-adjoint in the dG weights $W$. See the [theory guide](https://www.overleaf.com/read/rpbjsqmmfzyj) on overleaf for more details on the discretization.
Since $M$ is self-adjoint we can use `dg::PCG` to invert the equation:

```c++
#include <iostream>
// include the basic dg header
#include "dg/algorithm.h"
```

We want to solve on the domain $[0,2\pi]\times [0,2\pi]$

```c++
const double lx = 2.*M_PI;
const double ly = 2.*M_PI;
```

With the somewhat trivial solution $\phi = \sin(x)\sin(y)$

```c++
double fct(double x, double y){
    return sin(y) * sin(x);
}
```

```c++
double laplace_fct( double x, double y) {
    return 2 * sin(y) * sin(x);
}
```

```c++
double initial( double x, double y) {
    return sin(0);
}

```

```c++
// The numbers in parentheses are our suggestions ...
unsigned n = 3, Nx = 10, Ny = 10;
const double eps = 1e-4;
// clang as script is unfortunately not very fast so don't make the numbers too big
//std::cout << "Type n (3), Nx (20) and Ny (20)! \n";
//std::cin >> n >> Nx >> Ny;
std::cout << "Computing on the Grid " <<n<<" x "<<Nx<<" x "<<Ny <<"\n";
dg::CartesianGrid2d grid( 0, lx, 0, ly, n, Nx, Ny, dg::DIR, dg::PER);
std::cout<<"Evaluate initial guess for iterative scheme\n";
dg::DVec x = dg::evaluate( initial, grid);
// create volume and inverse volume on previously defined grid
const dg::DVec vol2d = dg::create::volume( grid);

// Create negative, unnormalized, positive definite Laplacian
dg::Elliptic<dg::CartesianGrid2d, dg::DMatrix, dg::DVec> laplaceM( grid);
// allocate memory in conjugate gradient
unsigned max_iter = n*n*Nx*Ny;
dg::PCG<dg::DVec > pcg( x, max_iter);

// Evaluate right hand side and solution on the grid
dg::DVec b = dg::evaluate ( laplace_fct, grid);
const dg::DVec solution = dg::evaluate ( fct, grid);
dg::Timer t;
t.tic();
// we do not use a preconditioner in solution method
unsigned number = pcg.solve( laplaceM, x, b, 1., vol2d, eps);
t.toc();
std::cout << "Number of pcg iterations "<< number <<"\n";
std::cout << "For a precision of "<< eps<<"\n";
std::cout << "Took "<< t.diff()<<"s\n";
```

    Computing on the Grid 3 x 10 x 10
    Evaluate initial guess for iterative scheme
    Number of pcg iterations 15
    For a precision of 0.0001
    Took 1.82155s

This program executes on the device as is evident from the use of
 `dg::DVec`, a typedef for `thrust::device_vector<double>`, and `dg::DMatrix`.
 The new class that we encounter in this program is `dg::Elliptic`, a
 level 4 class. What this class does is to create and store the `dx` and `dy` matrices from the given grid. Furthermore, it allocates some
 internal workspace and defines a member function that uses `dg::blas1` and `dg::blas2` functions to discretize the Laplacian.
 The class depends on three template parameters,
 the geometry, the matrix and the vector class. The matrix and the
 vector class must fit together, that is, they must be useable in
 a `dg::blas2::symv` function.
 ```{note}
 The `dg::Elliptic` class
 acts as a matrix itself. This becomes clear in the line `dg::blas2::symv( laplaceM, x, lap_x)` in the example below. The `M` in `laplaceM`
 reminds us that `dg::Elliptic` discretizes the negative Laplacian in order to
 generate a postive definite operator.
 ```

 The geometry `dg::CartesianGrid2d` defines a two-dimensional Euclidean metric
 tensor. Recall, that in general the Laplacian depends on metric coefficients.

Since the `dg::Elliptic` is a matrix, we can use it in a
conjugate gradient solver. The `dg::PCG` is a Level 2 class and
only depends on the vector class that we use. Recall here that the
conjugate gradient algorithm can be implemented with vector addition `dg::blas1::axpby`,
a scalar product (`dg::blas1::dot`) and a matrix-vector multiplication (`dg::blas2::symv`) alone.

```c++
//compute error
dg::DVec error( solution);
dg::blas1::axpby( 1.,x,-1.,error);

dg::DVec lap_x(x), residuum( b);
dg::blas2::symv(  laplaceM, x, lap_x);
dg::blas1::axpby( 1., lap_x, -1., residuum);

//global relative error in L2 norm is O(h^n)
double result;
result = sqrt(dg::blas2::dot( x, vol2d, x));
std::cout << "L2 Norm of x is               " << result << std::endl;
result = sqrt(dg::blas2::dot(solution, vol2d , solution));
std::cout << "L2 Norm of Solution is        " << result << std::endl;
result = sqrt(dg::blas2::dot(error, vol2d , error));
std::cout << "L2 Norm of Error is           " << result << std::endl;
result = sqrt(dg::blas2::dot( residuum, vol2d, residuum));
std::cout << "L2 Norm of Residuum is        " << result << std::endl;
```

    L2 Norm of x is               3.14152
    L2 Norm of Solution is        3.14159
    L2 Norm of Error is           0.00348843
    L2 Norm of Residuum is        0.000467984

(sec:operator)=
## Writing an operator to invert
Feltor has an assortment of already made Operators available that have special implementations, for example the
`dg::Elliptic` or `dg::Helmholtz` classes. Sometimes, however your own problem deviates slightly from what is already available or you want to start entirely from scratch.

For example in the first example we inverted the simple Laplacian. However, what if you want to solve the equation
\begin{align}
 \exp(\phi) - \Delta \phi = \rho
 \end{align}
which is non-linear. This means our function is now $f(x) = \exp(x)-\Delta x$ instead of $f(x) = \Delta x$. Feltor accepts several ways a function or matrix is implemented. One way you could implement the new $f$ based on the existing old $f$ (or in fact any $f$) is with **lambda functions**.
````{admonition} dg::apply
In Feltor the function that enables the implementation of a matrix-free algorithm is `dg::blas2::symv` or its alias `dg::apply`:
```cpp
    dg::apply( op, x,y); // compute y = f(x)
    dg::blas2::symv( op, x,y );//completely equivalent
```
`dg::apply` will accept many types, you will find the full list in the doxygen documentation.
Notably, it accepts functors and will try to call
```cpp
op(x,y); // compute y = f(x)
```
i.e. the apply call is equivalent to a functor call.
In order for this to work your own matrix class must be a functor/lambda with the signature `void operator()(const container&,container&)` where `container` is a placeholder for the vector class you use
````

### Lambda functions
In C++14 lambda functions are a very powerful tool.
They are basically compiler generated functors that
can be written in a very concise and short way.
```{seealso}
If you are unfamiliar with lambdas a good watch on youtube is for example [Lambdas from Scratch - Arthur O'Dwye](https://www.youtube.com/watch?v=3jCOwajNch0)
```

```cpp
auto op = [&lap = laplaceM, tmp=VectorType(copyble)]( const auto& x, auto& y) mutable
{
    dg::blas2::symv( lap, x, y);
    dg::blas1::transform( x, tmp, dg::EXP<double>());
    dg::blas1::axpby( 1., tmp, 1., y);
}
```
Several things are noteworthy here

  - Our lambda is a template or a "generic lambda" through the use of "auto" in the interface
  - Our lambda uses "init capture", a feature from C++14. We caught the elliptic object by reference (in order to avoid a deep copy but we should beware "dangling references") and allocated a "workspace" object tmp
  - Our lambda is declared "mutable" (because we write in the temporary tmp)

Note that the lambda now has the correct interface to be used in a solver. For example
```cpp
dg::AndersonAcceleration acc( copyable, 3);
acc.solve( op, phi, rho, weights, 1e-5, 1e-5, 1000, 1e-5, 3);
```

### Overload operator()
The equivalent of a lambda function is of course to write an operator yourself. The above lambda can be implemented like:
```cpp
template<class Geometry, class Matrix, class Container>
struct Operator
{
    Operator( dg::Elliptic<Geometry,Matrix,Container>& ell, const Container& copyable):
        m_tmp( copyable), m_lap(ell) {}
    template<class ContainerType0, class ContainerType1>
    void operator()( const ContainerType0& x, ContainerType1& y)
    {
        dg::blas2::symv( m_lap, x, y);
        dg::blas1::transform( x, m_tmp, dg::EXP<double>());
        dg::blas1::axpby( 1., m_tmp, 1., y);
    }
    private:
    Container m_tmp;
    dg::Elliptic<Geometry,Matrix,Container>& m_lap;
};
```
As you can see this is a lot more verbose than a lambda function. It is useful for complicated functors that hold
internal state. It is less useful for very small classes, where the parenthesis operator only consists of a line or two and when the operator is basically only
an adaptor of another type.

(sec:preconditioner)=
## Preconditioning and inverse operators

A preconditioner is a matrix that represents the approximate inverse of the matrix to invert.
From an
interface point of view, the preconditioner has the same conditions
as the matrix itself, i.e. must be usable in the `dg::blas2::symv` function
```cpp
Precond p(...);
dg::blas2::symv( p, x, y); // computes y = P(x)
```
This example works if `using Precond = Vector` is a
vector, which will be interpreted as a **diagonal preconditioner**.
For example the `dg::Elliptic` class comes with a default diagonal preconditioner:
```cpp
unsigned number = pcg.solve( laplaceM, x, b, laplaceM.precond(), vol2d, eps);
```
Here, the `precond` method returns a vector which contains the inverse of the volume per default.

But what if we want to use a more involved preconditioner? In fact, the interface of the preconditioner
must look exactly the same as that of the operator, which we discussed in the [previous section](sec:operator).
When constructing a preconditioner manually we thus have again two options: (i) write an entirely new class
and overload the function call operator or (ii) use lambda functions.

Consider the following piece of code
```cpp
dg::PCG<dg::DVec > pcg( x, max_iter);
auto inverse_op = [&] ( const auto& y, auto& x)
{
    pcg.solve( laplaceM, x, y, 1., vol2d, eps);
}
```
`inverse_op` now has the signature of a matrix in Feltor. That is we can write
```cpp
dg::blas2::symv( inverse_op, b, x);
```
which will compute $x = \Delta^{-1} b$. This means that `inverse_op` could be used as a preconditioner.
Of course, here the "preconditioner" is the exact inverse of the operator which means that any iterative scheme converges in one iteration.

```{admonition} Preconditioner
A preconditioner is nothing but an approximate inverse to a matrix.
```
We should remember that the preconditioner only needs to approximately invert the equation.
In the next example, we show
how to use a lambda to combine an elliptic operator with a Chebyshev solver that applies 10 iterations as a preconditioner that can be used in PCG (solves $-\Delta \Phi = \rho $)

```cpp
// The operator that we want to solve
dg::Elliptic<...> pol( grid);
pol.set_chi( chi);
//estimate EigenValue
dg::EVE<Vector> eve( copyable);
double eps_ev = 1e-2, ev;
// compute largest Eigenvalue in a few iterations
counter = eve.solve( pol, x, b, 1., pol.weights(), ev, eps_ev);
// Here we combine the Elliptic operator with the Chebyshev solver
dg::ChebyshevIterations<Vector> cheby( x);
auto precond = [nu=10, ev, &pol, &cheby](const auto& y, auto& x)
        {
            cheby.solve( pol, x, y, 1., ev/100., ev*1.1, nu+1, true);
        };
// Now we can call the solver
dg::PCG< Vector > pcg( x, 1000);
pcg.solve( pol, x, y, precond, pol.weights(), eps, 1, 1);
```

```{admonition} Is it faster?
Preconditioning aims at reducing the number of iterations. But the real question is:
Is the solve with preconditioner _faster_ than without preconditioner? We emphasize here that _faster_ is not the same as _less iterations_. In fact, the above example uses less PCG iterations than the unpreconditioned CG method, but is overall slower to execute (on desktop PCs).
Predicting why code is slower or faster is not always an easy task.
The hope in the above code for example is that the lack of dot products in the Chebyshev Iteration makes the code faster (because it lacks global communication) and for high enough process number the above code might actually perform better than unpreconditioned CG.
The only sure way to tell is to try and measure.
```

(sec:multigrid)=
## The multigrid solvers
The basic feature of a multigrid solver is that it (approximately) solves the equation on more than one grid.
The idea is that on a coarse grid a solver generally works faster than on a fine grids with more resolution.

The easiest multigrid solver is called _nested iterations_ or _full approximation scheme_ (for the non-linear case). Here the solution on a coarse grid is used as an improved initial guess for the fine grid solution.

### Construct Nested Grids
In Feltor the first thing to do for a multigrid solver is to construct multiple grids:
```cpp
unsigned stages = 3;
Geometry grid(...);
dg::NestedGrids<Geometry, Matrix, Container> nested( grid, stages);
```
The nested grids provide the functionality to project and interpolate between
various grids and also provides a workspace for a solve function.

### Construct Nested operators and solvers
We use the nested grids to construct an operator and a solver on each grid.
Like a preconditioner the solver needs to be represented as an inverse operator
```cpp
Container chi = ...;
std::vector<Container> multi_chi = nested.project( chi);
std::vector<dg::Elliptic<Geometry, Matrix, Container> >
    multi_pol( stages);
std::vector<std::function<void( const Container&, Container&)> >
    multi_inv_pol(stages);
for(unsigned u=0; u<stages; u++)
{
    multi_pol[u] = { nested.grid(u), dg::centered};
    multi_pol[u].set_chi( multi_chi[u]);
    multi_inv_pol[u] = [eps = multi_eps[u],
                        pcg = dg::PCG<Container>( multi_chi[u], 1000 ),
                        &pol = multi_pol[u]) ]
        ( const auto& y, auto& x) mutable
        {
            // Note that we do not need to provide an intial guess ourselves.
            //It is already contained in x and will be provided by the multigrid solver.
            pcg.solve( pol, x, y, pol.precond(), pol.weights(),
                eps, 1., 1 );
        };
}
```

Note how we init the solver in-place and capture the operator by reference in the lambda.
This
is very similar to how we constructed the preconditioner in section [preconditioner](sec:preconditioner).
```{admonition} Nested Type
Note how we store the lambdas in a vector of `std::function`, which erases the type of the lambda.
Therefore, it is possible to use different kind of solvers at each stage.
```
Often, the lambda body is used to provide more refined functionality.
```cpp
    multi_inv_pol[u] = [u=u, eps = multi_eps[u],
                        pcg = dg::PCG<Container>( multi_chi[u], 1000 ),
                        &pol = multi_pol[u]) ](
        const auto& y, auto& x)
    {
#ifdef MPI_VERSION
        int rank;
        MPI_Comm_rank(MPI_COMM_WORLD, &rank);
#endif //MPI
        dg::Timer t;
        t.tic();
        if( u==0)
            number = pcg.solve( pol, x, y, pol.precond(), pol.weights(),
                eps, 1., 1 );
        else // check stopping criterion only every 10th iteration
            number = pcg.solve( pol, x, y, pol.precond(), pol.weights(),
                eps, 1., 10 );
        t.toc();
        DG_RANK0 std::cout << "# Nested iterations stage: " << u << ", iter: " << number << ", took "<<t.diff()<<"s\n";
    };
```
```{seealso}
Combining elliptic operators and PCG solves with nested iteration appears so often in application codes that we packaged it into the utilitiy class `dg::MultigridCG2d`. Check it out in the doxygen documentation.
```

### Solve
The final step is simply to call the appropriate function
```cpp
dg::nested_iterations( multi_pol, x, b, multi_inv_pol, nested);
```
or in the functional programming style make the multigrid itself an inverse operator
```cpp
auto inverse_op = [&] (const auto& y, auto& x)
{
    dg::nested_iterations( multi_pol, x, y, multi_inv_pol, nested);
};
```

```{admonition} Combine and build
The possibility to use lambdas in our solve methods opens up the possibility to combine various solvers arbitrarily in Feltor. For example we could use `dg::fmg_solve` as a preconditioner for a `dg::PCG` solver.
```


# Binary black hole initial data {#example_bbh_id}

In this example we run the elliptic solver to compute initial data for binary
black holes.

```python
# Distributed under the MIT License.
# See LICENSE.txt for details.

# Dependencies:
%pip install numpy matplotlib pandas 'h5py>=3.0.0' ruamel.yaml
```

```python
import h5py
import matplotlib.pyplot as plt
import multiprocessing
import numpy as np
import os
import pandas as pd
from ruamel.yaml import YAML
plt.style.use('../plots.mplstyle')
```

```bash
%%bash
# Clean up output files from previous runs
rm -f Bbh*.h5
```

First, make sure you have compiled the `SolveXcts` executable. Put in the path
to your build directory below. Make sure you have compiled in `Release` mode.

```python
SPECTRE_BUILD_DIR = "/Users/nlf/Work/spectre/build-Default-Release"
SPECTRE_HOME = "/Users/nlf/Projects/spectre/develop"
```

## Setup input file

We set up an input file based on the
`tests/InputFiles/Xcts/BinaryBlackHole.yaml` example:

```python
# Load example input file
load_input_file_path = os.path.join(
    SPECTRE_HOME, 'tests/InputFiles/Xcts/BinaryBlackHole.yaml')
yaml = YAML()
with open(load_input_file_path, 'r') as open_input_file:
    input_file = yaml.load(open_input_file)

# Modify example input file
# - Set output file names
input_file['Observers']['VolumeFileName'] = 'BbhVolume'
input_file['Observers']['ReductionFileName'] = 'BbhReductions'

# Write modified input file
with open('Bbh.yaml', 'w') as open_input_file:
    yaml.dump(input_file, open_input_file)
```

## Run executable

We pass the input file to the `SolveXcts` executable to solve the elliptic
problem. It will take a few minutes to complete on ~10 cores. Adapt the command
below to your system, or run the `SolveXcts` executable with the `Bbh.yaml`
input file manually.

```python
NUM_CORES = multiprocessing.cpu_count()
SOLVE_XCTS = os.path.join(SPECTRE_BUILD_DIR, 'bin/SolveXcts')
!{SOLVE_XCTS} --input-file Bbh.yaml +p {NUM_CORES}
```

    Charm++: standalone mode (not using charmrun)
    Charm++> Running in Multicore mode: 12 threads (PEs)
    Converse/Charm++ Commit ID: v6.10.2-0-g7bf00fa
    Warning> Randomization of virtual memory (ASLR) is turned on in the kernel, thread migration may not work! Run 'echo 0 > /proc/sys/kernel/randomize_va_space' as root to disable it, or try running with '+isomalloc_sync'.
    CharmLB> Load balancer assumes all CPUs are same.
    Charm++> Running on 1 hosts (12 sockets x 1 cores x 1 PUs = 12-way SMP)
    Charm++> cpu topology info is gathered in 0.002 seconds.

    Executing '/Users/nlf/Work/spectre/build-Default-Release/bin/SolveXcts' using 12 processors.
    Charm++ startup time in seconds: 3.621687
    Date and time at startup: Tue Feb 22 15:08:18 2022

    SpECTRE Build Information:
    Version:                      2022.02.17
    Compiled on host:             98c21919c2ac
    Compiled in directory:        /Users/nlf/Work/spectre/build-Default-Release
    Source directory is:          /Users/nlf/Projects/spectre/develop
    Compiled on git branch:       xcts_input_files
    Compiled on git revision:     db7100f700b
    Linked on:                    Thu Jan 27 17:45:20 2022

    The following options differ from their suggested values:

    Option parsing completed.
    Multigrid level 0 has 232 elements in 54 blocks distributed on 12 procs.
    Multigrid level 1 has 54 elements in 54 blocks distributed on 12 procs.
    NewtonRaphson initialized with residual: 1.058476e+01
    Gmres initialized with residual: 1.058476e+01
    Gmres(1) iteration complete. Remaining residual: 2.003738e-01
    Gmres(2) iteration complete. Remaining residual: 1.206809e-01
    Gmres(3) iteration complete. Remaining residual: 8.694367e-02
    Gmres(4) iteration complete. Remaining residual: 7.275577e-02
    Gmres(5) iteration complete. Remaining residual: 4.520779e-02
    Gmres(6) iteration complete. Remaining residual: 1.464362e-02
    Gmres(7) iteration complete. Remaining residual: 6.754772e-03
    Gmres has converged in 7 iterations: RelativeResidual - The residual magnitude has decreased to a fraction of 0.001 of its initial value or below (0.00063816).
    NewtonRaphson(1) iteration complete (0 globalization steps, step length 1). Remaining residual: 2.865222e+00
    Gmres initialized with residual: 2.865222e+00
    Gmres(1) iteration complete. Remaining residual: 4.062729e-02
    Gmres(2) iteration complete. Remaining residual: 2.455538e-02
    Gmres(3) iteration complete. Remaining residual: 1.602018e-02
    Gmres(4) iteration complete. Remaining residual: 1.181106e-02
    Gmres(5) iteration complete. Remaining residual: 5.591212e-03
    Gmres(6) iteration complete. Remaining residual: 2.012421e-03
    Gmres has converged in 6 iterations: RelativeResidual - The residual magnitude has decreased to a fraction of 0.001 of its initial value or below (0.000702361).
    NewtonRaphson(2) iteration complete (0 globalization steps, step length 1). Remaining residual: 8.164886e-03
    Gmres initialized with residual: 8.164886e-03
    Gmres(1) iteration complete. Remaining residual: 1.279278e-03
    Gmres(2) iteration complete. Remaining residual: 7.680224e-04
    Gmres(3) iteration complete. Remaining residual: 6.026001e-04
    Gmres(4) iteration complete. Remaining residual: 5.116235e-04
    Gmres(5) iteration complete. Remaining residual: 3.771227e-04
    Gmres(6) iteration complete. Remaining residual: 1.521095e-04
    Gmres(7) iteration complete. Remaining residual: 4.029530e-05
    Gmres(8) iteration complete. Remaining residual: 1.632393e-05
    Gmres(9) iteration complete. Remaining residual: 5.028700e-06
    Gmres has converged in 9 iterations: RelativeResidual - The residual magnitude has decreased to a fraction of 0.001 of its initial value or below (0.000615893).
    NewtonRaphson(3) iteration complete (0 globalization steps, step length 1). Remaining residual: 1.690461e-03
    Gmres initialized with residual: 1.690461e-03
    Gmres(1) iteration complete. Remaining residual: 6.584416e-06
    Gmres(2) iteration complete. Remaining residual: 5.651935e-06
    Gmres(3) iteration complete. Remaining residual: 5.288583e-06
    Gmres(4) iteration complete. Remaining residual: 4.922907e-06
    Gmres(5) iteration complete. Remaining residual: 4.250901e-06
    Gmres(6) iteration complete. Remaining residual: 1.596286e-06
    Gmres has converged in 6 iterations: RelativeResidual - The residual magnitude has decreased to a fraction of 0.001 of its initial value or below (0.00094429).
    NewtonRaphson(4) iteration complete (0 globalization steps, step length 1). Remaining residual: 1.606242e-06
    Gmres initialized with residual: 1.606242e-06
    Gmres(1) iteration complete. Remaining residual: 7.008839e-07
    Gmres(2) iteration complete. Remaining residual: 5.285042e-07
    Gmres(3) iteration complete. Remaining residual: 4.919028e-07
    Gmres(4) iteration complete. Remaining residual: 3.871318e-07
    Gmres(5) iteration complete. Remaining residual: 1.603757e-07
    Gmres(6) iteration complete. Remaining residual: 2.922369e-08
    Gmres(7) iteration complete. Remaining residual: 1.438336e-08
    Gmres(8) iteration complete. Remaining residual: 5.271254e-09
    Gmres(9) iteration complete. Remaining residual: 2.125873e-09
    Gmres(10) iteration complete. Remaining residual: 1.044162e-09
    Gmres has converged in 10 iterations: RelativeResidual - The residual magnitude has decreased to a fraction of 0.001 of its initial value or below (0.000650065).
    NewtonRaphson(5) iteration complete (0 globalization steps, step length 1). Remaining residual: 1.066508e-09
    Gmres initialized with residual: 1.066508e-09
    Gmres(1) iteration complete. Remaining residual: 8.000055e-10
    Gmres(2) iteration complete. Remaining residual: 5.991477e-10
    Gmres(3) iteration complete. Remaining residual: 4.073811e-10
    Gmres(4) iteration complete. Remaining residual: 2.559880e-10
    Gmres(5) iteration complete. Remaining residual: 1.242268e-10
    Gmres(6) iteration complete. Remaining residual: 3.521815e-11
    Gmres has converged in 6 iterations: AbsoluteResidual - The residual magnitude has decreased to 1e-10 or below (3.52181e-11).
    NewtonRaphson(6) iteration complete (0 globalization steps, step length 1). Remaining residual: 3.589802e-11
    NewtonRaphson has converged in 6 iterations: AbsoluteResidual - The residual magnitude has decreased to 1e-10 or below (3.5898e-11).

    Done!
    Wall time in seconds: 769.172052
    Date and time at completion: Tue Feb 22 15:21:03 2022

    [Partition 0][Node 0] End of program

## Load initial data into evolutions

The executable has created H5 output files with volume data and diagnostics:

```python
!ls *.h5
```

    BbhReductions.h5  BbhVolume0.h5

The volume data in the `BbhVolume*.h5` files (one per node) can be imported into
evolution executables. It contains the following data:

```python
!h5ls -r BbhVolume*.h5
```

    /                        Group
    /VolumeData.vol          Group
    /VolumeData.vol/ObservationId1423324405707320626 Group
    /VolumeData.vol/ObservationId1423324405707320626/ConformalFactor Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/ExtrinsicCurvature_xx Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/ExtrinsicCurvature_yx Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/ExtrinsicCurvature_yy Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/ExtrinsicCurvature_zx Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/ExtrinsicCurvature_zy Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/ExtrinsicCurvature_zz Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/HamiltonianConstraint Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/InertialCoordinates_x Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/InertialCoordinates_y Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/InertialCoordinates_z Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/Lapse Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/MomentumConstraint_x Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/MomentumConstraint_y Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/MomentumConstraint_z Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/ShiftExcess_x Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/ShiftExcess_y Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/ShiftExcess_z Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/Shift_x Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/Shift_y Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/Shift_z Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/SpatialMetric_xx Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/SpatialMetric_yx Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/SpatialMetric_yy Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/SpatialMetric_zx Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/SpatialMetric_zy Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/SpatialMetric_zz Dataset {61344}
    /VolumeData.vol/ObservationId1423324405707320626/bases Dataset {696}
    /VolumeData.vol/ObservationId1423324405707320626/connectivity Dataset {294400}
    /VolumeData.vol/ObservationId1423324405707320626/grid_names Dataset {5272}
    /VolumeData.vol/ObservationId1423324405707320626/quadratures Dataset {696}
    /VolumeData.vol/ObservationId1423324405707320626/total_extents Dataset {696}
    /src.tar.gz              Dataset {5307433}

- `Lapse` ($\alpha$), `Shift` ($\beta^i$), `SpatialMetric` ($\gamma_{ij}$) and
  `ExtrinsicCurvature` ($K_{ij}$): These quantities solve the Einstein
  constraint equations in "corotating" coordinates, such that, e.g.,
  $\partial_t K = 0$. The black holes remain approximately at the same
  coordinate positions when starting to evolve the initial data in these
  coordinates. Note that $\beta^i \propto r$ in these coordinates, which can be
  large and hence numerically undesireable.
- `ShiftExcess` ($\beta_\text{excess}^i$): This shift vector excludes the
  rotational part, so it is asymptotically small. Import `ShiftExcess` instead
  of `Shift` in an evolution to obtain coordinates in which the black holes are
  orbiting. Note that only
  \begin{equation}
    \beta^i = \beta_\text{background}^i + \beta_\text{excess}^i
  \end{equation}
  fulfills all coordinate conditions imposed on the initial data, such as
  $\partial_t K = 0$. The background shift is typically
  \begin{equation}
    \beta_\text{background}^i = \epsilon^{ijk} \Omega^{j} x^k + \dot{a}_0 x^i
    \text{,}
  \end{equation}
  where $\Omega$ is the orbital angular velocity and $\dot{a}_0$ is the
  expansion parameter.
  See the [Xcts::AnalyticData::Binary](https://spectre-code.org/classXcts_1_1AnalyticData_1_1Binary.html)
  class for details.

## Plot diagnostics

We plot the diagnostics in `BbhReductions.h5` to see what happened during the
elliptic solve:

```python
# These routines read the data and process them a bit. You can skip to the plot
# below to see the results.

def split_iteration_sequence(data):
    left_bounds = np.where(data.index == 0)[0]
    right_bounds = list(left_bounds[1:]) + [-1]
    return [data.iloc[i:j] for i, j in zip(left_bounds, right_bounds)]

def load_dataset(subfile):
    legend = subfile.attrs['Legend']
    return pd.DataFrame(data=subfile, columns=legend).set_index(legend[0])

with h5py.File('BbhReductions.h5', 'r') as h5_file:
    nonlinear_residuals = load_dataset(h5_file['NewtonRaphsonResiduals.dat'])
    all_linear_residuals = split_iteration_sequence(
        load_dataset(h5_file['GmresResiduals.dat']))
```

```python
# Plot linear residuals
n = 0
lin_label = "Linear residual"
for linear_residuals in all_linear_residuals:
    plt.semilogy(
        linear_residuals.index + n,
        linear_residuals["Residual"] /
        all_linear_residuals[0]["Residual"].iloc[0],
        color="black",
        label=lin_label,
        marker='.',
        markersize=2.5,
        zorder=30,
    )
    n += len(linear_residuals) - 1
    lin_label = None

# Plot nonlinear residuals
nonlin_xticks = [0] + list(
    np.cumsum([len(l) - 1 for l in all_linear_residuals]))
plt.semilogy(
    nonlin_xticks,
    nonlinear_residuals["Residual"] / nonlinear_residuals["Residual"].iloc[0],
    color="black",
    ls="dotted",
    marker=".",
    label="Nonlinear residual",
    zorder=40,
)
plt.xticks(nonlin_xticks)
xticks = range(len(
    all_linear_residuals)) if nonlinear_residuals is None else nonlin_xticks

plt.xlabel("Cumulative linear solver iteration")
plt.legend();
```

The plot shows the convergence of the nonlinear solver (dotted line), along with
the convergence of the linear solver that runs in each nonlinear iteration
(solid line). A few things to note:

- The residual converges down to almost machine precision, but that doesn't
  reflect the discretization error of the solution. It only shows that we have
  solved the discretized problem very accurately. To get an idea of the
  discretization error we would have to look at quantities such as constraint
  norms.
- The Newton-Raphson nonlinear solver converges slowly at first, and then begins
  to converge quadratically once we are closer to the solution and hence the
  linearization is more accurate.
- The linear solver needs only a few steps to converge, which is a feature of
  our multigrid-Schwarz preconditioner.

You can read more about the elliptic solver in this paper:

- N. L. Vu _et al_., A scalable elliptic solver with task-based parallelism for
  the SpECTRE numerical relativity code (2022),
  [arXiv:2111.06767](https://arxiv.org/abs/2111.06767)

```python

```


```python
%matplotlib inline

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

plt.rcParams["figure.figsize"] = (12, 9)
plt.rcParams["font.size"] = 18
```

# Depletion and Conversion

A key feature of reactor physics is the depletion of fuel and buildup of other fission products.

## Learning Objectives

- Describe the relationship between reactivity loss and fuel burnup.
- Calculate changes in core power distribution with fuel depletion.
- Calculate the need for core reloading based on fuel burnup.
- Recognize the relationship between reactivity control and core power distribution.
- Calculate depletion of both fuel isotopes and burnable poisons.
- Quantify the conversion of uranium into plutonium.
- Identify the physical meaning of the capture cross section.
- Evaluate the evolution of fuel composition throughout the lifetime of a reactor.
- Analyze the energy produced during the cycle based on core power, reactivity control, and depletion.

## Fuel Depletion

Decays, fissions, and absorptions compete throughout the life of the reactor.

### Decay

Recall decay.

$$
\begin{align}
        \frac{dN}{dt} &= -\lambda N \\
       \Rightarrow N_i(t) &= N_i(0)e^{-\lambda t}\\
\end{align}
$$

where

$$
\begin{align}
        N_i(t) &= \mbox{number of isotopes i adjusted for decay}\\
        N_i(0)&= \mbox{initial condition}\\
        \lambda &= \frac{ln(2)}{t_{1/2}} = \mbox{decay constant} \\
        t &= \mbox{time elapsed [s]}\\
        t_{1/2} &= 1 = \mbox{half-life [s]} \\
\end{align}
$$

```python
import math
def n_decay(t, n_initial=100, half_life=1):
    """This function describes the decay of an isotope"""
    lam = math.log(2)/half_life
    return n_initial*math.exp(-lam*t)

# This code plots the decay of an isotope
import numpy as np
y = np.arange(24)
x = np.arange(24)
for t in range(0,24):
    x[t] = t
    y[t] = n_decay(t)

# creates a figure and axes with matplotlib
fig, ax = plt.subplots()
scatter = plt.scatter(x, y, color='blue', s=y*20, alpha=0.4)
ax.plot(x, y, color='red')

# adds labels to the plot
ax.set_ylabel('N_i(t)')
ax.set_xlabel('Time')
ax.set_title('N_i')

# adds tooltips
import mpld3
labels = ['{0}% remaining'.format(i) for i in y]
tooltip = mpld3.plugins.PointLabelTooltip(scatter, labels=labels)
mpld3.plugins.connect(fig, tooltip)

mpld3.display()
```

### Fission Yield

Recall fission yield.

These are percentages expressed relative to number of fissioning nuclei. That is, the numer of fissions, not the number of fission product nuclei, so yields should sum to 2, reflecting the two fission product nuclei per fission (200%).

\begin{align}
\sum_{i=1}^I\gamma_i &= 2
\end{align}

So, the amount of a single isotope produced by fission is:

\begin{align}
F_i &= \gamma_i\Sigma_f\phi\\
    &= Y_i\Sigma_f\phi\\
    \end{align}
where
    \begin{align}
\gamma_i &= Y_i = \mbox{fission yield of nuclide i}\\
\end{align}

### Reaction Rates

Similarly, recall that we went over reaction rates.

- The microscopic cross section is just the likelihood of the event per unit area.
- The macroscopic cross section is just the likelihood of the event per unit area of a certain density of target isotopes.
- The reaction rate is the macroscopic cross section times the flux of incident neutrons.

\begin{align}
R_{i,j}(\vec{r}) &= N_j(\vec{r})\int dE \phi(\vec{r},E)\sigma_{i,j}(E)\\
R_{i,j}(\vec{r}) &= \mbox{reactions of type i involving isotope j } [reactions/cm^2s]\\
N_j(\vec{r}) &= \mbox{number of nuclei participating in the reactions }\\
E &= \mbox{energy}\\
\phi(\vec{r},E)&= \mbox{flux of neutrons with energy E at position i}\\
\sigma_{i,j}(E)&= \mbox{cross section}\\
\end{align}

We said this can be written more simply as $R_x = \sigma_x I N$, where I is intensity of the neutron flux. In the notation of the above equation, we can describe the production of an isotope by neutron absorption by another isotope as :

\begin{align}
\mbox{isotope i production via neutron absorption in m} = f_{im}\sigma_{am}N_m  \phi
\end{align}

### Total composition evolution

\begin{align}
\frac{dN_i}{dt} &= \sum_{m=1}^{M}l_{im}\lambda_mN_m + \phi\sum_{m=1}^{M}f_{im}\sigma_mN_m - (\lambda_i + \phi\sigma_i + r_i - c_i)N_i + F_i\Big|_{i\in [1,M]}\\
\end{align}
\begin{align}
N_i &= \mbox{atom density of nuclide i}\\
M &= \mbox{number of nuclides}\\
l_{im} &= \mbox{fraction of decays of nuclide m that result in formation of nuclide i}\\
\lambda_i &= \mbox{radioactive decay constant of nuclide i}\\
\phi &= \mbox{neutron flux, averaged over position and energy}\\
f_{im} &= \mbox{fraction of neutron absorption by nuclide m leading to the formation of nuclide i}\\
\sigma_m &= \mbox{average neutron absorption cross section of nuclide m}\\
r_i &= \mbox{continuous removal rate of nuclide i from the system}\\
c_i &= \mbox{continuous feed rate of nuclide i}\\
F_i &= \mbox{production rate of nuclide i directly from fission}\\
\end{align}

### Example: $^{135}Xe$

**Discussion: What is interesting about Xenon?**

$^{135}Xe$ is produced directly by fission and from the decay of iodine.

\begin{align}
\frac{dN_{xe}}{dt} &= \sum_{m=1}^{M}l_{Xem}\lambda_mN_m + \phi\sum_{m=1}^{M}f_{Xem}\sigma_mN_m - (\lambda_{Xe} + \phi\sigma_{Xe} + r_{Xe} - c_{Xe})N_{Xe} + F_{Xe}\\
 &= -\lambda_{Xe}N_{Xe}  - \sigma_{aXe}\phi N_{Xe} + \lambda_IN_I + F_{Xe}\\
 &= -\lambda_{Xe}N_{Xe}  - \sigma_{aXe}\phi N_{Xe} + \lambda_IN_I + \gamma_{Xe}\Sigma_f\phi\\
 \gamma_{Xe} &= 0.003\\
 \gamma_{I} &= 0.061\\
\end{align}

### Example: $^{239}Pu$

\begin{align}
\frac{dN_{Pu}}{dt} &= \sum_{m=1}^{M}l_{Pum}\lambda_mN_m + \phi\sum_{m=1}^{M}f_{Pum}\sigma_mN_m - (\lambda_{Pu} + \phi\sigma_{Pu} + r_{Pu} - c_{Pu})N_{Pu} + F_{Pu}\\
\end{align}

Let's formulate this equation together.

$$\mathrm{^{238}_{\ 92}U \ + \ ^{1}_{0}n \ \longrightarrow \ ^{239}_{\ 92}U \ \xrightarrow [23.5\ min]{\beta^-} \ ^{239}_{\ 93}Np \ \xrightarrow [2.3565\ d]{\beta^-} \ ^{239}_{\ 94}Pu}$$

- Decay of what nuclides result in the formation of $^{239}Pu$?
- Does $^{239}Pu$ decay?
- Is there a nuclide that becomes  $^{239}Pu$ after it absorbs a neutron?
- Does  $^{239}Pu$ ever absorb neutrons?
- Is  $^{239}Pu$ ever produced directly from fission?

Answers:

- Decay of what nuclides result in the formation of $^{239}Pu$?
- Does $^{239}Pu$ decay?
- Neutron absorption in what nuclide results in  $^{239}Pu$?
- Does  $^{239}Pu$ ever absorb neutrons?
- Is  $^{239}Pu$ ever produced directly from fission?

## Burnable Poisons

- Gadolinia ($Gd_2O_3$) or erbia ($Er_2O_3$) common
- Natural Erbium consists of Er166, Er167, Er168  and Er170 primarily.  Er167 has large thermal  cross section.
- Gd is an early life burnable poison, typically gone by 10‐20 GWd
- Boron also used widely.
- Can be mixed with the fuel or a coating on the pellet.

\begin{align}
\frac{dN^P(t)}{dt} &= -g(t)\sigma_{aP}N^P(t)\phi\\
g(t) &= \frac{\mbox{average flux inside BP}}{\mbox{average flux in core}}\\
\sigma_{aP} &=\mbox{neutron absorption cross section of the BP}\\
N^P(t) &= \mbox{number of atoms of the BP at time t}
\end{align}

## Wrap-up
After this lesson, you should be more equipped to:

- Describe the relationship between reactivity loss and fuel burnup.
- Calculate changes in core power distribution with fuel depletion.
- Calculate the need for core reloading based on fuel burnup.
- Recognize the relationship between reactivity control and core power distribution.
- Calculate depletion of both fuel isotopes and burnable poisons.
- Quantify the conversion of uranium into plutonium.
- Identify the physical meaning of the capture cross section.
- Evaluate the evolution of fuel composition throughout the lifetime of a reactor.
- Analyze the energy produced during the cycle based on core power, reactivity control, and depletion.

```python

```