# [OOP III: The Samuelson Accelerator](https://lectures.quantecon.org/py/samuelson.html)
##  1. Paul Samuelson’s (1939) multiplier-accelerator model
Samuelson使用二阶线性差分方程来表示一个基于三个组成部分的国家产出模型：
- 1.国家产出：消费+投资+政府采购
- 2.符合凯恩斯主义的消费函数：时间t时的消费量等于边际消费倾向乘以t-1期的国家产出
- 3.investment accelerator：时间t时的投资额等于accelerator coefficient乘以t-2与t-1期的产出的差
- 4.由消费+投资+政府采购得出的总产出得出总供给

Samuelson用该模型分析了边际消费倾向和accelerator coefficient的特定值如何导致具有替代动态特性的国家产出中的短暂商业周期：
- 1.平滑收敛到一个恒定的产出水平
- 2.趋于特定值的商业周期
- 3.没有太大的波动的持续的商业周期，
结论：这使得国家产出受到二阶随机线性差分方程的控制，该方程在适当的参数值下引起反复的不规则商业周期。

### Structure of the model
- $ {Gt}$ is a sequence of levels of government expenditures. We’ll start by setting $G_t = G,∀t$  ##国家采购
-  ${Ct}$ is a sequence of levels of aggregate consumption expenditures, a key endogenous variable in the model  ##总消费，外生
-  ${It}$ is a sequence of rates of investment, another key endogenous variable  ##投资额，外生
- ${Yt}$ is a sequence of levels of national income, yet another endogenous variable  ##国民收入，外生
- $ a$ is the marginal propensity to consume in the Keynesian consumption function $C_t = aY_{t−1} + γ$  ##消费函数，a表示边际消费倾向
- $b$ is the “accelerator coefficient” in the “investment accelerator” $I_t = b (Y_{t-1} - Y_{t-2})$  ##投资函数，b表示投资乘数
- ${ϵt}$ is an IID sequence standard normal random variables  ##标准正态随机变量
- σ ≥ 0 is a “volatility” parameter — setting σ = 0 recovers the nonstochastic case that we’ll start with  ##波动率参数，初始值为0

由$C_t = aY_{t−1} + γ$，
$I_t = b(Y_{t−1} − Y_{t−2})$,
$Y_t = C_t + I_t + G_t$
可以得出：$Y_t = (a + b)Y_{t−1} − bY_{t−2} + (γ + G_t)$
或者$Y_t = ρ_1Y_{t−1} + ρ_2Y_{t−2} + (γ + G_t)\tag{1.12}$
其中， $ρ_1 = (a + b)$ and $ρ_2 = −b $
假设从$t=0$开始，赋予$Y$两个初始值：$Y_{−1} = \bar{Y}_{−1}$, $Y_{−2} = \bar{Y}_{−2}$
We are interested in studying
- the transient fluctuations in Yt as it converges to its steady state level  ##总产出的短期波动
- the rate at which it converges to a steady state level  ##利率？

### Stochastic version of the model
我们可以通过对总需求增加一个随机冲击来将模型转换为持续不规则波动的模型
在总需求方程右侧添加一个随机冲击，得到了二阶标量线性随机差分方程：$Y_t = G_t + a(1 − b)Y_{t−1} − abY_{t−2} + σϵt\tag{1.13}$

### Mathematical analysis of the model
初始， $G_t ≡ 0$, $σ = 0$, and $γ = 0$ （1.12）
变为$Y_t = ρ_1Y_{t−1} + ρ_2Y_{t−2}$或者$Y_{t+2} − ρ_1Y_{t+1} − ρ_2Y_t = 0\tag{1.14}$
特征方程：$z^2 − ρ_1z − ρ_2\tag{1.15}$，
 $z^2 − ρ_1z − ρ_2 = (z − λ_1)(z − λ_2) = 0\tag{1.16}$
对该特征方程进行求解，得到$λ_1$和$λ_2$（可能会是复数）
注意：
   1.模型设定时要使得$λ_1$和$λ_2$的绝对值均小于1
   2.在两者绝对值都小于1的情况下，较大的λ值决定了模型随机模式下的收敛速度

## 2 Let’s get to work

```python
import numpy as np
import matplotlib.pyplot as plt
def param_plot():
    """this function creates the graph on page 189 of Sargent Macroeconomic,→Theory, second edition, 1987"""
    fig, ax = plt.subplots(figsize=(12, 8))   ##产生一个子窗口，长度12单位，宽度8单位
    ax.set_aspect('equal')   ##将坐标轴的长度单位设为相等

    # Set axis   ##横轴和纵轴
    xmin, ymin = -3, -2
    xmax, ymax = -xmin, -ymin
    plt.axis([xmin, xmax, ymin, ymax])

    # Set axis labels  ##设置坐标轴的标签
    ax.set(xticks=[], yticks=[])
    ax.set_xlabel(r'$\rho_2$', fontsize=16)
    ax.xaxis.set_label_position('top')    ##标签位置
    ax.set_ylabel(r'$\rho_1$', rotation=0, fontsize=16)
    ax.yaxis.set_label_position('right')

    # Draw (t1, t2) points
    rho1 = np.linspace(-2, 2, 100)
    ax.plot(rho1, -abs(rho1) + 1, c='black')
    ax.plot(rho1, np.ones_like(rho1) * -1, c='black')  ##np.ones_like(rho1)：返回一个与rho1形状和类型一致的数组
    ax.plot(rho1, -(rho1**2 / 4), c='black')

    # Turn normal axes off
    for spine in ['left', 'bottom', 'top', 'right']:   ##去掉上下左右的边框
        ax.spines[spine].set_visible(False)

    # Add arrows to represent axes   添加箭头
    axes_arrows = {'arrowstyle': '<|-|>', 'lw': 1.3}
    ax.annotate('', xy=(xmin, 0), xytext=(xmax, 0), arrowprops=axes_arrows)
    ##''为注释文本内容 ，xy 为被注释的坐标点，xytext 为注释文字的坐标位置,arrowprops为箭头参数,参数类型为字典dict
    ax.annotate('', xy=(0, ymin), xytext=(0, ymax), arrowprops=axes_arrows)

    # Annotate the plot with equations   划线
    plot_arrowsl = {'arrowstyle': '-|>', 'connectionstyle': "arc3, rad=-0.2"}   ##字典格式
    plot_arrowsr = {'arrowstyle': '-|>', 'connectionstyle': "arc3, rad=0.2"}
    ax.annotate(r'$\rho_1 + \rho_2 < 1$', xy=(0.5, 0.3), xytext=(0.8, 0.6),
    arrowprops=plot_arrowsr, fontsize='12')
    ax.annotate(r'$\rho_1 + \rho_2 = 1$', xy=(0.38, 0.6), xytext=(0.6, 0.8),
    arrowprops=plot_arrowsr, fontsize='12')
    ax.annotate(r'$\rho_2 < 1 + \rho_1$', xy=(-0.5, 0.3), xytext=(-1.3, 0.6),
    arrowprops=plot_arrowsl, fontsize='12')
    ax.annotate(r'$\rho_2 = 1 + \rho_1$', xy=(-0.38, 0.6), xytext=(-1, 0.8),
    arrowprops=plot_arrowsl, fontsize='12')
    ax.annotate(r'$\rho_2 = -1$', xy=(1.5, -1), xytext=(1.8, -1.3),
    arrowprops=plot_arrowsl, fontsize='12')
    ax.annotate(r'${\rho_1}^2 + 4\rho_2 = 0$', xy=(1.15, -0.35),
    xytext=(1.5, -0.3), arrowprops=plot_arrowsr, fontsize='12')
    ax.annotate(r'${\rho_1}^2 + 4\rho_2 < 0$', xy=(1.4, -0.7),
    xytext=(1.8, -0.6), arrowprops=plot_arrowsr, fontsize='12')

    # Label categories of solutions    给解的分类加标签
    ax.text(1.5, 1, 'Explosive\n growth', ha='center', fontsize=16)   ##\n换行
    ax.text(-1.5, 1, 'Explosive\n oscillations', ha='center', fontsize=16)
    ax.text(0.05, -1.5, 'Explosive oscillations', ha='center', fontsize=16)
    ax.text(0.09, -0.5, 'Damped oscillations', ha='center', fontsize=16)

    # Add small marker to y-axis
    ax.axhline(y=1.005, xmin=0.495, xmax=0.505, c='black')
    ax.text(-0.12, -1.12, '-1', fontsize=10)
    ax.text(-0.12, 0.98, '1', fontsize=10)
    return fig
param_plot()
plt.show()
```

### Explanation of the graph
• ($λ_1$, $λ_2$) are complex with modulus less than 1 - in this case, the ${Y_t}$ sequence displays damped oscillations  阻尼振荡
• ($λ_1$, $λ_2$) are both real, but one is strictly greater than 1 - this leads to explosive growth  爆炸增长
• ($λ_1$, $λ_2$) are both real, but one is strictly less than −1 - this leads to explosive oscillations    爆炸震荡
• ($λ_1$, $λ_2$) are both real and both are less than 1 in absolute value - in this case, there is smooth convergence to the steady state without damped cycles  平滑收敛
Asiaphoria Meets Regression to the Mean，NBER Working Paper 20573，2014 亚洲欣快症  Summers
我国的a和b可能与其他国家不一样

### Function to describe implications of characteristic polynomial
特征多项式

```python
def categorize_solution(rho1, rho2):
    """this function takes values of rho1 and rho2 and uses them to classify,→the type of solution"""
    discriminant = rho1 ** 2 + 4 * rho2
    if rho2 > 1 + rho1 or rho2 < -1:
        print('Explosive oscillations')
    elif rho1 + rho2 > 1:
        print('Explosive growth')
    elif discriminant < 0:
        print('Roots are complex with modulus less than one; therefore damped oscillations')
    else:
        print('Roots are real and absolute values are less than zero;therefore get smooth convergence to a steady state')
```

```python
### Test the categorize_solution function
categorize_solution(1.3, -.4)
```

    Roots are real and absolute values are less than zero;therefore get smooth convergence to a steady state

### Function for plotting Yt paths

```python
def plot_y(function=None):
    """function plots path of Y_t"""
    plt.subplots(figsize=(12, 8))
    plt.plot(function)
    plt.xlabel('Time $t$')
    plt.ylabel('$Y_t$', rotation=0)
    plt.grid()
    plt.show()
```

### Manual or “by hand” root calculations
#求解根的值
方法一：

```python
from cmath import sqrt
##=== This is a 'manual' method ===#
def y_nonstochastic(y_0=100, y_1=80, alpha=.92, beta=.5, gamma=10, n=80):
    """Takes values of parameters and computes roots of characteristic polynomial.  计算特征方程的解
    It tells whether they are real or complex and whether they are less than unity in absolute value.
    It also computes a simulation of length n starting from the two given initial conditions for national income"""
    roots = []
    rho1 = alpha + beta
    rho2 = -beta
    print('rho_1 is ', rho1)
    print('rho_2 is ', rho2)
    discriminant = rho1 ** 2 + 4 * rho2  ##判别式deta
    if discriminant == 0:
        roots.append(-rho1 / 2)
        print('Single real root: ')
        print(''.join(str(roots)))
    elif discriminant > 0:
        roots.append((-rho1 + sqrt(discriminant).real) / 2)  ##给列表赋值
        roots.append((-rho1 - sqrt(discriminant).real) / 2)
        print('Two real roots: ')
        print(''.join(str(roots)))
    else:
        roots.append((-rho1 + sqrt(discriminant)) / 2)
        roots.append((-rho1 - sqrt(discriminant)) / 2)
        print('Two complex roots: ')
        print(''.join(str(roots)))   ##join??
    if all(abs(root) < 1 for root in roots):
        print('Absolute values of roots are less than one')
    else:
        print('Absolute values of roots are not less than one')
    def transition(x, t): return rho1 * x[t - 1] + rho2 * x[t - 2] + gamma
    y_t = [y_0, y_1]
    for t in range(2, n):
        y_t.append(transition(y_t, t))
    return y_t
plot_y(y_nonstochastic())
```

### Reverse engineering 逆向工程 parameters to generate damped cycles

The next cell writes code that takes as inputs the modulus $r$ and phase $ϕ$ of a conjugate pair of complex numbers in polar form:
    $λ_1 = r*exp(iϕ)$, $λ_2 = r*exp(−iϕ)$     ##复数时，解的形式
The code assumes that these two complex numbers are the roots of the characteristic polynomial

```python
### code to reverse engineer a cycle
### y_t = r^t (c_1 cos(phi t) + c2 sin(phi t))
###
import cmath
import math
def f(r, phi):
    """
    Takes modulus r and angle phi of complex number r exp(j phi) and creates rho1 and rho2 of characteristic polynomial for which
    r exp(j phi) and r exp(- j phi) are complex roots.
    Returns the multiplier coefficient a and the accelerator coefficient b that verifies those roots.
    """
    g1 = cmath.rect(r, phi) # Generate two complex roots
    g2 = cmath.rect(r, -phi)
    rho1 = g1 + g2 # Implied rho1, rho2
    rho2 = -g1 * g2
    b = -rho2 # Reverse engineer a and b that validate these
    a = rho1 - b
    return rho1, rho2, a, b
    ## Now let's use the function in an example
    ## Here are the example paramters
r = .95
period = 10 # Length of cycle in units of time
phi = 2 * math.pi/period
## Apply the function
rho1, rho2, a, b = f(r, phi)
print("a, b = ", a, b)
print("rho1, rho2 =", rho1, rho2)

```

    a, b =  (0.6346322893124001+0j) (0.9024999999999999-0j)
    rho1, rho2 = (1.5371322893124+0j) (-0.9024999999999999+0j)

