```python
import numpy as np
import math
import roboticstoolbox as rtb
from spatialmath.base import *
from spatialmath import SE3
import spatialmath.base.symbolic as sym

%matplotlib notebook
```

We use the Spatial Math Toolbox wrapper of SymPy which is `spatialmath.base.symbolic` which is imported above.

## Creating a symbolic variable
We will first create a symbolic variable

```python
theta = sym.symbol('theta')
```

```python
theta
```

which displays as a Greek letter, it has the type

```python
type(theta)
```

The function specifies that the symbolic variables are real valued by default, which will simplify subsequent simplification steps

```python
theta.is_real
```

We can test if a variable is symbolic

```python
sym.issymbol(theta)
```

```python
sym.issymbol(3.7)
```

## Symbolics with the Spatial Math Toolbox
Many Spatial Toolbox functions handle symbolic variables

```python
R = rot2(theta)
R
```

and return a NumPy array with symbolic values.

The 3D case is similar

```python
R = rotx(theta)
R
```

```python
T = trotx(theta)
T
```

The elements of this NumPy array are all objects, as indicated by the `dtype`.  However when we index the elements, they will be converted back to numeric types if possible

```python
type(T[0,0])
```

```python
type(T[1,1])
```

We can perform arithmetic on such matrices, for example

```python
T @ T
```

## Pose classes
The symbolic capability extends to the pose classes

```python
T = SE3.Rx(theta)
T
```

```python
T2 = T * T
T2
```

but the colored layout is problematic.

## Robot forward kinematics

We will create a symbolic version of the robot model

```python
puma = rtb.models.DH.Puma560(symbolic=True)
```

```python
print(puma)
```

We see that the $\alpha_j$ values are now given in radians and are colored red.  This means the value is a symbolic expression, for the first link it is $\pi/2$ which is a precise value, compared to the non-symbolic case which is a floating point number 1.5707963267948966 that only  approximates $\pi/2$.

The next thing we need to do is create a vector of joint coordinates which are all symbols

```python
q = sym.symbol('q_:6')
q
```

We use the underscore, because the value of the symbol is pretty printed by SymPy as a subscript (just as it is with LaTeX math notation)

```python
q[0]
```

We are now set to compute the forward kinematics which will be a matrix whose elements will be complicated expressions of symbolic joint variables and kinematic parameters

```python
T = puma.fkine(q)
T
```

The color coding helps us identify the rotational and translational parts, but the format is not very readable.  We can display the underlying NumPy array

```python
T.A
```

which is not a lot better.

As this stage it is far better to convert the result to a SymPy matrix

```python
from sympy import Matrix
Matrix(T.A)
```

which is decently pretty printed by SymPy.

Often after a round of symbolic calculations there are simplifications that can be achieved. We can symbolically simplify each element of the `SE3` object by

```python
Ts = T.simplify()

M = Matrix(Ts.A)
M
```

which is more compact (it takes a few seconds to compute). We can see that a trigometric _sum of angles_  substition has been performed, there are instances of sine and cosine of $q_1 + q_2$, the shoulder and elbow joints. This is to be expected since these joints are adjacent and have with parallel axes.

We can _slice_ the end-effector translationfrom the SymPy matrix

```python
M[:3,3]
```

