*This notebook contains course material from [CBE30338](https://jckantor.github.io/CBE30338)
by Jeffrey Kantor (jeff at nd.edu); the content is available [on Github](https://github.com/jckantor/CBE30338.git).
The text is released under the [CC-BY-NC-ND-4.0 license](https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode),
and code is released under the [MIT license](https://opensource.org/licenses/MIT).*

< [PID Control with Anti-Reset-Windup](http://nbviewer.jupyter.org/github/jckantor/CBE30338/blob/master/notebooks/04.04-PID-Control-with-Anti--Reset--Windup.ipynb) | [Contents](toc.ipynb) | [PID Controller Tuning](http://nbviewer.jupyter.org/github/jckantor/CBE30338/blob/master/notebooks/04.06-PID-Controller-Tuning.ipynb) >

# Realizable PID Control

The next part that needs attention is the derivative control term which is currently implemented as

\begin{align}
D & = K_D\frac{de^D}{dt} \approx K_D\frac{e^D_k - e^D_{k-1}}{t_k - t_{k-1}}
\end{align}

where is a weighted difference between the setpoing and process variable $e^D = \gamma SP - PV$.

This term clearly presents problems as the control sampling period gets small. Any difference between $e^D_k$ and $e^D_{k-1}$ due to measurement noise, for example, will be magnified when divided by the small difference between $t_k$ and $t_{k-1}$.

## Example: PID Control with Significant Measurement Noise

Here we start with the PID control algorithm that implements setpoint weighting and anti-reset windup.

```python
def PID(Kp, Ki, Kd, MV_bar=0, beta=1, gamma=0):
    # initialize stored data
    eD_prev = 0
    t_prev = -100
    P = 0
    I = 0
    D = 0

    # initial control
    MV = MV_bar

    while True:
        # yield MV, wait for new t, SP, PV, TR
        data = yield MV

        # see if a tracking data is being supplied
        if len(data) < 4:
            t, PV, SP = data
        else:
            t, PV, SP, TR = data
            I = TR - MV_bar - P - D

        # PID calculations
        P = Kp*(beta*SP - PV)
        I = I + Ki*(SP - PV)*(t - t_prev)
        eD = gamma*SP - PV
        D = Kd*(eD - eD_prev)/(t - t_prev)
        MV = MV_bar + P + I + D

        # Constrain MV to range 0 to 100 for anti-reset windup
        MV = 0 if MV < 0 else 100 if MV > 100 else MV
        I = MV - MV_bar - P - D

        # update stored data for next iteration
        eD_prev = eD
        t_prev = t
```

We modify the control problem by introducing a signficant degree of measurement noise.

\begin{align}
T^{meas}_{1,k} & = T_{1,k} + e_k
\end{align}

where the measurement error is a normal random variable with zero mean and a standard deviation of 2°C. For this example we've deliberately chosen a large value of $K_D = 10$ to illustrate the issue of using significant amounts of derivative control in presence of measurement noise.

```python
%matplotlib inline
from tclab import clock, setup, Historian, Plotter
import numpy as np

TCLab = setup(connected=False, speedup=10)

controller = PID(2, 0.1, 10, beta=0)   # create pid control
controller.send(None)                  # initialize

tfinal = 600

with TCLab() as lab:
    h = Historian([('SP', lambda: SP), ('PV', lambda: PV), ('MV', lambda: MV), ('Q1', lab.Q1)])
    p = Plotter(h, tfinal)
    T1 = lab.T1
    for t in clock(tfinal, 1):
        SP = T1 if t < 50 else 50                     # get setpoint
        PV = lab.T1 + 2*np.random.normal()            # get measurement
        MV = controller.send([t, PV, SP, lab.Q1()])   # compute manipulated variable
        lab.Q1(MV)                                    # apply
        p.update(t)                                   # update information display
```

## Modifying the Derivative Term

To avoid problems with high frequency measurement noise and disturbances, consider an implementation of the derivative term of PID control using a first-order model

\begin{align}
\tau_f \frac{dD}{dt} + D & = K_D \frac{de^D}{dt}
\end{align}

where $e^D = \gamma SP - PV$ is the setpoint weighted error. The idea behind this model is that, for slowly varying changes, $D \sim K_d \frac{de_D}{dt}$ which provides the desired derivative control function, and for more rapidly varying changes $\tau_f \frac{dD}{dt} + D \sim K_d \frac{de_D}{dt}$ which dissipates the influence of more rapidly varying changes attributable to measurement noise.

The characteristic time constant $\tau_f$ is chosen as a fraction of the derivative time constant $\tau_D$

\begin{align}
\tau_f & = \frac{\tau_D}{N} = \frac{K_D}{NK_P}
\end{align}

Rearraging for $D$ and integrating this model gives

\begin{align}
D & =  \frac{K_D}{\tau_f} e^D - \frac{1}{\tau_f} \int_0^t D\ dt
\end{align}

In discrete time we define and use a backwards difference approximation to obtain

\begin{align}
S_k & = \int_0^{t_k} D\ dt \\
& \approx \sum_{k'=1 }^{k} D_{k'} (t_{k'} - t_{k'-1}) = D_k (t_k - t_{k-1}) + S_{k-1}
\end{align}

The backwards difference approximation provides a recursion relationship that is stable for all $t_k \geq t_{k-1}$. We are thereby tracking a pair of implicit recursion relations

\begin{align}
D_k & = \frac{K_D}{\tau_f} e^D_k - \frac{1}{\tau_f} \left[ D_k(t_k - t_{k-1}) + S_{k-1} \right] \\
\\
S_k & = D_k (t_k - t_{k-1}) + S_{k-1}
\end{align}

Making these explicit

\begin{align}
D_k & = \frac{K_D e^D_k - S_{k-1}}{\tau_f + (t_k - t_{k-1})}  \\
\\
S_k & = D_k (t_k - t_{k-1}) + S_{k-1}
\end{align}

Finally, using parameters for the non-interacting model of PID control

\begin{align}
D_k & = \frac{NK_P(K_De^D_k - S_{k-1})}{K_D + NK_P(t_k - t_{k-1})}  \\
\\
S_k & = D_k (t_k - t_{k-1}) + S_{k-1}
\end{align}

This recursive calculation can be initialized with $S_0 = K_De^D_0$.

## Example Revisited

To show the impact of this change in the implementation of the derivative term, we repeat the experiment above with the revised algorithm and $N = 1$.  (A more typical value of $N$ is 5 or 10, but we've chosen a somewhat more conservative value because of the magnitude of the measurement noise present in this example, and the large value of $K_D$ chosen to demonstrate the issues.

```python
def PID(Kp, Ki, Kd, MV_bar=0, MV_min=0, MV_max=100, beta=1, gamma=0, N=10):

    # initial yield and return
    data = yield MV_bar
    t,  = data[0:3]

    P = Kp*(beta*SP - PV)
    MV = MV_bar + P
    MV = 0 if MV < 0 else 100 if MV > 100 else MV
    I = 0
    D = 0
    dI = 0

    S = Kd*(gamma*SP - PV)
    t_prev = t

    while True:
        # yield MV, wait for new t, SP, PV, TR
        data = yield MV, P, I, D, dI

        # see if a tracking data is being supplied
        if len(data) < 4:
            t, PV, SP = data
        else:
            t, PV, SP, TR = data
            d = MV - TR
            #I = TR - MV_bar - P - D

        # PID calculations
        P = Kp*(beta*SP - PV)
        eD = gamma*SP - PV
        D = N*Kp*(Kd*eD - S)/(Kd + N*Kp*(t - t_prev))

        # conditional integration
        dI = Ki*(SP - PV)*(t - t_prev)
        if (MV_bar + P + I + D + dI) > MV_max:
            dI = max(0, min(dI, MV_max - MV_bar - P - I - D))
        if (MV_bar + P + I + D + dI) < MV_min:
            dI += min(0, max(dI, MV_min - MV_bar - P - I - D))
        I += dI
        MV = MV_bar + P + I + D

        # Clamp MV to range 0 to 100 for anti-reset windup
        MV = max(MV_min, min(MV_max, MV))

        # update stored data for next iteration
        S = D*(t - t_prev) + S
        t_prev = t
```

The desired setpoint is 50°C which is acheived with a power setting of approximately 50%.  We'll start out at the power setting for a period of 400 seconds, then turn on the controller.

```python
%matplotlib inline
from tclab import clock, setup, Historian, Plotter
import numpy as np

TCLab = setup(connected=False, speedup=10)

controller = PID(2, 1, 0, beta=1, N=1)   # create pid control
controller.send(None)                  # initialize

tfinal = 600

with TCLab() as lab:
    h = Historian([('SP', lambda: SP), ('PV', lambda: PV), ('MV', lambda: MV), ('Q1', lab.Q1),
                   ('P', lambda: P), ('I', lambda: I), ('D', lambda: D), ('d', lambda: d)])
    p = Plotter(h, tfinal)
    T1 = lab.T1
    for t in clock(tfinal, 1):
        SP = T1 if t < 50 else 120                     # get setpoint
        PV = lab.T1 + 2*np.random.normal()            # get measurement
        MV, P, I, D, d = controller.send([t, PV, SP, lab.Q1()])   # compute manipulated variable
        lab.Q1(MV)                                    # apply
        p.update(t)                                   # update information display
```

```python

```

```python

```

< [PID Control with Anti-Reset-Windup](http://nbviewer.jupyter.org/github/jckantor/CBE30338/blob/master/notebooks/04.04-PID-Control-with-Anti--Reset--Windup.ipynb) | [Contents](toc.ipynb) | [PID Controller Tuning](http://nbviewer.jupyter.org/github/jckantor/CBE30338/blob/master/notebooks/04.06-PID-Controller-Tuning.ipynb) >


# Attention的介绍

当我们去看张照片的时候，我们首先就是先去看整体，这里有车、有街道、还有很多的广告牌，不知道大家是否有感受到，当我开始描述这些的时候，其实就是我把注意力放在了这些上面了。那当我们想要跟深入了解这张图片的时候，我就要把注意力放的更聚焦。比如说，我想知道这是拍的哪里，那我可能会试着去看看广告牌上的文字，看这些文字是不是能给我一些启示。

我们可能会试着把注意力放到不同的区域，那我们就能够得到更多的关于不同角度的信息。这些信息，正是我们希望在图像处理的时候希望得到的。

比如说上面的这句话，“她正在吃一个绿色的苹果”，这里我们可以比较清楚的看到，“吃”和“苹果”有很强的联系，那我们就希望在处理吃这个单词的时候，能够在语义中，包含一定的苹果的信息，这样能够帮助我们更好的理解“吃”这个动作。“绿色的”和“苹果”也是一样的，attention的机制能够帮助我们在处理单个的token的时候，带有一定的上下文信息。这就像是一种“软性记忆”一样，帮助我们记住上下文中包含的信息。

当我们看一篇文章的时候，其实也是类似的。我们从拿到一篇文章开始，首先关注的也只是一些关键性的词语，这些关键性的词语，就能够帮助我们快速的判断文章的内容和结构。这些场景就是我们在一些具体场景中对attention的应用。

假设我们的时间序列： $X = \{x_0, ..., x_n\}$, 我们把它放到坐标轴上，这些点是我们从整体数据中采样出来的，这里有很多的噪音（noisy），我们想办法能不能通过一些方法，得到这些数据的更好的表示，从而能够使噪音减少。

**Re-Weighting**: 让我们的这些点都包含一些其他点的信息，使得所有的数据能够更平滑一些. 我们定义这些**re-weighting**的参数为 $w_i = {w_{0i}, ..., w_{ni}}$, 我们使用这些weights就能够得到一个点的新的表示$y_i = w_i * X$.

$$Y = \{ w_i * x_i | w_i \in W x_i \in X\}$$

这里的$w_i$， 其实本质上是对所有输入 X的一个总结，然后得到的一个对应的值。

Self-attention 是一个序列到序列的操作：一个向量的序列作为输入，一个向量的序列作为输出。 我们把输入的序列定义为 $x_1, x_2, ..., x_n$, 并且与它相关的输出向量是 $y_1, y_2, ..., y_n$. 这两个向量的维度都是$k$. 那么对于每一个点$x_i$, 我们都可以通过一个不同的权重值，来将它转化为一个新的序列，这个新的序列就可能是我们原始序列的一个更好的表示，这些 $w_i$ 就是一组attention的值.  它能够帮我们找到子序列和全局的attention的关系，也就是找到权重值 $w_i$.

# Self-attention

Self-attention就本质上是一种特殊的attention。attention机制能够帮我们找到子序列和全局的attention的关系，也就是找到权重值 $w_i$. self-attention对于attention的变化，其实就是寻找权重值$w_i$的过程不同。

为了能够产生输出的向量$y_i$，self-attention其实是对所有的输入做了一个加权平均的操作: $y_i = \sum^{n}_{j} w_{i,j} * x_j$. $n$代表整个序列的长度, 并且$\sum^{n}_{j} w_{i,j} == 1$. 值得一提的是, 这里的$w_{i, j}$ 并不是一个需要神经网络学习的参数，它是来源于 $x_i$ 和 $x_j$的之间的计算的结果。 它们之间最简单的一种计算方式，就是使用点积的方式
$$w'_{i,j} = {x_i}^{T} * x_j$$

这个点积的输出的取值范围在负无穷和正无穷之间，所以我们要使用一个softmax把它映射到[0, 1]之间, 并且要确保它们对于整个序列而言的和为1
$$w_{i, j} = softmax(w'_{i, j}) = \frac{e^{w'_{i,j}}}{\sum^n_{j} e^{w'_{i,j}}}$$

## Attention 和 self-attention 的区别是什么 ？
这里有几个重要的区别，可以帮助大家更好的区分在不同任务中的使用方法：

1. 在神经网络中，通常来说你会有输入层（input），应用激活函数后的输出层（output），在RNN当中你会有状态（state）。如果attention (AT) 被应用在某一层的话，它更多的是被应用在输出或者是状态层上，而当我们使用self-attention（SA），这种注意力的机制更多的实在关注input上。

2. Attention (AT) 经常被应用在从编码器（encoder）转换到解码器（decoder）。比如说，解码器的神经元会接受一些AT从编码层生成的输入信息。在这种情况下，AT连接的是两个不同的组件（component），编码器和解码器。但是如果我们用SA，它就不是关注的两个组件，它只是在关注你应用的那一个组件。那这里他就不会去关注解码器了，就比如说在Bert中，使用的情况，我们就没有解码器。

3. SA可以在一个模型当中被多次的、独立的使用（比如说在Transformer中，使用了18次；在Bert当中使用12次）。但是，AT在一个模型当中经常只是被使用一次，并且起到连接两个组件的作用。

4. SA比较擅长在一个序列当中，寻找不同部分之间的关系。比如说，在词法分析的过程中，能够帮助去理解不同词之间的关系。AT却更擅长寻找两个序列之间的关系，比如说在翻译任务当中，原始的文本和翻译后的文本。这里也要注意，在翻译任务重，SA也很擅长，比如说Transformer。

5. AT可以连接两种不同的模态，比如说图片和文字。SA更多的是被应用在同一种模态上，但是如果一定要使用SA来做的话，也可以将不同的模态组合成一个序列，再使用SA。

6. 对我来说，大部分情况，SA这种结构更加的general，在很多任务作为降维、特征表示、特征交叉等功能尝试着应用，很多时候效果都不错。

# Scaled-Dot Attention

\begin{equation}
Attention(Q, K, V) = softmax(\frac{QK^{T}}{\sqrt{d_k}}) * V
\end{equation}

其中 $Q \in \mathbb{R}^{n \times d_k}, K \in \mathbb{R}^{m \times d_k}, V \in \mathbb{R}^{m \times d_k}$. softmax 则是在 m 的那一维进行归一化。而自注意力，则是对于同一个 $X \in \mathbb{R}^{n \times d}$，通过不同的投影矩阵得到:

$$Q = X * W_q, \; \text{where} \; W_q \in \mathbb{R}^{d \times R'} $$

$$K = X * W_k, \; \text{where} \; W_k\in \mathbb{R}^{d \times R'} $$

$$V = X * W_v, \; \text{where} \; W_v \in \mathbb{R}^{d \times R'} $$

至于 Multi-Head Attention，则不过是 Attention 运算在不同的参数下重复多次然后将多个输出拼接起来，属于比较朴素的增强。

## Dot-Product操作为什么要被缩放

1. $d_k$ 较大时，向量内积容易取很大的值

假设 query 和 key 向量中的元素都是相互独立的均值为 0，方差为 1 的随机变量，那么这两个向量的内积 $q^{T}k = \sum_{i=1}^{d_k} q_i k_i$ 的均值为0, 方差为$d_k$

当$d_k$较大的时候，点积的方差较大，不同的q与不同的k算出的对齐分数相差较大，有的远大于0，有的远小于0.

2. 向量内积的值（对齐分数）较大时，softmax 函数梯度很小

Softmax 公式为
\begin{equation}
S(x_i) = \frac{e^{x_i}}{\sum_{j=0}^{n}e^{x_j}}
\end{equation}
求偏导:
\begin{aligned}
\frac{\partial}{\partial_{x_i}}S(x_i) = S(x_i)(1 - S(x_i)) \\
\frac{\partial}{\partial_{x_j}}S(x_i) = -S(x_i)S(x_j)
\end{aligned}

这里$x_i$就是q的转置与k的点积运算: $x_i = q^{T}k$。所以当方差较大的时候，很有可能存在某个key，其与 query 计算出来的对齐分数远大于其他的 key 与该 query 算出的对齐分数。导致softmax的反向传播时候误差为0，梯度消失

解决方法:
\begin{equation}
score(q,k) = \frac{q^Tk}{\sqrt{d_k}}
\end{equation}

这样对齐分数公式算的方差从$d_k$变为1，从而softmax反向运算的梯度不趋向于0

- [Self-attention中dot-product操作为什么要被缩放](https://zhuanlan.zhihu.com/p/149903065)
- [Properties of Dot Product of Random Vectors](https://zhuanlan.zhihu.com/p/436614439)

## Attention必须有个Softmax吗?
简单起见我们就没显式地写出 Attention 的缩放因子:
\begin{equation}
Attention(Q, K, V) = softmax(QK^{T}) * V
\end{equation}

本文我们主要关心 Self Attention 场景，所以为了介绍上的方便统一设 $Q, K, V \in \mathbb{R}^{n \times d}$, 一般长序列场景下都有 $n >> d$

### 摘掉Softmax

$QK^T$这一步我们得到一个$n \times n$的矩阵， 就是这一步决定了 Attention 的复杂度是$O(n^2)$; 如果没有 Softmax，那么就是三个矩阵连乘 $QK^TV$, ，而矩阵乘法是满足结合率的，所以我们可以先算 $K^TV$， 得到一个$d \times d$的矩阵，然后再用$Q$左乘它, 由于$d << n$, 所以这样算大致的复杂度只是$O(n)$（就是$Q$左乘那一步占主导). **也就是说，去掉 Softmax 的 Attention 的复杂度可以降到最理想的线性级别$O(n)$, 这显然就是我们的终极追求：Linear Attention，复杂度为线性级别的 Attention**

### 一般的定义

Scaled-Dot Attention的定义等价修改为：
\begin{aligned}
Attention(Q, K, V)_i &= Softmax(Q_i K*T)V \\
&= \sum_{j=1}^{n} score(q_i, k_j)  v_j \\
&=\sum_{j=1}^{n}\frac{e^{q_i^Tk_j}}{\sum_{j=1}^n e^{q_i^Tk_j}}v_j \\
&= \sum_{j=1}^{n} \frac{sim(q_i, k_j)}{\sum_{j=1}^{n}sim(q_i, k_j)} v_j
\end{aligned}

[线性Attention的探索：Attention必须有个Softmax吗?](https://mp.weixin.qq.com/s?__biz=MzIwMTc4ODE0Mw==&mid=2247508324&idx=1&sn=0238864a68d94e7057574bee65315c6c&chksm=96ea7ee4a19df7f2332408475299041a876ff87ed0431bbb56736755ff3fc9fd5fbed29d6aa2&scene=21#wechat_redirect)

[去掉 Attention 的 Softmax，复杂度降为 O (n)](https://wmathor.com/index.php/archives/1540/)

## Multi-Head Attention

1. https://mp.weixin.qq.com/s?__biz=MzIwMTc4ODE0Mw==&mid=2247508324&idx=1&sn=0238864a68d94e7057574bee65315c6c&chksm=96ea7ee4a19df7f2332408475299041a876ff87ed0431bbb56736755ff3fc9fd5fbed29d6aa2&scene=21#wechat_redirect

2. https://mp.weixin.qq.com/s?__biz=MzIwMTc4ODE0Mw==&mid=2247486960&idx=1&sn=1b4b9d7ec7a9f40fa8a9df6b6f53bbfb&chksm=96e9d270a19e5b668875392da1d1aaa28ffd0af17d44f7ee81c2754c78cc35edf2e35be2c6a1&token=1416940766&lang=zh_CN&scene=21#wechat_redirect

3. https://mp.weixin.qq.com/s/zBUowlIU0JezWRnFBKYbug

```python

```


# FFM232, Klassisk fysik och vektorfält - Föreläsningsanteckningar

 **[Christian Forssén](http://fy.chalmers.se/subatom/tsp/), Institutionen för fysik, Chalmers, Göteborg, Sverige**

Date: **Oct 10, 2016**

# 10. Värmeledning, diffusionsekvation

Betrakta ett temperaturfält $T(\vec{r},t)$
* På ett område $V$.

* Med randvillkor längs $\partial V$.

* I närvaro av eventuella värmekällor.

* Med ett explicit tidsberoende.

Vi söker nu en differentialekvation för detta fält.

## Värmeledning (diffusion)

[Comment 1: Vi vet att värme strömmar från varmare till kallare.  Det innebär att
vi har ett flöde av värmeenergi i en riktning som är motsatt $\vec{\nabla} T$.]

### Antagande 1:

Värmeströmmen kan skrivas

$$
\begin{equation}
  \vec{q} = -\lambda\vec{\nabla} T,
\end{equation}
$$

där $\lambda$ är värmekonduktiviteten (värmeledningsförmågan), och $\vec{q}$ är själva
värmeflödet med enheten J\,s$^{-1}$\,m$^{-2}$.

### Antagande 2:

Värmetätheten $\varepsilon$ är proportionell mot temperaturen
$$
\varepsilon = c \rho T,
$$
där $c$ är värmekapacitiviteten och $\rho$ är densiteten.
* $[\varepsilon] = \mathrm{J}/\mathrm{m}^3$

* $[c] = \mathrm{J} \mathrm{kg}^{-1} \mathrm{K}^{-1}$

* $[\rho] = \mathrm{kg} \mathrm{m}^{-3}$

Betrakta nu en volym $\Delta V$, vilken begränsas av en sluten yta $S = \partial(\Delta V)$.
* Värmeenergin i denna volym är

$$
\begin{equation}
  H = \int_{\Delta V} Tc\rho \mbox{d}V
  \label{eq:H} \tag{1}
\end{equation}
$$

* Utflödet av värme från denna volym är

$$
\begin{equation}
  \oint_{\partial (\Delta V)} \vec{q}\cdot \mbox{d}\vec{S}.
\end{equation}
$$

Förutsatt att det inte finns några värmekällor i $\Delta V$  måste
utflödet motsvara förändringen per tidsenhet av värmen
i $\Delta V$

$$
\begin{equation}
  \frac{\partial H}{\partial t} = - \oint_{\partial (\Delta V)} {\vec{q}\cdot \mbox{d}S}.
\end{equation}
$$

Med insättning av ekv. [(1)](#eq:H) i VL och användande av Gauss sats i HL fås

$$
\begin{equation}
  \int_{\Delta V}
\frac{\partial}{\partial t} \left( Tc\rho \right) \mbox{d}V = -\int_{\Delta V} \vec{\nabla} \cdot \vec{q}\mbox{d}V.
\label{energi} \tag{2}
\end{equation}
$$

Volymen $\Delta V$ är helt godtyckligt vald, så likheten måste gälla för alla volymer $\Delta V$. I så fall kan vi sätta integranderna lika med varandra

$$
\begin{equation}
  \frac{\partial}{\partial t}\left(T c\rho\right) = - \vec{\nabla} \cdot \vec{q}
= \vec{\nabla} \cdot \lambda \vec{\nabla} T = \lambda \nabla^2 T.
\end{equation}
$$

[Comment 2: Här skriver vi Laplacianen som $\nabla^2$.]

Om vi nu antar att $c$, $\rho$ och $\lambda$  är konstanter, så kan vi skriva ekvationen som

$$
\begin{equation}
  \frac{\partial T}{\partial t} = k \nabla^2 T,
\end{equation}
$$

där

$$
\begin{equation}
  k \equiv \frac{\lambda}{c\rho}.
\end{equation}
$$

Den här ekvationen kallas för värmeledningsekvationen.

[Comment 3: Värmeledningsekvationen är en kontinuitetsekvation för värmeenergin. Ni känner antagligen igen härledningen från det liknande bevis som gjordes i kap. 4 i kurskompendiet.]

### Stationär lösning

För en tidsoberoende värmefördelning gäller $\partial T / \partial t = 0$ och därmed

$$
\begin{equation}
  \nabla^2 T = 0
\end{equation}
$$

som vi kallar för Laplace-ekvation.

### Värmekälla

Vad händer nu om vi har en värmekälla i volymen $\Delta V$?  Antag att värme
produceras av en källa med tätheten $u = u(\vec{r},t)$ med enheten W\,m$^{-3}$.
Då måste vi komplettera ekv. [(2)](#energi) med en term för denna
uppvärmning

$$
\begin{equation}
  \int_{\Delta V} \frac{\partial}{\partial t} \left( Tc\rho \right) \mbox{d}V = \int_{\Delta V} \lambda
\nabla^2 T
\mbox{d}V + \int_{\Delta V} u \mbox{d}V.
\end{equation}
$$

Värmeledningsekvationen (med konstant $c,\rho$) blir då

$$
\begin{equation}
  \frac{\partial T}{\partial t} = k \nabla^2 T + \frac{u}{c\rho}.
\end{equation}
$$

Om temperaturfördelningen är tidsoberoende kan vi skriva ekvationen som

$$
\begin{equation}
  \nabla^2 T = -\frac{u}{\lambda}
\end{equation}
$$

som är ett exempel på Poissons ekvation.
Högerledet kallar vi då för en källterm.

-------------------------------------------------------------------

### Exempel: En-dimensionell värmeledning

--------------------------

Betrakta ett område $x \in [0,L]$ i en dimension med följande villkor på temperaturfördelningen $T = T(x,t)$
* Begynnelsevillkor: $T(x,0) = T_0 \sin \frac{\pi x}{L}$.

* Randvillkor: $T(0,t) = T(L,t) = 0$ (dvs Dirichlets homogena RV).

[Comment 4: Teckna $T(x,0)$ och jämför gärna med Neumanns homogena randvillkor som hade stoppat värmetransport genom ändarna eftersom $\partial T / \partial x = 0$ (vid $x=0$ och $x=L$).]

Finn temperaturfördelningen för $t > 0$ i avsaknad av någon värmekälla.

*Lösning*: Värmeledningsekvationen är
$$
\frac{\partial T(x,t)}{\partial t} - k \frac{\partial^2 T(x,t)}{\partial x^2} = 0.
$$
Notera att
$$
\frac{\partial^2}{\partial x^2} \sin \frac{\pi x}{L} = - \frac{\pi^2}{L^2} \sin \frac{\pi x}{L},
$$
vilket gör det naturligt att ansätta lösningen $T(x,t) = f(t) \sin \frac{\pi x}{L}$.

Denna ansats uppfyller randvillkoren och begynnelsevillkoret om $f(0) = T_0$. Insättning i värmeledningsekvationen ger
$$
f'(t) \sin \frac{\pi x}{L} + k f(t) \frac{\pi^2}{L^2} \sin \frac{\pi x}{L} = 0,
$$
vilket har lösningen
$$
f(t) = A e^{-\pi^2 k t / L^2},
$$
där $A = T_0$ bestäms av begynnelsevillkoret. Lösningen
$$
T(x,t) = T_0 e^{-\pi^2 k t / L^2} \sin \frac{\pi x}{L}
$$
innebär att temperaturen minskar kontinuerligt (flödar ut genom ändarna) och att en stationär lösning, $T=0$, erhålls för stora $t$.

-------------------------------------------------------------------

### Exempel: Värmeledning med källterm

--------------------------

Granitberggrunden i Sverige innehåller en viss mängd
radium, vars radioaktiva sönderfall ger en uppvärming som av en rymdkälla
för värme med konstant källtäthet $\rho_\mathrm{T}$.  Granitens
värmeledningsförmåga är $\lambda$ (i W\,m$^{-1}$\,K$^{-1}$).  Låt
oss göra det orealistiska antagandet att Jorden alltigenom bestod av granit
med dessa egenskaper.  Hur skulle i så fall den stationära
temperaturfördelningen i Jordens inre se ut?  Vad blir temperaturen i
centrum?

*Lösning:*  Vi kan ställa upp differentialekvationen

$$
\begin{equation}
  \nabla^2T = - \frac{\rho_{\rm T}}{\lambda}
\end{equation}
$$

I sfäriska koordinater under antagande om sfärisk symmetri blir ekvationen

$$
\begin{equation}
  \frac{1}{r^2} \frac{\partial}{\partial r}\left(r^2 \frac{\partial T}
{\partial r}\right) = -Q,
\end{equation}
$$

där $Q = \rho_{\rm T}/\lambda$.  Vi kan skriva detta som

$$
\begin{equation}
  \frac{\partial}{\partial r}\left(r^2 \frac{\partial T}
{\partial r}\right) = -Q r^2,
\end{equation}
$$

och sedan integrera en gång.

$$
\begin{equation}
  r^2 \frac{\partial T}{\partial r} = -\frac{1}{3}Qr^3 + A,
\end{equation}
$$

där $A$ är en integrationsvariabel.  Om vi dividerar med $r^2$ får vi

$$
\begin{equation}
  \frac{\partial T}{\partial r} = -\frac{1}{3}Qr + \frac{A}{r^2}.
\end{equation}
$$

Integrerar vi än en gång får vi

$$
\begin{equation}
  T\left(r\right) = - \frac{1}{6} Q r^2 - \frac{A}{r} + B,
\end{equation}
$$

där $B$ är ännu en integrationsvariabel.  Vi måste nu bestämma
värden på de båda integrationsvariablerna.  Först kan vi notera
att temperaturen inte bör bli oändlig i Jordens inre, så $A = 0$.
För det andra noterar vi att temperaturen vid jordytan, $r = R$, är
praktiskt taget 0 jämfört med temperaturen i Jordens centrum, så vi
får ekvationen

$$
\begin{equation}
  0 = -\frac{1}{6} Q R^2 + B.
\end{equation}
$$

vilket ger $B = QR^2/6$. Fysikaliskt så är $B$ temperaturen i Jordens centrum.  Om vi sätter in realistiska värden på $\rho_{\rm T} = 5\times
10^{-8}$ W\,m$^{-3}$, $\lambda = 3,5$ W\,m$^{-1}$\,K$^{-1}$ och $R = 6,4\times
10^6$ m, så får vi $B = 6\times 10^5$ K, vilket är en grov
överskattning av den verkliga temperaturen.

-------------------------------------------------------------------

## Greensfunktioner för värmeledningsekvationen

* Kan vi använda Greensfunktioner för att teckna lösningar till allmänna källfördelningar?

* Notera att fälten (temperatur, värmekälla) är både rums- och tidsberoende.

* Ja, det kan man - Greensfunktionen är då lösningen (med givna randvillkor) till värmeledningsekvationen för en punktkälla i både tid och rum.

[Comment 5: En punktlik energikälla som bara existerar under ett ögonblick, men är precis så stark att den tillförda energimängden är ändlig. Fundera på hur temperaturfältet borde se ut.]

Vi söker alltså lösningen till Greensfunktionsekvationen svarande mot värmeledningsekvationen:
$$
\left(
\frac{\partial}{\partial t}-k\Delta\right)G(\vec{r},t;\vec{r}{\;}',t')
=\delta^D(\vec{r}-\vec{r}{\;}')\delta(t-t')
$$
på hela $D$-dimensionella rummet $\mathbb{R}^D$.
Finner vi lösningen till denna ekvation, kan lösningen till värmeledningsekvationen för godtycklig källfördelning $u$ skrivas
$$
T(\vec{r},t)=\int_{-\infty}^\infty dt'\int d^Dx'\, G(\vec{r},t;\vec{r}{\;}',t')u(\vec{r}{\;}',t')
$$
vilket ses genom direkt insättning

$$
\begin{equation}
\left( \frac{\partial}{\partial t}-k\Delta\right)T(\vec{r},t)
=\int_{-\infty}^\infty dt'\int d^Dx'\,
\left( \frac{\partial}{\partial t}-k\Delta\right)G(\vec{r},t;\vec{r}{\;}',t')u(\vec{r}{\;}',t')
\nonumber
\end{equation}
$$

$$
\begin{equation}
=\int_{-\infty}^\infty dt'\int d^Dx'\,
\delta^D(\vec{r}-\vec{r}{\;}')\delta(t-t')u(\vec{r}{\;}',t')
=u(\vec{r},t).
\end{equation}
$$

som alltså visar att värmeledningsekvationen uppfylls för detta $T$.

* Vi studerar lösningen på ett oändligt, $D$-dimensionellt rum.

* För det första kan vi använda translationsinvarians i rum och tid för att skriva $G(\vec{r},t;\vec{r}{\;}',t') = \tilde G(\vec{r}-\vec{r}{\;}',t-t')$.

* Följande lösning uppfyller ekvationen

$$
\tilde G(\vec{r}-\vec{r}\,',t-t')=\frac{\sigma(t-t')}{(4\pi k (t-t'))^{D/2}}
e^{-\frac{|\vec{r}-\vec{r}\,'|^2}{4k(t-t')}},
$$
där $\sigma(t)$ är stegfunktionen som tar värdet $0$ för $t<0$
och $1$ för $t>0$.

Faktorn $\sigma(t-t')$ gör att en källa vid tidpunkten $t'$ bara kan påverka vad som händer vid senare tidpunkter $t\geq t'$, så vi har kausalitet.

Skissa Greensfunktionens utseende för olika $t$:
* den börjar som en deltafunktion vid $t-t'=0^+$

* för att när tiden går bli bredare och lägre, hela tiden med Gaussisk form.

```
import numpy as np
import pylab as p

# Greens function D=1
def G(x,t):
    if t<0:
        g = np.zeros(len(x))
        return g
    else:
        return np.exp(-x**2/(4*t)) / (np.sqrt(4*np.pi*t))

# End initialization

# Start plot 1
x=np.linspace(-1,1,200)
p.figure(figsize=(6,6))
for t in [-1,.001,0.005,0.01,0.05,0.1]:
    p.plot(x,G(x,t))
p.xlabel(r'$x$')
p.ylabel(r'$G(x,t)$')
# p.ylim([-1,6])
```

[Comment 6: Det faktum att rumsintegralen av $G$ är konstant i tiden för $t-t'>0$,
$$
\int_{\mathbb{R}^D}d^Dx\,G(\vec{r},\vec{r}\,',t,t')=1,
$$
är ett uttryck för energins bevarande, och naturlig om vi minns att vi kan se värmeledningsekvationen som en kontinuitetsekvation.]

Värmeledningsekvationen heter på engelska "the heat equation". Dess Greensfunktion kallas "heat kernel", på svenska ibland "värmekärna".

## Värmeledning (konvektion)

* Ovan har vi enbart behandlat värmeledning via diffusion.

* Konvektion erbjuder betydligt effektivare värmetransport för fluider (vätskor och gaser) genom att varm materia strömmar.

* Vi beskriver detta med en värmeström

$$
\vec{q}_\mathrm{konv} = \rho c T \vec{v}
$$
som skall adderas till diffusionsströmmen från tidigare
$$
\vec{q}_\mathrm{diff} = - \lambda \vec{\nabla} T.
$$
Kontinuitetsekvationen för värmeenergin säger att
$$
\frac{\partial  T}{\partial t} (T c \rho) = -\vec{\nabla} \cdot \vec{q} = -\vec{\nabla} \cdot (\vec{q}_\mathrm{diff} + \vec{q}_\mathrm{konv}).
$$
Antar vi återigen att $c$, $\rho$ och $\lambda$ är konstanter (notera att detta innebär att $\vec{\nabla} \cdot \vec{v} \propto \partial \rho / \partial t = 0$) får vi

$$
\begin{equation}
  \frac{\partial T}{\partial t} + \vec{v} \cdot \vec{\nabla} T = k \nabla^2 T + \frac{u}{c\rho},
\end{equation}
$$

där vi också inkluderat möjligheten att det finns värmekällor.


## Computer Science Intensive Course - MindX

# LAB 5. THUẬT TOÁN SẮP XẾP (1)

```python
# run this cell FIRST
%run test_cases_5.ipynb
```

## Bài 1. Dãy Số Ngược

**Input**: Một dãy *0 < n < 106* số thực đã được sắp xếp theo thứ tự giảm dần. Mỗi phần tử có giá trị trong khoảng [-109, 109].
**Output**: Dãy đã được sắp xếp theo thứ tự tăng dần.
**Ví dụ**:
- Input: [90.12, 50.2, 20.0, 3.14159, 0.0, -12.2, -12.2, -1000.0, -99999999.0]
- Output: [-99999999.0, -1000.0, -12.2, -12.2, 0.0, 3.14159, 20.0, 50.2, 90.12]

```python
def sort_descending_list(inp_list):
    pass
```

```python
# !!! DO NOT MODIFY THIS CELL
# Check result on test cases
test1(sort_descending_list)
```

    Testing on 9 cases.
    - Test 1 PASSED.
    - Test 2 PASSED.
    - Test 3 PASSED.
    - Test 4 PASSED.
    - Test 5 PASSED.
    - Test 6 PASSED.
    - Test 7 PASSED.
    - Test 8 PASSED.
    - Test 9 PASSED.
    CONGRATULATIONS! All test cases passed!

## Bài 2. Số Phức

Một số phức có dạng:
\begin{equation} z = a + bi \end{equation}
với a, b là số thực từ âm vô cùng đến dương vô cùng
**Ví dụ**: z = 3.5 + 1.3i

Modulo của một số phức là giá trị được tính bằng công thức:
\begin{equation} |z| = \sqrt{a^2 + b^2} \end{equation}

Cho danh sách *n* số phức, hãy sắp xếp danh sách theo thứ tự tăng dần của modulo.

**Input**: Một *list* gồm *0 < n < 104* số phức, mỗi số được lưu dưới dạng tuple *(a, b)*, với *-109 < a, b < 109*.
**Output**: List các số phức sau khi đã sắp xếp theo thứ tự tăng dần của modulo.
**Ví dụ**:
- Input: [(32.4, -30.0), (2.57, -2.3), (-111.0, 20.2), (34.5, 20.0), (8.97, -13.2), (6.67, 12.2), (9.09, 12.0)]
- Output: [(2.57, -2.3), (6.67, 12.2), (9.09, 12.0), (8.97, -13.2), (34.5, 20.0), (32.4, -30.0), (-111.0, 20.2)]
- Giải thích: Ở phần tử thứ nhất của input, modulo được tính bằng
\begin{equation} |z| = \sqrt{32.4^2 + (-30.0)^2} = 44.15608678313784 \end{equation}
Tương tự, ta tính modulo của các phần tử còn lại và sắp xếp dãy dựa theo modulo.

**Gợi ý**: Sử dụng hàm *math.sqrt()* để tính căn bậc 2

```python
def sort_complex_list(arr):
    pass
```

```python
# !!! DO NOT MODIFY THIS CELL
# Check result on test cases
test2(sort_complex_list)
```

    Testing on 8 cases.
    - Test 1 PASSED.
    - Test 2 PASSED.
    - Test 3 PASSED.
    - Test 4 PASSED.
    - Test 5 PASSED.
    - Test 6 PASSED.
    - Test 7 PASSED.
    - Test 8 PASSED.
    CONGRATULATIONS! All test cases passed!

## Bài 3. Sắp Xếp Hai Tầng

Một báo cáo điểm cuối năm của một trường THPT cho từng học sinh có dạng:
{'id': 984, 'math': 9.0, 'literature': 5.4}

Cho danh sách điểm cuối năm, hãy sắp xếp danh sách theo quy tắc sau:
- Học sinh có điểm toán cao hơn đứng trước
- Nếu hai học sinh có cùng điểm toán, học sinh có điểm ngữ văn cao hơn đứng trước
- Nếu có cùng điểm ngữ văn, học sinh nào đứng trước trong danh sách gốc đứng trước

**Input**: Một *list* gồm *0 < n < 104* báo cáo điểm của học sinh.
**Output**: List các báo cáo điểm sau khi đã sắp xếp.
**Ví dụ**:
- Input: [{'id': 984, 'math': 9.0, 'literature': 5.4}, {'id': 12, 'math': 9.5, 'literature': 4.3}, {'id': 324, 'math': 9.0, 'literature': 5.4}]
- Output: [{'id': 12, 'math': 9.5, 'literature': 4.3}, {'id': 984, 'math': 9.0, 'literature': 5.4}, {'id': 324, 'math': 9.0, 'literature': 5.4}]

```python
def sort_student_grades(grade_list):
    pass
```

```python
# !!! DO NOT MODIFY THIS CELL
# Check result on test cases
test3(sort_student_grades)
```

    Testing on 8 cases.
    - Test 1 PASSED.
    - Test 2 PASSED.
    - Test 3 PASSED.
    - Test 4 PASSED.
    - Test 5 PASSED.
    - Test 6 PASSED.
    - Test 7 PASSED.
    - Test 8 PASSED.
    CONGRATULATIONS! All test cases passed!

## Bài 4. Nhập Dãy

**Input**: Hai dãy *0 < n1, n2 < 106* số thực đã được sắp xếp theo thứ tự tăng dần. Mỗi phần tử có giá trị trong khoảng [-109, 109]. Hãy nhập hai dãy số này thành một dãy được sắp xếp theo thứ tự tăng dần. (Biết *n1 + n2 < 106*)
**Output**: Dãy đã nhập được sắp xếp theo thứ tự tăng dần.
**Ví dụ**:
- Input: arr1 = [-99999999.0, -12.2, 0.0, 20.0, 50.2, 90.12], arr2 = [-1000.0, -12.2, 3.14159]
- Output: [-99999999.0, -1000.0, -12.2, -12.2, 0.0, 3.14159, 20.0, 50.2, 90.12]

```python
def merge_lists(arr1, arr2):
    pass
```

    [-99999999.0, -1000.0, -12.2, -12.2, 0.0, 3.14159, 20.0, 50.2, 90.12]

```python
# !!! DO NOT MODIFY THIS CELL
# Check result on test cases
test4(merge_lists)
```

    Testing on 10 cases.
    - Test 1 PASSED.
    - Test 2 PASSED.
    - Test 3 PASSED.
    - Test 4 PASSED.
    - Test 5 PASSED.
    - Test 6 PASSED.
    - Test 7 PASSED.
    - Test 8 PASSED.
    - Test 9 PASSED.
    - Test 10 PASSED.
    CONGRATULATIONS! All test cases passed!

```python

```


```python
%%file reaction_coeffs.py
"""This module contains functions that return reaction rate coefficients

   MEMBERS:
   ========
   k_const:   Returns a constant reaction rate coefficient
   k_arr:     Returns the Arrhenius reaction rate coefficient
   k_mod_arr: Returns the modified Arrhenius reaction rate coefficient
"""

import numpy as np

def k_const(k=1.0):
    """Simply returns a constant reaction rate coefficient

    INPUTS:
    =======
    k: float, default value = 1.0
       Constant reaction rate coefficient

    RETURNS:
    ========
    k: float
       Constant reaction rate coefficient

    EXAMPLES:
    =========
    >>> k_const(5.0)
    5.0
    """
    if k < 0:
        raise ValueError("Negative reaction rate coefficients are prohibited.")

    return k

def k_arr(A, E, T, R=8.314):
    """Calculates the Arrhenius reaction rate coefficient

    INPUTS:
    =======
    A: float
       Arrhenius prefactor
       Must be positive
    E: float
       Activation energy
    T: float
       Temperature
       Must be positive
    R: float, default value = 8.314
       Ideal gas constant
       Must be positive

    RETURNS:
    ========
    k: float
       Arrhenius reaction rate coefficient

    EXAMPLES:
    =========
    >>> k_arr(2.0, 3.0, 100.0)
    1.9927962618542914
    """

    if A < 0.0:
        raise ValueError("A = {0:18.16e}:  Negative Arrhenius prefactor is prohibited!".format(A))

    if T < 0.0:
        raise ValueError("T = {0:18.16e}:  Negative temperatures are prohibited!".format(T))

    if R < 0.0:
        raise ValueError("R = {0:18.16e}:  Negative ideal gas constant is prohibited!".format(R))

    return A * np.exp(-E / R / T)

def k_mod_arr(A, b, E, T, R=8.314):
    """Calculates the modified Arrhenius reaction rate coefficient

    INPUTS:
    =======
    A: float
       Arrhenius prefactor
       Must be positive
    b: float
       Modified Arrhenius parameter
    E: float
       Activation energy
    T: float
       Temperature
       Must be positive
    R: float, default value = 8.314
       Ideal gas constant
       Must be positive

    RETURNS:
    ========
    k: float
       Modified Arrhenius reaction rate coefficient

    EXAMPLES:
    =========
    >>> k_mod_arr(2.0, -0.5, 3.0, 100.0)
    0.19927962618542916
    """
    if A < 0.0:
        raise ValueError("A = {0:18.16e}:  Negative Arrhenius prefactor is prohibited!".format(A))

    if T < 0.0:
        raise ValueError("T = {0:18.16e}:  Negative temperatures are prohibited!".format(T))

    if R < 0.0:
        raise ValueError("R = {0:18.16e}:  Negative ideal gas constant is prohibited!".format(R))

    return A * T**b * np.exp(-E / R / T)
```

    Overwriting reaction_coeffs.py

---
# Problem 6
Pretend you're a client who needs to compute the reaction rates at three different temperatures ($T = \left\{750, 1500, 2500\right\}$) of the following system of irreversible reactions:
\begin{align}
  2H_{2} + O_{2} \longrightarrow 2OH + H_{2} \\
  OH + HO_{2} \longrightarrow H_{2}O + O_{2} \\
  H_{2}O + O_{2} \longrightarrow HO_{2} + OH
\end{align}

The client also happens to know that reaction 1 is a modified Arrhenius reaction with $A_{1} = 10^{8}$, $b_{1} = 0.5$, $E_{1} = 5\times 10^{4}$, reaction 2 has a constant reaction rate parameter $k = 10^{4}$, and reaction 3 is an Arrhenius reaction with $A_{3} = 10^{7}$ and $E_{3} = 10^{4}$.

You should write a script that imports your `chemkin` module and returns the reaction rates of the species at each temperature of interest given the following species concentrations:

\begin{align}
  \mathbf{x} =
  \begin{bmatrix}
    H_{2}  \\
    O_{2}  \\
    OH     \\
    HO_{2} \\
    H_{2}O
  \end{bmatrix} =
  \begin{bmatrix}
    2.0 \\
    1.0 \\
    0.5 \\
    1.0 \\
    1.0
  \end{bmatrix}
\end{align}

You may assume that these are elementary reactions.

```python
%%file chemkin.py

import numpy as np
def progress_rate(nu_react, concs, k):
    """Returns the progress rate of a system of irreversible, elementary reactions

    INPUTS:
    =======
    nu_react: numpy array of floats,
              size: num_species X num_reactions
              stoichiometric coefficients for the reaction
    k:        array of floats
              Reaction rate coefficient for the reaction
    concs:    numpy array of floats
              concentration of species

    RETURNS:
    ========
    omega: numpy array of floats
           size: num_reactions
           progress rate of each reaction

    EXAMPLES:
    =========
    >>> progress_rate_2(np.array([[2.0, 1.0], [1.0, 0.0], [0.0, 1.0]]), np.array([2.0, 1.0, 1.0]), 10.0)
    array([ 40.,  20.])
    """
    progress = k # Initialize progress rates with reaction rate coefficients
    for jdx, rj in enumerate(progress):
        if rj < 0:
            raise ValueError("k = {0:18.16e}:  Negative reaction rate coefficients are prohibited!".format(rj))
        for idx, xi in enumerate(concs):
            nu_ij = nu_react[idx,jdx]
            if xi  < 0.0:
                raise ValueError("x{0} = {1:18.16e}:  Negative concentrations are prohibited!".format(idx, xi))
            if nu_ij < 0:
                raise ValueError("nu_{0}{1} = {2}:  Negative stoichiometric coefficients are prohibited!".format(idx, jdx, nu_ij))

            progress[jdx] *= xi**nu_ij
    return progress

def reaction_rate(nu_react, nu_prod, rj):
    """Returns the reaction rate of a system of irreversible, elementary reactions

    INPUTS:
    =======
    nu_react: numpy array of floats,
              size: num_species X num_reactions
              stoichiometric coefficients for the reactants
    nu_prod:  numpy array of floats,
              size: num_species X num_reactions
              stoichiometric coefficients for the products
    k:        float, default value is 10,
              Reaction rate coefficient for the reaction
    concs:    numpy array of floats
              concentration of species

    RETURNS:
    ========
    f: numpy array of floats
       size: num_species
       reaction rate of each specie

    EXAMPLES:
    =========
    >>> nu_react = np.array([[1.0, 0.0], [2.0, 0.0], [0.0, 2.0]])
    >>> nu_prod = np.array([[0.0, 1.0], [0.0, 2.0], [1.0, 0.0]])
    >>> r = np.array([ 40.,  20.])
    >>> reaction_rate(nu_react, nu_prod, r)
    array([-20., -40.,   0.])
    """
    nu = nu_prod - nu_react
    return np.dot(nu, rj)
```

    Writing chemkin.py

```python
#%%file main.py

import numpy as np
import chemkin
import reaction_coeffs as rrc

M = 3 # Number of reaction
N = 5 # Number of species

concs = np.array([2.0, 1.0, 0.5, 1.0, 1.0])

Temps = [750.0, 1500.0, 2500.0]

A1, b1, E1 = 1.0e+08, 0.5, 5.0e+04
A2, E2 = 1.0e+07, 1.0e+04

nu_react = np.array([[2.0, 0.0, 0.0], [1.0, 0.0, 1.0], [0.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
nu_prod = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [2.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]])

k = np.zeros(M)
for T in Temps:
    # Get the reaction rate coefficients
    k[0] = rrc.k_mod_arr(A1, b1, E1, T)
    k[1] = rrc.k_const(1.0e+04)
    k[2] = rrc.k_arr(A2, E2, T)

    # Get progress rates for reactions
    omega = chemkin.progress_rate(nu_react, concs, k)

    # Get reaction rates for species
    f = chemkin.reaction_rate(nu_react, nu_prod, omega)

    # Print reaction rates to screen
    print(f)
```

    [-3607077.87280407 -5613545.1836208   9220623.05642486  2006467.31081673
     -2006467.31081673]
    [ -2.81117621e+08  -2.85597559e+08   5.66715180e+08   4.47993847e+06
      -4.47993847e+06]
    [ -1.80426143e+09  -1.81043736e+09   3.61469878e+09   6.17593098e+06
      -6.17593098e+06]

```python

```


```python
import animation
import numpy as np
import sympy as sym
import matplotlib.pyplot as plt
```

# CW08 Examples

This notebook is intended as a gallery of useful examples and functionality, to reinforce what we have been doing in the class so far.

Go through the examples and corresponding code, and see if you can understand what is happening. Discuss with your group.

Feel free to create new code in new cells to play with the ideas further, which can help you understand. Save your updated notebooks and turn them in if you do so.

## Language-specific Cells

You can change which kernel evaluates individual cells in Jupyter, which can be useful for quick demonstrations. For example, the cell below has been switched to `bash` instead of the default `python3` kernel being used for the rest of the notebook.

```python
%%script bash

# The line above acts exactly as the #!/bin/bash line of a script file
# It tells Jupyter that you are running a script, and which interpreter to use for the script

acc=0
acc2=1
for i in $(seq 20); do
  echo $acc
  tmp=$acc2
  acc2=$(($acc2 + $acc))
  acc=$tmp
done
```

    0
    1
    1
    2
    3
    5
    8
    13
    21
    34
    55
    89
    144
    233
    377
    610
    987
    1597
    2584
    4181

```python
%%script octave

# For example, this script is now in Octave, a free clone of MATLAB
# Next week, we will be looking into using MATLAB itself
# Note in the code below that the function names look very similar to numpy
# The similarity of numpy to MATLAB is deliberate - you will be able to use most of
# the syntax you are familiar with from numpy with only a few minor tweaks

e = zeros(10,10);           # 10x10 matrix of zeros
v = linspace(0,1,10);       # vector of length 10 storing domain of points between 0 and 1
e(2:end,1:end-1) = -eye(9); # Set lower off-diagonal to -1 using 9x9 identity matrix
e(1:end-1,2:end) += eye(9)  # Set upper off-diagonal to +1 using 9x9 identity matrix, do not suppress output
```

    e =

       0   1   0   0   0   0   0   0   0   0
      -1   0   1   0   0   0   0   0   0   0
       0  -1   0   1   0   0   0   0   0   0
       0   0  -1   0   1   0   0   0   0   0
       0   0   0  -1   0   1   0   0   0   0
       0   0   0   0  -1   0   1   0   0   0
       0   0   0   0   0  -1   0   1   0   0
       0   0   0   0   0   0  -1   0   1   0
       0   0   0   0   0   0   0  -1   0   1
       0   0   0   0   0   0   0   0  -1   0

    octave: X11 DISPLAY environment variable not set
    octave: disabling GUI features

## Matplotlib Animations

Recent versions of matplotlib have now enabled easy creation of plot animations. Below I highlight two distinct animation modes, using the code from the supplementary module `animation.py`.

### HTML5 MP4 Video Rendering

First we define a generator that yields new frames for the animation. We define a frame to be the updated range for the plot, seeding the generator with the constant domain. For simplicity, we will show a Gaussian wave packet.

```python
def wave_packet(x):
    for i in range(100):
        yield 10*np.sin(2*np.pi*(x - 0.01*i))*np.exp(-x**2/2)
```

Now, using the code in the helper module, we plot the wave packet, choosing a domain of points $x\in(-4,4)$.

```python
animation.plot_anim(wave_packet,xlim=(-4,4),title="Gaussian Wave Packet",xlabel="x",ylabel="g(x)")
```

The plot then renders as an MP4 video directly inside the notebook using an HTML5 wrapper. You can play, pause, and fullscreen the animation, as well as download it as a compressed mp4 file.

### Animated GIF File Output

Alternatively, you can have matplotlib directly save an animated gif file. These are nice for web display, but are not optimized in filesize by default.  As a test below, we define a sinc fuction, but animate the drawing of the sinc one point at a time for visual effect.  The file will be output to `draw_sinc.gif` in the same directory as the notebook.

```python
def draw_sinc(x):
    y = np.zeros_like(x)
    for ind in range(len(x)):
        y[ind] = 1 if x[ind]==0 else np.sin(x[ind])/x[ind]
        yield y
```

```python
animation.plot_anim(draw_sinc,xlim=(-15*np.pi,15*np.pi),n=200,delay=10,ylim=(-0.5,1),title="Sinc Function",xlabel="x",ylabel="sinc(x)",gif=True)
```

## SymPy (Symbolic Python)

Symbolic python aims to reproduce the functionality of Mathematica completely within python. This means that it can perform symbolic manipulations of expressions, including automatic algebraic simplification.

```python
sym.init_printing()        # Tell sympy to use the prettiest printing available (e.g., LaTeX)
x,y,z = sym.symbols('x y z')   # Define the variables x and y to be mathematical variable symbols
f,g = sym.symbols('f g', cls=sym.Function)  # define the variables f and g to be mathematical function symbols
```

Expand $(x+y)^3$:

```python
sym.expand((x+y)**3)
```

$$x^{3} + 3 x^{2} y + 3 x y^{2} + y^{3}$$

Factor $x^2 + 2xy + y^2$:

```python
sym.factor(x**2 + 2*x*y + y**2)
```

$$\left(x + y\right)^{2}$$

Differentiate: $\frac{d}{dy}(x+y)^3$

```python
sym.diff((x+y)**3, y)
```

$$3 \left(x + y\right)^{2}$$

Differentiate: $\frac{d^3}{dy^2dx}(x+y)^3$

```python
sym.diff((x+y)**3, y, 2, x)
```

$$6$$

Integrate: $\int_{-\infty}^\infty e^{-x^2}dx$

```python
sym.integrate(sym.exp(-x**2),(x,-sym.oo,sym.oo))  # Note that sym.oo is infinity
```

$$\sqrt{\pi}$$

Solve algebraic equation: $x^3 = 1$

```python
sym.solve( sym.Eq(x**3,1) )
```

$$\left [ 1, \quad - \frac{1}{2} - \frac{\sqrt{3} i}{2}, \quad - \frac{1}{2} + \frac{\sqrt{3} i}{2}\right ]$$

Solve (damped Harmonic Oscillator) differential equation: $f''(x) + z f'(x) + y^2 f(x) = 0$

```python
diffeq = sym.Eq(f(x).diff(x,x) + z * f(x).diff(x) +  y**2 * f(x), 0)
diffeq
```

$$y^{2} f{\left (x \right )} + z \frac{d}{d x} f{\left (x \right )} + \frac{d^{2}}{d x^{2}}  f{\left (x \right )} = 0$$

```python
sym.dsolve(diffeq, f(x))
```

$$f{\left (x \right )} = C_{1} e^{\frac{x}{2} \left(- z - \sqrt{- 4 y^{2} + z^{2}}\right)} + C_{2} e^{\frac{x}{2} \left(- z + \sqrt{- 4 y^{2} + z^{2}}\right)}$$

Define a Gaussian function, take its derivative and integral symbolically, then convert them to numerical versions that use numpy:

```python
gaussexp = sym.exp(-x**2/2)/sym.sqrt(2*sym.pi)
gaussdiffexp = gaussexp.diff(x)
gaussintexp = sym.integrate(gaussexp, (x, -sym.oo, x))
```

$g(x) = $

```python
gaussexp
```

$$\frac{\sqrt{2} e^{- \frac{x^{2}}{2}}}{2 \sqrt{\pi}}$$

$\frac{d}{dx}g(x) = $

```python
gaussdiffexp
```

$$- \frac{\sqrt{2} x e^{- \frac{x^{2}}{2}}}{2 \sqrt{\pi}}$$

$\int_{-\infty}^x g(x')dx' = $

```python
gaussintexp
```

$$\frac{1}{2} \operatorname{erf}{\left (\frac{\sqrt{2} x}{2} \right )} + \frac{1}{2}$$

```python
# If functions are known to numpy, just replace them with numpy versions
gauss = sym.lambdify(x, gaussexp)
dgaussdx = sym.lambdify(x, gaussdiffexp)
# Erf (error function) is not known to numpy, so evaluate it pointwise explicitly
intgauss = np.vectorize(lambda x0: gaussintexp.replace(x,x0))
```

```python
xnp = np.linspace(-5,5,1000)
plt.plot(xnp, gauss(xnp), label="Gaussian")
plt.plot(xnp, dgaussdx(xnp), label="Gaussian Derivative")
plt.plot(xnp, intgauss(xnp), label="Gaussian Integral")
plt.title("Gaussian Functions")
plt.xlabel("x")
plt.ylabel("g(x)")
plt.legend()
```


# Solutions to the Julia Set Exercises

1: Write down as many questions as you can about material from this section.

What is the _dimension_ of the boundary of the Newton fractal for $z^3-1$? How do you compute a dimension of a boundary, anyway? Which colouring scheme gives the most pleasing results? (We think the scheme in the notebook is pretty ugly; we hope you can do better).  Which fractal is hardest to compute?  Which is the simplest? Which is the most interesting to look at? What is the history of the fractal idea? Are there fractal _sounds_?  Fractals seem to be good models for trees, rivers, clouds, blood vessel networks, algae mats, lichen, ice crystals growing in water as it freezes, frost on the windowpane, lots of things; do they really tell us anything interesting about those real-life things?  There is lots of fractal art (again, see the [Bridges Math Art site](https://www.bridgesmathart.org/). How is it made?  What makes it _artistic_?  Is it art, if it's computer-generated?  (Some of the [Deep Dream art](https://en.wikipedia.org/wiki/DeepDream) makes us shudder)  What are the open mathematical questions about fractals?

2: Write a Python program to draw the Sierpinski gasket (perhaps by using binomial coefficients mod 2).

This turns out to be a very popular question, and there are several web tutorials and examples on the subject.  For instance, here is a [YouTube video](https://youtu.be/G2A_Yg-JXxY) and [here is another](https://youtu.be/ssK2Z9Opokc) (maybe more detailed) with a program that uses the Turtle package.  Some links show _recursive_ programs, which we haven't talked much about although we probably should have.

Sierpinski triangles are also part of [this free book](https://runestone.academy/ns/books/published/pythonds/index.html) by Miller and Radum.  Again we don't know those people; these links are the result of a Google search.  We just wanted to show you that there was a lot of stuff online about this.

The [Wikipedia page](https://en.wikipedia.org/wiki/Sierpi%C5%84ski_triangle) is particularly good and has many references. It even connects to the Chaos Game, which we cover in our final unit.  Compared to the animations of that page, and of some of the external links, our little code below is pretty basic.  We used some ideas from [Robin Truax's video](https://youtu.be/5nuYD2M2AX8) and we chose not to do a _full_ Sierpinski triangle but rather a random sampling of it.  For basic code, the result is pretty cool.

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

rng = np.random.default_rng(2022)

fig = plt.figure(figsize=(10, 10))
ax = fig.add_axes([0,0,1,1])

level = 7
scales = np.zeros(level)
for i in range(level):
    scales[i] = 2**(-i-1)
numberpossible = 3**(level)
print( "Using {} levels".format(level))
Nsample = 1729 # Ramanujan's number, why not
print( "We will sample {} out of {} possible top vertices.".format(Nsample,numberpossible))
waterfall = np.transpose( np.array( [ [0,0], [1,0], [0,1]], dtype=float) )
v0 = np.array( [-0.5, -np.sqrt(3)/2] )  # These directions give equilateral triangles
v1 = np.array( [ 0.5, -np.sqrt(3)/2] )
p0 = np.array( [0,np.sqrt(3)/2] )

for k in range(Nsample):
        # Choose a random number base 3 with "level" trinary digits
        pick = rng.integers(low=0, high=3, size=level) # exclusive top range.  Sigh.
        # Matrix whose columns are the allowed waterfall coordinate choices
        A = np.transpose( np.array([waterfall[:,pick[i]] for i in range(level)], dtype=float) )
        xy = A.dot(scales) # Waterfall coordinates in binary
        vertx = p0 + xy[0]*v0 + xy[1]*v1
        lft = vertx + v0*2**(-level-1)
        rgt = vertx + v1*2**(-level-1)
        # Draw a triangle below the vertex
        plt.plot( [vertx[0],lft[0]], [vertx[1],lft[1]], 'k')
        plt.plot( [vertx[0],rgt[0]], [vertx[1],rgt[1]], 'k')
        plt.plot( [lft[0],rgt[0]], [lft[1],rgt[1]], 'k')

ax.set_xlim( [-0.5,0.5 ] )
ax.set_ylim( [0, np.sqrt(3)/2] )
plt.gca().set_aspect('equal')

plt.show()
```

3: Explore pictures of the binomial coefficients mod 4 (and then consult Andrew Granville's paper previously referenced).

4: Investigate pictures (mod 2 or otherwise) of other combinatorial families of numbers, such as [Stirling Numbers](https://en.wikipedia.org/wiki/Stirling_number) (both kinds).  Try also "Eulerian numbers of the first kind" mod 3.

Talk about an open-ended activity.  There is an _infinite_ amount of information about Stirling numbers and other numbers; for instance the [Digital Library of Mathematical Functions Chapter 26](https://dlmf.nist.gov/26) lists a remarkable number of facts and links many references.  As one fractal fact, computing the Stirling cycle numbers mod 2 gives a Sierpinski gasket, but tilted to one side.

5: Write a Python program to animate Newton's method for real functions and real initial estimates in general (the animated GIF at the top of this vignette was produced by a Maple program, Student:-Calculus1:-NewtonsMethod, which is quite a useful model).  This exercise asks you to "roll your own" animation.

6: Write your own code for computing Newton fractals, perhaps based on the code above (but at least improve the colour scheme).

```python
import numpy as np
import sympy
import cmath
from sympy.abc import x
from sympy.utilities.lambdify import lambdify, implemented_function
import colorsys
import matplotlib
import matplotlib.pyplot as plt
from PIL import Image

np.seterr(all='ignore')

def gen_colours(n):
    degree = 0
    start = 60
    space = 120
    flag = 0
    colours = []
    for i in range(n):
        if degree >= 360:
            if flag == 0:
                flag = 1
                degree = start
                start /= 2
            else:
                degree = start
                start /= 2
                space /= 2

        colours.append(degree/360.0)
        degree += space
    return colours

def gen_shade(colours, n_shade):
    col_len = len(colours)
    shade = np.linspace(170, 0, n_shade)
    list_shade = []
    for c in colours:
        temp = []
        for s in shade:
            if s <= 90.0:
                t = colorsys.hsv_to_rgb(c, 0.1+s/100.0, 1.0)
                temp.append(tuple(int(round(255*x)) for x in t))
            else:
                t = colorsys.hsv_to_rgb(c, 1.0, 1.0-(s - 90.0)/100.0)
                temp.append(tuple(int(round(255*x)) for x in t))
        list_shade.append(temp)
    return list_shade

def colours(n, n_shade):
    col = gen_colours(n)
    return gen_shade(col, n_shade)

def newton(func, dfunc, z, tol, max_iter):
    count = 0
    while (abs(func(z)) > tol) and (abs(func(z)/dfunc(z))/abs(z)) and (count < max_iter):
        if abs(func(z)) > 1e16:
            return (None, max_iter)
        z = z - func(z)/dfunc(z)
        count += 1
    if (count == max_iter):
        return (None, max_iter)
    else:
        return(z, count)

def fractal(func, dfunc, bl, ur, size, tol, max_i, converge_tol):
    roots = []
    max_iter = 0
    A = np.zeros((size[1], size[0], 3))
    for i in range(size[1]):
        y = i*(ur[1] - bl[1])/(size[1] - 1) + bl[1]
        for j in range(size[0]):
            x = j*(ur[0] - bl[0])/(size[0] - 1) + bl[0]
            (root, count) = newton(func, dfunc, complex(x, y), tol, max_i)
            if count > max_iter:
                max_iter = count
            if root:
                alreadyFound = False
                for test_root in roots:
                    if abs(test_root - root) < converge_tol:
                        root = test_root
                        alreadyFound = True
                        break
                if not alreadyFound:
                    roots.append(root)
            if root:
                A[i][j][0] = root.real
                A[i][j][1] = root.imag
                A[i][j][2] = count
    return (A, roots, max_iter)

def f_embed(M, **kwargs):
    filename = kwargs.get('filename', None)
    A = M[0]
    (size_y, size_x, temp) = A.shape
    roots = M[1]
    n_shade = M[2]
    C = colours(len(roots), n_shade)
    img = Image.new("RGB", (size_x, size_y), (255, 255, 255))
    for y in range(size_y):
        for x in range(size_x):
            if A[y][x][0]:
                i = int(A[y][x][2])
                if i > n_shade - 1:
                    i = n_shade - 1
                img.putpixel((x, size_y - y - 1), C[roots.index(complex(A[y][x][0], A[y][x][1]))][i])
    display(img)
    if filename != None:
        img.save(filename + ".png", "PNG")

def Newt_Fractal(func, bl, ur, size, **kwargs):
    i = kwargs.get('iterationlimit', 25)
    filename = kwargs.get('filename', None)
    tolerance = kwargs.get('tolerance', 1e-6)
    converge_tol = kwargs.get('converge_tolerance', 1e-3)
    dfunc = kwargs.get('derivative', None)
    if dfunc == None:
        dfunc = lambdify(x, sympy.diff(func(x), x), np)
    else:
        dfunc = lambdify(x, dfunc(x), np)
    func = lambdify(x, func(x), np)
    if type(size) == int:
        size = (size, size)
    M = fractal(func, dfunc, bl, ur, size, tolerance, i, converge_tol)
    print('This program found ' + str(len(M[1])) + ' roots.')
    roots = kwargs.get('printroots', False);
    if (roots == True):
        print('The roots are')
        print(M[1])
    if filename == None:
        f_embed(M)
    else:
        f_embed(M, filename = filename)
```

7: Compute Newton fractals for several functions of your own choosing. Test your code on the function $f(z) = z^{8}+4 z^{7}+6 z^{6}+6 z^{5}+5 z^{4}+2 z^{3}+z^{2}+z$ used above.

```python
f = lambda z: z**8+3*z**7+5*z**6+5*z**5+4*z**4+2*z**3+z*z+z
Newt_Fractal(f, (-1.8, -1.4), (1.0, 1.4), 500)
```

8: Compute Halley fractals for the same functions.

```python
df = lambda z: 8*z**7+21*z**6+30*z**5+25*z**4+16*z**36*z**2+2*z+1
halley = lambda z: f(z)/sympy.sqrt(df(z))
Newt_Fractal(halley, (-1.8, -1.4), (1.0, 1.4), 500)
```

9: Compute secant fractals for the same functions, using the $x_1 = x_0 - f(x_0)/f'(x_0)$ rule to generate the needed second
initial estimate.  Try a different rule for generating $x_1$ and see if it affects your fractals.

```python
# A very short hacky program to draw the edges of a secant fractal
# RMC 2022.2.6
import numpy as np
from matplotlib import pyplot as plt
# We will take an N by N grid of initial estimates
N = 200  # 800 by 800 is a lot and it takes a few seconds to draw
x = np.linspace(-2,2,N)
y = np.linspace(-2,2,N)
F = np.zeros((N,N))
# Here is the function and its derivative whose zeros we are looking for
f = lambda x: x ** 3 - 2;
df = lambda x: 3*x**2 ;
# SirIsaac performs one Newton iteration
SirIsaac = lambda x, fx: x - fx/df(x);
secantIter = lambda x,y,fx,fy: x - fx*(x-y)/(fx-fy)
for k in range(N):
    for i in range(N):
        # We range over all initial estimates in the grid
        z0 = x[i]+1j*y[k];
        f0 = f(z0)
        z1 = SirIsaac( z0, f0 )
        f1 = f(z1)
        # Hard-wire in 20 iterations (maybe not enough)
        for m in range(20):
            z = secantIter( z1, z0, f1, f0 )
            z0 = z1
            f0 = f1
            z1 = z
            if z1==z0:
                break
            f1 = f(z1)
        # After twenty iterations we hope the iteration has settled down, except on
        # the boundary between basins of attraction.
        # The phase (angle) is a likely candidate for a unique identifier for the root
        F[i,k] = np.angle( z )
# A magic incantation
X,Y = np.meshgrid( x, y )
# I admit to being quite puzzled as to why if we say X,Y in the call
# this contour plot has the real axis vertical, pointing up
# and the imaginary axis horizontal (not sure which way it's pointing)
# so I have just empirically input Y,X to get the graph looking right.
plt.figure(figsize=(10,10))
plt.contourf( Y,X, F, levels=[-3,-2,0,2,3], colors=['brown','red','black','yellow','black','blue','black'] )
plt.gca().set_aspect('equal', adjustable='box')
```

10: Try a few different values of "c" in the Mandelbrot example above, and generate your own "Julia sets".

```python
import numpy as np
from numpy.polynomial import Polynomial as Poly
N = 10001
History = np.zeros(N,dtype=complex)
# History[0] is deliberately 0.0 for this example
#print( History[0 ])
here = 0
there = 1
c = [-0.73, 0, 1 ] # Try c=0.8 (Solution to Problem 1)
d = len(c)-1
while there <= N-d:
    cc = c.copy()
    cc[0] = c[0] - History[here]
    p = Poly( cc );
    rts = p.roots();
    #print(here, rts)
    for j in range(d):
        #print( j, rts[j], there )
        History[there] = rts[j];
        there += 1;
    here += 1;

import matplotlib.pyplot as plt
x = [e.real for e in History]
y = [e.imag for e in History]
plt.scatter( x, y, s=0.5, marker="." )
plt.show()
```

```python
N = 10001
History = np.zeros(N,dtype=complex)
# History[0] is deliberately 0.0 for this example
#print( History[0 ])
here = 0
there = 1
c = [-0.2-0.8j, 0, 1 ] # Try c=0.2+0.8j (Solution to Problem 1)
d = len(c)-1
while there <= N-d:
    cc = c.copy()
    cc[0] = c[0] - History[here]
    p = Poly( cc );
    rts = p.roots();
    #print(here, rts)
    for j in range(d):
        #print( j, rts[j], there )
        History[there] = rts[j];
        there += 1;
    here += 1;

import matplotlib.pyplot as plt
x = [e.real for e in History]
y = [e.imag for e in History]
plt.scatter( x, y, s=0.5, marker="." )
plt.show()
```

11: These are not really Julia sets; they include too much of the history!  Alter the program so that it plots only (say) the last half of the points computed; increase the number of points by a lot, as well.  Compare your figure to (say) the Maple Julia set for c=1.2.

```python
N = 64001
History = np.zeros(N,dtype=complex)
# History[0] is deliberately 0.0 for this example
#print( History[0 ])
here = 0
there = 1
c = [-0.2-0.8j, 0, 1 ] # Try c=0.2+0.8j (Same as olution to Problem 1)
d = len(c)-1
while there <= N-d:
    cc = c.copy()
    cc[0] = c[0] - History[here]
    p = Poly( cc );
    rts = p.roots();
    #print(here, rts)
    for j in range(d):
        #print( j, rts[j], there )
        History[there] = rts[j];
        there += 1;
    here += 1;

import matplotlib.pyplot as plt
import math
x = [History[j].real for j in range(math.floor(63*N/64), N)] # Not just the last half, the last sixty-fourth
y = [History[j].imag for j in range(math.floor(63*N/64), N)]
plt.scatter( x, y, s=0.5, marker="." )
plt.show()
```

12: Change the function F to be a different polynomial; find places where both F and F' are zero (if any).  If necessary, change your polynomial so that there is such a "critical point".  Start your iteration there, and go backwards---plot your "Julia set".

```python
N = 3**10+1
History = np.zeros(N,dtype=complex)
# History[0] is deliberately 0.0 for this example
#print( History[0 ])
here = 0
there = 1
# Newton's method on f(z) = z^3-1 gives z_{n+1} = (2z_n^3 + 1)/(3z_n^2)
p = [1, 0, 0, 2 ]
q = [0, 0, 3 ]
d = max( len(p), len(q) ) - 1
while there <= N-d:
    cc = Poly( p ) - History[here]*Poly( q ) # Python has polynomial arithmetic so this is simple
    rts = cc.roots();
    #print(here, rts)
    for j in range(d):
        #print( j, rts[j], there )
        History[there] = rts[j];
        there += 1;
    here += 1;

import matplotlib.pyplot as plt
x = [e.real for e in History]
y = [e.imag for e in History]
fractal = plt.figure(figsize=(8,8))
fractalax = plt.scatter( x, y, s=0.1, marker="." );
plt.xlim(-2,2);
plt.ylim(-2,2);

```

```python
N = 3**10+1
History = np.zeros(N,dtype=complex)
# History[0] is deliberately 0.0 for this example
#print( History[0 ])
here = 0
there = 1
# Halley's method on f(z) = z^3-1 gives z_{n+1} = z_n(z_n**3+2)/(2z_n^3+1)
p = [0, 2, 0, 0, 1 ]
q = [1, 0, 0, 2 ]
d = max( len(p), len(q) ) - 1
while there <= N-d:
    cc = Poly( p ) - History[here]*Poly( q ) # Python has polynomial arithmetic so this is simple
    rts = cc.roots();
    #print(here, rts)
    for j in range(d):
        #print( j, rts[j], there )
        History[there] = rts[j];
        there += 1;
    here += 1;

import matplotlib.pyplot as plt
x = [e.real for e in History]
y = [e.imag for e in History]
fractal = plt.figure(figsize=(8,8))
fractalax = plt.scatter( x, y, s=0.01, marker="." );
plt.xlim(-2,2);
plt.ylim(-2,2);
```

13: Extend the program so that it works for _rational_ functions F, say $F(z) = p(z)/q(z)$.  This means solving the polynomial equation $p(z_n) - z_{n+1}q(z_n)=0$ for $z_n$. Try it out on the rational functions you get from Newton iteration on polynomial (or rational!) functions; or on Halley iteration on polynomial functions.  Try any of the that arise from the methods that you can find listed in [Revisiting Gilbert Strang's "A Chaotic Search for _i_"](https://doi.org/10.1145/3363520.3363521).

```python
N = 90001
History = np.zeros(N,dtype=complex)
# History[0] is deliberately 0.0 for this example
#print( History[0 ])
here = 0
there = 1
# Halley's method on a Mandelbrot polynomial gives
p = [0,0,0,-1,-9,-9,32,198,525,927,1236,1278,1014,570,192,28]
q = [1,3,9,26,69,186,474,948,1497,1874,1842,1404,766,252,36]
d = max( len(p), len(q) ) - 1
while there <= N-d:
    cc = Poly( p ) - History[here]*Poly( q ) # Python has polynomial arithmetic so this is simple
    rts = cc.roots();
    #print(here, rts)
    for j in range(d):
        #print( j, rts[j], there )
        History[there] = rts[j];
        there += 1;
    here += 1;

import matplotlib.pyplot as plt
x = [e.real for e in History]
y = [e.imag for e in History]
fractal = plt.figure(figsize=(8,8))
fractalax = plt.scatter( x, y, s=0.05, marker="." );
plt.xlim(-2,2);
plt.ylim(-2,2);
```

```python
N = 15001
History = np.zeros(N,dtype=complex)
# History[0] is deliberately 0.0 for this example
#print( History[0 ])
here = 0
there = 1
# Halley's method on the Wilkinson N=8 polynomial gives
p = [2209213440,-7144139520,8138672640,-1235703640,-6860019600,9045979866,-6258158928,2854960371,-921502440,216337751,-37195200,4642407,-409752,24255,-864,14]
q = [3622946688,-15347414304,29274362808,-33337014480,25349245110,-13634595702,5357600661,-1564677432,341763501,-55631772,6651225,-567000,32613,-1134,18]
d = max( len(p), len(q) ) - 1
while there <= N-d:
    cc = Poly( p ) - History[here]*Poly( q ) # Python has polynomial arithmetic so this is simple
    rts = cc.roots();
    #print(here, rts)
    for j in range(d):
        #print( j, rts[j], there )
        History[there] = rts[j];
        there += 1;
    here += 1;

import matplotlib.pyplot as plt
x = [e.real for e in History]
y = [e.imag for e in History]
fractal = plt.figure(figsize=(8,8))
fractalax = plt.scatter( x, y, s=0.5, marker="." );
plt.xlim(0,9);
plt.ylim(-1.75,1.75);
```

```python
N = 180001
History = np.zeros(N,dtype=complex)
# History[0] is deliberately 0.0 for this example
#print( History[0 ])
here = 0
there = 1
# Halley's method on the Schroeder iteration for x**2 + 1 gives
p = [-1, 0, -6, 0, 3]
q = [0, 0, 0, 8]
d = max( len(p), len(q) ) - 1
while there <= N-d:
    cc = Poly( p ) - History[here]*Poly( q ) # Python has polynomial arithmetic so this is simple
    rts = cc.roots();
    #print(here, rts)
    for j in range(d):
        #print( j, rts[j], there )
        History[there] = rts[j];
        there += 1;
    here += 1;

import matplotlib.pyplot as plt
x = [History[j].real for j in range(math.floor(44*N/45), N)] # Not just the last half, the last forty-fifth
y = [History[j].imag for j in range(math.floor(44*N/45), N)]
fractal = plt.figure(figsize=(8,8))
fractalax = plt.scatter( x, y, s=0.5, marker="." );
plt.xlim(-2,2);
plt.ylim(-1.,1.);
```

14: Read the [Wikipedia entry on Julia sets](https://en.wikipedia.org/wiki/Julia_set); it ought to be a little more intelligible now (but you will see that there are still lots of complications left to explain). One of the main items of interest is the theorem that states that the Fatou sets all have a _common boundary_.  This means that if the number of components is $3$ or more, then the Julia set (which is that boundary!) _must be a fractal_.

book/Solutions/Solutions to the Julia Set Exercises.ipynb


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

```

## Definición

La [transformada $z$](https://en.wikipedia.org/wiki/Z-transform) permite representar una señal discreta $x[k]$ en el dominio de las frecuencias. La función base para la transformación es $z^{-k}$ con $z \in \mathbb{C}$

### Transformada bilateral
La transformada $z$ bilateral se define como

\begin{equation}
X(z) = \sum_{k = -\infty}^{\infty} x[k] \, z^{-k}
\end{equation}

donde $X(z) = \mathcal{Z} \{ x[k] \}$ es la transformada Z de $x[k]$.

La variable $z$ puede ser interpretada como una [frecuencia compleja](../discrete_signals/standard_signals.ipynb#Complex-Exponential-Signal) $z = e^{\Sigma + j \Omega}$ con $\Sigma, \Omega \in \mathbb{R}$.

La existencia de $X(z) = \mathcal{Z} \{ x[k] \}$ depende de la señal $x[k]$ y los valores $z$. Los valores de $z$ que hacen que la transformada $z$ converja forman la ROC (región de convergencia).

### Transformada de un lado

En las señales causales se cumple que $x[k] = 0$ para $k <0$, por lo cual la transformada se convierte en:

\begin{equation}
X(z) = \sum_{k = -\infty}^{\infty} x[k] \cdot \epsilon[k] \, z^{-k} = \sum_{k = 0}^{\infty} x[k] \, z^{-k}
\end{equation}

\begin{equation}
X(z) = \sum_{k = 0}^{\infty} x[k] \, z^{-k}
\end{equation}

Así como la transformada de Laplace se usa para resolver ecuacioes diferenciales, la transforada $z$ se usa para resolver ecuaciones en diferencias.

### Señales de duración finita

Dada una señal $x[k]$ de duración finita, $x[k] = 0$ para $\{k : k < M_1 \wedge k \geq M_2\}$ con $M_1 < M_2$, por lo cual

\begin{equation}
X(z) = \sum_{k=M_1}^{M_2 - 1} x[k] \, z^{-k} = x[M_1] \, z^{-M_1} + x[M_1 + 1] \, z^{- (M_1 + 1)} + \dots + x[M_2 - 1] \, z^{- (M_2-1)}
\end{equation}

La transformada $z$ de una señal de duración finita es un polinomio en $z$.

- Si la señal es causal, la ROC corresponde a $z \in \mathbb{C} \setminus \{ 0 \}$.

\begin{equation}
X(z) = x[0] + x[1] \, z^{-1} + \dots + x[M_2 - 1] \, z^{- (M_2-1)}
\end{equation}

- Si la señal es anticausal, la ROC corresponde a $z \in \mathbb{C} \setminus \{ infty \}$.

\begin{equation}
X(z) = x[M_1] \, z^{(M_1)}+ \dots + x[-2] \, z^{2} + x[-1] \, z^{1} + x[0]
\end{equation}

- Si la señal tiene parte causal y parte anticausal, la ROC corresponde a $z \in \mathbb{C} \setminus \{ infty , 0 \}$.

\begin{equation}
X(z) = x[M_1] \, z^{(M_1)} + \dots + x[0] + \dots + x[M_2 - 1] \, z^{- (M_2-1)}
\end{equation}

### Transformada del delta de Dirac

\begin{equation}
\mathcal{Z} \{ \delta[k] \} = \sum_{k = -\infty}^{\infty} \delta[k] \, z^{-k} = 1
\end{equation}

para $z \in \mathbb{C}$. La ROC corresponde a todo el plano $z$.

Como $\mathcal{Z} \{ \delta[k] \} = 1$, todas las frecuencias están presentes en la señal con igual peso.

```python

k = sym.symbols('k', integer=True)
z = sym.symbols('z', complex=True)

X = sym.summation(sym.KroneckerDelta(k, 0) * z**(-k), (k, -sym.oo, sym.oo))
X
```

### Transformada de un pulso

Suponga que $x[k] = \text{rect}_N[k]$ es una señal causal rectangular de duración $N$.

\begin{equation}
\mathcal{Z} \{ \text{rect}_N[k] \} = \sum_{k=0}^{N-1} z^{-k} = 1 + z^{-1} + \dots + z^{-N + 1}
\end{equation}

con $z \in \mathbb{C} \setminus \{ 0 \}$.

Observe que la sumatoria resultante puede reescribirse como

\begin{equation}
\mathcal{Z} \{ \text{rect}_N[k] \} =  \begin{cases}
\frac{1 - z^{-N}}{1 - z^{-1}} & \text{for } z \neq 1 \\
N & \text{for } z = 1
\end{cases}
\end{equation}

### Transformada de una Exponencial Compleja y causal

La transformada $X(z) = \mathcal{Z} \{ x[k] \}$ de una señal exponencial compleja y causal

\begin{equation}
x[k] = \epsilon[k] \cdot z_0^k
\end{equation}

con frecuencia compleja $z_0 \in \mathbb{C}$ se obtiene de la expresión integral de la transfomada $z$

\begin{equation}
X(z) = \sum_{k=0}^{\infty} z_0^k \cdot z^{-k} = \sum_{k=0}^{\infty} \left( \frac{z_0}{z} \right)^k = \frac{z}{z - z_0}
\end{equation}

Lo anterior ya que la sumatoria resultante es una [serie geométrica](https://en.wikipedia.org/wiki/Geometric_series). Esta transformada converge si $\left| \frac{z_0}{z} \right| < 1$, es decir, la ROC es

\begin{equation}
|z| > |z_0|
\end{equation}

Así, la transformada $z$ de la señal causal exponencial compleja es:

\begin{equation}
\mathcal{Z} \{ \epsilon[k] \cdot z_0^k \} = \frac{z}{z - z_0} \qquad \text{for } |z| > |z_0|
\end{equation}

```python
z0 = sym.symbols('z0')

X = sym.summation(z0**k * z**(-k), (k, 0, sym.oo))
X
```

$\displaystyle \begin{cases} \frac{1}{1 - \frac{z_{0}}{z}} & \text{for}\: \left|{\frac{z_{0}}{z}}\right| < 1 \\\sum_{k=0}^{\infty} z^{- k} z_{0}^{k} & \text{otherwise} \end{cases}$

```python
X
```

## Convergence

The definition of the $z$-transform constitutes an infinite series. A sufficient but not necessary condition for the convergence of an infinite series is that its elements are absolutely summable. Applying this to the definition of the two-sided $z$-transform $X(z) = \mathcal{Z} \{ [k] \}$ of a given signal $x[k]$ yields

\begin{equation}
\sum_{k = -\infty}^{\infty} | x[k] \cdot z^{- k} | = \sum_{k = -\infty}^{\infty} | x[k] | \cdot | z |^{- k}  < \infty
\end{equation}

It can be concluded from this result that the ROC is determined solely by the magnitude $|z|$ of the complex frequency. It follows further that the ROC is given as a ring in the $z$-plane which may include $z=0$ and/or $z=\infty$. The phase $\Omega$ of the complex frequency $z$ has no effect in terms of convergence, as $z = e^{\Sigma} \cdot e^{j \Omega}$.

For a right-sided signal of infinite length with $x[k] = 0$ for $k < M$, the ROC is given in the form of $|z| > a$ with $a \in \mathbb{R}^+$. This can be concluded from the decay of the term $| z |^{- k}$ for $k \to \infty$, which ensures convergence for a given $a$. However this holds only for signals $x[k]$ with [exponential growth](https://en.wikipedia.org/wiki/Exponential_growth). Please refer to the $z$-transform of the causal exponential signal derived above.

The same reasoning leads to the ROCs of a left-sided and two-sided signal. The resulting ROCs are illustrated in the following

The gray areas denote the values $z$ for which the $z$-transform converges. The borders $a$ of these areas (dashed lines) depend on the signal $x[k]$. In case that the $z$-transform $X(z)$ is given in terms of a rational function in $z$, the ROC has to be chosen such that it does not include zeros of the denominator polynomial. A more detailed discussion of the ROCs for the $z$-transform can be found in the literature, e.g. [[Girod et al.](index.ipynb#Literature)].


## ゲート付きRNN
* 前回のRNNはそこまで性能はよくない
* 原因として長期の依存関係をうまく学習することができていないため。
* 一般的にRNNはLSTMであることが多い

### RNNの問題点
* BPTTにおいて勾配消失と勾配爆発が生じる

### 勾配消失と勾配爆発の原因
* tanh関数を微分した式は1.0以下の値であり、勾配がtanhノードを通るたびにその値が小さくなる。
* "https://arxiv.org/abs/1511.03771" ,ではrelu関数を使用して勾配消失を抑えている。

```python
import os
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
os.chdir("./deep-learning-from-scratch-2-master")
```

```python
x = np.linspace(-5, 5, 100)
y = np.tanh(x)
plt.plot(x, y, label="tanh(x)")

x = np.linspace(-5, 5, 100)
y = 1 / np.cosh(x)
plt.plot(x, y, label="dy/dx")
plt.legend()
plt.show()
```

```python
N = 2 #ミニバッチのサイズ
H = 3 #隠れ状態ベクトルの次元数
T = 20 #時系列データの長さ

dh = np.ones((N, H))
np.random.seed(0)
Wh = np.random.randn(H, H)

norm_list = []
for t in range(T):
    dh = np.dot(dh, Wh.T)
    norm = np.sqrt(np.sum(dh ** 2)) / N
    norm_list.append(norm)
plt.plot(np.arange(len(norm_list)), norm_list)
plt.title("gradient explosion")
plt.xlabel("time step")
plt.ylabel("norm")
plt.show()
```

```python
Wh = np.random.randn(H, H) * 0.5

norm_list = []
for t in range(T):
    dh = np.dot(dh, Wh.T)
    norm = np.sqrt(np.sum(dh ** 2)) / N
    norm_list.append(norm)
plt.plot(np.arange(len(norm_list)), norm_list)
plt.title("vanishing gradient")
plt.xlabel("time step")
plt.ylabel("norm")
plt.show()

#RNNの勾配消失と勾配爆発に関しての論文:
"https://arxiv.org/abs/1211.5063"
```

### 勾配爆発の対策
* 勾配クリッピング

##### パラメータ
* g：ニューラルネットワークで使われる全てのパラメータの勾配を一つにまとめたもの
* 例：重みW1, W2→勾配dW1, dW2をまとめるみたいな感じ

\begin{align}
1. \quad & \hat{g} \leftarrow \frac{\partial \epsilon}{\partial \theta} \\
2. \quad & if \quad ||\hat{g}|| \geqq threshold \quad then \\
& \qquad \hat{g} \leftarrow \frac{threshold}{||\hat{g}||} \hat{g} \\
&   end if
\end{align}

```python
dW1 = np.random.rand(3, 3) * 10
dW2 = np.random.rand(3, 3) * 10
grads = [dW1, dW2]
max_norm = 5.0 #閾値：threshold

def clip_grads(grads, max_norm):
    total_norm = 0
    for grad in grads:
        total_norm += np.sum(grad ** 2)
    total_norm = np.sqrt(total_norm)

    rate = max_norm / (total_norm + 1e-6)
    if rate < 1:
        for grad in grads:
            grad *= rate

clip_grads(grads, max_norm)

dh = np.ones((N, H))

np.random.seed(3)

Wh = np.random.randn(H, H)

norm_list = []
for t in range(T):
    dh = np.dot(dh, Wh.T)
    clip_grads(dh, max_norm=100)
    norm = np.sqrt(np.sum(dh**2)) / N
    norm_list.append(norm)

plt.plot(np.arange(len(norm_list)), norm_list)
plt.xlabel("time step")
plt.ylabel("norm")
plt.show()
```

### 勾配消失とLSTM
* 防ぐ方法としてLSTMとGRU

#### LSTM
* LSTMには記憶セルcがある。

#### LSTMのゲートについて
* output：次の隠れ状態への出力をどれだけ通すかを指示。
$$o= \sigma(W_{x} x_t + W_{h} h_{(t-1)} + b)$$
* input:重み付けされた情報を新たに追加する。
$$i= \sigma(x_tW_x+h_{t-1}W_h+b)$$
* forget：記憶セルに対して何を忘れるかを明示的に指示する。
$$f= \sigma(x_tW_x+h_{t-1}W_h+b)$$
* tanhノード：新たに情報を追加する層。
$$g=\tanh(x_tW_x+h_{t-1}W_h+b)$$

#### LSTMの実装
* 記憶セル
$$c_t=f \odot c_{t-1}+g \odot i$$
* 隠れ状態ベクトル
$$h_t=o \odot \tanh(c_t)$$

```python
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
```

```python
class LSTM:
    def __init__(self, Wx, Wh, b):
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeors_like(b)]
        self.cache = None

    def forward(self, x, h_prev, c_prev):
        Wx, Wh, b = self.params
        N, H = h_prev.shape

        A = np.dot(x, Wx) + np.dot(h_prev, Wh) + b

        f = A[:, :H]
        g = A[:, H:2*H]
        i = A[:, 2*H:3*H]
        o = A[:, 3*H:]

        f = np.sigmoid(f)
        g = np.tanh(g)
        i = sigmoid(i)
        o = sigmoid(o)

        c_next = f * c_prev + g * i
        h_next = o * np.tanh(c_next)

        self.cache = (x, h_prev, c_prev, i, f, g, o, c_next)
        return h_next, c_next

    def backward(self, dh_next, dc_next):
        Wx, Wh, b = self.params
        x, h_prev, c_prev, i, f, g, o, c_next = self.cache

        tanh_c_next = np.tanh(c_next)

        ds = dc_next + (dh_next * o) * (1 - tanh_c_next ** 2)

        dc_prev = ds * f

        di = ds * g
        df = ds * c_prev
        do = dh_next * tanh_c_next
        dg = ds * i

        di *= i * (1 - i)
        df *= f * (1 - f)
        do *= o * (1 - o)
        dg *= (1 - g ** 2)
        #逆伝播では勾配を結合する必要性がある（縦方向に連結する際はvstack()）
        dA = np.hstack((df, dg, di, do))

        dWh = np.dot(h_prev.T, dA)
        dWx = np.dot(x.T, dA)
        db = dA.sum(axis=0)

        self.grads[0][...] = dWx
        self.grads[1][...] = dWh
        self.grads[2][...] = db

        dx = np.dot(dA, Wx.T)
        dh_prev = np.dot(dA, Wh.T)

        return dx, dh_prev, dc_prev
```

```python
class TimeLSTM:
    def __init__(self, Wx, Wh, b, stateful=False):
        self.params = [Wx, Wh, b]
        self.grads = [np.zeros_like(Wx), np.zeros_like(Wh), np.zeros_like(b)]
        self.layers = None

        self.h, self,c = None, None
        self.dh = None
        self.stateful = stateful

    def forward(self, xs):
        Wx, Wh, b = self.params
        N, T, D = cs.shape
        H = wh.shape[0]

        self.layer = []
        hs = np.empty((N, T, H), dtype="f")

        if not self.stateful or self.h is None:
            self.h = np.zeros((N, H), dtype="f")
        if not self.stateful or self.c is None:
            self.c = np.zeros((N, H), dtype="f")

        for t in range(T):
            layer = LSTM(*self.params)
            self.h, self.c = layer.forward(xs[:, t, :], self.h, self.c)
            hs[:, t, :] = self.h

            self.layers.append(layer)

        return hs

    def backward(self, dhs):
        Wx, Wh, b = self.params
        N, T, H = dhs.shape
        D = Wx.shape[0]

        dxs = np.empty((N, T, D), dtype="f")
        dh, dc = 0, 0

        grads = [0, 0, 0]
        for t in reversed(range(T)):
            layer = self.layers[t]
            dx, dh, dc = layer.backward(dhs[:, t, :] + dh, dc)
            dxs[:, t, :] = dx
            for i, grad in enumerate(layer.grads):
                grads[i] += grad

        for i, grad in enumerate(grads):
            self.grads[i][...] = grad
            self.dh = dh
            return dxs

    def set_stateful(self, h, c=None):
        self.h, self.c = h, c

    def reset_stateful(self):
        self.h, self.c = None, None
```

```python
from common.time_layers import *
from common.base_model import BaseModel

class Rnnlm(BaseModel):
    def __init__(self, vocab_size=10000, wordvec_size=100, hidden_size=100):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        # 重みの初期化
        embed_W = (rn(V, D) / 100).astype('f')
        lstm_Wx = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b = np.zeros(4 * H).astype('f')
        affine_W = (rn(H, V) / np.sqrt(H)).astype('f')
        affine_b = np.zeros(V).astype('f')

        # レイヤの生成
        self.layers = [
            TimeEmbedding(embed_W),
            TimeLSTM(lstm_Wx, lstm_Wh, lstm_b, stateful=True),
            TimeAffine(affine_W, affine_b)
        ]
        self.loss_layer = TimeSoftmaxWithLoss()
        self.lstm_layer = self.layers[1]

        # すべての重みと勾配をリストにまとめる
        self.params, self.grads = [], []
        for layer in self.layers:
            self.params += layer.params
            self.grads += layer.grads

    def predict(self, xs):
        for layer in self.layers:
            xs = layer.forward(xs)
        return xs

    def forward(self, xs, ts):
        score = self.predict(xs)
        loss = self.loss_layer.forward(score, ts)
        return loss

    def backward(self, dout=1):
        dout = self.loss_layer.backward(dout)
        for layer in reversed(self.layers):
            dout = layer.backward(dout)
        return dout

    def reset_state(self):
        self.lstm_layer.reset_state()

```

```python
from common.optimizer import SGD
from common.trainer import RnnlmTrainer
from common.util import eval_perplexity
from dataset import ptb
from ch06.rnnlm import Rnnlm

# ハイパーパラメータの設定
batch_size = 20
wordvec_size = 100
hidden_size = 100  # RNNの隠れ状態ベクトルの要素数
time_size = 35  # RNNを展開するサイズ
lr = 20.0
max_epoch = 4
max_grad = 0.25

# 学習データの読み込み
corpus, word_to_id, id_to_word = ptb.load_data('train')
corpus_test, _, _ = ptb.load_data('test')
vocab_size = len(word_to_id)
xs = corpus[:-1]
ts = corpus[1:]

# モデルの生成
model = Rnnlm(vocab_size, wordvec_size, hidden_size)
optimizer = SGD(lr)
trainer = RnnlmTrainer(model, optimizer)

# 勾配クリッピングを適用して学習
trainer.fit(xs, ts, max_epoch, batch_size, time_size, max_grad,
            eval_interval=20)
trainer.plot(ylim=(0, 500))

# テストデータで評価
model.reset_state()
ppl_test = eval_perplexity(model, corpus_test)
print('test perplexity: ', ppl_test)

# パラメータの保存
model.save_params()
```

### RNNLMのさらなる改善
* LSTMレイヤの多層化
* Dropoutによる過学習の抑制→多層化することでより豊かなモデルをさくせいできると同時に過学習が生じてしまう。→正則化など
* Drooutの際は時間軸方向に対して行うのではなく深さ方向に実装する。
* 重みの共有→パラメータが少なくなる
* 重み共有有効性についての根拠→https://arxiv.org/abs/1611.01462

```python
from common.time_layers import *
from common.np import *  # import numpy as np
from common.base_model import BaseModel

class BetterRnnlm(BaseModel):
    '''
     LSTMレイヤを2層利用し、各層にDropoutを使うモデル
     [1]で提案されたモデルをベースとし、weight tying[2][3]を利用

     [1] Recurrent Neural Network Regularization (https://arxiv.org/abs/1409.2329)
     [2] Using the Output Embedding to Improve Language Models (https://arxiv.org/abs/1608.05859)
     [3] Tying Word Vectors and Word Classifiers (https://arxiv.org/pdf/1611.01462.pdf)
    '''
    def __init__(self, vocab_size=10000, wordvec_size=650,
                 hidden_size=650, dropout_ratio=0.5):
        V, D, H = vocab_size, wordvec_size, hidden_size
        rn = np.random.randn

        embed_W = (rn(V, D) / 100).astype('f')
        lstm_Wx1 = (rn(D, 4 * H) / np.sqrt(D)).astype('f')
        lstm_Wh1 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b1 = np.zeros(4 * H).astype('f')
        lstm_Wx2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_Wh2 = (rn(H, 4 * H) / np.sqrt(H)).astype('f')
        lstm_b2 = np.zeros(4 * H).astype('f')
        affine_b = np.zeros(V).astype('f')

        self.layers = [
            TimeEmbedding(embed_W),
            TimeDropout(dropout_ratio),
            TimeLSTM(lstm_Wx1, lstm_Wh1, lstm_b1, stateful=True),
            TimeDropout(dropout_ratio),
            TimeLSTM(lstm_Wx2, lstm_Wh2, lstm_b2, stateful=True),
            TimeDropout(dropout_ratio),
            TimeAffine(embed_W.T, affine_b)  # weight tying!!
        ]
        self.loss_layer = TimeSoftmaxWithLoss()
        self.lstm_layers = [self.layers[2], self.layers[4]]
        self.drop_layers = [self.layers[1], self.layers[3], self.layers[5]]

        self.params, self.grads = [], []
        for layer in self.layers:
            self.params += layer.params
            self.grads += layer.grads

    def predict(self, xs, train_flg=False):
        for layer in self.drop_layers:
            layer.train_flg = train_flg

        for layer in self.layers:
            xs = layer.forward(xs)
        return xs

    def forward(self, xs, ts, train_flg=True):
        score = self.predict(xs, train_flg)
        loss = self.loss_layer.forward(score, ts)
        return loss

    def backward(self, dout=1):
        dout = self.loss_layer.backward(dout)
        for layer in reversed(self.layers):
            dout = layer.backward(dout)
        return dout

    def reset_state(self):
        for layer in self.lstm_layers:
            layer.reset_state()

```