```python
## Print the real components of rho1 and rho2     根的实数部分
rho1 = rho1.real
rho2 = rho2.real
rho1, rho2

```

    (1.5371322893124, -0.9024999999999999)

### Root finding using numpy
用numpy求解上述的根

```python
r1, r2 = np.roots([1, -rho1, -rho2])  ##求根公式  【参数】
p1 = cmath.polar(r1)
p2 = cmath.polar(r2)
print("r, phi =", r, phi)
print("p1, p2 = ", p1, p2)
# print("g1, g2 = ", g1, g2)
print("a, b =", a, b)
print("rho1, rho2 =", rho1, rho2)

```

    r, phi = 0.95 0.6283185307179586
    p1, p2 =  (0.95, 0.6283185307179586) (0.95, -0.6283185307179586)
    a, b = (0.6346322893124001+0j) (0.9024999999999999-0j)
    rho1, rho2 = 1.5371322893124 -0.9024999999999999

```python
##=== This method uses numpy to calculate roots 阻尼震荡（中国式阻尼的决定因素） ===#
def y_nonstochastic(y_0=100, y_1=80, alpha=.9, beta=.8, gamma=10, n=80):
    """ Rather than computing the roots of the characteristic polynomial by
    ,→hand as we did earlier, this function
    enlists numpy to do the work for us """
    # Useful constants
    rho1 = alpha + beta
    rho2 = -beta
    categorize_solution(rho1, rho2)   # 判别delta 先前定义的函数
    # Find roots of polynomial
    roots = np.roots([1, -rho1, -rho2])
    print('Roots are', roots)
    # Check if real or complex
    if all(isinstance(root, complex) for root in roots):
        print('Roots are complex')
    else:
        print('Roots are real')
    # Check if roots are less than one
    if all(abs(root) < 1 for root in roots):
        print('Roots are less than one')
    else:
        print('Roots are not less than one')
    # Define transition equation
    def transition(x, t): return rho1 * x[t - 1] + rho2 * x[t - 2] + gamma
    # Set initial conditions
    y_t = [y_0, y_1]
    # Generate y_t series
    for t in range(2, n):
        y_t.append(transition(y_t, t))
    return y_t
plot_y(y_nonstochastic())

```

### Reverse engineered complex roots: example

```python
r = 1 # generates undamped, nonexplosive cycles
period = 10 # length of cycle in units of time
phi = 2 * math.pi/period
## Apply the reverse engineering function f
rho1, rho2, a, b = f(r, phi)
a = a.real # drop the imaginary part so that it is a valid input into y_
,→nonstochastic
b = b.real
print("a, b =", a, b)
ytemp = y_nonstochastic(alpha=a, beta=b, y_0=20, y_1=30)
plot_y(ytemp)

```

### Digression: using sympy to find roots

```python
import sympy
from sympy import Symbol, init_printing
init_printing()
r1 = Symbol("rho_1")
r2 = Symbol("rho_2")
z = Symbol("z")
sympy.solve(z**2 - r1*z - r2, z)
```

```python
a = Symbol("alpha")
b = Symbol("beta")
r1 = a + b
r2 = -b
sympy.solve(z**2 - r1*z - r2, z)

```

## 3 Stochastic shocks
Now we’ll construct some code to simulate the stochastic version of the model that emerges when we add a random shock process to aggregate demand

```python
def y_stochastic(y_0=0, y_1=0, alpha=0.8, beta=0.2, gamma=10, n=100, sigma=5):
    """This function takes parameters of a stochastic version of the model and proceeds to analyze
    the roots of the characteristic polynomial and also generate a simulation"""
    # Useful constants
    rho1 = alpha + beta
    rho2 = -beta
    # Categorize solution
    categorize_solution(rho1, rho2)
    # Find roots of polynomial
    roots = np.roots([1, -rho1, -rho2])
    print(roots)
    # Check if real or complex
    if all(isinstance(root, complex) for root in roots):
        print('Roots are complex')
    else:
        print('Roots are real')
    # Check if roots are less than one
    if all(abs(root) < 1 for root in roots):
        print('Roots are less than one')
    else:
        print('Roots are not less than one')
    # Generate shocks
    epsilon = np.random.normal(0, 1, n)
    # Define transition equation 关注震荡的参数
    def transition(x, t): return rho1 * \
        x[t - 1] + rho2 * x[t - 2] + gamma + sigma * epsilon[t]
    # Set initial conditions
    y_t = [y_0, y_1]
    # Generate y_t series
    for t in range(2, n):
        y_t.append(transition(y_t, t))
    return y_t
plot_y(y_stochastic())

```

Let’s do a simulation in which there are shocks and the characteristic polynomial has complex roots。
根是复数的形式

```python
r = .95 # 决定是否收敛 均值由a,b决定
period = 10 # length of cycle in units of time
phi = 2 * math.pi/period
### apply the reverse engineering function f
rho1, rho2, a, b = f(r, phi)
a = a.real # drop the imaginary part so that it is a valid input into y_
,→nonstochastic
b = b.real
print("a, b = ", a, b)
plot_y(y_stochastic(y_0=40, y_1 = 42, alpha=a, beta=b, sigma=2, n=100))
```

## 4 Government spending

```python
def y_stochastic_g(y_0=20,
                   y_1=20,
                   alpha=0.8,
                   beta=0.2,
                   gamma=10,
                   n=100,
                   sigma=2,
                   g=0,
                   g_t=0,
                   duration='permanent'):

    """This program computes a response to a permanent increase in government expenditures that occurs
       at time 20"""

    # Useful constants
    rho1 = alpha + beta
    rho2 = -beta

    # Categorize solution
    categorize_solution(rho1, rho2)

    # Find roots of polynomial
    roots = np.roots([1, -rho1, -rho2])
    print(roots)

    # Check if real or complex
    if all(isinstance(root, complex) for root in roots):
        print('Roots are complex')
    else:
        print('Roots are real')

    # Check if roots are less than one
    if all(abs(root) < 1 for root in roots):
        print('Roots are less than one')
    else:
        print('Roots are not less than one')

    # Generate shocks
    epsilon = np.random.normal(0, 1, n)

    def transition(x, t, g):

        # Non-stochastic - separated to avoid generating random series when not needed
        if sigma == 0:
            return rho1 * x[t - 1] + rho2 * x[t - 2] + gamma + g

        # Stochastic
        else:
            epsilon = np.random.normal(0, 1, n)
            return rho1 * x[t - 1] + rho2 * x[t - 2] + gamma + g + sigma * epsilon[t]

    # Create list and set initial conditions
    y_t = [y_0, y_1]

    # Generate y_t series
    for t in range(2, n):

        # No government spending
        if g == 0:
            y_t.append(transition(y_t, t))

        # Government spending (no shock)
        elif g != 0 and duration == None:
            y_t.append(transition(y_t, t))

        # Permanent government spending shock
        elif duration == 'permanent':
            if t < g_t:
                y_t.append(transition(y_t, t, g=0))
            else:
                y_t.append(transition(y_t, t, g=g))

        # One-off government spending shock
        elif duration == 'one-off':
            if t == g_t:
                y_t.append(transition(y_t, t, g=g))
            else:
                y_t.append(transition(y_t, t, g=0))
    return y_t
```

A permanent government spending shock can be simulated as follows

```python
plot_y(y_stochastic_g(g=10, g_t=20, duration='permanent'))
```

We can also see the response to a one time jump in government expenditures

```python
plot_y(y_stochastic_g(g=500, g_t=50, duration='one-off'))
```

## 5 Wrapping everything into a class
Now we’ll roll up our sleeves and write a Python class called Samuelson for the Samuleson model
$Y_t = + \alpha (1 + \beta) Y_{t-1} - \alpha \beta Y_{t-2}$

```python
class Samuelson():
    r"""This class represents the Samuelson model, otherwise known as the
    multiple-accelerator model. The model combines the Keynesian multipler
    with the accelerator theory of investment.
    The path of output is governed by a linear second-order difference
    ,→equation
    .. math::
    Y_t = + \alpha (1 + \beta) Y_{t-1} - \alpha \beta Y_{t-2}
    Parameters
    ----------
    y_0 : scalar
    Initial condition for Y_0
    y_1 : scalar
    Initial condition for Y_1
    alpha : scalar
    Marginal propensity to consume
    beta : scalar
    Accelerator coefficient
    n : int
    Number of iterations
    sigma : scalar
    Volatility parameter. Must be greater than or equal to 0. Set
    equal to 0 for non-stochastic model.
    g : scalar
    Government spending shock
    g_t : int
    Time at which government spending shock occurs. Must be specified
    when duration != None.
    duration : {None, 'permanent', 'one-off'}
    Specifies type of government spending shock. If none, government
    spending equal to g for all t.
    """
    def __init__(self,
                y_0=100,
                y_1=50,
                alpha=1.3,
                beta=0.2,
                gamma=10,
                n=100,
                sigma=0,
                g=0,
                g_t=0,
                duration=None):
        self.y_0, self.y_1, self.alpha, self.beta = y_0, y_1, alpha, beta
        self.n, self.g, self.g_t, self.duration = n, g, g_t, duration
        self.gamma, self.sigma = gamma, sigma
        self.rho1 = alpha + beta
        self.rho2 = -beta
        self.roots = np.roots([1, -self.rho1, -self.rho2])
    def root_type(self):
        if all(isinstance(root, complex) for root in self.roots):
            return 'Complex conjugate'
        elif len(self.roots) > 1:
            return 'Double real'
        else:
            return 'Single real'

    def root_less_than_one(self):
        if all(abs(root) < 1 for root in self.roots):
            return True
    def solution_type(self):
        rho1, rho2 = self.rho1, self.rho2
        discriminant = rho1 ** 2 + 4 * rho2
        if rho2 >= 1 + rho1 or rho2 <= -1:
            return 'Explosive oscillations'
        elif rho1 + rho2 >= 1:
            return 'Explosive growth'
        elif discriminant < 0:
            return 'Damped oscillations'
        else:
            return 'Steady state'

    def _transition(self, x, t, g):
    # Non-stochastic - separated to avoid generating random series when not needed
        if self.sigma == 0:
            return self.rho1 * x[t - 1] + self.rho2 * x[t - 2] + self.gamma + g
    # Stochastic
        else:
            epsilon = np.random.normal(0, 1, self.n)
            return self.rho1 * x[t - 1] + self.rho2 * x[t - 2] + self.gamma + g + self.sigma * epsilon[t]

    def generate_series(self):
    # Create list and set initial conditions
        y_t = [self.y_0, self.y_1]
    # Generate y_t series
        for t in range(2, self.n):
    # No government spending
            if self.g == 0:
                y_t.append(self._transition(y_t, t))
    # Government spending (no shock)
            elif self.g != 0 and self.duration == None:
                y_t.append(self._transition(y_t, t))
    # Permanent government spending shock
            elif self.duration == 'permanent':
                if t < self.g_t:
                    y_t.append(self._transition(y_t, t, g=0))
                else:
                    y_t.append(self._transition(y_t, t, g=self.g))
    # One-off government spending shock
            elif self.duration == 'one-off':
                if t == self.g_t:
                    y_t.append(self._transition(y_t, t, g=self.g))
                else:
                    y_t.append(self._transition(y_t, t, g=0))
        return y_t

    def summary(self):
        print('Summary\n' + '-' * 50)
        print('Root type: ' + self.root_type())
        print('Solution type: ' + self.solution_type())
        print('Roots: ' + str(self.roots))
        if self.root_less_than_one() == True:
            print('Absolute value of roots is less than one')
        else:
            print('Absolute value of roots is not less than one')
        if self.sigma > 0:
            print('Stochastic series with sigma = ' + str(self.sigma))
        else:
            print('Non-stochastic series')
        if self.g != 0:
            print('Government spending equal to ' + str(self.g))
        if self.duration != None:
            print(self.duration.capitalize() +  ' government spending shock at t = ' + str(self.g_t))

    def plot(self):
        fig, ax = plt.subplots(figsize=(12, 8))
        ax.plot(self.generate_series())
        ax.set(xlabel='Iteration', xlim=(0, self.n))
        ax.set_ylabel('$Y_t$', rotation=0)
        ax.grid()
        # Add parameter values to plot
        paramstr = '$\\alpha=%.2f$\n$\\beta=%.2f$\n$\\gamma=%.2f$\n$\\sigma=%.2f$\n$\\rho_1=%.2f$\n$\\rho_2=%.2f$'%(self.alpha, self.beta, self.gamma, self.sigma, self.rho1,self.rho2)
        props = dict(fc='white', pad=10, alpha=0.5)
        ax.text(0.87, 0.05, paramstr, transform=ax.transAxes,
        fontsize=12, bbox=props, va='bottom')
        return fig
    def param_plot(self):
        # Uses the param_plot() function defined earlier (it is then able
        # to be used standalone or as part of the model)
        fig = param_plot()
        ax = fig.gca()
        # Add lambda values to legend
        for i, root in enumerate(self.roots):
            if isinstance(root, complex):
                operator = ['+', ''] # Need to fill operator for positive as string is split apart
                label = r'$\lambda_{0} = {1.real:.2f} {2} {1.imag:.2f}i$'.format(i+1, sam.roots[i], operator[i])
            else:
                label = r'$\lambda_{0} = {1.real:.2f}$'.format(i+1, sam.roots[i])
            ax.scatter(0, 0, 0, label=label) # dummy to add to legend
        # Add rho pair to plot
        ax.scatter(self.rho1, self.rho2, 100, 'red', '+', label=r'$(\ \rho_1,\ \rho_2 \ )$', zorder=5)
        plt.legend(fontsize=12, loc=3)
        return fig

```