The floating point constants here have been inherited from the kinematic model we imported at [this step](#Robot-forward-kinematics).  It would be possible to replace the non-zero kinematic constants $a_i$ and $d_i$ with symbols created using `sym.symbol` as shown in [this section](#Creating-a-symbolic-variable).

## Code generation
We can now use some of SymPy's superpowers to turn our forward kinematic expression into code

```python
from sympy import ccode, pycode, octave_code
print(ccode(M, assign_to="T"))
```

which is pure C code that does not require any linear algebra package to compute. We simply need to define the values of `q_0` to `q_5` in order to determine the end-effector pose. The result is computed using symbolically simplified expressions.  The code is not optimized, but we could expect the compiler to perform some additional simplification.

The equivalent MATLAB code is

```python
octave_code(M)
```

We can also output Python code

```python
print(pycode(M))
```

Which constructs an instance of a SymPy `ImmutableDenseMatrix` which we can turn into a function

```python
from sympy import lambdify
T_func = lambdify(q, M, modules='numpy')
```

If we pass in the zero joint angles we get the familiar forward kinematic solution result

```python
T_func(0, 0, 0, 0, 0, 0)
```

SymPy also supports printing code in C++ (`cxxcode`), Fortran (`fcode`), JavaScript (`jscode`), Julia (`julia_code`), Rust (`rust_code`), Mathematica (`mathematica_code`) and Octave/MATLAB (`octave_code`).

The [SymPy autowrap](https://docs.sympy.org/latest/modules/codegen.html#autowrap) capability automatically generates code, writes it to disk, compiles it, and imports it into the current session.  It creates a wrapper using Cython and creates a numerical function.

## Robot dynamics

We can also compute the dynamics symbolically.  To do this we must use the Python version of the inverse dynamics rather than the default efficient C-code implementation.

We need to setup the problem. Firstly we make the gravitational constant a symbol

```python
g = sym.symbol('g')
puma.gravity = [0, 0, g]
```

Next we create symbolic vectors for joint velocity and acceleration, just as we did earlier for joint coordinates

```python
qd = sym.symbol('qd_:6')
qd
```

```python
qdd = sym.symbol('qdd_:6')
qdd
```

Now we compute the inverse dynamics as a function of symbolic joint coordinates, velocities, accelerations and gravity as well as a lot of numerical kinematic and dynamic parameters.

Note that this next step might take 10 minutes or more to execute (but the result will be impressive and worth the wait!)

```python
%time tau = puma.rne_python(q, qd, qdd)
```

```python
from sympy import trigsimp, simplify
```

```python
%time z = simplify(tau[5])
```

```python
%time z = simplify(tau[4])
```

```python
%time z = simplify(tau[3])
```

```python
%time z = simplify(tau[2])
```

```python
%time z = simplify(tau[1])
```

```python
%time z = simplify(tau[0])
```

```python

```

```python

```

The result `tau` is not a NumPy array as it would be for the numeric case

```python
type(tau)
```

but it is the symbolic (SymPy) equivalent and it has the expected shape

```python
tau.shape
```

The torque on the first joint $\tau_0$ is

```python
tau[0]
```

which is a complicated expression.  To make it easier to work with, we will expand it

```python
tau_0 = tau[0].expand()
tau_0
```

to form a multinomial, a sum of products of trigonometric functions. The number of product terms is

```python
len(tau_0.args)
```

With the expression in this form we can find all the terms that contain $\ddot{q}_0$

```python
m = tau[0].coeff(qdd[0]).args
m
```

so we can write our torque expression as $\tau_0 = m \ddot{q}_0 + \cdots$ which means that $m$ must be element $\textbf{M}_{00}$ of the manipulator inertia matrix.  We see it has contributions due to the centre of mass of the arm which depends on the configuration of joints 1 to 5, and motor inertia.

Similarly

```python
m = tau[0].coeff(qdd[1]).args
m
```

must be the element $\textbf{M}_{01}$, an off-diagonal element of the inertia matrix.  In this way we can generate expressions for each element of the manipulator inertia matrix.  Remember that this matrix is symmetric so we only need to compute half of the off-diagnonal terms.

In a similar way we can find elements of the Coriolis and centripetal matrix.  Terms containing $\dot{q}_0$ are

```python
c = tau[0].coeff(qd[0]).args
c
```

so we can write our torque expression as $\tau_0 = c  \dot{q}_0 + \cdots$
A subset of these terms still contain $\dot{q}_0$

```python
C = tau[0].coeff(qd[0]).coeff(qd[0]).args
```

ie. they originally contained $\dot{q}_0^2$.  The squared velocity terms belong on the diagonal of the Coriolis matrix and represent centripetal torques.  This particular expression is the element $\textbf{C}_{00}$.

The off-diagonal terms are coefficients of $\dot{q}_i \dot{q}_j$ and we can find $\dot{q}_0 \dot{q}_1$ by

c = tau[0].coeff(qd[0]).coeff(qd[1]).args

We add $C/2$ to both $C_{01}$ and $C_{10}$ and repeat for all the off-diagonal terms.  $C_{ij} = \Sum_{i=0}^n \Sum_{j=0}^n c

The elements of this matrix map velocities to force and therefore have the same dimensions as viscous friction.  In fact the motor friction parameter $B_m$ will be mixed up in these expressions, so we should first set $B_m = 0$ for all joints before proceeding.

SymPy provides tools that allow us to pull apart the expression's parse tree.  The top node is given by `.func` which in this case is

```python
tau_0.func
```

indicating an addition node, and its arguments are

```python
tau_0.args
```

expressed as a tuple.  A term, say the first one, can be further decomposed

```python
tau_0.args[0].func
```

```python
tau_0.args[0].args
```

which is we see here is a product of three terms.  There is a standard ordering of the arguments and it looks like constants are always first.

We can see that many of the terms have very small coefficients, they literally don't add much to the result, so we can cull them out, or rather, we can select those with significant coefficients

```python
signif_terms = [t for t in tau_0.args if abs(t.args[0]) > 1e-6]
signif_terms
```

and there are now only

```python
len(signif_terms)
```

product terms remaining, just 25% of the number we started with.

We can reconstruct the expression with just the significant terms

```python
tau_0s = tau_0.func(*signif_terms)
tau_0s
```

and the top-level node function, `tau_0.func` (which is `Add`) acts as a constructor to which we pass the significant terms.

We can generate executable code in C to compute the torque on joint 0 which could be used in a feed-forward controller for example.

```python
print(ccode(tau_0s, assign_to="tau0"))
```

We can do this for any of the expressions we generated above, ie. for elements of the inertia matrix or the velocity matrix.

The gravity vector is obtained when $\dot{q}_j =0$ and $\ddot{q}_j =0$

To set these symbolic values to zero we define a substituion dictionary where a key is the symbols to be substituted and the value is its new (substituted) value.

```python
subsdict = {}
sqdict = {}
for j in range(puma.n):
    subsdict[qd[j]] = 0
    subsdict[qdd[j]] = 0}
    sqdict[]
```

```python
tau_g = tau.subs(subsdict)
tau_g
```

```python
subsdict = {}
sqdict = {}
for j in range(puma.n):
    S = sym.symbol(f'S{j:d}')
    C = sym.symbol(f'C{j:d}')
    subsdict[sym.sin(q[j])] = S
    subsdict[sym.cos(q[j])] = C
    sqdict[S**2] = 1 - C**2
subsdict
```

```python
sqdict
```

```python
t1 = tau[1].subs(subsdict)
```

```python
t1.expand().subs(sqdict)
```

```python

```


```python
import numpy as np
import sympy as sym

x_1, x_2, x_3, x_4 = sym.symbols('x_1, x_2, x_3, x_4')
y_1, y_2, y_3, y_4 = sym.symbols('y_1, y_2, y_3, y_4')
XY = sym.Matrix([sym.symbols('x_1, x_2, x_3, x_4'), sym.symbols('y_1, y_2, y_3, y_4')]).transpose()
xi, eta = sym.symbols('xi, eta')
basis = sym.Matrix([xi, eta])
N1 = (1-xi)*(1-eta)/4
N2 = (1+xi)*(1-eta)/4
N3 = (1+xi)*(1+eta)/4
N4 = (1-xi)*(1+eta)/4
NN = sym.Matrix([N1, N2, N3, N4])

coordinate = (NN.transpose()*XY).transpose()

NN_diff = NN.jacobian(basis)
jacob = coordinate.jacobian(basis)

jacob_det = sym.det(jacob)
jacob_inv = sym.inv_quick(jacob)

NN_diff_global = (NN_diff*jacob_inv)
temp = NN_diff_global*NN_diff_global.transpose()/jacob_det

sq3 = np.sqrt(1/3)
```

```python
NN
```

$\displaystyle \left[\begin{matrix}\frac{\left(1 - \eta\right) \left(1 - \xi\right)}{4}\\\frac{\left(1 - \eta\right) \left(\xi + 1\right)}{4}\\\frac{\left(\eta + 1\right) \left(\xi + 1\right)}{4}\\\frac{\left(1 - \xi\right) \left(\eta + 1\right)}{4}\end{matrix}\right]$

```python
coordinate
```

$\displaystyle \left[\begin{matrix}\frac{x_{1} \left(1 - \eta\right) \left(1 - \xi\right)}{4} + \frac{x_{2} \left(1 - \eta\right) \left(\xi + 1\right)}{4} + \frac{x_{3} \left(\eta + 1\right) \left(\xi + 1\right)}{4} + \frac{x_{4} \left(1 - \xi\right) \left(\eta + 1\right)}{4}\\\frac{y_{1} \left(1 - \eta\right) \left(1 - \xi\right)}{4} + \frac{y_{2} \left(1 - \eta\right) \left(\xi + 1\right)}{4} + \frac{y_{3} \left(\eta + 1\right) \left(\xi + 1\right)}{4} + \frac{y_{4} \left(1 - \xi\right) \left(\eta + 1\right)}{4}\end{matrix}\right]$

```python
jacob
```

$\displaystyle \left[\begin{matrix}- \frac{x_{1} \left(1 - \eta\right)}{4} + \frac{x_{2} \left(1 - \eta\right)}{4} + \frac{x_{3} \left(\eta + 1\right)}{4} - \frac{x_{4} \left(\eta + 1\right)}{4} & - \frac{x_{1} \left(1 - \xi\right)}{4} - \frac{x_{2} \left(\xi + 1\right)}{4} + \frac{x_{3} \left(\xi + 1\right)}{4} + \frac{x_{4} \left(1 - \xi\right)}{4}\\- \frac{y_{1} \left(1 - \eta\right)}{4} + \frac{y_{2} \left(1 - \eta\right)}{4} + \frac{y_{3} \left(\eta + 1\right)}{4} - \frac{y_{4} \left(\eta + 1\right)}{4} & - \frac{y_{1} \left(1 - \xi\right)}{4} - \frac{y_{2} \left(\xi + 1\right)}{4} + \frac{y_{3} \left(\xi + 1\right)}{4} + \frac{y_{4} \left(1 - \xi\right)}{4}\end{matrix}\right]$

```python
jacob.subs(([x_1, -0.7], [x_2, 0.2], [x_3, 0.8], [x_4, 0], [y_1, 0.3], [y_2, -0.8], [y_3, 0], [y_4, 0.8]))
```

$\displaystyle \left[\begin{matrix}0.425 - 0.025 \eta & 0.325 - 0.025 \xi\\0.075 \eta - 0.475 & 0.075 \xi + 0.325\end{matrix}\right]$

```python
jacob.subs(([xi, sq3], [eta, -sq3], [x_1, -0.8], [x_2, 0], [x_3, 0.8], [x_4, 0], [y_1, 0], [y_2, -0.8], [y_3, 0], [y_4, 0.8]))
```

$\displaystyle \left[\begin{matrix}0.5 & 0.5\\-0.5 & 0.5\end{matrix}\right]$

```python
jaco_inv = sym.inv_quick(jaco)
jaco_inv
```

$\displaystyle \left[\begin{matrix}1.25 & -1.25\\1.25 & 1.25\end{matrix}\right]$

```python
NN_diff.subs(([xi, sq3], [eta, -sq3]))
```

$\displaystyle \left[\begin{matrix}-0.394337567297406 & -0.105662432702594\\0.394337567297406 & -0.394337567297406\\0.105662432702594 & 0.394337567297406\\-0.105662432702594 & 0.105662432702594\end{matrix}\right]$

```python
NN_diff
```

$\displaystyle \left[\begin{matrix}\frac{\eta}{4} - \frac{1}{4} & \frac{\xi}{4} - \frac{1}{4}\\\frac{1}{4} - \frac{\eta}{4} & - \frac{\xi}{4} - \frac{1}{4}\\\frac{\eta}{4} + \frac{1}{4} & \frac{\xi}{4} + \frac{1}{4}\\- \frac{\eta}{4} - \frac{1}{4} & \frac{1}{4} - \frac{\xi}{4}\end{matrix}\right]$

```python
jacob_det
```

$\displaystyle - \frac{\eta x_{1} y_{2}}{8} + \frac{\eta x_{1} y_{3}}{8} + \frac{\eta x_{2} y_{1}}{8} - \frac{\eta x_{2} y_{4}}{8} - \frac{\eta x_{3} y_{1}}{8} + \frac{\eta x_{3} y_{4}}{8} + \frac{\eta x_{4} y_{2}}{8} - \frac{\eta x_{4} y_{3}}{8} - \frac{x_{1} \xi y_{3}}{8} + \frac{x_{1} \xi y_{4}}{8} + \frac{x_{1} y_{2}}{8} - \frac{x_{1} y_{4}}{8} + \frac{x_{2} \xi y_{3}}{8} - \frac{x_{2} \xi y_{4}}{8} - \frac{x_{2} y_{1}}{8} + \frac{x_{2} y_{3}}{8} + \frac{x_{3} \xi y_{1}}{8} - \frac{x_{3} \xi y_{2}}{8} - \frac{x_{3} y_{2}}{8} + \frac{x_{3} y_{4}}{8} - \frac{x_{4} \xi y_{1}}{8} + \frac{x_{4} \xi y_{2}}{8} + \frac{x_{4} y_{1}}{8} - \frac{x_{4} y_{3}}{8}$

```python
jacob_inv
```

$\displaystyle \left[\begin{matrix}\frac{- \frac{y_{1} \left(1 - \xi\right)}{4} - \frac{y_{2} \left(\xi + 1\right)}{4} + \frac{y_{3} \left(\xi + 1\right)}{4} + \frac{y_{4} \left(1 - \xi\right)}{4}}{\left(- \frac{x_{1} \left(1 - \eta\right)}{4} + \frac{x_{2} \left(1 - \eta\right)}{4} + \frac{x_{3} \left(\eta + 1\right)}{4} - \frac{x_{4} \left(\eta + 1\right)}{4}\right) \left(- \frac{y_{1} \left(1 - \xi\right)}{4} - \frac{y_{2} \left(\xi + 1\right)}{4} + \frac{y_{3} \left(\xi + 1\right)}{4} + \frac{y_{4} \left(1 - \xi\right)}{4}\right) - \left(- \frac{x_{1} \left(1 - \xi\right)}{4} - \frac{x_{2} \left(\xi + 1\right)}{4} + \frac{x_{3} \left(\xi + 1\right)}{4} + \frac{x_{4} \left(1 - \xi\right)}{4}\right) \left(- \frac{y_{1} \left(1 - \eta\right)}{4} + \frac{y_{2} \left(1 - \eta\right)}{4} + \frac{y_{3} \left(\eta + 1\right)}{4} - \frac{y_{4} \left(\eta + 1\right)}{4}\right)} & \frac{\frac{x_{1} \left(1 - \xi\right)}{4} + \frac{x_{2} \left(\xi + 1\right)}{4} - \frac{x_{3} \left(\xi + 1\right)}{4} - \frac{x_{4} \left(1 - \xi\right)}{4}}{\left(- \frac{x_{1} \left(1 - \eta\right)}{4} + \frac{x_{2} \left(1 - \eta\right)}{4} + \frac{x_{3} \left(\eta + 1\right)}{4} - \frac{x_{4} \left(\eta + 1\right)}{4}\right) \left(- \frac{y_{1} \left(1 - \xi\right)}{4} - \frac{y_{2} \left(\xi + 1\right)}{4} + \frac{y_{3} \left(\xi + 1\right)}{4} + \frac{y_{4} \left(1 - \xi\right)}{4}\right) - \left(- \frac{x_{1} \left(1 - \xi\right)}{4} - \frac{x_{2} \left(\xi + 1\right)}{4} + \frac{x_{3} \left(\xi + 1\right)}{4} + \frac{x_{4} \left(1 - \xi\right)}{4}\right) \left(- \frac{y_{1} \left(1 - \eta\right)}{4} + \frac{y_{2} \left(1 - \eta\right)}{4} + \frac{y_{3} \left(\eta + 1\right)}{4} - \frac{y_{4} \left(\eta + 1\right)}{4}\right)}\\\frac{\frac{y_{1} \left(1 - \eta\right)}{4} - \frac{y_{2} \left(1 - \eta\right)}{4} - \frac{y_{3} \left(\eta + 1\right)}{4} + \frac{y_{4} \left(\eta + 1\right)}{4}}{\left(- \frac{x_{1} \left(1 - \eta\right)}{4} + \frac{x_{2} \left(1 - \eta\right)}{4} + \frac{x_{3} \left(\eta + 1\right)}{4} - \frac{x_{4} \left(\eta + 1\right)}{4}\right) \left(- \frac{y_{1} \left(1 - \xi\right)}{4} - \frac{y_{2} \left(\xi + 1\right)}{4} + \frac{y_{3} \left(\xi + 1\right)}{4} + \frac{y_{4} \left(1 - \xi\right)}{4}\right) - \left(- \frac{x_{1} \left(1 - \xi\right)}{4} - \frac{x_{2} \left(\xi + 1\right)}{4} + \frac{x_{3} \left(\xi + 1\right)}{4} + \frac{x_{4} \left(1 - \xi\right)}{4}\right) \left(- \frac{y_{1} \left(1 - \eta\right)}{4} + \frac{y_{2} \left(1 - \eta\right)}{4} + \frac{y_{3} \left(\eta + 1\right)}{4} - \frac{y_{4} \left(\eta + 1\right)}{4}\right)} & \frac{- \frac{x_{1} \left(1 - \eta\right)}{4} + \frac{x_{2} \left(1 - \eta\right)}{4} + \frac{x_{3} \left(\eta + 1\right)}{4} - \frac{x_{4} \left(\eta + 1\right)}{4}}{\left(- \frac{x_{1} \left(1 - \eta\right)}{4} + \frac{x_{2} \left(1 - \eta\right)}{4} + \frac{x_{3} \left(\eta + 1\right)}{4} - \frac{x_{4} \left(\eta + 1\right)}{4}\right) \left(- \frac{y_{1} \left(1 - \xi\right)}{4} - \frac{y_{2} \left(\xi + 1\right)}{4} + \frac{y_{3} \left(\xi + 1\right)}{4} + \frac{y_{4} \left(1 - \xi\right)}{4}\right) - \left(- \frac{x_{1} \left(1 - \xi\right)}{4} - \frac{x_{2} \left(\xi + 1\right)}{4} + \frac{x_{3} \left(\xi + 1\right)}{4} + \frac{x_{4} \left(1 - \xi\right)}{4}\right) \left(- \frac{y_{1} \left(1 - \eta\right)}{4} + \frac{y_{2} \left(1 - \eta\right)}{4} + \frac{y_{3} \left(\eta + 1\right)}{4} - \frac{y_{4} \left(\eta + 1\right)}{4}\right)}\end{matrix}\right]$

```python
D
```

$\displaystyle \left[\begin{matrix}1 & 1\\1 & 1\end{matrix}\right]$

```python
a = NN_diff_global.subs(([xi, -sq3], [eta, -sq3], [x_1, -0.8], [x_2, 0], [x_3, 0.8], [x_4, 0], [y_1, 0], [y_2, -0.8], [y_3, 0], [y_4, 0.8]))
```

```python
b = NN_diff_global.subs(([xi, sq3], [eta, -sq3], [x_1, -0.8], [x_2, 0], [x_3, 0.8], [x_4, 0], [y_1, 0], [y_2, -0.8], [y_3, 0], [y_4, 0.8]))
```

```python
c = NN_diff_global.subs(([xi, sq3], [eta, +sq3], [x_1, -0.8], [x_2, 0], [x_3, 0.8], [x_4, 0], [y_1, 0], [y_2, -0.8], [y_3, 0], [y_4, 0.8]))
```

```python
d = NN_diff_global.subs(([xi, -sq3], [eta, +sq3], [x_1, -0.8], [x_2, 0], [x_3, 0.8], [x_4, 0], [y_1, 0], [y_2, -0.8], [y_3, 0], [y_4, 0.8]))
```

```python
a
```

$\displaystyle \left[\begin{matrix}-0.985843918243516 & 0\\0.360843918243516 & -0.625\\0.264156081756484 & 0\\0.360843918243516 & 0.625\end{matrix}\right]$

```python
aa = np.array(a, dtype=np.float32)
bb = np.array(b, dtype=np.float32)
cc = np.array(c, dtype=np.float32)
dd = np.array(d, dtype=np.float32)
```

```python
aa
```

    array([[-0.9858439 ,  0.        ],
           [ 0.36084393, -0.625     ],
           [ 0.26415607,  0.        ],
           [ 0.36084393,  0.625     ]], dtype=float32)

```python
np.einsum('mi,ni->imn', aa, aa)*0.32
```

    array([[[ 0.31100422, -0.11383545, -0.08333333, -0.11383545],
            [-0.11383545,  0.04166667,  0.03050212,  0.04166667],
            [-0.08333333,  0.03050212,  0.0223291 ,  0.03050212],
            [-0.11383545,  0.04166667,  0.03050212,  0.04166667]],

           [[ 0.        ,  0.        ,  0.        ,  0.        ],
            [ 0.        ,  0.125     ,  0.        , -0.125     ],
            [ 0.        ,  0.        ,  0.        ,  0.        ],
            [ 0.        , -0.125     ,  0.        ,  0.125     ]]],
          dtype=float32)

```python
(np.einsum('mi,ni->imn', aa, aa)+np.einsum('mi,ni->imn', bb, bb)+\
np.einsum('mi,ni->imn', cc, cc)+np.einsum('mi,ni->imn', dd, dd))*0.32
```

    array([[[ 0.5833333 , -0.08333333, -0.41666663, -0.08333333],
            [-0.08333333,  0.08333334, -0.08333333,  0.08333334],
            [-0.41666663, -0.08333333,  0.5833333 , -0.08333333],
            [-0.08333333,  0.08333334, -0.08333333,  0.08333334]],

           [[ 0.08333334, -0.08333333,  0.08333334, -0.08333333],
            [-0.08333333,  0.5833333 , -0.08333333, -0.41666663],
            [ 0.08333334, -0.08333333,  0.08333334, -0.08333333],
            [-0.08333333, -0.41666663, -0.08333333,  0.5833333 ]]],
          dtype=float32)

```python

```


### Diffusion thermique

On cherche à résoudre numériquement l'équation de diffusion thermique suivante,

$$
\begin{align}
\frac{\partial{u}}{\partial{t}}
  &= \alpha \nabla^2 u \\
  &= \alpha \left( \frac{\partial^2{u}}{\partial{x}^2} + \frac{\partial^2{u}}{\partial{y}^2} \right)
\end{align}
$$

avec $u = u(\vec{x}, t)$, $\vec{x} \in \Omega$ où $\Omega = [-1,1] \times [-1,1]$

#### Conditions initiales / aux bords

On impose que initialement, il n'y ait pas de diffusion de chaleur dans le milieu,

$$
u(\vec{x}, t=0) = 0
$$

On impose également une température nulle aux bords du domaine $\Omega$ à l'exception du bord inférieur où une température unitaire est imposée.

$$
\begin{array}[rcl]
  u(x = \pm 1, y, t) = 0 &;& u(x, y = 1, t) = 0 &;& u(x, y = -1, t) = 1
\end{array}
$$

#### Méthode numérique

Afin de simuler cette équation nous allons discrétiser le système et en particulier utiliser une méthode à différences finies (grille régulière).

Le système discrétisé est le suivant (on utilisera la notation en indice pour les dérivées),

$$
u_t(x_i, y_j, t_k) = \alpha \left( u_{xx}(x_i, y_j, t_k) + u_{yy}(x_i, y_j, t_k) \right)
$$

On peut prendre un schéma centré type LeapFrog dans l'espace, l'idée est d'évaluer 2 dérivées à mis chemins du pas de temps.

$$
u_{xx}(x_i, y_j, t_k) \approx \frac{u(x_{i+1}, y_j, t_k) - 2u(x_i, y_j, t_k) + u(x_{i-1}, y_j, t_k)}{(\Delta x)^2}
$$

$$
u_{yy}(x_i, y_j, t_k) \approx \frac{u(x, y_{j+1}, t_k) - 2u(x_i, y_j, t_k) + u(x, y_{j-1}, t_k)}{(\Delta y)^2}
$$

On peut également prendre un schéma Euler avant dans le temps (qui est simplement une dérivé première), on appelle ce schéma, un schéma d'ordre 1,

$$
u_{t}(x_i, y_j, t_k) \approx \frac{u(x_i, y_j, t_{k+1}) - u(x_i, y_j, t_k)}{\Delta t}
$$

Pour une méthode aux différences finies, on prend un maillage uniforme: $\Delta x = \Delta y$.

On obtient donc,
$$
\frac{u(x_i, y_j, t_{k+1}) - u(x_i, y_j, t_k)}{\Delta t} = \alpha \left(  \frac{u(x_{i+1}, y_j, t_k) - 2u(x_i, y_j, t_k) + u(x_{i-1}, y_j, t_k)}{(\Delta x)^2} + \frac{u(x, y_{j+1}, t_k) - 2u(x_i, y_j, t_k) + u(x, y_{j-1}, t_k)}{(\Delta x)^2} \right)
$$

Posons,
$$
\beta = \frac{(\Delta t) \cdot \alpha}{(\Delta x)^2}
$$

Dès lors,
$$
u(x_i, y_j, t_{k+1}) = u(x_i, y_j, t_k) + \beta \left( u(x_{i+1}, y_j, t_k) - 2u(x_i, y_j, t_k) + u(x_{i-1}, y_j, t_k) + u(x, y_{j+1}, t_k) - 2u(x_i, y_j, t_k) + u(x, y_{j-1}, t_k) \right)
$$
$$
u(x_i, y_j, t_{k+1}) = u(x_i, y_j, t_k) + \beta \left( u(x_{i+1}, y_j, t_k) + u(x_{i-1}, y_j, t_k) + u(x, y_{j+1}, t_k) + u(x, y_{j-1}, t_k) - 4u(x_i, y_j, t_k) \right)
$$

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

import matplotlib
colors = matplotlib.cm.jet

# plate size: \Gamma: [-1,1] x [-1,1]
width = height = 2.
# space intervals
dx = dy = 0.1
# thermal diffusivity parameter
alpha = 4

# beta parameter
beta = 0.25

# initial temperature
t0 = 1

# number of steps
nx = int(width/dx)
ny = int(height/dy)
nt = 10

# mesh points in space
x_mesh = np.linspace(start=-1, stop=1, num=nx+1)
y_mesh = np.linspace(start=-1, stop=1, num=ny+1)

# mesh points in time
# numpy doc advice to use linspace
# instead of arange for decimal time steps.
time_mesh = np.linspace(start=0, stop=1, num=nt+1)

# set initial conditions (t=0)
u0 = t0 * np.ones((nx+1, ny+1))

# numerical solution for each step time
u = u0.copy()

"""
apply finite difference equation, leverage numpy vectors (way faster) instead of python loop
@u: current u solution
@u0: prev u solution (was first initial conditions)
http://hplgit.github.io/fdm-book/doc/pub/diffu/html/._diffu-solarized001.html
https://scipython.com/book/chapter-7-matplotlib/examples/the-two-dimensional-diffusion-equation/
"""
def finite_difference(u, u0, beta):
    # at each time step, compute mesh solution
    for t in range(0,nt):
        u[1:nx, 1:ny] = u0[1:nx, 1:ny] + beta * (u0[2:nx+1, 1:ny] + u0[0:nx-1, 1:ny] + u0[1:nx, 2:ny+1] + u0[1:nx, 0:ny-1] - 4*u0[1:nx, 1:ny])

        # set boundary conditions
        u[0, 1:ny] = 0
        u[nx, 1:ny] = 0
        u[1:nx, 0] = 1
        u[1:ny, ny] = 0

        u0 = u

    return u

def plot_mesh(u):
    plt.contourf(x_mesh, y_mesh, u, time_mesh, cmap=colors)
    plt.contour(x_mesh, y_mesh, u, time_mesh, colors="k", linewidths=1)
    plt.axis("off")
    plt.title("Equation de la chaleur")
    plt.show

result = finite_difference(u, u0, beta)
plot_mesh(result)
```

```python

```


# Rocket Model Derrivation

```python
from sympy import *
init_printing()
```

```python
# State
x,y,vx,vy,theta,omega,m = symbols('x y vx vy theta, omega, m')
r = Matrix([x, y]) # Position
v = Matrix([vx, vy]) # Velocity
s = Matrix([r, v,[theta], [omega], [m]])

# Costate
lx,ly,lvx,lvy,ltheta,lomega,lm = symbols('lx ly lvx lvy ltheta lomega, lm')
lr = Matrix([lx, ly])
lv = Matrix([lvx,lvy])
l  = Matrix([lr, lv, [ltheta], [lomega], [lm]])

# Parametres
c1, c2, c3, g, a = symbols('c1 c2 c3 g a')
# Control
u, ut1, ut2 = symbols('u ut1 ut2')
c  = Matrix([u, ut1, ut2])

# Fullstate
fs = Matrix([s,l])
```

```python
# State dynamics
dx     = vx
dy     = vy
dvx    = c1*u/m*ut1 - 1/2*rho*
dvy    = c1*u/m*ut2 - g
dtheta = omega
domega = -c1/c3*u/m*(ut1*cos(theta) + ut2*sin(theta))
dm     = -c1/c2*u
ds     = Matrix([dx, dy, dvx, dvy, dtheta, domega, dm])
```

```python
# Homotopic Cost Lagrangian
L = a*c1/c2*u+(1-a)*c1**2/c2*u**2
```

```python
# Hamiltonian
H = l.dot(ds) + L
print cse(H)
```

    ([(x0, 1/c2), (x1, c1*u*x0), (x2, c1*u/m)], [a*x1 + c1**2*u**2*x0*(-a + 1) - lm*x1 + ltheta*omega + lvx*ut1*x2 + lvy*(-g + ut2*x2) + lx*vx + ly*vy - lomega*x2*(ut1*cos(theta) + ut2*sin(theta))/c3])

```python
# Costate dynamics
dl = -Matrix([diff(H, i) for i in s])
```

```python
# Fullstate dynamics
dfs = Matrix([ds, dl])
```

```python
lax = lvx - lomega /c3*cos(theta)
lay = lvy + lomega /c3*sin(theta)
la  = sqrt(lax**2 + lay**2)
ut1s = -lax / la
ut2s = -lay / la
uts = Matrix([ut1s, ut2s])
S = 1 - lm - la *c2 /m
us = 1/2/c1/(1-a)*(lm+la*c2/m-a)
us = (lm + c2/m * la - a)/(2*(1-a)*c1)
print cse(Matrix([ut1s, ut2s, S, us]))
```

    ([(x0, lomega/c3), (x1, x0*cos(theta)), (x2, x0*sin(theta)), (x3, sqrt((lvx - x1)**2 + (lvy + x2)**2)), (x4, 1/x3), (x5, c2*x3/m)], [Matrix([
    [                x4*(-lvx + x1)],
    [                x4*(-lvy - x2)],
    [                  -lm - x5 + 1],
    [(-a + lm + x5)/(c1*(-2*a + 2))]])])

```python

```


# Sampled Softmax

For classification and prediction problems a typical criterion function is cross-entropy with softmax. If the number of output classes is high the computation of this criterion and the corresponding gradients could be quite costly. Sampled Softmax is a heuristic to speed up training in these cases. (see: [Adaptive Importance Sampling to Accelerate Training of a Neural Probabilistic Language Model](http://www.iro.umontreal.ca/~lisa/pointeurs/importance_samplingIEEEtnn.pdf), [Exploring the Limits of Language Modeling](https://arxiv.org/pdf/1602.02410v1.pdf), [What is Candidate Sampling](https://www.tensorflow.org/extras/candidate_sampling.pdf))

#### Select the notebook runtime environment devices / settings

Before we dive into the details we run some setup that is required for automated testing of this notebook.

```python
import os
import cntk as C

# Select the right target device when this notebook is being tested:
if 'TEST_DEVICE' in os.environ:
    if os.environ['TEST_DEVICE'] == 'cpu':
        C.device.try_set_default_device(C.device.cpu())
    else:
        C.device.try_set_default_device(C.device.gpu(0))
```

## Basics

The softmax function is used in neural networks if we want to interpret the network output as a probability distribution over a set of classes $C$ with $|C|=N_C$.

Softmax maps an $N_C$-dimensional vector $z$, which has unrestricted values, to an $N_C$ dimensional vector $p$ with non-negative values that sum up to 1 so that they can be interpreted as probabilities. More precisely:

$$
\begin{align}
p_i &= softmax(z, i)\\
    &= \frac{exp(z_i)}{\sum_{k\in C} exp(z_k)}\\
\end{align}
$$

In what follows we assume that the input $z$ to the softmax is computed from some hidden vector $h$ of dimension $N_h$  in a specific way, namely:

$$ z = W h + b $$

where $W$ is a learnable weight matrix of dimension $(N_c, N_h)$ and $b$ is a learnable bias vector.
We restrict ourselves to this specific choice of $z$ because it helps in implementing an efficient sampled softmax.

In a typical use-case like for example a recurrent language model, the hidden vector $h$ would be the output of the recurrent layers and $C$ would be the set of words to predict.

As a training criterion, we use cross-entropy which is a function of the expected (true) class $t\in C$ and the probability predicted for it:

$$cross\_entropy := -log(p_t)$$

## Sampled Softmax from the outside

For the normal softmax the CNTK Python-api provides the function [cross_entropy_with_softmax](https://cntk.ai/pythondocs/cntk.ops.html?highlight=softmax#cntk.ops.cross_entropy_with_softmax). This takes as input the $N_C$-dimensional vector $z$. As mentioned for our sampled softmax implementation we assume that this z is computed by $ z = W h + b $. In sampled softmax this has to be part of the whole implementation of the criterion.

Below we show the code for `cross_entropy_with_sampled_softmax_and_embedding`. Let’s look at the signature first.

One fundamental difference to the corresponding function in the Python-api (`cross_entropy_with_softmax`) is that in the Python api function the input corresponds to $z$ and must have the same dimension as the target vector, while in cross_entropy_with_full_softmax the input corresponds to our hidden vector $h$ can have any dimension (hidden_dim).
Actually, hidden_dim will be typically much lower than the dimension of the target vector.

We also have some additional parameters `num_samples, sampling_weights, allow_duplicates` that control the random sampling.
Another difference to the api function is that we return a triple (z, cross_entropy_on_samples, error_on_samples).

We will come back to the details of the implementation below.

```python
from __future__ import print_function
from __future__ import division

# Creates a subgraph computing cross-entropy with sampled softmax.
def cross_entropy_with_sampled_softmax_and_embedding(
    hidden_vector,            # Node providing hidden input
    target_vector,            # Node providing the expected labels (as sparse vectors)
    num_classes,              # Number of classes
    hidden_dim,               # Dimension of the hidden vector
    num_samples,              # Number of samples to use for sampled softmax
    sampling_weights,         # Node providing weights to be used for the weighted sampling
    allow_duplicates = True,  # Boolean flag to control whether to use sampling with replacemement
                              # (allow_duplicates == True) or without replacement.
    ):
    # define the parameters learnable parameters
    b = C.Parameter(shape = (num_classes, 1), init = 0)
    W = C.Parameter(shape = (num_classes, hidden_dim), init = C.glorot_uniform())

    # Define the node that generates a set of random samples per minibatch
    # Sparse matrix (num_samples * num_classes)
    sample_selector = C.random_sample(sampling_weights, num_samples, allow_duplicates)

    # For each of the samples we also need the probablity that it in the sampled set.
    inclusion_probs = C.random_sample_inclusion_frequency(sampling_weights, num_samples, allow_duplicates) # dense row [1 * vocab_size]
    log_prior = C.log(inclusion_probs) # dense row [1 * num_classes]

    # Create a submatrix wS of 'weights
    W_sampled = C.times(sample_selector, W) # [num_samples * hidden_dim]
    z_sampled = C.times_transpose(W_sampled, hidden_vector) + C.times(sample_selector, b) - C.times_transpose (sample_selector, log_prior)# [num_samples]

    # Getting the weight vector for the true label. Dimension hidden_dim
    W_target = C.times(target_vector, W) # [1 * hidden_dim]
    z_target = C.times_transpose(W_target, hidden_vector) + C.times(target_vector, b) - C.times_transpose(target_vector, log_prior) # [1]

    z_reduced = C.reduce_log_sum_exp(z_sampled)

    # Compute the cross entropy that is used for training.
    # We don't check whether any of the classes in the random samples conincides with the true label, so it might
    # happen that the true class is counted
    # twice in the normalising demnominator of sampled softmax.
    cross_entropy_on_samples = C.log_add_exp(z_target, z_reduced) - z_target

    # For applying the model we also output a node providing the input for the full softmax
    z = C.times_transpose(W, hidden_vector) + b
    z = C.reshape(z, shape = (num_classes))

    zSMax = C.reduce_max(z_sampled)
    error_on_samples = C.less(z_target, zSMax)
    return (z, cross_entropy_on_samples, error_on_samples)

```

To give a better idea of what the inputs and outputs are and how this all differs from the normal softmax we give below a corresponding function using normal softmax:

```python
# Creates subgraph computing cross-entropy with (full) softmax.
def cross_entropy_with_softmax_and_embedding(
    hidden_vector,  # Node providing hidden input
    target_vector,  # Node providing the expected labels (as sparse vectors)
    num_classes,    # Number of classes
    hidden_dim      # Dimension of the hidden vector
    ):
    # Setup bias and weights
    b = C.Parameter(shape = (num_classes, 1), init = 0)
    W = C.Parameter(shape = (num_classes, hidden_dim), init = C.glorot_uniform())

    z = C.reshape( C.times_transpose(W, hidden_vector) + b, (1, num_classes))

    # Use cross_entropy_with_softmax
    cross_entropy = C.cross_entropy_with_softmax(z, target_vector)

    zMax = C.reduce_max(z)
    zT = C.times_transpose(z, target_vector)
    error_on_samples = C.less(zT, zMax)

    return (z, cross_entropy, error_on_samples)
```

As you can see the main differences to the api function `cross_entropy_with_softmax` are:
* We include the mapping $ z = W h + b $ into the function.
* We return a triple (z, cross_entropy, error_on_samples) instead of just returning the cross entropy.

## A toy example

To explain how to integrate sampled softmax let us look at a toy example. In this toy example we first transform one-hot input vectors via some random projection into a lower dimensional vector $h$. The modeling task is to reverse this mapping using (sampled) softmax. Well, as already said this is a toy example.

```python
import numpy as np
from math import log, exp, sqrt
from cntk.logging import ProgressPrinter
import timeit

# A class with all parameters
class Param:
    # Learning parameters
    learning_rate = 0.03
    minibatch_size = 100
    num_minbatches = 100
    test_set_size = 1000
    momentum_time_constant = 5 * minibatch_size
    reporting_interval = 10
    allow_duplicates = False

    # Parameters for sampled softmax
    use_sampled_softmax = True
    use_sparse = True
    softmax_sample_size = 10

    # Details of data and model
    num_classes = 50
    hidden_dim = 10

data_sampling_distribution = lambda: np.repeat(1.0 / Param.num_classes, Param.num_classes)

softmax_sampling_weights = lambda: np.repeat(1.0 / Param.num_classes, Param.num_classes)

# Creates random one-hot vectors of dimension 'num_classes'.
# Returns a tuple with a list of one-hot vectors, and list with the indices they encode.
def get_random_one_hot_data(num_vectors):
    indices = np.random.choice(
        range(Param.num_classes),
        size=num_vectors,
        p = data_sampling_distribution()).reshape((1, num_vectors))
    list_of_vectors = C.Value.one_hot(indices, Param.num_classes)
    return (list_of_vectors, indices.flatten())

# Create a network that:
# * Transforms the input one hot-vectors with a constant random embedding
# * Applies a linear decoding with parameters we want to learn
def create_model(labels):
    # random projection matrix
    random_data = np.random.normal(scale = sqrt(1.0/Param.hidden_dim), size=(Param.num_classes, Param.hidden_dim)).astype(np.float32)
    random_matrix = C.constant(shape = (Param.num_classes, Param.hidden_dim), value = random_data)

    h = C.times(labels, random_matrix)

    # Connect the latent output to (sampled/full) softmax.
    if Param.use_sampled_softmax:
        sampling_weights = np.asarray(softmax_sampling_weights(), dtype=np.float32)
        sampling_weights.reshape((1, Param.num_classes))
        softmax_input, ce, errs = cross_entropy_with_sampled_softmax_and_embedding(
            h,
            labels,
            Param.num_classes,
            Param.hidden_dim,
            Param.softmax_sample_size,
            softmax_sampling_weights(),
            Param.allow_duplicates)
    else:
        softmax_input, ce, errs = cross_entropy_with_softmax_and_embedding(
            h,
            labels,
            Param.num_classes,
            Param.hidden_dim)

    return softmax_input, ce, errs

def train(do_print_progress):
    labels = C.input_variable(shape = Param.num_classes, is_sparse = Param.use_sparse)
    z, cross_entropy, errs = create_model(labels)

    # Setup the trainer
    learning_rate_schedule = C.learning_rate_schedule(Param.learning_rate, C.UnitType.sample)
    momentum_schedule = C.momentum_as_time_constant_schedule(Param.momentum_time_constant)
    learner = C.momentum_sgd(z.parameters, learning_rate_schedule, momentum_schedule, True)
    progress_writers = None
    if do_print_progress:
        progress_writers = [ProgressPrinter(freq=Param.reporting_interval, tag='Training')]
    trainer = C.Trainer(z, (cross_entropy, errs), learner, progress_writers)

    minbatch = 0
    average_cross_entropy = compute_average_cross_entropy(z)
    minbatch_data = [0] # store minibatch values
    cross_entropy_data = [average_cross_entropy] # store cross_entropy values

    # Run training
    t_total= 0

    # Run training
    for minbatch in range(1,Param.num_minbatches):
        # Specify the mapping of input variables in the model to actual minibatch data to be trained with
        label_data, indices = get_random_one_hot_data(Param.minibatch_size)
        arguments = ({labels : label_data})

        # If do_print_progress is True, this will automatically print the progress using ProgressPrinter
        # The printed loss numbers are computed using the sampled softmax criterion
        t_start = timeit.default_timer()
        trainer.train_minibatch(arguments)
        t_end = timeit.default_timer()

        t_delta = t_end - t_start
        samples_per_second = Param.minibatch_size / t_delta

        # We ignore the time measurements of the first two minibatches
        if minbatch > 2:
            t_total += t_delta

        # For comparison also print result using the full criterion
        if minbatch % Param.reporting_interval == int(Param.reporting_interval/2):
            # memorize the progress data for plotting
            average_cross_entropy = compute_average_cross_entropy(z)
            minbatch_data.append(minbatch)
            cross_entropy_data.append(average_cross_entropy)

            if do_print_progress:
                print("\nMinbatch=%d Cross-entropy from full softmax = %.3f perplexity = %.3f samples/s = %.1f"
                    % (minbatch, average_cross_entropy, exp(average_cross_entropy), samples_per_second))

    # Number of samples we measured. First two minbatches were ignored
    samples_measured = Param.minibatch_size * (Param.num_minbatches - 2)
    overall_samples_per_second = samples_measured / t_total
    return (minbatch_data, cross_entropy_data, overall_samples_per_second)

def compute_average_cross_entropy(softmax_input):
    vectors, indices = get_random_one_hot_data(Param.test_set_size)
    total_cross_entropy = 0.0
    arguments = (vectors)
    z = softmax_input.eval(arguments).reshape(Param.test_set_size, Param.num_classes)

    for i in range(len(indices)):
        log_p = log_softmax(z[i], indices[i])
        total_cross_entropy -= log_p

    return total_cross_entropy / len(indices)

# Computes log(softmax(z,index)) for a one-dimensional numpy array z in an numerically stable way.
def log_softmax(z,    # numpy array
                index # index into the array
            ):
    max_z = np.max(z)
    return z[index] - max_z - log(np.sum(np.exp(z - max_z)))

np.random.seed(1)

print("start...")
train(do_print_progress = True)
print("done.")
```

    start...

    Minbatch=5 Cross-entropy from full softmax = 3.843 perplexity = 46.645 samples/s = 15268.3
     Minibatch[   1-  10]: loss = 2.321515 * 1000, metric = 78.90% * 1000;

    Minbatch=15 Cross-entropy from full softmax = 3.483 perplexity = 32.561 samples/s = 16016.7
     Minibatch[  11-  20]: loss = 2.042852 * 1000, metric = 61.10% * 1000;

    Minbatch=25 Cross-entropy from full softmax = 3.061 perplexity = 21.339 samples/s = 16125.2
     Minibatch[  21-  30]: loss = 1.701563 * 1000, metric = 38.80% * 1000;

    Minbatch=35 Cross-entropy from full softmax = 2.781 perplexity = 16.142 samples/s = 17111.2
     Minibatch[  31-  40]: loss = 1.435229 * 1000, metric = 17.20% * 1000;

    Minbatch=45 Cross-entropy from full softmax = 2.452 perplexity = 11.609 samples/s = 17150.7
     Minibatch[  41-  50]: loss = 1.268124 * 1000, metric = 14.20% * 1000;

    Minbatch=55 Cross-entropy from full softmax = 2.262 perplexity = 9.602 samples/s = 16039.8
     Minibatch[  51-  60]: loss = 1.052258 * 1000, metric = 7.70% * 1000;

    Minbatch=65 Cross-entropy from full softmax = 2.092 perplexity = 8.100 samples/s = 16275.1
     Minibatch[  61-  70]: loss = 0.979096 * 1000, metric = 10.10% * 1000;

    Minbatch=75 Cross-entropy from full softmax = 1.924 perplexity = 6.850 samples/s = 16244.5
     Minibatch[  71-  80]: loss = 0.908541 * 1000, metric = 9.50% * 1000;

    Minbatch=85 Cross-entropy from full softmax = 1.751 perplexity = 5.761 samples/s = 15610.1
     Minibatch[  81-  90]: loss = 0.825795 * 1000, metric = 5.10% * 1000;

    Minbatch=95 Cross-entropy from full softmax = 1.660 perplexity = 5.261 samples/s = 13370.0
    done.

In the above code we use two different methods to report training progress:
1. Using a function that computes the average cross entropy on full softmax.
2. Using the built-in ProgressPrinter

ProgressPrinter reports how the value of the training criterion changes over time.
In our case the training criterion is cross-entropy from **sampled** softmax.
The same is true for the error rate computed by progress printer, this is computed only for true-class vs sampled-classes and will therefore underestimate the true error rate.

Therefore while ProgressPrinter already gives us some idea how training goes on, if we want to compare the behavior for different sampling strategies (sample size, sampling weights, ...) we should not rely on numbers that are computed only using the sampled subset of classes.

## Importance sampling

Often the we don't have uniform distribution for the classes on the output side. The typical example is when we have words as output classes. A typical example are words where e.g. 'the' will be much more frequent than most others.

In such cases one often uses a non uniform distribution for drawing the samples in sampled softmax but instead increases the sampling weight for the frequent classes. This is also called importane sampling.
In our example the sampling distribution is controlled by the weight array `softmax_sampling_weights`.

As an example let's look at the case where the classes are distrubted according to zipf-distrubtion like:
$$
p[i] \propto \frac{1}{i+5},
$$
actually we use this distribution already in our example.

How does training behavior change if we switch uniform sampling to sampling with the zipfian distribution in sampled softmax?

```python
# We want to lot the data
import matplotlib.pyplot as plt
%matplotlib inline

# Define weights of zipfian distributuion
def zipf(index):
    return 1.0 / (index + 5)

# Use zipifian distribution for the classes
def zipf_sampling_weights():
    return np.asarray([ zipf(i) for i in range(Param.num_classes)], dtype=np.float32)

data_sampling_distribution = lambda: zipf_sampling_weights() / np.sum(zipf_sampling_weights())

print("start...")

# Train using uniform sampling (like before)
np.random.seed(1)
softmax_sampling_weights = lambda: np.repeat(1.0/Param.num_classes, Param.num_classes)
minibatch_data, cross_entropy_data, _ = train(do_print_progress = False)

# Train using importance sampling
np.random.seed(1)
softmax_sampling_weights = zipf_sampling_weights
minibatch_data2, cross_entropy_data2, _ = train(do_print_progress = False)

plt.plot(minibatch_data, cross_entropy_data, 'r--',minibatch_data, cross_entropy_data2, 'b--')
plt.xlabel('number of mini-batches')
plt.ylabel('cross entropy')
plt.show()

```

In the example above we compare uniform sampling (red) vs sampling with the same distribution the classes have (blue).
You will need to experiment to find the best settings for all the softmax parameters.

## What speedups to expect?

The speed difference between full softmax and sampled softmax in terms of training instances depends strongly on the concrete settings, namely
* Number of classes. Typically the speed-up will increase the more output classes you have.
* Number of samples used in sampled softmax
* Dimension of hiddlen layer input
* Minibatch size
* Hardware

Also you need to test how much you can reduce sample size without degradation of the result.

```python
print("start...")

# Reset parameters
class Param:
    # Learning parameters
    learning_rate = 0.03
    minibatch_size = 8
    num_minbatches = 100
    test_set_size = 1 # we are only interrested in speed
    momentum_time_constant = 5 * minibatch_size
    reporting_interval = 1000000 # Switch off reporting to speed up
    allow_duplicates = False

    # Parameters for sampled softmax
    use_sampled_softmax = True
    use_sparse = True
    softmax_sample_size = 10

    # Details of data and model
    num_classes = 50000
    hidden_dim = 10

data_sampling_distribution = lambda: np.repeat(1.0 / Param.num_classes, Param.num_classes)
softmax_sampling_weights = lambda: np.repeat(1.0 / Param.num_classes, Param.num_classes)

sample_sizes = [5, 10, 100, 1000]
speed_with_sampled_softmax = []

# Get the speed with sampled softmax for different sizes
for sample_size in sample_sizes:
    print("Measuring speed of sampled softmax for sample size %d ..." % (sample_size))
    Param.use_sampled_softmax = True
    Param.softmax_sample_size = sample_size
    _, _,  samples_per_second = train(do_print_progress = False)
    speed_with_sampled_softmax.append(samples_per_second)

# Get the speed with full softmax
Param.use_sampled_softmax = False
print("Measuring speed of full softmax ...")
_, _,  samples_per_second = train(do_print_progress = False)
speed_without_sampled_softmax = np.repeat(samples_per_second, len(sample_sizes))

# Plot the speed of sampled softmax (blue) as a function of sample sizes
# and compare it to the speed with full softmax (red).
plt.plot(sample_sizes, speed_without_sampled_softmax, 'r--',sample_sizes, speed_with_sampled_softmax, 'b--')
plt.xlabel('softmax sample size')
plt.ylabel('speed: instances / second')
plt.title("Speed 'sampled softmax' (blue) vs. 'full softmax' (red)")
plt.ylim(ymin=0)
plt.show()

```


## 1. Exploring political differences through latent factor analysis

- Motivation
    - Political spectrum is usually considered 1-dimensional
    - Latent factor analysis has in other instances been used to understand human actions and motivatation

- Overview of project
    - Survey data
    - Naive Bayesian latent factor analysis
    - Extension with ordered probit to accomadate the discrete nature of the data

## 2. Introduction to factor models

\begin{equation}
    \underset{(p \times 1)}{y_i} = \underset{(p \times k)}{\beta} \underset{(k \times 1)}{F_i} + \underset{(p \times 1)}{\epsilon_i}
\end{equation}

for $i=1,...,T$ individuals. $\beta$ denotes the factor loadings and F are the factor scores

\begin{equation}
    \epsilon \sim I.I.D.N(0,\Sigma)
\end{equation}

\begin{equation}
    \underset{(p \times p)}{var(y)} = \underset{(p \times k)(k \times k)(k \times p)}{\beta\psi\beta'} + \underset{(p \times p)}{\Sigma} = \underset{(p \times p)}{\Omega}
\end{equation}

where a normal assumption is...

\begin{equation}
    \psi = I_k
\end{equation}

## 3. Naive Factor model

\begin{equation}
    \underset{(p \times t)}{y} = \underset{(p \times t)}{\beta} \underset{(k \times t)}{F} + \underset{(p \times t)}{\epsilon}
\end{equation}

Mispecification!

\begin{equation}
    p(\beta \vert y, \Sigma, F) \propto P(Y \vert \beta, F, \Sigma)P(\beta)
\end{equation}

## 4. Naive Factor model 2

For Q being an orthogonal matrix $Q\cdot Q' = I_k$, we have the following problem:

define new factor loading matrix s.t. $\tilde\beta = \beta Q'$

\begin{equation}
    var(y) = \tilde\beta \tilde\beta' + \Sigma =  \beta Q'(\beta Q') + \Sigma = \beta\beta' + \Sigma
\end{equation}
                                    ....No identification!

\begin{array}{lcr}
\beta_{11} & 0 & 0 \\
\beta_{11} & \beta_{22} & 0 \\
\beta_{31} & \beta_{32} & \beta_{33} \\
... & ... & ... \\
\beta_{p1} & \beta_{p2} & \beta_{p3} \\ \end{array}

Second problem: Sign switching matrix

## 5. Ordered probit Factor Model

\begin{equation}
    Y_i = l \text{ if } \tau_{l-1}\leq Y_i^* \leq \tau_{l}\text{ for l=1,2,3,4}
\end{equation}

\begin{equation}
    Y^*_i = \beta F_i + \epsilon_i
\end{equation}

Posterior:
\begin{equation}
    P(\beta \vert Y^*, Y, \tau, F) \propto  P(Y \vert Y^*, \tau)P(Y^*\vert \beta, F)P(\beta)P(F)
\end{equation}

## 6. Probit Factor Model

In order to properly identify the model we do the following:

\begin{equation}
    \Sigma = I_p
\end{equation}

If we include a constant term, we would have to fix one of our $\tau=0$, but we don't

\begin{equation}
    Y^* \sim TN_{[\tau_l, \tau_{l+1}]}(\beta F; 1)
\end{equation}

\begin{equation}
    \tau_l \sim U[\underline{\tau_l};\bar{\tau_l}]
\end{equation}

## 7. Data, simulations and factor loading

- Data
    - Survey data from municipality elections 2017 (KV17)
    - Politicians answered questionaire of 15 questions by the danish broadcasting service
    - 1200 answers by the largest danish parties used in analysis
    - dataset of size 15 variables and 1200 rows

- Simulations
    - To test if our algorithm converges 2 test data strategies utilized:
    - Strategy 1: simulate from 3 underlying factors
    - Strategy 2: Use cholesky decomposition to create dataset with correct covariance matrix
    - Both had fast convergence

- Factor loading
    - Upper triangular matrix require we think about the ordering of the questions
    - 2 first questions should be different in nature (which latent factor the questions represent)
        + loads only to factor 0: _Municipality tax should be reduced_
        + loads only to factor 0 & 1: _Institutions run by local authorities takes to much into consideration the concerns of religious minorities._

## 8. Results Naive - Trace and distribution of $\beta$

## 9. Results Naive - scatter plots 1

## 9. Results Naive - scatter plots 2

## 10. Feedback/issues

- Computation issues
- Factor loading matrix - not what expected
- Unknown unknowns

```python

```


# Example Notebook for sho1d.py

Import the sho1d.py file as well as the test_sho1d.py file

```
from sympy import *
from IPython.display import display_pretty
from sympy.physics.quantum import *
from sympy.physics.quantum.sho1d import *
from sympy.physics.quantum.tests.test_sho1d import *
init_printing(pretty_print=False, use_latex=False)
```

### Printing Of Operators

Create a raising and lowering operator and make sure they print correctly

```
ad = RaisingOp('a')
a = LoweringOp('a')
```

```
ad
```

    RaisingOp(a)

```
a
```

    a

```
print(latex(ad))
print(latex(a))
```

    a^{\dag}
    a

```
display_pretty(ad)
display_pretty(a)
```

    RaisingOp(a)

    a

```
print(srepr(ad))
print(srepr(a))
```

    RaisingOp(Symbol('a'))
    LoweringOp(Symbol('a'))

```
print(repr(ad))
print(repr(a))
```

    RaisingOp(a)
    a

### Printing of States

Create a simple harmonic state and check its printing

```
k = SHOKet('k')
b = SHOBra('b')
```

```
k
```

    |k>

```
b
```

    <b|

```
print(pretty(k))
print(pretty(b))
```

    ❘k⟩
    ⟨b❘

```
print(latex(k))
print(latex(b))
```

    {\left|k\right\rangle }
    {\left\langle b\right|}

```
print(srepr(k))
print(srepr(b))
```

    SHOKet(Symbol('k'))
    SHOBra(Symbol('b'))

### Properties

Take the dagger of the raising and lowering operators. They should return each other.

```
Dagger(ad)
```

    a

```
Dagger(a)
```

    RaisingOp(a)

Check Commutators of the raising and lowering operators

```
Commutator(ad,a).doit()
```

    -1

```
Commutator(a,ad).doit()
```

    1

Take a look at the dual states of the bra and ket

```
k.dual
```

    <k|

```
b.dual
```

    |b>

Taking the InnerProduct of the bra and ket will return the KroneckerDelta function

```
InnerProduct(b,k).doit()
```

    KroneckerDelta(k, b)

Take a look at how the raising and lowering operators act on states. We use qapply to apply an operator to a state

```
qapply(ad*k)
```

    sqrt(k + 1)*|k + 1>

```
qapply(a*k)
```

    sqrt(k)*|k - 1>

But the states may have an explicit energy level. Let's look at the ground and first excited states

```
kg = SHOKet(0)
kf = SHOKet(1)
```

```
qapply(ad*kg)
```

    |1>

```
qapply(ad*kf)
```

    sqrt(2)*|2>

```
qapply(a*kg)
```

    0

```
qapply(a*kf)
```

    |0>

Notice that a*kg is 0 and a*kf is the |0> the ground state.

### NumberOp & Hamiltonian

Let's look at the Number Operator and Hamiltonian Operator

```
k = SHOKet('k')
ad = RaisingOp('a')
a = LoweringOp('a')
N = NumberOp('N')
H = Hamiltonian('H')
```

The number operator is simply expressed as ad*a

```
N().rewrite('a').doit()
```

    RaisingOp(a)*a

The number operator expressed in terms of the position and momentum operators

```
N().rewrite('xp').doit()
```

    -1/2 + (m**2*omega**2*X**2 + Px**2)/(2*hbar*m*omega)

It can also be expressed in terms of the Hamiltonian operator

```
N().rewrite('H').doit()
```

    -1/2 + H/(hbar*omega)

The Hamiltonian operator can be expressed in terms of the raising and lowering operators, position and momentum operators, and the number operator

```
H().rewrite('a').doit()
```

    hbar*omega*(1/2 + RaisingOp(a)*a)

```
H().rewrite('xp').doit()
```

    (m**2*omega**2*X**2 + Px**2)/(2*m)

```
H().rewrite('N').doit()
```

    hbar*omega*(1/2 + N)

The raising and lowering operators can also be expressed in terms of the position and momentum operators

```
ad().rewrite('xp').doit()
```

    sqrt(2)*(m*omega*X - I*Px)/(2*sqrt(hbar)*sqrt(m*omega))

```
a().rewrite('xp').doit()
```

    sqrt(2)*(m*omega*X + I*Px)/(2*sqrt(hbar)*sqrt(m*omega))

### Properties

Let's take a look at how the NumberOp and Hamiltonian act on states

```
qapply(N*k)
```

    k*|k>

Apply the Number operator to a state returns the state times the ket

```
ks = SHOKet(2)
qapply(N*ks)
```

    2*|2>

```
qapply(H*k)
```

    hbar*k*omega*|k> + hbar*omega*|k>/2

Let's see how the operators commute with each other

```
Commutator(N,ad).doit()
```

    RaisingOp(a)

```
Commutator(N,a).doit()
```

    -a

```
Commutator(N,H).doit()
```

    0

### Representation

We can express the operators in NumberOp basis. There are different ways to create a matrix in Python, we will use 3 different ways.

#### Sympy

```
represent(ad, basis=N, ndim=4, format='sympy')
```

    [0,       0,       0, 0]
    [1,       0,       0, 0]
    [0, sqrt(2),       0, 0]
    [0,       0, sqrt(3), 0]

#### Numpy

```
represent(ad, basis=N, ndim=5, format='numpy')
```

    array([[ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ],
           [ 1.        ,  0.        ,  0.        ,  0.        ,  0.        ],
           [ 0.        ,  1.41421356,  0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  1.73205081,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  2.        ,  0.        ]])

#### Scipy.Sparse

```
represent(ad, basis=N, ndim=4, format='scipy.sparse', spmatrix='lil')
```

    <4x4 sparse matrix of type '<type 'numpy.float64'>'
    	with 3 stored elements in Compressed Sparse Row format>

```
print(represent(ad, basis=N, ndim=4, format='scipy.sparse', spmatrix='lil'))
```

      (1, 0)	1.0
      (2, 1)	1.41421356237
      (3, 2)	1.73205080757

The same can be done for the other operators

```
represent(a, basis=N, ndim=4, format='sympy')
```

    [0, 1,       0,       0]
    [0, 0, sqrt(2),       0]
    [0, 0,       0, sqrt(3)]
    [0, 0,       0,       0]

```
represent(N, basis=N, ndim=4, format='sympy')
```

    [0, 0, 0, 0]
    [0, 1, 0, 0]
    [0, 0, 2, 0]
    [0, 0, 0, 3]

```
represent(H, basis=N, ndim=4, format='sympy')
```

    [hbar*omega/2,              0,              0,              0]
    [           0, 3*hbar*omega/2,              0,              0]
    [           0,              0, 5*hbar*omega/2,              0]
    [           0,              0,              0, 7*hbar*omega/2]

#### Ket and Bra Representation

```
k0 = SHOKet(0)
k1 = SHOKet(1)
b0 = SHOBra(0)
b1 = SHOBra(1)
```

```
print(represent(k0, basis=N, ndim=5, format='sympy'))
```

    [1]
    [0]
    [0]
    [0]
    [0]

```
print(represent(k1, basis=N, ndim=5, format='sympy'))
```

    [0]
    [1]
    [0]
    [0]
    [0]

```
print(represent(b0, basis=N, ndim=5, format='sympy'))
```

    [1, 0, 0, 0, 0]

```
print(represent(b1, basis=N, ndim=5, format='sympy'))
```

    [0, 1, 0, 0, 0]

```

```


# Consumption problem

Imports and set magics:

```python
import numpy as np
from scipy import stats as st
from scipy import optimize
import sympy as sm
import matplotlib.pyplot as plt
import math as m
import time
import seaborn as sns

# autoreload modules when code is run
%load_ext autoreload
%autoreload 2

# local modules
import modelproject
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

# Model description

In this analysis, we wish to find the optimal consumption of xxx for a consumer, here defined as the entire household. The initial amount 10 that needs to be split over a period of 7 days.

<b>Theory<b>

We consider an Euler Equation where the consumer preferences in each period, t, follows the condition:
* $u(c_t)$ = $ln(c_t)$

The consumption in future periods is discounted with factor $β<1$
indicating the time-preference of the consumer.

The present value of the consumption path set in period 0 is given by:
* $V(c_0, c_1,..., c_T)$ = $\sum_{t=0}^T$ $\beta^t U(c_t)$

The consumer wishes to optimize his consumption for every period, $t=0, ..., T$ but for period $t+1$ the amount left to consume is smaller by the amount consumed the period before, given by

* $k_{t+1}$ = $k_t - c_t$

Therefore the following restriction must hold
* $k_t \geq 0$

# Analytical solution of the problem

The analytical solution that we want to derive using **sympy** is found by first inserting the constraing into the consumption path as follows

$\beta^t U(k_{t-1}-k_t)$ + $\beta^{t+1} U(k_t-k_{t+1})$

We then derive the the objective function s.t. $k_t$ to find the Euler equation
$U'(c_{t-1})= \beta U'(c_t)$

This can be interpreted as stating that if the consumer wishes to shift in consumption between two periods, he will not gain from such a shift as a reduction in consumption in period $t$ equals a decrease in $U_t$ by the amount $U'_t$. The amount that the consumer then saves will be shifted to the next period, $t_{+1}$, increasing the total utility by a discounted amount, $U'_{t+1}$. This amount equals $\beta \bullet U'_{t1}$.

We know that in optimum, $k_{t+1}=0$.

By inserting the utility function in the Euler equation, we then find

$c_{t+1}= \beta c_t$

Combining this and the budget constraint we find the solution of the problem given by

$c_0 = ((1-\beta)/(1-\beta)^{T+1}) k_0 $

First we define all **symbols**:

```python
t = 7
b = 0.7
k = np.zeros(t+1,float)
c = np.zeros(t+1,float)
u = np.zeros(t+1,float)
k[0] = 10  # k0
c[0] = (1.0-b)/(1.0-b**(t+1)) * k[0]  # c0
u[0] = np.log(c[0])

for i in range(1,t+1):
    #print "i=" + str(i)
    c[i] = beta * c[i-1]
    k[i] = k[i-1] - c[i-1]

    # Period utility with discounting
    u[i] = beta**(i-1)*np.log(c[i])

np.sum(u)  # total utility

print("c = " + str(c))
print("k = " + str(k))
```

    c = [3.18352381 2.22846667 1.55992667 1.09194867 0.76436407 0.53505485
     0.37453839 0.26217688]
    k = [10.          6.81647619  4.58800952  3.02808285  1.93613418  1.17177012
      0.63671527  0.26217688]

Next we **plot** the functions

```python
fig, ax = plt.subplots()
plt.subplots_adjust(wspace=0.5, hspace=0.8)
ax.plot(c, color='coral', linewidth=3)
ax.set_ylabel('$c^*_t$')
ax.set_xlabel('Period (t)')
ax.set_title('Optimal $c^*_t$')
plt.show()

fig, ax = plt.subplots()
plt.subplots_adjust(wspace=0.5, hspace=0.8)
ax.plot(k, color='green', linewidth=3)
ax.set_ylabel('$k_t$')
ax.set_xlabel('Period (t)')
ax.set_title('Amount, $k_t$')
plt.show()
#, colour="green", size=0.4
```

###WRITE SOMETHING ABOUT FINDINGS

## Numerical solution

The general model is given by the utility function

$\sum_{t=0}^T$ $\beta^t U(c_t)$

with one constraint
    $c_0,...c_t = k_0$

Stating that it is not possible to consume more of the good than the 10 acquired goods.

We now define the **utility function**:

```python
def utility(c):
    t = len(c)
    u= np.zeros(t,float)
    for i in range(t):
        b = 0.7
        #Calculating the utility of the entire period, disregarding the discount factor
        u[i] = (b**i) * np.log(c[i])

#to maximize the result, we take the negative result
    return (-np.sum(u))

#We then define the constraint above as
def constraint(c):
    k[0] = 10
    x_1 = np.sum(c) - k[0]
    return np.array([x_1])
```

**Solve** by calling the optimizer:

```python
time = 10

#Now we initialize a guess for optimal c
c_guess = 0.3*np.ones(time,float)

#Using the optimizing function
c_op = optimize.fmin_slsqp(utility, c_guess, f_eqcons = constraint)

#Printing the solution
print(c_op)

```

    Optimization terminated successfully.    (Exit mode 0)
                Current function value: -1.2914761304307483
                Iterations: 13
                Function evaluations: 157
                Gradient evaluations: 13
    [3.08814661 2.15928281 1.51399323 1.05921944 0.74034874 0.51902058
     0.36328604 0.25423064 0.17795569 0.12451622]

Plotting the optimal consumtion of the period

```python
palette = sns.color_palette("mako_r", 2)
ax = sns.lineplot(data=c, hue="coherence", style="choice", palette=palette)
ax = sns.lineplot(data=c_op, hue="coherence", style="choice", palette=palette)
ax.set_title("Optimal consumption")
ax.set_xlabel("Period (t)")
ax.set_ylabel("$c^*_t$")
ax.legend(['C', 'C_op'], loc='mid', shadow=False)
plt.show()

```

# Results from the analysis

```python
#Printing the results from calculations above

print('Analytical result')
print('c* = {}'.format(c))

print('Numerical result')
print('c* = {}'.format(c_op))

```

    Analytical result
    c* = [3.18352381 2.22846667 1.55992667 1.09194867 0.76436407 0.53505485
     0.37453839 0.26217688]
    Numerical result
    c* = [3.08814661 2.15928281 1.51399323 1.05921944 0.74034874 0.51902058
     0.36328604 0.25423064 0.17795569 0.12451622]

# Conclusion

From the analysis above we see, that the analytical solution and the numerical solution are ...


# Sizing of a multi-rotor drone

*Written by Marc Budinger, Aitor Ochotorena (INSA Toulouse) and Scott Delbecq (ISAE-SUPAERO), Toulouse, France.*

The objective of this notebook is to select the best compromise of components (propeller, motor, ESC, battery) of a multi-rotor drone for given specifiations.

**Scipy** and **math** packages will be used for this notebook for the optimization algorithms of python.

**widgets** provides an interactive layout to display the results.

```python
import scipy
import scipy.optimize

from math import pi
from math import sqrt
from math import sin

import numpy as np
import pandas as pd

import ipywidgets as widgets
from ipywidgets import interactive
from IPython.display import display

pd.options.display.float_format = '{:,.2f}'.format
```

## Contents
1. [Sizing the propeller for vertical climb speed ](#section_1)
2. [Architecture defintion and design assumptions](#section_2)
3. [Sizing code](#section_3)
4. [Optimization problem](#section_4)

## Sizing propeller to vertical climb speed scenario

This exercise targets to minimize the propeller torque for vertical climb speed scenario. This Notebook shows the role of the design variables as well as the constraints in an optimization problem.

Let us say that $Q_{pro,cl}$ is the climb torque to minimize.

For the vertical climb, advance ratio ($J=\frac{V_{cl}}{n_{cl}   D_{pro}}$) is a key parameter to estimate the vertical speed.

Given that $D_{pro}$ and $V_{cl}$ are known from the specifications of the problem, we face a problem with one equation and two undetermined parameters.

**STRATEGY:** One of these parameters is going to be defined within a design range (e.g. J) and thus, the rest of calculations are done. This particular case is called **underconstraint singularity**.

Finally, estimated parameter is compared to the real value through the constraint.

The sizing code is defined here in a function which can give:
- an evaluation of the objective: here minimize the torque in climb scenario
- an evaluation of the constraints:

<math>\begin{align}
&\underset{\mathbf{x}}{\operatorname{minimize}}& & Q_{pro,cl}(\mathbf{x}) \\
&\operatorname{subject\ to}
& & 5\% \leq  V_{cl}-J n_{cl}   D_{pro} \leq 0 \\
\end{align}</math>

where it is expected that the product $J n_{cl}   D_{pro}$ calculated does not differ more than a 5\%.

Here is an non-exhaustive XDSM diagram of the whole multirotor sizing code:

More details in the setting up of sizing code can be found in the  [following paper](https://www.researchgate.net/profile/Marc_Budinger/publication/277933677_Computer-aided_definition_of_sizing_procedures_and_optimization_problems_of_mechatronic_systems/links/55969de508ae793d137c7ea5/Computer-aided-definition-of-sizing-procedures-and-optimization-problems-of-mechatronic-systems.pdf):

> Reysset, A., Budinger, M., & Maré, J. C. (2015). Computer-aided definition of sizing procedures and optimization problems of mechatronic systems. Concurrent Engineering, 23(4), 320-332.

## Architecture defintion and design assumptions

```python
Mtotal = 1.35 # [kg] mass of the drone

rho_air=1.18# [kg/m^3] Air density

C_D=1.3 #[-]drag coefficient

pitch=4.5*0.0254#[m] propeller pitch

Dpro=10*0.0254#[m] propeller diameter

k_maxthrust= 3 #Ratio max thrust-hover [-]

A_top=0.045 #[m^2] top surface

N_arm = 4 # [-] number of arm

N_pro_arm = 2 # [-] number of propeller per arm (1 or 2)

N_pro = N_pro_arm * N_arm # [-] Propellers number

V_cl = 5 # [m/s] max vertical speed

bounds=[(0.01,0.5)]#J
```

## Sizing code

```python
# -----------------------
# sizing code
# -----------------------
# inputs:
# - param: optimisation variables vector (reduction ratio, oversizing coefficient)
# - arg: selection of output
# output:
# - objective if arg='Obj', problem characteristics if arg='Prt', constraints other else

def SizingCode(param, arg):

# Design variables
# ---
    J = param[0] # advance ratio

# Hover, Climbing & Take-Off thrust
# ---

    F_pro_hov=Mtotal*(9.81)/N_pro # [N] Thrust per propeller for hover

    F_pro_to=F_pro_hov*(k_maxthrust) # [N] Max Thrust per propeller

    F_pro_cl=(Mtotal*9.81+0.5*rho_air*(C_D)*(A_top)*(V_cl)**2)/N_pro # [N] Thrust per propeller for climbing

# Propeller models
# ---

    # Statics APC MR

    beta=pitch/Dpro # [-] Ratio pitch-to-diameter

    C_t_sta=4.27e-02 + 1.44e-01 * beta # Thrust coef with T=C_T.rho.n^2.D^4
    C_p_sta=-1.48e-03 + 9.72e-02 * beta  # Power coef with P=C_p.rho.n^3.D^5

    # Dynamics APC MR

    C_t_dyn=0.02791-0.06543*J+0.11867*beta+0.27334*beta**2-0.28852*beta**3+0.02104*J**3-0.23504*J**2+0.18677*beta*J**2 # thrust coef for APC props in dynamics
    C_p_dyn=0.01813-0.06218*beta+0.00343*J+0.35712*beta**2-0.23774*beta**3+0.07549*beta*J-0.1235*J**2 # power coef for APC props in dynamics

# Propeller mass
# ---

    Dpro_ref=11*.0254 # [m] diameter
    Mpro_ref=0.53*0.0283 # [kg] mass

    Mpro=Mpro_ref*(Dpro/Dpro_ref)**3 # [kg] Propeller mass

#Rotational speed
# ---

    n_pro_hover=sqrt(F_pro_hov/(C_t_sta*(rho_air)*Dpro**4)) # [Hz] hover speed
    Wpro_hover=n_pro_hover*2*3.14 # [rad/s] Propeller speed

    n_pro_to=sqrt(F_pro_to/(C_t_sta*(rho_air)*Dpro**4)) # [Hz] climbing speed
    Wpro_to=n_pro_to*2*3.14 # [rad/s] Propeller speed

    n_pro_cl=sqrt(F_pro_cl/(C_t_dyn*(rho_air)*Dpro**4)) # [Hz] climbing speed
    Wpro_cl=n_pro_cl*2*3.14 # [rad/s] Propeller speed

# Propeller selection with take-off scenario

    Ppro_to=C_p_sta*(rho_air)*n_pro_to**3*Dpro**5# [W] Power per propeller
    Qpro_to=Ppro_to/Wpro_to # [N.m] Propeller torque

# Propeller torque& speed for hover

    Ppro_hover=C_p_sta*(rho_air)*n_pro_hover**3*Dpro**5# [W] Power per propeller
    Qpro_hover=Ppro_hover/Wpro_hover # [N.m] Propeller torque

# Propeller torque& speed for climbing

    Ppro_cl=C_p_sta*(rho_air)*n_pro_to**3*Dpro**5# [W] Power per propeller
    Qpro_cl=Ppro_cl/Wpro_cl # [N.m] Propeller torque

# Objective and Constraints sum up
# ---
    Qpro_cl # minimize torque in climbing

    constraints = [(-J*n_pro_cl*Dpro+V_cl),
                    (0.05+J*n_pro_cl*Dpro-V_cl)]

    # Run algorithm slsqp
    if arg == 'Obj':
        return Qpro_cl # for torque optimisation

     # Run algorithm differential evolution
    elif arg == 'ObjP':
        P = 0. # Penalisation nulle
        for C in constraints:
            if (C < 0.):
                P = P-1e9*C
        return Qpro_cl + P # for torque optimisation

    elif arg=='Prt':

        col_names = ['Type', 'Name', 'Value', 'Unit', 'Comment']

        df = pd.DataFrame()
        df = df.append([{'Type': 'Optimization', 'Name': 'J', 'Value': J, 'Unit': '[-]', 'Comment': 'advance ratio '}])[col_names]
        df = df.append([{'Type': 'Optimization', 'Name': 'Const 0', 'Value': constraints[0], 'Unit': '[-]', 'Comment': '(V_cl-J*n_pro_cl*Dpro)'}])[col_names]
        df = df.append([{'Type': 'Optimization', 'Name': 'Const 1',  'Value': constraints[1], 'Unit': '[-]', 'Comment': '-V_cl+J*n_pro_cl*Dpro+0.05'}])[col_names]
        df = df.append([{'Type': 'Optimization', 'Name': 'Objective',  'Value': Qpro_cl, 'Unit': '[kg]', 'Comment': 'Torque vertical climb'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'beta', 'Value': beta, 'Unit': '[-]', 'Comment': 'Ratio pitch to diameter'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'pitch', 'Value': pitch, 'Unit': '[m]', 'Comment': 'Pitch'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'F_pro_to', 'Value': F_pro_to, 'Unit': '[N]', 'Comment': 'Thrust for 1 propeller during Take Off'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'F_pro_cl', 'Value': F_pro_cl, 'Unit': '[N]', 'Comment': 'Thrust for 1 propeller during Climbing'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'F_pro_hov', 'Value': F_pro_hov, 'Unit': '[N]', 'Comment': 'Thrust for 1 propeller during Hover'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'rho_air', 'Value': rho_air, 'Unit': '[kg/m^3]', 'Comment': 'Air density'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'Dpro_ref', 'Value': Dpro_ref, 'Unit': '[m]', 'Comment': 'Reference propeller diameter'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'M_pro_ref', 'Value': Mpro_ref, 'Unit': '[kg]', 'Comment': 'Reference propeller mass'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'C_t_sta', 'Value': C_t_sta, 'Unit': '[-]', 'Comment': 'Static thrust coefficient of the propeller'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'C_p_sta', 'Value': C_p_sta, 'Unit': '[-]', 'Comment': 'Static power coefficient of the propeller'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'C_t_dyn', 'Value': C_t_dyn, 'Unit': '[-]', 'Comment': 'Dynamic thrust coefficient of the propeller'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'C_p_dyn', 'Value': C_p_dyn, 'Unit': '[-]', 'Comment': 'Dynamic power coefficient of the propeller'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'D_pro', 'Value': Dpro, 'Unit': '[m]', 'Comment': 'Diameter of the propeller'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'n_pro_to', 'Value': n_pro_to, 'Unit': '[Hz]', 'Comment': 'Rev speed of the propeller during takeoff'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'n_pro_hov', 'Value': n_pro_hover, 'Unit': '[Hz]', 'Comment': 'Rev speed of the propeller during hover'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'n_pro_cl', 'Value': n_pro_cl, 'Unit': '[Hz]', 'Comment': 'Rev speed of the propeller during climb'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'P_pro_to', 'Value': Ppro_to, 'Unit': '[W]', 'Comment': 'Power on the mechanical shaft of the propeller during takeoff'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'P_pro_hov', 'Value': Ppro_hover, 'Unit': '[W]', 'Comment': 'Power on the mechanical shaft of the propeller during hover'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'P_pro_cl', 'Value': Ppro_cl, 'Unit': '[W]', 'Comment': 'Power on the mechanical shaft of the propeller during climb'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'M_pro', 'Value': Mpro, 'Unit': '[kg]', 'Comment': 'Mass of the propeller'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'Omega_pro_to', 'Value': Wpro_to, 'Unit': '[rad/s]', 'Comment': 'Rev speed of the propeller during takeoff'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'Omega_pro_hov', 'Value': Wpro_hover, 'Unit': '[rad/s]', 'Comment': 'Rev speed of the propeller during hover'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'Omega_pro_cl', 'Value': Wpro_cl, 'Unit': '[rad/s]', 'Comment': 'Rev speed of the propeller during climb'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'Qpro_hov', 'Value': Qpro_hover, 'Unit': '[N.m]', 'Comment': 'Torque on the mechanical shaft of the propeller during hover'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'Qpro_to', 'Value': Qpro_to, 'Unit': '[N.m]', 'Comment': 'Torque on the mechanical shaft of the propeller during takeoff'}])[col_names]
        df = df.append([{'Type': 'Propeller', 'Name': 'Qpro_cl', 'Value': Qpro_cl, 'Unit': '[N.m]', 'Comment': 'Torque on the mechanical shaft of the propeller during climb'}])[col_names]
        df = df.append([{'Type': 'Specifications', 'Name': 'M_total', 'Value': Mtotal, 'Unit': '[kg]', 'Comment': 'Total mass'}])[col_names]
        df = df.append([{'Type': 'Specifications', 'Name': 'N_pro', 'Value': N_pro, 'Unit': '[-]', 'Comment': 'Number of propellers '}])[col_names]
        df = df.append([{'Type': 'Specifications', 'Name': 'N_pro_arm', 'Value': N_pro_arm, 'Unit': '[-]', 'Comment': 'Number of propellers per arm '}])[col_names]

        items = sorted(df['Type'].unique().tolist())
        def f(Type):
            return df[df['Type']==Type]
        widgets.interact(f, Type=items)
        return f

    else:
        return constraints

```

## Optimization problem

We will now use the [optimization algorithms](https://docs.scipy.org/doc/scipy/reference/optimize.html) of the Scipy package to solve and optimize the configuration. We can use the SLSQP algorithm without explicit expression of the gradient (Jacobian) or a differential evolution optimization to have a more exhaustive calculation. A course on Multidisplinary Gradient optimization algorithms and gradient optimization algorithm is given [here](http://mdolab.engin.umich.edu/sites/default/files/Martins-MDO-course-notes.pdf):
> Joaquim R. R. A. Martins (2012). A Short Course on Multidisciplinary Design Optimization. University of Michigan

The first step is to give an initial value of optimisation variables:

```python
# Optimisation variables
J = .01 # advance ratio

# Vector of parameters
parameters = np.array([J])
```

We can print of the characterisitcs of the problem before optimization with the initial vector of optimization variables. Through the droplist menu, an interactive display of the parameters grouped by their type of variable is given:

```python
# Initial characteristics before optimization
print("-----------------------------------------------")
print("Initial characteristics before optimization :")
SizingCode(parameters,'Prt')
print("-----------------------------------------------")
```

    -----------------------------------------------
    Initial characteristics before optimization :

    interactive(children=(Dropdown(description='Type', options=('Optimization', 'Propeller', 'Specifications'), va…

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

Then we can solve the problem and print of the optimized solution:

```python
# Optimization with SLSQP algorithm
contrainte = lambda x: SizingCode(x, 'Const')
objectif = lambda x: SizingCode(x, 'Obj')
objectifP = lambda x: SizingCode(x, 'ObjP')

SLSQP = False # Optimization algorithm choice

if SLSQP == True:
    # SLSQP omptimisation
    result = scipy.optimize.fmin_slsqp(func=objectif, x0=parameters,
                                   bounds=[(0.01,0.5)],
                                   f_ieqcons=contrainte,
                                   iter=1500, acc=1e-12)
else:
    # Differential evolution omptimisation
    result = scipy.optimize.differential_evolution(func=objectifP,
                                   bounds=[(0.01,0.5)],
                                   tol=1e-12)

# Final characteristics after optimization
print("-----------------------------------------------")
print("Final characteristics after optimization :")

if SLSQP == True:
    SizingCode(result,'Obj')
    SizingCode(result, 'Prt')
else:
    SizingCode(result.x,'Obj')
    SizingCode(result.x, 'Prt')
print("-----------------------------------------------")

```

    -----------------------------------------------
    Final characteristics after optimization :

    interactive(children=(Dropdown(description='Type', options=('Optimization', 'Propeller', 'Specifications'), va…

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

```python
parameters.shape

```

    (1,)

```python

```


# Introduction

The next step is to provide some information about the mass and inertia of the bodies involved. Each of the three rigid bodies have both a mass which resists linear accelerations and inertia which resists rotational accelerations. In this notebook we will specify the mass of the three bodies, the inertia tensor/dyadic, and also create three `RigidBody` objects that hold all of the necessary information for each rigid body.

# Setup

First, we will import the results from the previous notebook. Even if you didn't get everything correctly working, the following import statement will bring in the correct solution so you can move forward. We will do this in all of the subsquent notebooks.

```python
from __future__ import print_function, division
from solution.kinematics import *
```

We will also need the function for easily generating inertial quantities and the `RigigBody` class so we can create some rigid bodies.

```python
from sympy.physics.mechanics import inertia, RigidBody
```

We will need to specify some constants for the mass and inertia values.

```python
from sympy import symbols
```

Once again, initalize SymPy printing so that we get nicely renderd symbols.

```python
from sympy.physics.vector import init_vprinting
init_vprinting(use_latex='mathjax', pretty_print=False)
```

# Mass

The masses of each rigid body can be represented by constant values, so we create a symbol for each body.

```python
lower_leg_mass, upper_leg_mass, torso_mass = symbols('m_L, m_U, m_T')
```

```python
lower_leg_mass
```

```python
upper_leg_mass
```

```python
torso_mass
```

# Inertia

Since we are studying a 2D planar problem, we are only concerned about the rotational inertia about the $\hat{i}_z$ axis. We will assume that the rigid bodies are symmetric about the $XZ$ and $YZ$ planes, so we only need a single variable for each rigid body to specify the rotation inertia.

```python
lower_leg_inertia, upper_leg_inertia, torso_inertia = symbols('I_Lz, I_Uz, I_Tz')
```

The `inertia()` function is a convenience function for creating inertia dyadics (i.e. basis dependent tensors). You specify a reference frame to define the inertia with respect to and at a minimum for symmetric bodies provide the diagonal entries of the inertia tensor. In our case the rotational inertia about the $x$ and $y$ are not neeed so they are set to zero and $z$ inertia entry is set to the defined variable.

```python
lower_leg_inertia_dyadic = inertia(lower_leg_frame, 0, 0, lower_leg_inertia)
```

```python
lower_leg_inertia_dyadic
```

In general, we store the inertia as dyadics, i.e. basis dependent tensors. If you want to see what the inertia is expressed in a particular frame, use the `to_matrix()` method.

```python
lower_leg_inertia_dyadic.to_matrix(lower_leg_frame)
```

We will also eventually need to know what point the inertia is defined with respect to. In our case, we will simply define all inertia's about the mass center. We can store the total information needed by PyDy in a tuple of an inertia `Dyadic` and a `Point`.

```python
lower_leg_central_inertia = (lower_leg_inertia_dyadic, lower_leg_mass_center)
```

The upper leg and torso inertias are found in the same fashion.

```python
upper_leg_inertia_dyadic = inertia(upper_leg_frame, 0, 0, upper_leg_inertia)
upper_leg_inertia_dyadic.to_matrix(upper_leg_frame)
```

```python
upper_leg_central_inertia = (upper_leg_inertia_dyadic, upper_leg_mass_center)
```

## Exercise

Create a tuple of an inertia `Dyadic` and `Point` for the torso.

```python
torso_inertia_dyadic =
```

```python
torso_central_inertia =
```

```python
%load exercise_solutions/n04_inertia_inertia-dyadic.py
```

# Rigid Bodies

To completely define a rigid body, the mass center point, the reference frame, the mass, and the inertia defined about a point must be specified.

```python
lower_leg = RigidBody('Lower Leg', lower_leg_mass_center, lower_leg_frame,
                      lower_leg_mass, lower_leg_central_inertia)
```

## Exercise

Create RigidBody objects for the upper leg and torso

```python
upper_leg =
```

```python
torso =
```

```python
%load exercise_solutions/n04_inertia_define-rigid-body.py
```


## The SIR model with two age groups

The partitioning of the population can be refined to include other attributes relevant to the disease. One of the most important of these is the age. Let us assume we partition the population into two age groups, children and adults, and label them by the index $i=1,2$. Children can catch the infection from other children or from adults; likewise, adults can catch the infection from other adults or from children. Calling their respective rates of infection $\lambda_1(t)$ and $\lambda_2(t)$ we get
\begin{align}
\lambda_1(t) = \beta(C_{11}\frac{I_1}{N_1} + C_{12}\frac{I_2}{N_2})S_1\\
\lambda_2(t) = \beta(C_{21}\frac{I_1}{N_1} + C_{22}\frac{I_2}{N_2})S_2
\end{align}
where $C_{ij}$ are contact matrices, quantifying how much each age group interacts with the other. The ordinary differential equations of this age-structured SIR model are

\begin{align}
\dot S_i &= -\lambda_i(t)S_i \\
\dot I_i &= \lambda(t)_iI_i - \gamma I_i \\
\dot R_i &= \gamma I_i
\end{align}

Again, for each $i$ the sum $N_i = S_i + I_i + R_i$ remains constant. What do we expect qualitatively ? The group that has a greater rate will catch the disease faster and catch more of it. This depends on how the entries of the contact matrix are distributed.

This example creates stochastic trajectories for the above equations for both the children and the adults. We see that they have unequal rates of infection.

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

```python
M  = 2                  # the population has two age groups
N  =  100000           # and this is the total population

beta  = 0.0131          # infection rate
gIa   = 1./7            # removal rate of asymptomatic infectives
gIs   = 1./7            # removal rate of asymptomatic infectives
alpha = 0               # fraction of asymptomatic infectives
fsa   = 1               # the self-isolation parameter

Ni = np.zeros((M))      # population in each group
fi = np.zeros((M))      # fraction of population in age age group

# set the age structure
fi = np.array((0.25, 0.75))
for i in range(M):
    Ni[i] = fi[i]*N

# set the contact structure
C = np.array(([18., 9.], [3., 12.]))

Ia_0 = np.array((5,5))  # each age group has asymptomatic infectives
Is_0 = np.array((5,5))  # and also symptomatic infectives
R_0  = np.array((0,0))  # there are no removed individuals initially
S_0  = Ni - (Ia_0 + Is_0 + R_0)
```

```python
# duration of simulation and data file
Tf=200; Nf=Tf+1

# the contact structure is independent of time
def contactMatrix(t):
    return C

# instantiate model
parameters = {'alpha':alpha, 'beta':beta, 'gIa':gIa, 'gIs':gIs,'fsa':fsa}
model = pyross.stochastic.SIR(parameters, M, Ni)

# simulate model
data=model.simulate(S_0, Ia_0, Is_0, contactMatrix, Tf, Nf)
```

```python
IK = data.get('X')[:,2*M].flatten()
IA = data.get('X')[:,2*M+1].flatten()
t  = data.get('t')
```

```python
fig = plt.figure(num=None, figsize=(10, 8), dpi=80, facecolor='w', edgecolor='k')
plt.rcParams.update({'font.size': 22})

plt.fill_between(t, 0, IK/Ni[0], color="#348ABD", alpha=0.3)
plt.plot(t, IK/Ni[0], '-', color="#348ABD", label='$Children$', lw=4)

plt.fill_between(t, 0, IA/Ni[1], color='#A60628', alpha=0.3)
plt.plot(t, IA/Ni[1], '-', color='#A60628', label='$Adults$', lw=4)

plt.legend(fontsize=26); plt.grid()
plt.xlabel(r'time') ; plt.ylabel(r'fraction of infectives')
plt.autoscale(enable=True, axis='x', tight=True)
```

```python
# run 100 simulations, calculate mean and standard deviation

N_runs = 100
trajectories_s = np.zeros([N_runs,Nf,3*len(S_0)],dtype=float)
for i in range(N_runs):
    print('Running simulation {0} of {1}'.format(i+1,N_runs),end='\r')
    cur_result = model.simulate(S_0, Ia_0, Is_0, contactMatrix, Tf, Nf,
                               method='tau-leaping')
    trajectories_s[i] = cur_result['X']
t_s = cur_result['t']
mean_s = np.mean(trajectories_s,axis=0)
std_s = np.std(trajectories_s,axis=0)
```

    Running simulation 100 of 100

```python
# plot mean and standard deviation

# Children
S_s  = mean_s[:,0].flatten()
Ia_s = mean_s[:,M].flatten()
Is_s = mean_s[:,2*M].flatten()
dS_s = std_s[:,0].flatten()
dIa_s = std_s[:,M].flatten()
dIs_s = std_s[:,2*M].flatten()
# for the removed, we still need to calculate mean and variance
R_s_trajectories = Ni[0] - trajectories_s[:,:,0] - trajectories_s[:,:,M] - trajectories_s[:,:,2*M]
R_s = np.mean(R_s_trajectories,axis=0)
dR_s = np.std(R_s_trajectories,axis=0)

# Adults
S_s_  = mean_s[:,1].flatten()
Ia_s_ = mean_s[:,M+1].flatten()
Is_s_ = mean_s[:,2*M+1].flatten()
dS_s_ = std_s[:,1].flatten()
dIa_s_ = std_s[:,M+1].flatten()
dIs_s_ = std_s[:,2*M+1].flatten()
# for the removed, we still need to calculate mean and variance
R_s_trajectories_ = Ni[1] - trajectories_s[:,:,1] - trajectories_s[:,:,1+M] - trajectories_s[:,:,1+2*M]
R_s_ = np.mean(R_s_trajectories,axis=0)
dR_s_ = np.std(R_s_trajectories,axis=0)

fig = plt.figure(num=None, figsize=(10, 8), dpi=80, facecolor='w', edgecolor='k')
plt.rcParams.update({'font.size': 22})

# children
#plt.fill_between(t_s,(S_s - dS_s)/Ni[0],(S_s + dS_s)/Ni[0],color="#348ABD", alpha=0.2)
#plt.plot(t_s, S_s/Ni[0], '-', color="#348ABD", label=r'$\langle S\rangle$', lw=4)
plt.fill_between(t_s,(Is_s - dIs_s)/Ni[0],(Is_s + dIs_s)/Ni[0],color="#348ABD", alpha=0.2)
plt.plot(t_s, Is_s/Ni[0], '-', color="#348ABD", label=r'Children', lw=4)
#plt.fill_between(t_s,(R_s - dR_s)/Ni[0],(R_s + dR_s)/Ni[0],color="dimgrey", alpha=0.2)
#plt.plot(t_s, R_s/Ni[0], '-', color="dimgrey", label=r'$\langle R\rangle $', lw=4)

# adults
plt.fill_between(t_s,(Is_s_ - dIs_s_)/Ni[1],(Is_s_ + dIs_s_)/Ni[1],color="#A60628", alpha=0.2)
plt.plot(t_s, Is_s_/Ni[1], '-', color="#A60628", label=r'Adults', lw=4)

plt.legend(fontsize=26,loc='upper right'); plt.grid()
plt.xlabel(r'time') ; plt.ylabel(r'fraction of infectives')
plt.autoscale(enable=True, axis='x', tight=True)
```

```python

```

```python

```


```python
from IPython.display import Image
from IPython.core.display import HTML
from sympy import *; x,h = symbols("x h")
Image(url= "https://i.imgur.com/6Jq28eO.png")
```

```python
# https://www.onlinemath4all.com/how-to-find-equation-of-tangent-to-the-curve.html
# screenshot,upload,copy to clipboard all at once with ShareX (not sponsored)
Image(url= "https://i.imgur.com/cdZ7reJ.png")
```

```python
expr0 = 2*x*cos(x)
x0 = pi
y0 = -2*pi
# m = derivative of expr0
expr0
```

$\displaystyle 2 x \cos{\left(x \right)}$

```python
#okay now we find the first derivative of the curve stored as a Function of x
def F(x):
    return 2*x*cos(x)   #not using lambdify here cause loop function errors
f(x)
```

$\displaystyle 2 x \cos{\left(x \right)}$

```python
def dF(x):    #d means d/dx of F(x)
    return limit(  (( F(x+h)-F(x) ) / h)  ,h,0)
dF(x)
```

$\displaystyle - 2 x \sin{\left(x \right)} + 2 \cos{\left(x \right)}$

```python
dF = diff(F(x))  #does the same thing in less code
dF
```

$\displaystyle - 2 x \sin{\left(x \right)} + 2 \cos{\left(x \right)}$

```python
# now that we have both variables we need  (x0,y0) and dF
# we can move on
Image(url= "https://i.imgur.com/DV4ROWu.png")
```

```python
#now we insert dF into point slope form with x0,y0
EofT = y0 + dF*(x - x0)
EofT
```

$\displaystyle \left(x - \pi\right) \left(- 2 x \sin{\left(x \right)} + 2 \cos{\left(x \right)}\right) - 2 \pi$

```python
simplify(EofT)   # simplify does some magic here and gives us our first correct answer m = -2
```

$\displaystyle - 2 \left(x - \pi\right) \left(x \sin{\left(x \right)} - \cos{\left(x \right)}\right) - 2 \pi$

```python
simplify??
```

```python
# from here it is just trial and error trying to find a sympy function that gives us b
# finding where we made the error proves to be difficult here.
# is handwriting this better here?
# gut tells me b solution is simple since somehow we found m.
# maybe we have dF correct but made some error when doing point slope form?
#now we insert dF into point slope form with x0,y0
#EofT2 = y0 + dF*(x - x0)
#EofT2
#
dF = diff(F(x))  #does the same thing in less code
dF
# correct answer found by guessing 0, looking here at the original dF, it kind of looked like a point-slope for
```

$\displaystyle - 2 x \sin{\left(x \right)} + 2 \cos{\left(x \right)}$

```python
Image(url= "https://i.imgur.com/57Du8iT.png")
```

```python

```


```python
%matplotlib inline
```

Stochastic Volatility
=====================

Generative model:

\begin{align}\begin{align}
        \sigma & \sim \text{Exponential}(50) \\
        \nu & \sim \text{Exponential}(.1) \\
        s_i & \sim \text{Normal}(s_{i-1}, \sigma^{- 2}) \\
        r_i & \sim \text{StudentT}(\nu, 0, \exp(s_i))
    \end{align}\end{align}

This example is from PyMC3 [1], which itself is adapted from the original experiment
from [2]. A discussion about translating this in Pyro appears in [3].

We take this example to illustrate how to use the functional interface `hmc`. However,
we recommend readers to use `MCMC` class as in other examples because it is more stable
and has more features supported.

**References:**

    1. *Stochastic Volatility Model*, https://docs.pymc.io/notebooks/stochastic_volatility.html
    2. *The No-U-Turn Sampler: Adaptively Setting Path Lengths in Hamiltonian Monte Carlo*,
       https://arxiv.org/pdf/1111.4246.pdf
    3. Pyro forum discussion, https://forum.pyro.ai/t/problems-transforming-a-pymc3-model-to-pyro-mcmc/208/14

```python
import argparse
import os

import matplotlib
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as onp

import jax.numpy as np
import jax.random as random

import numpyro
import numpyro.distributions as dist
from numpyro.examples.datasets import SP500, load_dataset
from numpyro.infer.mcmc import hmc
from numpyro.infer.util import initialize_model
from numpyro.util import fori_collect

matplotlib.use('Agg')  # noqa: E402

def model(returns):
    step_size = numpyro.sample('sigma', dist.Exponential(50.))
    s = numpyro.sample('s', dist.GaussianRandomWalk(scale=step_size, num_steps=np.shape(returns)[0]))
    nu = numpyro.sample('nu', dist.Exponential(.1))
    return numpyro.sample('r', dist.StudentT(df=nu, loc=0., scale=np.exp(s)),
                          obs=returns)

def print_results(posterior, dates):
    def _print_row(values, row_name=''):
        quantiles = [0.2, 0.4, 0.5, 0.6, 0.8]
        row_name_fmt = '{:>8}'
        header_format = row_name_fmt + '{:>12}' * 5
        row_format = row_name_fmt + '{:>12.3f}' * 5
        columns = ['(p{})'.format(q * 100) for q in quantiles]
        q_values = onp.quantile(values, quantiles, axis=0)
        print(header_format.format('', *columns))
        print(row_format.format(row_name, *q_values))
        print('\n')

    print('=' * 20, 'sigma', '=' * 20)
    _print_row(posterior['sigma'])
    print('=' * 20, 'nu', '=' * 20)
    _print_row(posterior['nu'])
    print('=' * 20, 'volatility', '=' * 20)
    for i in range(0, len(dates), 180):
        _print_row(np.exp(posterior['s'][:, i]), dates[i])

def main(args):
    _, fetch = load_dataset(SP500, shuffle=False)
    dates, returns = fetch()
    init_rng_key, sample_rng_key = random.split(random.PRNGKey(args.rng_seed))
    init_params, potential_fn, constrain_fn = initialize_model(init_rng_key, model, model_args=(returns,))
    init_kernel, sample_kernel = hmc(potential_fn, algo='NUTS')
    hmc_state = init_kernel(init_params, args.num_warmup, rng_key=sample_rng_key)
    hmc_states = fori_collect(args.num_warmup, args.num_warmup + args.num_samples, sample_kernel, hmc_state,
                              transform=lambda hmc_state: constrain_fn(hmc_state.z),
                              progbar=False if "NUMPYRO_SPHINXBUILD" in os.environ else True)
    print_results(hmc_states, dates)

    fig, ax = plt.subplots(1, 1)
    dates = mdates.num2date(mdates.datestr2num(dates))
    ax.plot(dates, returns, lw=0.5)
    # format the ticks
    ax.xaxis.set_major_locator(mdates.YearLocator())
    ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
    ax.xaxis.set_minor_locator(mdates.MonthLocator())

    ax.plot(dates, np.exp(hmc_states['s'].T), 'r', alpha=0.01)
    legend = ax.legend(['returns', 'volatility'], loc='upper right')
    legend.legendHandles[1].set_alpha(0.6)
    ax.set(xlabel='time', ylabel='returns', title='Volatility of S&P500 over time')

    plt.savefig("stochastic_volatility_plot.pdf")
    plt.tight_layout()

if __name__ == "__main__":
    assert numpyro.__version__.startswith('0.2.4')
    parser = argparse.ArgumentParser(description="Stochastic Volatility Model")
    parser.add_argument('-n', '--num-samples', nargs='?', default=600, type=int)
    parser.add_argument('--num-warmup', nargs='?', default=600, type=int)
    parser.add_argument('--device', default='cpu', type=str, help='use "cpu" or "gpu".')
    parser.add_argument('--rng_seed', default=21, type=int, help='random number generator seed')
    args = parser.parse_args()

    numpyro.set_platform(args.device)

    main(args)
```