### Illustration of Samuelson class
Now we’ll put our Samuelson class to work on an example

```python
sam = Samuelson(alpha=0.8, beta=0.5, sigma=2, g=10, g_t=20, duration='permanent')
sam.summary()

```

    Summary
    --------------------------------------------------
    Root type: Complex conjugate
    Solution type: Damped oscillations
    Roots: [ 0.65+0.27838822j  0.65-0.27838822j]
    Absolute value of roots is less than one
    Stochastic series with sigma = 2
    Government spending equal to 10
    Permanent government spending shock at t = 20

```python
sam.plot()
plt.show()

```

### Using the graph
We’ll use our graph to show where the roots lie and how their location is consistent with the behavior of the path just graphed
The red $+$ sign shows the location of the roots

```python
sam.param_plot()
plt.show()

```

## 6 Using the LinearStateSpace class
Here is how we map the Samuelson model into an instance of a LinearStateSpace class

```python

from quantecon import LinearStateSpace  ##要安装这个包
""" This script maps the Samuelson model in the the LinearStateSpace class"""
alpha = 0.8
beta = 0.9
rho1 = alpha + beta
rho2 = -beta
gamma = 10
sigma = 1
g = 10
n = 100

A = [[1, 0, 0],
    [gamma + g, rho1, rho2],
    [0, 1, 0]]
G = [[gamma + g, rho1, rho2], # this is Y_{t+1}
    [gamma, alpha, 0], # this is C_{t+1}
    [0, beta, -beta]] # this is I_{t+1}
mu_0 = [1, 100, 100]
C = np.zeros((3,1))
C[1] = sigma # stochastic

sam_t = LinearStateSpace(A, C, G, mu_0=mu_0)

x, y = sam_t.simulate(ts_length=n)

fig, axes = plt.subplots(3, 1, sharex=True, figsize=(15, 8))
titles = ['Output ($Y_t$)', 'Consumption ($C_t$)', 'Investment ($I_t$)']
colors = ['darkblue', 'red', 'purple']
for ax, series, title, color in zip(axes, y, titles, colors):
    ax.plot(series, color=color)
    ax.set(title=title, xlim=(0, n))
    ax.grid()
axes[-1].set_xlabel('Iteration')
plt.show()

```

### Other methods in the LinearStateSpace class

Let’s plot impulse response functions for the instance of the Samuelson model using a method in the LinearStateSpace class

```python
imres = sam_t.impulse_response()
imres = np.asarray(imres)
y1 = imres[:, :, 0]
y2 = imres[:, :, 1]
y1.shape

```

Now let’s compute the zeros of the characteristic polynomial by simply calculating the eigenvalues of A

```python
A = np.asarray(A)
w, v = np.linalg.eig(A)
print(w)

```

    [ 0.85+0.42130749j  0.85-0.42130749j  1.00+0.j        ]

### Inheriting methods from LinearStateSpace
We could also create a subclass of LinearStateSpace (inheriting all its methods and attributes) to add more functions to use

```python
class SamuelsonLSS(LinearStateSpace):
    """
    this subclass creates a Samuelson multiplier-accelerator model
    as a linear state space system
    """
    def __init__(self,
                y_0=100,
                y_1=100,
                alpha=0.8,
                beta=0.9,
                gamma=10,
                sigma=1,
                g=10):
        self.alpha, self.beta = alpha, beta
        self.y_0, self.y_1, self.g = y_0, y_1, g
        self.gamma, self.sigma = gamma, sigma

        # Define intial conditions
        self.mu_0 = [1, y_0, y_1]
        self.rho1 = alpha + beta
        self.rho2 = -beta
        # Define transition matrix
        self.A = [[1, 0, 0],
                [gamma + g, self.rho1, self.rho2],
                [0, 1, 0]]

     # Define output matrix
        self.G = [[gamma + g, self.rho1, self.rho2], # this is Y_{t+1}
                [gamma, alpha, 0], # this is C_{t+1}
                [0, beta, -beta]] # this is I_{t+1}
        self.C = np.zeros((3, 1))
        self.C[1] = sigma # stochastic

        # Initialize LSS with parameters from Samuleson model
        LinearStateSpace.__init__(self, self.A, self.C, self.G, mu_0=self.mu_0)

    def plot_simulation(self, ts_length=100, stationary=True):
        # Temporarily store original parameters
        temp_mu = self.mu_0
        temp_Sigma = self.Sigma_0
        # Set distribution parameters equal to their stationary values for simulation
        if stationary == True:
            try:
                self.mu_x, self.mu_y, self.sigma_x, self.sigma_y = self.stationary_distributions()
                self.mu_0 = self.mu_y
                self.Sigma_0 = self.sigma_y
            # Exception where no convergence achieved when calculating stationary distributions
            except ValueError:
                print('Stationary distribution does not exist')

        x, y = self.simulate(ts_length)
        fig, axes = plt.subplots(3, 1, sharex=True, figsize=(15, 8))
        titles = ['Output ($Y_t$)', 'Consumption ($C_t$)', 'Investment ($I_t$)']
        colors = ['darkblue', 'red', 'purple']
        for ax, series, title, color in zip(axes, y, titles, colors):
            ax.plot(series, color=color)
            ax.set(title=title, xlim=(0, n))
            ax.grid()

        axes[-1].set_xlabel('Iteration')

            # Reset distribution parameters to their initial values
        self.mu_0 = temp_mu
        self.Sigma_0 = temp_Sigma
        return fig

    def plot_irf(self, j=5):
        x, y = self.impulse_response(j)
        # Reshape into 3 x j matrix for plotting purposes
        yimf = np.array(y).flatten().reshape(j+1, 3).T
        fig, axes = plt.subplots(3, 1, sharex=True, figsize=(15, 8))
        labels = ['$Y_t$', '$C_t$', '$I_t$']
        colors = ['darkblue', 'red', 'purple']
        for ax, series, label, color in zip(axes, yimf, labels, colors):
            ax.plot(series, color=color)
            ax.set(xlim=(0, j))
            ax.set_ylabel(label, rotation=0, fontsize=14, labelpad=10)
            ax.grid()
            axes[0].set_title('Impulse Response Functions')
            axes[-1].set_xlabel('Iteration')
        return fig

    def multipliers(self, j=5):
        x, y = self.impulse_response(j)
        return np.sum(np.array(y).flatten().reshape(j+1, 3), axis=0)
```

### Illustrations
Let’s show how we can use the SamuelsonLSS

```python
samlss = SamuelsonLSS()

```

```python
samlss.plot_simulation(100, stationary=False)
plt.show()
```

```python
samlss.plot_simulation(100, stationary=True)
plt.show()

```

```python
samlss.plot_irf(100)  # 脉冲反应函数
plt.show()

```

```python
samlss.multipliers()

```

    array([ 7.414389,  6.835896,  0.578493])

```python
pure_multiplier = SamuelsonLSS(alpha=0.8, beta=0)

```

```python
pure_multiplier.plot_simulation()

```

```python
pure_multiplier.plot_irf(100)
```

3.3 OOP III The Samuelson Accelerator.ipynb


```python
import numpy as np
import sympy as sym
from scipy import linalg
import pandas as pd
import matplotlib.pyplot as plt

# Import for Simulation
import mujoco_py
import glfw
import time
```

```python
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
```

```python
class my_maths():

    def __init__(self) -> None:
        pass

    def SE3(self, SO3):
        temp = np.append(SO3, np.array([0,0,0]).reshape(1,-1), axis=0)
        temp = np.append(temp, np.array([0,0,0,1]).reshape(-1,1), axis=1)
        return temp

    def translate(self, X):
        """X should be [x,y,z]"""
        temp = np.identity(4)
        temp[0, 3] = X[0]
        temp[1, 3] = X[1]
        temp[2, 3] = X[2]
        return temp

    def Rx(self, theta):
        temp = np.array([
            [1, 0, 0],
            [0, sym.cos(theta), -sym.sin(theta)],
            [0, sym.sin(theta), sym.cos(theta)]
        ])
        return self.SE3(temp)

    def Ry(self, theta):
        temp = np.array([
            [sym.cos(theta), 0, sym.sin(theta)],
            [0, 1, 0],
            [-sym.sin(theta), 0, sym.cos(theta)]
        ])
        return self.SE3(temp)

    def Rz(self, theta):
        temp = np.array([
            [sym.cos(theta), -sym.sin(theta), 0],
            [sym.sin(theta), sym.cos(theta), 0],
            [0, 0, 1]
        ])
        return self.SE3(temp)
```

```python
class DHRevolute(my_maths):
    def __init__(self, a=0, d=0, alpha=0):
        self.a = a
        self.d = d
        self.alpha = alpha

    def T(self, theta):
        return self.Rz(theta) @ self.translate([0,0,self.d]) @ self.translate([self.a,0,0]) @ self.Rx(self.alpha)
```

```python
class robot(my_maths):

    def __init__(self, links, name, simulation=False, model_path=""):
        self.links = links
        self.name = name
        self.simulation = simulation

        if simulation:
            self.model_path = model_path
            self.setup_simulation()

    def setup_simulation(self):
        self.simulation = True
        model = mujoco_py.load_model_from_path(self.model_path)
        self.sim = mujoco_py.MjSim(model)
        self.current_angles = self.sim.data.ctrl[:]
        self.viewer = mujoco_py.MjViewer(self.sim)
        self.viewer.render()

    def reset_sim(self):
        glfw.destroy_window(self.viewer.window)
        self.setup_simulation()

    def close_sim(self):
        glfw.destroy_window(self.viewer.window)
        self.simulation = False

    def play(self, thetas, sim_horizon=1000, sleep_time=0.001):

        if len(thetas) == 7:
            thetas = np.append(thetas, np.array([0,0]))

        dt = (thetas - self.current_angles)/sim_horizon
        for i in range(sim_horizon):
            self.current_angles = self.current_angles + dt
            self.sim.data.ctrl[:] = self.current_angles
            self.sim.step()
            self.viewer.render()
            time.sleep(sleep_time)
        ## final steps
        for i in range(500):
            self.sim.step()
            self.viewer.render()

    def base_position(self):
        self.play(np.zeros(7))

    def fkine(self, thetas, sim=False):

        if len(thetas) < len(self.links):
            return f"Please give {len(self.links)} theta values"

        T = np.identity(4)
        for i, link in enumerate(self.links):
            T = T @ link.T(thetas[i])

        if self.simulation and sim:
            self.play(thetas)

        try:
            return T.astype(np.float16)
        except TypeError:
            return T

    def setup_inverse(self):
        theta_1 = sym.Symbol("theta_1")
        theta_2 = sym.Symbol("theta_2")
        theta_3 = sym.Symbol("theta_3")
        theta_4 = sym.Symbol("theta_4")
        theta_5 = sym.Symbol("theta_5")
        theta_6 = sym.Symbol("theta_6")
        theta_7 = sym.Symbol("theta_7")

        thetas = [theta_1, theta_2, theta_3, theta_4, theta_5, theta_6, theta_7]
        T_sym = self.fkine(thetas, False)

        F_sym = sym.Matrix([T_sym[0,0], T_sym[0,1], T_sym[0,2], T_sym[0,3], \
                        T_sym[1,0], T_sym[1,1], T_sym[1,2], T_sym[1,3], \
                        T_sym[2,0], T_sym[2,1], T_sym[2,2], T_sym[2,3],   ])

        J_sym = F_sym.jacobian(sym.Matrix([theta_1, theta_2, theta_3, theta_4, theta_5, theta_6, theta_7]))

        self.F_symnp = sym.lambdify([theta_1, theta_2, theta_3, theta_4, theta_5, theta_6, theta_7], F_sym, "numpy")
        self.J_symnp = sym.lambdify([theta_1, theta_2, theta_3, theta_4, theta_5, theta_6, theta_7], J_sym, "numpy")

    def netwons_inverse(self, Tdest, verbose=False):

        theta_init=2.0*np.random.rand(7, ).reshape(-1,1)*np.pi - np.pi

        df = pd.DataFrame(columns=['Theta1', 'Theta2', 'Theta3', 'Theta4', 'Theta5', 'Theta6', "Theta7"])
        df.loc[0] = [theta_init[0]] + [theta_init[1]] + [theta_init[2]] + [theta_init[3]] + [theta_init[4]] + [theta_init[5]] + [theta_init[6]]

        A = Tdest[:-1,:]
        A = A.reshape(-1,1)

        i = 0
        while i < 1000:

            temp1 = self.F_symnp(theta_init[0], theta_init[1],theta_init[2],theta_init[3],theta_init[4],theta_init[5],theta_init[6]).reshape(-1,1) - A
            temp1_5 = self.J_symnp(theta_init[0][0], theta_init[1][0],theta_init[2][0],theta_init[3][0],theta_init[4][0],theta_init[5][0], theta_init[6][0])
            temp2 = np.linalg.pinv(temp1_5)

            temp3 = np.matmul(temp2,temp1)

            theta_init = theta_init - temp3
            for k in range(0,7):
                while abs(theta_init[k]) > np.pi:
                    theta_init[k] -= abs(theta_init[k])/theta_init[k]*np.pi

            df.loc[i+1] = [theta_init[0]] + [theta_init[1]] + [theta_init[2]] + [theta_init[3]] + [theta_init[4]] + [theta_init[5]] + [theta_init[6]]

            f = np.zeros(12)
            T = self.fkine(theta_init.ravel(), False)
            f[0] = (T[0][3]-Tdest[0][3])
            f[1] = (T[1][3]-Tdest[1][3])
            f[2] = (T[2][3]-Tdest[2][3])
            f[3] = (T[1][2]-Tdest[1][2])
            f[4] = (T[2][2]-Tdest[2][2])
            f[5] = (T[2][1]-Tdest[2][1])
            f[6] = (T[0][0]-Tdest[0][0])
            f[7] = (T[0][1]-Tdest[0][1])
            f[8] = (T[0][2]-Tdest[0][2])
            f[9] = (T[1][0]-Tdest[1][0])
            f[10] = (T[1][1]-Tdest[1][1])
            f[11] = (T[2][0]-Tdest[2][0])

            sum = 0.0
            for k in range(0,2*6):
                sum += pow(f[k],2.0)
            if sum < 0.0000000001:
                if verbose:
                    print(f"Converged")
                    print(f"Iteration ended in {i+1} iterations.")
                    print(df)
                    print(f"Solution Theta Values = {theta_init.T*180/np.pi}")
                return theta_init.squeeze()
            i += 1

        if i >= 1000 and verbose:
            print("No solution found")
        elif i >= 1000:
            return

    def jacob0(self, thetas):

        jacob_final = np.empty(shape= (6, 7))

        # T07 rotation matrix
        Rotation = self.fkine([thetas[0],thetas[1],thetas[2],thetas[3],thetas[4],thetas[5],thetas[6]])[:-1, :-1]

        # 12 rows, 7 columns jacobian
        jacobian_full = self.J_symnp(thetas[0],thetas[1],thetas[2],thetas[3],thetas[4],thetas[5],thetas[6])

        for i in range(7):
            jacob = jacobian_full[:,i].reshape(3,-1)
            skew_symmetric_matrix = jacob[:, :-1] @ Rotation.T

            jacob_final[0,i] = jacob[0, -1]
            jacob_final[1,i] = jacob[1, -1]
            jacob_final[2,i] = jacob[2, -1]

            jacob_final[3,i] = skew_symmetric_matrix[-1, 1]
            jacob_final[4,i] = skew_symmetric_matrix[0, -1]
            jacob_final[5,i] = skew_symmetric_matrix[1, 0]

        return jacob_final

    def joint_rates(self, thetas, end_effector_velocities):
        jacob0 = self.jacob0(thetas)
        xd = np.array(end_effector_velocities).reshape(-1,1)
        rates = np.linalg.pinv(jacob0) @ xd
        return rates.squeeze()

    def null_space_rates(self, thetas, joint_velocity):
        jacob0 = self.jacob0(thetas)
        joint_velocity = np.array(joint_velocity).reshape(-1,1)
        N = linalg.null_space(jacob0)
        return N @ np.linalg.pinv(N) @ joint_velocity

    def manipulability(self, thetas):
        j0 = self.jacob0(thetas)
        return np.sqrt(np.linalg.det(j0 @ j0.T))
```

# Creating the robot

```python
my_links = [
    DHRevolute(d=0.237,a=0.081,alpha=-np.pi/2),
    DHRevolute(d=0.1925,a=0,alpha=-np.pi/2),
    DHRevolute(d=0.4,a=0,alpha=-np.pi/2),
    DHRevolute(d=-0.1685,a=0,alpha=-np.pi/2),
    DHRevolute(d=0.4, a=0,alpha=-np.pi/2),
    DHRevolute(d=0.1363, a=0,alpha=-np.pi/2),
    DHRevolute(d=0.11, a=8.08e-7,alpha=0),
]
```

```python
model = "sawyer_sim/sawyer.xml"

sawyer_robo = robot(my_links, "sawyer", True, model)
```

    Creating window glfw

# Forward Kinematics

```python
arr = sawyer_robo.fkine(np.array([-np.pi/4, -np.pi/2,np.pi/2, np.pi/2, np.pi/2, np.pi/2, np.pi/2]), True)
arr
```

    array([[-0.707  ,  0.707  ,  0.     ,  0.8555 ],
           [ 0.707  ,  0.707  , -0.     , -0.01753],
           [-0.     , -0.     , -1.     ,  0.2954 ],
           [ 0.     ,  0.     ,  0.     ,  1.     ]], dtype=float16)

```python
x = 1
y = 1
z = 1

vector_from_end_effector = np.array([x,y,z,1])

world = arr @ vector_from_end_effector
world
```

    array([ 0.85546875,  1.39653015, -0.70458984,  1.        ])

```python
sawyer_robo.base_position()
```

Change joint 0

```python
# sawyer_robo.fkine([np.pi/2, 0, 0, 0, 0, 0, 0], True)
# sawyer_robo.fkine([0, 0, 0, 0, 0, 0, 0], True);
```

Change joint 1

```python
# sawyer_robo.fkine([0, np.pi/2, 0, 0, 0, 0, 0], True)
# sawyer_robo.fkine([0, 0, 0, 0, 0, 0, 0], True);
```

Change joint 2

```python
# sawyer_robo.fkine([0, 0, np.pi/2, 0, 0, 0, 0], True)
# sawyer_robo.fkine([0, 0, 0, 0, 0, 0, 0], True);
```

Change joint 3

```python
# sawyer_robo.fkine([0, 0, 0, np.pi/2, 0, 0, 0], True)
# sawyer_robo.fkine([0, 0, 0, 0, 0, 0, 0], True);
```

Change joint 4

```python
# sawyer_robo.fkine([0, 0, 0, 0, np.pi/2, 0, 0], True)
# sawyer_robo.fkine([0, 0, 0, 0, 0, 0, 0], True);
```

Change joint 5

```python
# sawyer_robo.fkine([0, 0, 0, 0, 0, np.pi/2, 0], True)
# sawyer_robo.fkine([0, 0, 0, 0, 0, 0, 0], True);
```

Change joint 6

```python
# sawyer_robo.fkine([0, 0, 0, 0, 0, 0, np.pi/2], True)
# sawyer_robo.fkine([0, 0, 0, 0, 0, 0, 0], True);
```

```python
# sawyer_robo.close_sim()
```

```python
# sawyer_robo.reset_sim()
```

```python
sawyer_robo.current_angles
```

    array([-6.57785458e-15, -1.31557092e-14,  1.31557092e-14,  1.31557092e-14,
            1.31557092e-14,  1.31557092e-14,  1.31557092e-14,  0.00000000e+00,
            0.00000000e+00])

# Inverse Kinematics

### Newtons Method

```python
sawyer_robo.setup_inverse()
```

```python
angles = np.array([np.pi/2, -np.pi/4, np.pi/4, -np.pi/8, -np.pi/8, np.pi/4, -np.pi/4])
```

```python
Tdest = sawyer_robo.fkine(angles)
Tdest
```

    array([[ 0.01662,  0.587  , -0.8096 , -0.3474 ],
           [ 0.9985 , -0.05118, -0.01662,  0.3625 ],
           [-0.05118, -0.808  , -0.587  ,  0.3845 ],
           [ 0.     ,  0.     ,  0.     ,  1.     ]], dtype=float16)

```python
inverse_angles = sawyer_robo.netwons_inverse(Tdest)
inverse_angles
```

    array([ 1.05964427, -1.14636082,  0.4976794 , -0.18045638, -0.2811583 ,
            1.53021589, -0.64297665])

```python
sawyer_robo.fkine(inverse_angles)
```

    array([[ 0.01662,  0.587  , -0.8096 , -0.3474 ],
           [ 0.9985 , -0.05118, -0.01662,  0.3625 ],
           [-0.05118, -0.808  , -0.587  ,  0.3845 ],
           [ 0.     ,  0.     ,  0.     ,  1.     ]], dtype=float16)

# Manipulator Jacobian

```python
angles = np.array([np.pi/2, -np.pi/4, np.pi/4, -np.pi/8, -np.pi/8, np.pi/4, -np.pi/4])
jacob0 = sawyer_robo.jacob0(angles)
jacob0
```

    array([[-3.62661495e-01, -8.20670215e-18,  3.03559084e-01,
            -2.15829904e-01, -8.77795659e-02, -4.69458990e-02,
             4.74247331e-07],
           [-3.47416139e-01,  1.47635878e-01,  1.09542253e-01,
            -2.26936261e-01, -1.23899810e-01, -7.36884712e-02,
            -4.13507520e-08],
           [ 0.00000000e+00, -2.81661495e-01,  1.09542253e-01,
            -7.82933165e-02, -3.96404811e-02,  6.68288245e-02,
            -6.52873329e-07],
           [ 0.00000000e+00, -9.99983881e-01, -1.09708213e-06,
             7.07096159e-01,  2.70594405e-01, -4.03276134e-01,
            -8.09447054e-01],
           [-3.07622034e-06,  6.12358788e-17,  7.07151998e-01,
            -5.00028897e-01, -4.61970483e-01,  7.42313388e-01,
            -1.66128301e-02],
           [ 1.00007416e+00,  8.52771852e-05, -7.07159218e-01,
            -5.00097378e-01,  8.44662757e-01,  5.35237148e-01,
            -5.86914263e-01]])

```python
jacob0.shape
```

    (6, 7)

Now consider that we want the end-effector to move at $0.2 \frac{m}{s}$ in the $x$, $y$ and $z$ directions.

```python
end_effector_velocities = [0.2,0.2,0.2,0,0,0]

sawyer_robo.joint_rates(angles, end_effector_velocities)
```

    array([-0.3888681 , -0.68812759, -0.45417442, -0.81155528, -0.14131349,
           -0.19765005,  0.19240083])

# Manipulability

```python
sawyer_robo.play(angles)

sawyer_robo.manipulability(angles)
```

    0.027659711185380905

# Null space motion

```python
angles = np.array([0, np.pi/3, 0, 0, 0, -np.pi/2, 0])
```

```python
sawyer_robo.fkine(angles)
```

    array([[ 0.866 , -0.    ,  0.5   ,  0.136 ],
           [-0.    , -1.    ,  0.    ,  0.4973],
           [ 0.5   , -0.    , -0.866 ,  0.1417],
           [ 0.    ,  0.    ,  0.    ,  1.    ]], dtype=float16)

Rotate joint 1 at -0.1 rad/s without changing end effector pose

```python
for i in range(15):
    joint_velocity = [0,-0.1,0,0,0,0,0]
    null_space_rates = sawyer_robo.null_space_rates(angles, joint_velocity)
    angles = angles + null_space_rates.squeeze()

angles
```

    array([-2.28968257e-05,  2.94770347e-01, -1.04742118e-02, -4.39058411e-03,
           -1.04206602e-02, -8.22759638e-01,  1.25463277e-05])

```python
sawyer_robo.fkine(angles)
```

    array([[ 8.662e-01,  3.088e-05,  5.000e-01,  1.360e-01],
           [ 3.767e-05, -1.000e+00, -3.576e-06,  4.973e-01],
           [ 5.000e-01,  2.193e-05, -8.662e-01,  1.417e-01],
           [ 0.000e+00,  0.000e+00,  0.000e+00,  1.000e+00]], dtype=float16)

# Trajectory

#  polynomial function of time.

quintic (fifth-order) polynomial

$s(t) = At^5 + Bt^4 + Ct^3 + Dt^2 + E^t + F$
$v(t) = 5*At^4 + 4Bt^3 + 3ct^2 + 2D + E$
$a(t) = 20At^3 + 12Bt^2 + 6Ct + 2D$

$$
\left(\begin{matrix}
s_0\\
s_T\\
\dot{s_0}\\
\dot{s_T}\\
\ddot{s_0}\\
\ddot{s_T}\\
\end{matrix}\right)=
\left(\begin{matrix}
0 & 0 & 0 & 0 & 0 & 1\\
T^5 & T^4 & T^3 & T^2 & T & 1\\
0 & 0 & 0 & 0 & 1 & 0\\
5T^4 & 4T^3 & 3T^2 & 2T & 1 & 0\\
0 & 0 & 0 & 2 & 0 & 0\\
20T^3 & 12T^2 & 6T & 2 & 0 & 0\\
\end{matrix}\right)
\left(\begin{matrix}
A\\
B\\
C\\
D\\
E\\
F\\
\end{matrix}\right)
$$

```python
def parabolic_trajectory(boundary, T, step=0.1, plot=False):
    """
    s,v,a [initial, final ...]
    T = time
    """
    b = np.array(boundary)
    A = np.array([
        [0,0,0,0,0,1],
        [T**5, T**4, T**3, T**2, T, 1],
        [0,0,0,0,1,0],
        [5*T**4, 4*T**3, 3*T**2, 2*T, 1, 0],
        [0,0,0,2,0,0],
        [20*T**3, 12*T**2, 6*T, 2, 0, 0]
    ])

    A,B,C,D,E,F = np.linalg.solve(A,b)

    t = np.arange(0, T, step=step)
    t2 = np.power(t, 2)
    t3 = np.power(t, 3)
    t4 = np.power(t, 4)
    t5 = np.power(t, 5)

    s = A*t5 + B*t4 + C*t3 + D*t2 + E*t + F
    v = 5*A*t4 + 4*B*t3 + 3*C*t2 + 2*D*t + E
    a = 20*A*t3 + 12*B*t2 + 6*C*t + 2*D

    if plot:
        fig,ax = plt.subplots(figsize=(15,10))
        ax.plot(t, s, label="position")
        ax.plot(t, v, label="velocity")
        ax.plot(t, a, label="acceleration")
        ax.set_title("Quintic poltnomial trajectory")
        ax.set_xlabel("time")
        ax.set_ylabel("s, s', s''")
        ax.legend()

    return s,v,a
```

```python
initial_pos = 0
final_pos = 1
initial_vel = 0
final_vel = 0
initial_acc = 0
final_acc = 0

s,v,a = parabolic_trajectory([initial_pos,final_pos,initial_vel,final_vel,initial_acc,final_acc], T = 10, plot=True)
```

### Linear segment with parabolic blend (LSPB) trajectory

```python
def lspb(config, step=0.1, plot=False):
    """
    config = [t0, q0, tf, qf, V]
    """
    t0 = config[0]
    q0 = config[1]
    tf = config[2]
    qf = config[3]
    V = config[4]

    lower_boundary = (qf-q0)/tf
    upper_boundary = 2*lower_boundary
    if not lower_boundary < V and not V <= upper_boundary:
        print(f"V should be {lower_boundary} < V <= {upper_boundary}")
        return

    tb = (q0 - qf + V*tf)/V
    a0 = q0
    a1 = 0
    a2 = V/(2*tb)

    b0 = qf - (V*tf**2)/(2*tb)
    b1 = V*tf/tb
    b2 = -V/(2*tb);

    t = np.arange(t0,tf,step=step)
    t2 = t**2

    first_ = t <= tb
    second_ = (t>tb) * (t < (tf-tb))
    third_ = t > (tf-tb)

    q1 = (a0 + a1*t + a2*t2) * first_
    q2 = ((qf + q0 - V*tf)/2 + V*t) * second_
    q3 = (b0 + b1*t + b2*t2) * third_;
    q = q1+q2+q3

    qdot1 = (a1 + 2*a2*t) * first_
    qdot2 = V * second_
    qdot3 = (b1 + 2*b2*t) * third_
    qdot = qdot1 + qdot2 + qdot3

    qddot1 = 2*a2*first_
    qddot2 = 0 * second_
    qddot3 = 2*b2 * third_
    qddot = qddot1 + qddot2 + qddot3

    if plot:
        fig, ax = plt.subplots(3,1, figsize=(15,10))
        fig.suptitle("LSPB", fontsize = 25)

        ax[0].plot(t, q, label="position")
        ax[0].grid(True)
        ax[0].set_xlabel("Time")
        ax[0].set_ylabel("s")
        ax[0].legend()

        ax[1].plot(t, qdot, color = "orange", label="velocity")
        ax[1].grid(True)
        ax[1].set_xlabel("Time")
        ax[1].set_ylabel("ds/dt")
        ax[1].legend()

        ax[2].plot(t, qddot, "g", label = "acceleration")
        ax[2].grid(True)
        ax[2].set_xlabel("Time")
        ax[2].set_ylabel("ds^2/dt^2")
        ax[2].legend()

    return q, qdot, qddot
```

```python
initial_time = 0
initial_position = 0
final_time = 50
final_position = 1
max_omega = 0.03

s, v, a = lspb([initial_time, initial_position, final_time, final_position, max_omega], plot = True)
```


# Hands On Scipy Tutorial
**Tutors:** Axel Donath

This hands-on tutorial gives an introduction and overview of the [Scipy](http://www.scipy.org/) Python package. It based on the [excellent Scipy tutorial](https://github.com/Asterics2020-Obelics/School2019/blob/master/scipy/scipy_general.ipynb) by Maximilian Noethe and Kai Brügge from the past Asterics software summer school 2019.

## Preface
We recommend to follow this tutorial by **executing the code cells on your local machine**, along with the tutor. Every sub-topic we will cover in this tutorial will be concluded by a few **exercises** with different levels of difficulty** (*easy*, *advanced* and *hard*). The exercises will not be discussed during the course, however we provide a **sample solution** for all exercises in the [solutions folder](astropy_hands_on_solutions.ipynb) you can look at **after the course**. You can always ask for help on the exercises on the corresponding Slack channel **#d05-fri11-scipy**.

The estimated time for this tutorial is ~1.5 hours. We have marked some of the sections that deal with more advanced topics as "optional".

We're happy to receive any **feedback or questions** on the tutorial via mail to *axel.donath@mpi-hd.mpg.de* or using the
repository's [issue tracker](https://github.com/escape2020/school2021/issues).

The SciPy framework builds on top of the low-level NumPy framework for multidimensional arrays, and provides a large number of higher-level scientific algorithms. Some of the topics that SciPy covers are:

* Special functions ([scipy.special](http://docs.scipy.org/doc/scipy/reference/special.html))
* Integration and ODE  ([scipy.integrate](http://docs.scipy.org/doc/scipy/reference/integrate.html))
* Optimization ([scipy.optimize](http://docs.scipy.org/doc/scipy/reference/optimize.html))
* Interpolation ([scipy.interpolate](http://docs.scipy.org/doc/scipy/reference/interpolate.html))
* Fourier Transforms ([scipy.fftpack](http://docs.scipy.org/doc/scipy/reference/fftpack.html))
* Signal Processing ([scipy.signal](http://docs.scipy.org/doc/scipy/reference/signal.html))
* Linear Algebra ([scipy.linalg](http://docs.scipy.org/doc/scipy/reference/linalg.html))
* Sparse Eigenvalue Problems ([scipy.sparse](http://docs.scipy.org/doc/scipy/reference/sparse.html))
* Statistics ([scipy.stats](http://docs.scipy.org/doc/scipy/reference/stats.html))
* Multi-dimensional image processing ([scipy.ndimage](http://docs.scipy.org/doc/scipy/reference/ndimage.html))
* File IO ([scipy.io](http://docs.scipy.org/doc/scipy/reference/io.html))

Each submodule in SciPy  provides a number of functions and classes that can be used to solve problems in their respective domain.

## 0. Setup

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

from mpl_toolkits.mplot3d import Axes3D

rng = np.random.default_rng(42)
```

## 1. Linear algebra

The linear algebra module contains a lot of matrix related functions, including linear equation solving, eigenvalue solvers, matrix functions (for example matrix-exponentiation), a number of different decompositions (SVD, LU, cholesky), etc.

Detailed documetation is available at: http://docs.scipy.org/doc/scipy/reference/linalg.html

Here we will look at how to use some of these functions:

### 1.1 Linear equation systems

Linear equation systems on the matrix form

$A x = b$

where $A$ is a matrix and $x,b$ are vectors can be solved like:

```python
from scipy import linalg
```

```python
A = (np.arange(100) + rng.normal(0, 10, size=(100))).reshape(10, 10)
b = np.arange(10)
A.shape, b.shape
```

    ((10, 10), (10,))

```python
plt.matshow(A)
plt.grid(False) # turn off the grid for this plot
```

```python
x = linalg.solve(A, b)
x
```

    array([ 0.0298923 ,  0.00868663,  0.0258469 ,  0.01367623,  0.01353663,
            0.02030362, -0.05241789,  0.05267412,  0.01826446, -0.03321952])

```python
# check whether we found the correct x. All entries should be close to zero.
np.dot(A, x) - b
```

    array([ 1.66533454e-16, -1.11022302e-16,  0.00000000e+00,  0.00000000e+00,
            8.88178420e-16,  0.00000000e+00,  0.00000000e+00,  8.88178420e-16,
            0.00000000e+00,  0.00000000e+00])

We can also do the same with

$A X = B$

where $A, B, X$ are matrices:

```python
A = rng.uniform(size=(5, 5))
B = rng.uniform(size=(5, 5))
f, [ax1, ax2] = plt.subplots(1, 2)
ax1.matshow(A)
ax2.matshow(B)
ax1.grid(False)
ax2.grid(False)
ax1.set_title("Matrix A")
ax2.set_title("Matrix B");
```

```python
X = linalg.solve(A, B)
X
```

    array([[ 4.26661683,  0.97303145,  0.21291271,  4.38475543,  3.60122584],
           [-0.52762202, -0.69149307,  0.15805858,  0.11202386,  0.02839992],
           [-1.65294528, -0.62130048,  0.64404986, -2.65960636, -1.57717235],
           [ 4.4992729 ,  1.95322224, -0.05940088,  5.89156964,  6.56577378],
           [-5.3781515 , -0.8935471 , -0.18052208, -7.24626002, -7.87467682]])

```python
# check
linalg.norm(np.dot(A, X) - B)
```

    9.865072098181418e-16

### 1.2 Eigenvalues and eigenvectors

The eigenvalue problem for a matrix $A$:

$\displaystyle A v_n = \lambda_n v_n$

where $v_n$ is the $n$th eigenvector and $\lambda_n$ is the $n$th eigenvalue.

To calculate eigenvalues of a matrix, use the `eigvals` and for calculating both eigenvalues and eigenvectors, use the function `eig`. Both return complex numbers.

```python
A = rng.uniform(size=(4, 4))

eigen_values = linalg.eigvals(A)
eigen_values
```

    array([ 2.03197191+0.j        ,  0.32055586+0.j        ,
           -0.25719529+0.28106828j, -0.25719529-0.28106828j])

```python
values, vectors = linalg.eig(A)
values, vectors
```

    (array([ 2.03197191+0.j        ,  0.32055586+0.j        ,
            -0.25719529+0.28106828j, -0.25719529-0.28106828j]),
     array([[-0.40772097+0.j        , -0.66263393+0.j        ,
              0.69076099+0.j        ,  0.69076099-0.j        ],
            [-0.59942711+0.j        ,  0.1547345 +0.j        ,
              0.05160407+0.2579825j ,  0.05160407-0.2579825j ],
            [-0.34154935+0.j        ,  0.61135759+0.j        ,
              0.05395196-0.46534462j,  0.05395196+0.46534462j],
            [-0.5981595 +0.j        , -0.40399925+0.j        ,
             -0.45963856+0.15135145j, -0.45963856-0.15135145j]]))

There are also more specialized solvers, like the `eigh` for Hermitian matrices.

### 1.3 Exercise:  Simple Linear Algebra

1. Create a 10x10 matrix $A$ with random entries

2. Create a symmetric matrix from $A$ using its transpose (use A.T to transpose) and then plot the new matrix using `plt.matshow`

3. Use `linalg.eigh` to get the eigenvalues and vectors.

```python
# %load solutions/simple_linalg_solution.py
```

# 2. Special Functions

A large number of mathematical special functions are important for many computional physics problems. SciPy provides implementations of a very extensive set of special functions. For details, see the list of functions in the reference documention at http://docs.scipy.org/doc/scipy/reference/special.html#module-scipy.special.

- Airy functions
- Elliptic functions and integrals
- Bessel functions
- Gamma functions
- etc....

## 2.1 Polynomials

The module contains a large number of functions for evaluating polynomials and calculating their roots.

The following plot shows just a few of them.

```python
from scipy import special

functions = {
    "Chebyshev": special.eval_chebys,
    "Laguerre": special.eval_laguerre,
    "Legendre": special.eval_legendre,
    "Chebyshev II": special.eval_chebyu
}

fig, axes = plt.subplots(2, 2, sharex=True, sharey=True, figsize=(12, 8))

x = np.linspace(-2., 2., 200)

for name, ax in zip(functions, axes.flat):
    f = functions[name]
    for n in range(5):
        ax.plot(x, f(n, x), label=f"Order {n}")

    ax.set_title(name)
    ax.set_ylim([-3, 3])

plt.legend();
```

## 2.2 Statistical Functions

The module also contains a host of useful functions for statistical computations. for example the so called 'error function' `scipy.special.erf`

\begin{align}
\operatorname {erf}(x)&={\frac {1}{\sqrt {\pi }}}\int _{-x}^{x}e^{-t^{2}},dt \\ &={\frac {2}{\sqrt {\pi }}}\int _{0}^{x}e^{-t^{2}}\,dt.
\end{align}

Which allows you to get the Gaussian probability distribution
$$
\phi(z) = \frac 1 2 \left(1 + erf\left(\frac{z}{sqrt(2)}\right) \right)
$$

The module `scipy.stats` contains nicer methods to handle statistical methods. More on that later.

```python
x = np.linspace(-3, 3)
plt.plot(x, special.erf(x), label='$erf$')
plt.plot(x, 0.5 * (1 + special.erf(x / np.sqrt(2))), label='$phi$')
plt.xlabel('$x$')
plt.legend();
```

## 2.3 Bessel Functions
To demonstrate the typical usage of special functions we will look in more detail at the Bessel functions.
The `scipy.special` module includes a large number of Bessel-functions
Here we will use the functions $j_n$ and $y_n$, which are the Bessel functions
of the first and second kind and real-valued order.

Bessel functions are a family of solutions to Bessel’s differential equation with real or complex order alpha:
$$
x^2 \frac{d^2 y}{dx^2} +x  \frac{dy}{dx} +(x^2−α^2)y=0
$$

```python
from scipy import special

x = np.linspace(0, 10, 100)

for n in range(4):
    plt.plot(x, special.jn(n, x), label=f'$J_{n}(x)$')

plt.legend();
```

Among other uses, these functions arise in wave propagation problems such as the vibrational modes of a thin drum head. Here is an example of a circular drum head anchored at the edge. See https://en.wikipedia.org/wiki/Vibrations_of_a_circular_membrane

```python
# fix the radius of the drum
DRUM_RADIUS = 1

def drumhead_height(n, k, radius, theta, t):
    '''Solution to the Drumhead problem with a fixed membrane on the edge of the drum
    See https://en.wikipedia.org/wiki/Vibrations_of_a_circular_membrane
    '''
    kth_zero = special.jn_zeros(n, k)[-1] # fix the drum on the outter edge to be zero.
    return np.cos(t) * np.cos(n * theta) * special.jn(n, radius * kth_zero / DRUM_RADIUS)

# create two coordinate vectors containing all combinations of radius and theta we want to plot
r = np.linspace(0, DRUM_RADIUS, 50)
t = np.linspace(0, 2 * np.pi, 50)
radius, theta = np.meshgrid(r, t) # this creates all combinations we are interested in

x = radius * np.cos(theta)
y = radius * np.sin(theta)
```

```python
from itertools import product

fig = plt.figure(figsize=(12, 12))

idx = 1

for n in range(3):
    for k in range(1, 4):
        ax = fig.add_subplot(3, 3, idx, projection='3d')
        z = drumhead_height(n=n, k=k, radius=radius, theta=theta, t=0.5)
        ax.plot_surface(x, y, z, cmap='YlGnBu')
        ax.set_title(f"n={n}, k={k}", fontsize=14)
        ax.axis('off')
        idx += 1

```

In the `scripts` folder you find an example script that creates the following animation:

```python
from IPython.display import Video
Video('scripts/animation_membrane.mp4')
```

## 3. Ordinary Differential Equations

### 3.1 Zombie outbreak

To demonstrate the usefulness of SciPy lets try to predict what will happen to humanity in case of a zombie outbreak.  This example comes courtesy of Christopher Campo.

We will show that, in case of a Zombie outbreak, humanity is inevitably doomed.
As shown by [Phillip Munz et al](http://mysite.science.uottawa.ca/rsmith43/Zombies.pdf) we can model a simple outbreak scenario like so.

\begin{align}
\frac{S}{dt} &= B - \beta S Z - \delta S \\
\frac{Z}{dt} &= \beta S Z + \gamma R - \alpha S Z \\
\frac{R}{dt} &= \delta S + \alpha S Z - \gamma R
\end{align}

Where $S$ is the number of susceptible humans, $Z$ is the number of zombies and $R$ is the number of removed persons which have died either by natural causes or zombie attack. The human birthrate is assumed to be constant and modeled by $B$

Susceptibles can become zombies through an encounter with a zombie ($\beta$). Natural causes of human deaths are parameterized by $\delta$. Humans in the removed class can be resurrected and become zombies ($\gamma$).
Zombies can enter the removed class by cutting their heads of or removing their brains ($\alpha$)

The question I'm trying to answer is

> __Can Humanity survive the zombie Apocalypse?__

Since I don't know how to solve differential equations, I'm going to use SciPy instead.

Now we use SciPy to solve this system of differential equations. The function `solve_ivp` does the entire job for us.

If we go to [the SciPy documentation](https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html#scipy.integrate.solve_ivp) about this function we will read the following.

>This function numerically integrates a system of ordinary differential equations given an initial value:
>
> $y^{\prime} = f(t, y)$
>
>$y(t_0) = y_0$
>
>Here $t$ is a one-dimensional independent variable (time), $y(t)$ is an __n-dimensional vector-valued function__ (state), and an __n-dimensional vector-valued function f(t, y) determines the differential equations__. The goal is to find y(t) approximately satisfying the differential equations, given an initial value y(t0)=y0.

So we start by defining some starting values for the problem and the model function $f(t, y)$.

```python
from scipy.integrate import solve_ivp

# Type `solve_ivp?` to get inline documentation about the method in your notebook.
```

```python
# fixed model parameters
B = 0.1 # birth rate
delta = 0.0001 # rate of natural casues of death
beta = 0.00016 # transmission coefficent
gamma = 0.0001 # resurrection rate
alpha = 0.0001 # rate of destroyed zombies

# initial conditions
S0 = 5000.              # initial population
Z0 = 0                 # initial zombie population
R0 = 0                 # initial dead population
y0 = [S0, Z0, R0]     # initial condition vector

def f_model(t, y):
    Si, Zi, Ri = y

    # the model equations (see Munz et al. 2009)
    f0 = B - beta * Si * Zi - delta * Si
    f1 = beta * Si * Zi + gamma * Ri - alpha * Si * Zi
    f2 = delta * Si + alpha * Si * Zi - gamma * Ri
    return [f0, f1, f2]

```

Now we solve the equation and output the result

```python
t_start, t_end = 0, 150  # 200 days of zombie apocalypse
solution = solve_ivp(f_model, (t_start, t_end), y0, t_eval=np.linspace(t_start, t_end, 200))
```

```python
s = solution.y[0, :]
z = solution.y[1, :]
plt.plot(solution.t, s, label='survivors')
plt.plot(solution.t, z, label='zombies')
plt.xlabel('t / days')
plt.ylabel('Population')
plt.legend()
```

### 3.2 Exercise: Save the world.

The ODE defined above is a very pessimistic model. There is one unstable equilibrium state if $\beta = 0$.
Your task is to save the world by adding a the notion of a _cure_ to the model above. The cure
can move individuals from the $Z$ to the $S$ group.

1. Introduce a new constant $\rho$ which models the effectivness of the cure.
2. Add a term to the ODE to model the cure.
3. Solve the ODE using `solve_ivp`

```python
# %load solutions/solution_zombies.py
```

A more complex case of coupled differential equations is given again in the `scripts` folder, which creates the following animation:

```python
Video("scripts/animation_pendulum.mp4")
```

## 4. Numerical Integration

### 4.1 Quadrature

Numerical evaluation of a function of the type

$\displaystyle \int_a^b f(x) dx$

is called *numerical quadrature*, or simply *quadature*. SciPy provides a series of functions for different kind of quadrature, for example the `quad`, `dblquad` and `tplquad` for single, double and triple integrals, respectively.

```python
from scipy.integrate import quad, dblquad, tplquad
```

The `quad` function takes a large number of optional arguments, which can be used to fine-tune the behaviour of the function.

The basic usage is as follows:

```python
# define a simple function for the integrand
def f(x):
    return x ** 2
```

```python
x_lower = 0 # the lower limit of x
x_upper = 1 # the upper limit of x

val, abserr = quad(func=f, a=x_lower, b=x_upper)

print(f'integral value={val} , absolute error={abserr}' )

x = np.linspace(-1.2, 1.2, 200)
plt.plot(x, f(x))
plt.fill_between(x, f(x), alpha=0.3, where=(x > 0) & (x < 1))
```

If we need to pass extra arguments to integrand function we can use the `args` keyword argument:

```python
def integrand(x, n):
    return special.jn(n, x)

x_lower = 0  # the lower limit of x
x_upper = 10 # the upper limit of x

val, abserr = quad(integrand, x_lower, x_upper, args=(3,))
print(val, abserr)
```

    0.7366751370811073 9.389126882496403e-13

For simple functions we can use a lambda function (name-less function) instead of explicitly defining a function for the integrand:

```python
val, abserr = quad(lambda x: np.exp(-x ** 2), -np.Inf, np.Inf)
print(f'numerical= {val}, {abserr}')
print(f'analytical solution = {np.sqrt(np.pi)}')
```

    numerical= 1.7724538509055159, 1.4202636780944923e-08
    analytical solution = 1.7724538509055159

As show in the example above, we can also use 'Inf' or '-Inf' as integral limits.

Higher-dimensional integration works in the same way:

```python
def integrand(x, y):
    return np.exp(-x**2 - y**2)

x, y = np.meshgrid(np.linspace(-2, 2, 100), np.linspace(-2, 2, 100))
z = integrand(x, y)

fig = plt.figure()
ax = Axes3D(fig)
ax.plot_surface(x, y, z, cmap='YlGnBu')

# the integral boundarys can be functions when integrating in more than one dimensions
val, abserr = dblquad(integrand, -2, 2, lambda x: -2, lambda x: 2)

print(f'numerical solution= {val}, {abserr}')
```

Note how we had to pass lambda functions for the limits for the y integration, since these in general can be functions of x.

### 4.2 Exercise II:  Integrals and Special Functions

Find the circumference of an ellipse. The formula for calculating circumference $C$ is as follows.

$$
\frac{C}{4a} =  E(\epsilon)=\int _{0}^{\pi /2}{\sqrt {1-\epsilon^{2}\sin ^{2}\theta }}\ d\theta
$$

where $\epsilon$ is the eccentricity of the ellipse given by the length and width of the ellipse.

$$
\epsilon = \sqrt{1 - \frac{b^2}{a^2}}
$$

compare the solution found using `quad` with the appropriate function from the `scipy.special` package.

```python
# %load solutions/solution_2.py
```

## 5. Signal Processing

SciPy offers a large amount of utilities to handle time series data. However the methods in the `scipy.signal` are useful for much more than just time series data.

### 5.1 Convolution

The convolution of two continuos functions is defined by a simple integral transform.

\begin{aligned}
(f*g)(t)&\,{\stackrel {\mathrm {def} }{=}}\ \int _{-\infty }^{\infty }f(\tau )g(t-\tau )\,d\tau \\&=\int _{-\infty }^{\infty }f(t-\tau )g(\tau )\,d\tau .
\end{aligned}

Convolution happens whenever data gets smeared out by a detector or filter of any kind.

```python
from scipy import signal

# create two rectangular signals
f = np.repeat([0., 1., 0.], 120)
g = np.repeat([0., 1., 0.], 20)

result = signal.convolve(f, g, mode='same')  # same output shape as input, see docs
```

```python
fig, (ax_orig, ax_win, ax_filt) = plt.subplots(3, 1, sharex=True, figsize=(6, 6))

ax_orig.plot(f, color='gray')
ax_orig.set_title('Original pulse')
ax_orig.margins(0, 0.1)

ax_win.plot(g, color='gray')
ax_win.arrow(70, 0.5, 50.0, 0.0, width=0.08, head_length=8)
ax_win.set_title('Filter impulse response')

ax_filt.plot(result)
ax_filt.set_title('Filtered signal');
```

The naive algorithm for producing discrete convolutions is not very efficient. It takes $\mathcal{O}(n^2)$ steps to get a result. A more efficient implementation is the FFT based alogrithm. It is based on the *convolution theorem*

> The convolution theorem states that
>
 $$
 \mathcal{F}\{f*g\}=k\cdot {\mathcal {F}}\{f\}\cdot {\mathcal {F}}\{g\}
 $$
>
> where
> $\mathcal {F}\{f\}$ denotes the Fourier transform of $f$, and $\mathcal {F}\{g\}$ the Fourier transform of $g$.
>
> https://en.wikipedia.org/wiki/Convolution

SciPy uses a heuristic to automagically find the [fastest convolution method](https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.choose_conv_method.html).

### 5.2 Signal Windows

SciPy conveniently provides a handful of common signal windows ready to be used for convolution or other filtering operations. All of them can be found in `scipy.signal.window` but their are also available in the `scipy.signal` name space.

```python
windows = {
    "Nutall": signal.nuttall(150),
    "Hannings": signal.hann(150),
    "Cosine": signal.cosine(150),
    "Triangle": signal.triang(150),
    "Blackman": signal.blackman(150)
}

for name, window in windows.items():
    plt.plot(window, label=name)

plt.legend();
```

### 5.3 Correlation

The correlation of two functions $f$ and $g$ looks similar to a convolution

> For continuous functions $f$ and $g$, the cross-correlation is defined as:
>
$$
(f\star g)(\tau )\ {\stackrel {\mathrm {def} }{=}}\int _{-\infty }^{\infty }f^{*}(t)\ g(t+\tau )\,dt,
$$
>
> where $f^{*}$ denotes the complex conjugate of  $f$, and $\tau$ is the displacement, also known as lag, although a positive value of $\tau$ actually means that $g(t+\tau)$ leads $f(t)$.

> *https://en.wikipedia.org/wiki/Cross-correlation*

The result of the correlation is a function of the delay $\tau$. This is useful for finding the offset in shifted signals.

```python
offset = 0.25
t = np.linspace(-1, 1, 600)
f = signal.gausspulse(t, fc=3)
g = signal.gausspulse( t + offset, fc=3)

result = signal.correlate(f, g, mode='same')
offset = t[np.argmax(result)]
```

```python
grid = plt.GridSpec(2, 2, wspace=0.1, hspace=0.4, width_ratios=[1, 1])
ax1 = plt.subplot(grid[0, 0])
ax2 = plt.subplot(grid[1, 0])
ax3 = plt.subplot(grid[:, 1], )

ax1.plot(t, f, color='gray')
ax1.set_title('$f$')
ax1.set_yticks([])
ax1.set_xlabel('t')

ax2.plot(t, g, color='gray')
ax2.set_title('$g$')
ax2.set_yticks([])
ax2.set_xlabel('t')

ax3.plot(t, result)
ax3.axvline(offset, color='xkcd:tangerine')
ax3.set_title('$(f\star g)(\\tau )$')
ax3.set_yticks([])
ax3.set_xlabel('τ');
```

### 5.4 Exercise III:  Signal Filtering

If we ever find an alien civilization we will have to communicate with them using binary encoded signals recorded by large radio telescopes.

Assuming the aliens use binary signals with 0s and 1s.
Given the clock speed and the sample length with which the signal was created. Can you decode the following signal?

The plot below shows the shape of an example signal without any noise containing the message [0, 1 0]

```python
sample_length = 64
plt.plot(np.repeat([0, 1, 0], sample_length))
plt.text(25, 0.5, '0')
plt.text(100, 0.5, '1')
plt.text(175, 0.5, '0')
```

```python
alien_signal = np.loadtxt('data/alien_signal.txt')
sample_length = 128

# let's sample the signal in the middle of each period
clock = np.arange(sample_length // 2, len(alien_signal), sample_length)
plt.plot(alien_signal)

for c in clock:
    plt.axvline(c, color='gray', lw=1.5)
```

```python
# %load solutions/solution_3.py
```

### 5.5 Fourier transform

Fourier transforms are one of the universal tools in computational physics, which appear over and over again in different contexts. SciPy provides functions for accessing the classic [FFTPACK](http://www.netlib.org/fftpack/) library, which is an efficient and well tested FFT library written in FORTRAN. The SciPy API has a few additional convenience functions, but overall the API is closely related to the original FORTRAN library.

To use the `fftpack` module in a python program, include it using:

```python
from scipy import fftpack
```

To demonstrate how to do a fast Fourier transform with SciPy, we will create some artificial data first.

```python
# Number of sample points
n_samples = 1000
length = 10
sample_size = length / n_samples

# sample spacing
x = np.linspace(0.0, length, n_samples)

freqs = [8, 3, 32]
amps = [1, 0.5, 0.25]

y = np.zeros(n_samples)
for a, f in zip(amps, freqs):
    y += a * np.sin(2 * np.pi * f * x)

y += rng.normal(0, 0.75, len(y))
plt.plot(x, y, lw=2)
```

```python
yf = fftpack.fft(y)
xf = fftpack.fftfreq(n_samples, sample_size)
plt.plot(xf, 2.0 / n_samples * np.abs(yf))
plt.xlim(0, 0.5 / sample_size)
```

As expected, we now see a peaks in the spectrum that match the true frequencies in the signal.

## 6. Statistics

The `scipy.stats` module contains a large number of statistical distributions, statistical functions and tests. For a complete documentation of its features, see http://docs.scipy.org/doc/scipy/reference/stats.html.

Below we instantiate an object modeling a random variable following the normal distribution.

### 6.1 Continuous Distributions

```python
from scipy import stats

mean = 0
sigma = 1
X = stats.norm(loc=mean, scale=sigma)
```

All distributions in this submodule extend either `scipy.stats.rv_continuous` or `scipy.stats.rv_discrete`.
Below is a list of functions supported by most continuous distribution objects

```
rvs(*args, **kwds) 	Random variates of given type.
pdf(x, *args, **kwds) 	Probability density function at x of the given RV.
logpdf(x, *args, **kwds) 	Log of the probability density function at x of the given RV.
cdf(x, *args, **kwds) 	Cumulative distribution function of the given RV.
logcdf(x, *args, **kwds) 	Log of the cumulative distribution function at x of the given RV.
sf(x, *args, **kwds) 	Survival function (1 - cdf) at x of the given RV.
logsf(x, *args, **kwds) 	Log of the survival function of the given RV.
ppf(q, *args, **kwds) 	Percent point function (inverse of cdf) at q of the given RV.
isf(q, *args, **kwds) 	Inverse survival function (inverse of sf) at q of the given RV.
moment(n, *args, **kwds) 	n-th order non-central moment of distribution.
stats(*args, **kwds) 	Some statistics of the given RV.
entropy(*args, **kwds) 	Differential entropy of the RV.
expect([func, args, loc, scale, lb, ub, …]) 	Calculate expected value of a function with respect to the distribution.
median(*args, **kwds) 	Median of the distribution.
mean(*args, **kwds) 	Mean of the distribution.
std(*args, **kwds) 	Standard deviation of the distribution.
var(*args, **kwds) 	Variance of the distribution.
interval(alpha, *args, **kwds) 	Confidence interval with equal areas around the median.
__call__(*args, **kwds) 	Freeze the distribution for the given arguments.
fit(data, *args, **kwds) 	Return MLEs for shape (if applicable), location, and scale parameters from data.
fit_loc_scale(data, *args) 	Estimate loc and scale parameters from data using 1st and 2nd moments.
nnlf(theta, x) 	Return negative loglikelihood function.

```

```python
N = 1200
measurements = X.rvs(size=N)
x = np.linspace(-3.5, 3.5, 200)

f, [ax1, ax2] = plt.subplots(1, 2, figsize=(12, 5))
ax1.hist(measurements, bins=40, density=True)
ax1.plot(x, X.pdf(x), color='red')

ax2.hist(measurements, bins=40, cumulative=True, density=True)
ax2.plot(x, X.cdf(x), color='red');
```

### 6.2 Discrete Distributions

Discrete functions work in a very similar manner.

```
rvs(*args, **kwargs) 	Random variates of given type.
pmf(k, *args, **kwds) 	Probability mass function at k of the given RV.
logpmf(k, *args, **kwds) 	Log of the probability mass function at k of the given RV.
cdf(k, *args, **kwds) 	Cumulative distribution function of the given RV.
logcdf(k, *args, **kwds) 	Log of the cumulative distribution function at k of the given RV.
sf(k, *args, **kwds) 	Survival function (1 - cdf) at k of the given RV.
logsf(k, *args, **kwds) 	Log of the survival function of the given RV.
ppf(q, *args, **kwds) 	Percent point function (inverse of cdf) at q of the given RV.
isf(q, *args, **kwds) 	Inverse survival function (inverse of sf) at q of the given RV.
moment(n, *args, **kwds) 	n-th order non-central moment of distribution.
stats(*args, **kwds) 	Some statistics of the given RV.
entropy(*args, **kwds) 	Differential entropy of the RV.
expect([func, args, loc, lb, ub, …]) 	Calculate expected value of a function with respect to the distribution for discrete distribution.
median(*args, **kwds) 	Median of the distribution.
mean(*args, **kwds) 	Mean of the distribution.
std(*args, **kwds) 	Standard deviation of the distribution.
var(*args, **kwds) 	Variance of the distribution.
interval(alpha, *args, **kwds) 	Confidence interval with equal areas around the median.
__call__(*args, **kwds) 	Freeze the distribution for the given arguments.
```

```python
# create a (discreet) random variable with poissionian distribution
Y = stats.poisson(3.5)
```

```python
n = np.arange(0, 12)

fig, axes = plt.subplots(3,1, sharex=True, figsize=(8, 8))

# plot the probability mass function (PMF)
axes[0].plot(n, Y.pmf(n), 'o')

# plot the cumulative distribution function (CDF)
axes[1].step(n, Y.cdf(n))

# plot histogram of 1000 random realizations of the stochastic variable X
axes[2].hist(Y.rvs(size=200), bins=np.arange(13) - 0.5, density=True, lw=2, edgecolor='w');
axes[2].set_xticks(n);
```

The objects support easy access to some of its statistical properties:

```python
print(X.mean(), X.std(), X.var()) # normal distribution
print(Y.mean(), Y.std(), Y.var()) # poission distribution
```

    0.0 1.0 1.0
    3.5 1.8708286933869707 3.5

## 7. Optimization

Optimization (finding minima or maxima of a function) is a large field in mathematics, and optimization of complicated functions or in many variables can be rather complicated. Here we will only look at a few very simple cases. For a more detailed introduction to optimization with SciPy see: http://scipy-lectures.github.com/advanced/mathematical_optimization/index.html

To use the optimization module in scipy first include the `optimize` module:

```python
from scipy import optimize
```

### 7.1 Minimization

Let's first look at how to find the minima of a simple function of a single variable:

```python
def f(x):
    return 4.5*x**3 + (x-4)**2 + 0.75*x**4 - 20

x = np.linspace(-6, 3, 100)
plt.plot(x, f(x));
```

We can use the `minimize_scalar` function to find the minima of a scalar function. It provides wrapper around many optimization algorithms you can choose from using the `method` keyword.

```python
result = optimize.minimize_scalar(f)

plt.plot(x, f(x));
plt.axvline(result.x, color='xkcd:orange')

# add some boundaries
result = optimize.minimize_scalar(f, bracket=[-6, -2])
plt.axvline(result.x, color='xkcd:dark red')
```

### 7.2 Minimization in More Dimensions.

Below you'll find the infamous *Rosenbrock* function in two dimensions. It's very popular for benchmarking global optimization problems. Its solutions are well known and easily parametrized.

```python
from mpl_toolkits.mplot3d import Axes3D

def rosenbrock_2d(x, y, a=1, b=1):
    return (a - x)**2 + b*(y - x**2)**2

fig = plt.figure()
ax = Axes3D(fig)

x, y = np.meshgrid(np.linspace(-2 ,2, 100), np.linspace(-3. ,5, 100))
ax.plot_surface(x, y, rosenbrock_2d(x, y), cmap='magma_r')
```

```python
result = optimize.minimize(lambda x: rosenbrock_2d(x[0], x[1]), x0=[5, 2])
fig = plt.figure()
ax = Axes3D(fig)
ax.view_init(20, 30)

x, y = np.meshgrid(
    np.linspace(-2 ,2, 100),
    np.linspace(-3. ,5, 100)
)

ax.plot_surface(x, y, rosenbrock_2d(x, y), cmap='magma_r', alpha=0.6)

x, y, z = result.x[0], result.x[1], 20
ax.quiver(x, y, z, 0, 0, -1, length=20, arrow_length_ratio=0.04)

result
```

### 7.3 Least Squares Minimization

Lets presume a linear weighted combination of variables:

$$
f(x)= \hat{y} =  \hat{\beta}_0 + \sum_{j=1}^p x_j \hat{\beta}_j
$$
where $f:\mathbb{R}^{p} \to \mathbb{R}$.

When we include a 1 as the first entry into our sample $x$ e.g. $x = (1, x_1, x_2, \ldots, x_p)$ we can rewrite
$f$ in matrix form

$$
f(x)= \hat{y} =  x^T \mathbf{\beta}
$$

where $\beta = (\beta_0, \beta_1, \beta_2, \ldots, \beta_p)$.

How do you find those weights? Like before we choose a loss function and try to opimize it.
In this case we choose a loss function called the residual sum of squares (RSS).
We calculate it over all samples $x_i$ in a matrix $\mathbf{X}$.

$$L(\beta) = RSS(\mathbf{\beta}) = \sum_{i=1}^N (y_i - x_i^T \beta)^2 $$

Here $x_i$ is a row in $\mathbf{X}$, hence the transpose.

We can now rewrite the loss function in matrix form:

$$
RSS(\beta) = (\mathbf{y} - \mathbf{X} \beta)^T (\mathbf{y} - \mathbf{X} \beta )
$$

Now we optimize the loss function just like we would any other function, by differentiating with respect to $\beta$ and setting the result equals to zero.

$$
 \mathbf{X}^T (\mathbf{y} - \mathbf{X} \beta ) \stackrel{!}{=} 0
$$

Solving for $\beta$ leads to

$$
\hat{\beta} = (\mathbf{X}^T \mathbf{X})^{-1} \mathbf{X}^T \mathbf{y}
$$

We just performed  __Linear Least Squares__ regression. Below we fit some noisy a polynomial data.

```python
def f(x, beta=[4.5, 1, 0.75]):
    return beta[0]*x**3 + beta[1]*(x-4)**2 + beta[2]*x**4

x = np.linspace(-6, 3, 100)
data = f(x) + rng.normal(0, 35, size=x.shape)

# build the answer matrix
A = np.vstack([x ** 3, (x - 4) ** 2, x ** 4]).T

# find the linear least squares solution for the parameters
result = optimize.lsq_linear(A, data)

plt.plot(x, data, '.')
plt.plot(x, f(x), color='gray', label='truth')
plt.plot(x, f(x, beta=result.x), color='crimson', label='fit')
plt.legend();
```

### 7.4 Curve Fitting

For more general cases where the parameters are not linear there is the more convenient function `curve_fit`

```python
from scipy import stats

def f(x, scale=5, loc=4, ):
    return (1/(scale))**2*x**3 + (1/(2*loc))*x**2

x = np.linspace(-3,  1.5, 100)
data = f(x) + rng.normal(0, 0.15, size=x.shape)

popt, pcov = optimize.curve_fit(f, x, data, p0=[5, 4, ])

plt.plot(x, data, '.')
plt.plot(x, f(x), color='gray', label='truth')
plt.plot(x, f(x, *popt), color='crimson', label='fit')

pcov
```

### 7.5 Exercise:  Curve Fitting

Use `numpy.loadtxt` to load the file called `munich_temperatures_average.txt` and fit a model to it using `optimize.curve_fit`.

```python
# %load solution_6.py
```

### 7.6 Root Finding

To find the solution for a function of the form $f(x) = 0$ we can use the `fsolve` or the `root` function. It requires one or more initial guesses:

```python
def f(x):
    return 4.5 * x **3 + (x - 4) ** 2 + 0.75 * x ** 4 - 20

x = np.linspace(-6, 3, 100)

plt.plot(x, f(x))
plt.axhline(color='black')

result = optimize.root(f, [-6, -3, 0, 2])

for r in result.x:
    plt.axvline(r, color='xkcd:tangerine')
```

## 8. Interpolation

Interpolation is simple and convenient in scipy: The `interp1d` function, when given arrays describing X and Y data, returns and object that behaves like a function that can be called for an arbitrary value of x (in the range covered by X), and it returns the corresponding interpolated y value:

```python
from scipy import interpolate

def f(x):
    return np.sin(x)
```

```python
n = [0, 1, 2, 3, 3.5, 4, 6, 6.5, 7, 9]
x = np.linspace(0, 9, 100)

y_measured = f(n)

linear_interpolation = interpolate.interp1d(n, y_measured)

cubic_interpolation = interpolate.interp1d(n, y_measured, kind='cubic')
# this returns a new function we can call for values of x
cubic_interpolation
```

    <scipy.interpolate.interpolate.interp1d at 0x1195d1400>

```python
plt.plot(n, y_measured, 's', label='noisy data', color='black')
plt.plot(x, f(x), c='gray', lw=2, label='true function')
plt.plot(x, linear_interpolation(x), label='linear interpolation')
plt.plot(x, cubic_interpolation(x), label='cubic interpolation')
plt.legend();
```

## 9. SciPy IO Module

## 9.1 Wave files

SciPy provides some useful methods for reading file from MatLab, NetCDF or WAV data. Below is an example which uses the `scipy.io.wavfile` module to write a file containing the sound of hydrogen atoms.

```python
from scipy.io import wavfile
from scipy import signal

rate = 44100 #44.1 khz
duration = 10 # in sec

def rydberg(n, m):
    return (1 / (n ** 2) - 1 / (m ** 2))

def lyman(m):
    """Lyman series"""
    return rydberg(1, m)

def balmer(m):
    """Balmer series"""
    return rydberg(2, m)

def sound(frequency, time):
    return 2 ** 12 * np.sin(2 * np.pi * 440 * frequency * time)

t = np.linspace(0, duration, rate * duration)
hydrogen_sound = np.sum(
    [sound(lyman(i), t) + sound(balmer(i), t) for i in range(2, 12)], axis=0) / 6

plt.plot(t[:10000], hydrogen_sound[:10000])

wavfile.write('data/hydrogen.wav', rate, hydrogen_sound.astype(np.int16))
```

```python
%%HTML
<audio controls="controls" style="width:600px" >
  <source src="hydrogen.wav" type="audio/wav" />
</audio>
```

<audio controls="controls" style="width:600px" >
  <source src="hydrogen.wav" type="audio/wav" />
</audio>

### 9.2 Exercise:  Signal Processing and IO.

Load the file `synth_sound.wav` and plot a the frequency spectrum vs time.

```python
# %load solution_4.py
```


# $I_{Na,t}$ модель (заготовка для заполнения)

```python
%pylab inline

```

    Populating the interactive namespace from numpy and matplotlib

```python
from __future__ import division
```

** # If you use anaconda python distribution, run the following command in the terminal ***

```conda install bokeh```

```python
import bokeh
import bokeh.plotting as bp
from bokeh.models import ColumnDataSource, Range1d

from ipywidgets import interact
```

```python
bp.output_notebook()
```

    BokehJS successfully loaded.

**Run the following command to get the `PyDSTool` installed:**

```pip install PyDSTool```

```python

import PyDSTool as dst
from PyDSTool.Toolbox import phaseplane as pp
```

## Модель $I_{Na,t}$

\begin{equation}
C\dot{V} = I - (\bar{g}_{Na}m_\infty(V)^3h(V-E_{Na}) + gl(V-El))
\end{equation}

\begin{equation}
\tau_h \dot{h} = (h_\infty(V) - h)
\end{equation}

\begin{equation}
x_\infty =  \frac{1}{1 + \exp(\frac{V_{1/2}-V}{k})}
\end{equation}

```python
nat_pset = dict(
    I = 0.0,
    El = -70.0,
    Ena = 60.0,
    gl = 1.0,
    gna = 15.0,
    minf_vhalf = -40.,
    minfk = 15.,
    hinf_vhalf = -62.,
    hinfk = -7,
    htau = 5.0
)
```

```python
pset_str = ';\n'.join(['{k}=dst.Par({v},"{k}")'.format(k=k,v=v) for k,v in nat_pset.items()])
print pset_str
exec(pset_str)
```

    Ena=dst.Par(60.0,"Ena");
    hinf_vhalf=dst.Par(-62.0,"hinf_vhalf");
    El=dst.Par(-70.0,"El");
    I=dst.Par(0.0,"I");
    minf_vhalf=dst.Par(-40.0,"minf_vhalf");
    minfk=dst.Par(15.0,"minfk");
    gna=dst.Par(15.0,"gna");
    gl=dst.Par(1.0,"gl");
    hinfk=dst.Par(-7,"hinfk");
    htau=dst.Par(5.0,"htau")

```python
V = dst.Var('V')
h = dst.Var('h')
```

```python
boltzman = dst.Fun(1./(1. + dst.Exp(('Vhalf'- V)/'bk')), ['Vhalf','bk'], 'boltzman')
minf = boltzman(minf_vhalf,minfk)
hinf = boltzman(hinf_vhalf,hinfk)
```

```python
vtest = linspace(-89, 45, 250)
```

```python
plot(vtest, eval(str(minf.eval(V='vtest', Exp='np.exp', **nat_pset))))
plot(vtest, eval(str(hinf.eval(V='vtest', Exp='np.exp',**nat_pset))))
xlabel(u'V, mV'); legend(('$m_\infty$', '$h_\infty$'),loc='best')
```

### Уравнения системы

```python
# Инактивация Na-тока
dh = (hinf-h)/htau
```

```python
# Na-ток и уравнение для dV/dt

iNa = gna*(V-Ena)*minf**3*h
ileak = gl*(V-El)
dV = I - (ileak + iNa )
print dV
```

    I-(gl*(V-El)+gna*(V-Ena)*Pow(1.0/(1.0+Exp((minf_vhalf-V)/minfk)),3)*h)

```python
# Стационарный ток при разных потенциалах
Iinf =  gna*(V-Ena)*minf**3*hinf + ileak
# vnull = ... # Fill in
```

```python
Nat_model = dst.args(
    name = 'nat',
    pars = nat_pset,
    varspecs = {'V':dV, 'h':dh},
    tdomain=[0,250],
    xdomain=dict(V=[-150, 60], h=[0,1]),
    ics = {'V':-70,'h':0})
```

```python
# Система уравнений (модель)
odeset = dst.Generator.Vode_ODEsystem(Nat_model)
```

```python
# Траектория (динамика системы)
traj = odeset.compute('test')
pts = traj.sample(dt=0.1)
```

```python
# График
plot(pts['t'], pts['V'])
```

```python

```


```python
from luwiji.metrics import illustration
```

```python
illustration.type_of_mistake
```

### Accuracy

\begin{equation}
Accuracy = \frac{TP+TN}{(TP+FP + TN+FN)}
\end{equation}

Berapa yang benar dibandingkan semua data

### Precision

\begin{equation}
Precision = \frac{TP}{(TP+FP)}
\end{equation}

- Target: FP sekecil mungkin
- Contoh:
    - klasifikasi email spam vs non spam
    - kebanyakan email kita adalah non spam, beberapa email yang sebenarnya adalah spam
    - Akan fatal kalau email biasa tapi kita anggap spam (False Positive)

### Recall / Sensitivity

\begin{equation}
Recall = \frac{TP}{(TP+FN)}
\end{equation}

- Target: FN sekecil mungkin
- Contoh:
    - klasifikasi kanker vs non kanker
    - kebanyakan label kita tidak kanker
    - Akan fatal kalau kanker tapi kita bilang tidak kanker (False Negative)

### F1-Score

\begin{equation}
\frac{1}{F_1} = \frac{1}{2} \left(\frac{1}{Precision} + \frac{1}{Recall}\right)
\end{equation}

- rata-rata harmonik dari precision dan recall
- Kapan dipakai? Kalau mau balance antara precision dan recall

### PR_AUC dan ROC_AUC

Nanti kita bahas setelah materi logistic regression

09 - Classification Evaluation/Part 1 - Classification Metrics.ipynb


Spectroscope-Telescope matching
===============================
Tony Rodda 31 March 2019

The original Lowspec was produced with a 25.4mm diameter, 125mm focal length doublet collimating
lens giving an F5 focal ratio.

The Lowspec2 (LS2) has the same lens but the lens holder is stopped
down to F10 by baffling the lens holder and reducing the effective diameter to 12mm.

I have altered the .stl print file such that the collimating lens holder can be varied and
my own LS2 has a ratio of F7 which more accurately suits my SCT ‘scope.

Slit-Camera Matching
-------------------------------
The LS2 uses an Ovio disc with twelve various slits ranging from 10µm to 500µm.
I’ve used a collimating lens of 120mm focal length and a camera lens of 80mm focal length.
(nb. The LS2 specification is for 125mm and 100mm fl lenses respectively).

Using, for example, the 30µm slit, the slit image to “Detector” image magnification, for my machine, is:

$
\begin{align}
fl_{col} &= 120\\
fl_{cam} &= 80\\
W_{slit\;image} &= 30 \times \frac{fl_{cam}}{fl_{col}} = 20 \times \frac{80}{120} = 20 \;\;[\mu{m}]
\end{align}
$

the slit image to "Detector" image magnification for the LS2 specification, is:

$\begin{align}
W_{slit} &= 30 \times \frac{100}{125} = 24 \;\;[\mu{m}]
\end{align}
$

Using the Nyquist approximation for 2.5 to 3 sample pixels per FWHM, I need CCD pixels of the order of 6.5 $\mu{m}$.
This is 1x1 binning for my SXVH9 for which with 6.45 $\mu{m}$ is well sampled; and 2x2 binning for my Atik460, which possesses 4.54 µm pixels, for which 2.2 sampling is just adequate.

Dispersion and Resolution – LS2 with 300l/mm grating and 30µm slit.
--------------------
Using the lamp calibration frame before for the LS2 with 300 l/mm grating the calibrated image gives a dispersion (on the SXVH9 chip) of 0.3 $nm$ per pixel.

Ten x 8 second images were taken with the LS2 mounted on a 235mm 6.3 fl SCT with the neon lamp at the corrector plate with an SXVH9 recording 34,000+ counts for the brightest lines.

This suggests a range of 420 $nm$ over 1392 pixels giving a dispersion of:
420/1392 ≈ 0.3 $nm/pixel$ (confirmed by BASS[^1] software).
Or, a plate scale of ≈ 46.5$nm/mm$.

The following FWHM was produced by BASS from the above image/chart from 585nm to approximately H$\alpha$,

Profile        : 01 LS2/300 Neon Stacked
Date           : 23/03/2019 20:12:46 (23.8422 /03/2019)
FWHM           : 0.635315nm (R = 921 @585.255nm) 2.51862 px

And

FWHM           : 0.860413nm (R = 767 @659.825nm) 3.40332 px
(note, an ideal fitting the Nyquist requirement).

That is, FWHM of ≈ 3 pixels giving a Resolution (for the calibration image) of ≈ 800.
Check based on 3px x 0.3 dispersion (from above) = 0.9   so, R = $\lambda$/$\Delta\lambda$ = 659/0.9 = 732 worse case.

[1]: British Astronomical Society Spectrography  https://britastro.org/node/10520

INDICATIVE Dispersion and Resolution – TR2 with 1800l/mm grating and 30µm slit.
------------------------
Using the lamp calibration frame for the TR2 with a 1800l/mm grating the calibrated image gives a dispersion (on the SXVH9 chip) of 0.042 nm per pixel.

One image was taken with a bench mounted TR2 mounted on a 50mm F7 refractor (containing a single doublet objective) with the neon lamp at “infinity” with an SXVH9 recording 6,000+ counts for the brightest lines.  (Background counts of ~ 250).

There was some asymmetry in the emission lines due to the positioning of the neon bulb.

This suggests a range of 678 – 619 = 59 nm over 1392 pixels giving a dispersion of:

≈ 0.042 nm/pixel (confirmed by BASS software).

Or, a plate scale of ≈ 6.51 nm/mm.

The following FWHM was produced by BASS from for approximately H$\alpha$ wavelength,

Profile        : 01 TR21800Neon2.fit
Date           : 16/03/2019 16:33:19 (16.6898 /03/2019)
FWHM           : 0.104736nm (R = 6237 @653.207nm) 2.47864px

That is, FWHM of ≈ 2,5 pixels giving a Resolution (for the calibration image) of ≈ 6,200.
Check, based on 2.5px x 0.042 dispersion (from above) = 0.105   so, R = $\lambda/\Delta\lambda$ = 659/0.105 = 6,250.

The use of the smaller OVIO 20µm slit, the 1800 l/mm grating and the Atik460 camera binned 1x1 will give excellent sampling and should produce a greater domain and resolution in the 9,000+ range.