# Homework 3

## Imports

```python
import nbtools
nbtools.setup_nb()
```

```python
from astropy import units
import itertools
import sympy
from sympy.diffgeom import Manifold, Patch

from pystein import coords, metric, curvature
from pystein.utilities import tensor_pow as tpow
```

## Exercises

### B7

```python
M = Manifold('M', dim=4)
P = Patch('origin', M)

t, x, y, z = sympy.symbols('t, x y z')
phi = sympy.Function('phi')(x, y, z)
cs = coords.CoordSystem('cartesian', P, [t, x, y, z])

dt, dx, dy, dz = cs.base_oneforms()
ds2 = -(1 + 2 * phi) * tpow(dt, 2) + (1 - 2 * phi) * (tpow(dx, 2) + tpow(dy, 2) + tpow(dz, 2))
g = metric.Metric(twoform=ds2)
g
```

$\displaystyle \left(1 - 2 \phi{\left(x,y,z \right)}\right) \left(\operatorname{d}x \otimes \operatorname{d}x + \operatorname{d}y \otimes \operatorname{d}y + \operatorname{d}z \otimes \operatorname{d}z\right) + \left(- 2 \phi{\left(x,y,z \right)} - 1\right) \operatorname{d}t \otimes \operatorname{d}t$

```python
g.matrix
```

$\displaystyle \left[\begin{matrix}- 2 \phi{\left(x,y,z \right)} - 1 & 0 & 0 & 0\\0 & 1 - 2 \phi{\left(x,y,z \right)} & 0 & 0\\0 & 0 & 1 - 2 \phi{\left(x,y,z \right)} & 0\\0 & 0 & 0 & 1 - 2 \phi{\left(x,y,z \right)}\end{matrix}\right]$

```python
g.inverse.matrix
```

$\displaystyle \left[\begin{matrix}\frac{1}{- 2 \phi{\left(x,y,z \right)} - 1} & 0 & 0 & 0\\0 & \frac{1}{1 - 2 \phi{\left(x,y,z \right)}} & 0 & 0\\0 & 0 & \frac{1}{1 - 2 \phi{\left(x,y,z \right)}} & 0\\0 & 0 & 0 & \frac{1}{1 - 2 \phi{\left(x,y,z \right)}}\end{matrix}\right]$

```python
_phi = sympy.symbols('phi')
_subs = [
    (phi, _phi)
]
```

```python
sympy.simplify(curvature.christoffel_symbol_component(1, 0, 0, g).doit()).subs(_subs)
```

$\displaystyle - \frac{\frac{d}{d x} \phi}{2 \phi - 1}$

```python
sympy.simplify(curvature.einstein_tensor_component(1, 2, g).doit()).subs(_subs)
```

$\displaystyle \frac{2 \left(- 8 \phi^{3} \frac{d^{2}}{d yd x} \phi + 12 \phi^{2} \frac{d}{d x} \phi \frac{d}{d y} \phi + 4 \phi \frac{d}{d x} \phi \frac{d}{d y} \phi + 2 \phi \frac{d^{2}}{d yd x} \phi + \frac{d}{d x} \phi \frac{d}{d y} \phi\right)}{16 \phi^{4} - 8 \phi^{2} + 1}$

```python
sympy.simplify(sum([curvature.ricci_tensor_component(mu, mu, g).doit() for mu in range(4)])).subs(_subs)
```

$\displaystyle \frac{- \left(2 \phi - 1\right)^{2} \left(2 \phi + 1\right) \left(\frac{d^{2}}{d x^{2}} \phi + \frac{d^{2}}{d y^{2}} \phi + \frac{d^{2}}{d z^{2}} \phi\right) + \left(2 \phi - 1\right)^{2} \left(\left(\frac{d}{d x} \phi\right)^{2} + \left(\frac{d}{d y} \phi\right)^{2} + \left(\frac{d}{d z} \phi\right)^{2}\right) - 5 \left(2 \phi - 1\right) \left(2 \phi + 1\right)^{2} \left(\frac{d^{2}}{d x^{2}} \phi + \frac{d^{2}}{d y^{2}} \phi + \frac{d^{2}}{d z^{2}} \phi\right) + 5 \left(2 \phi + 1\right)^{2} \left(\left(\frac{d}{d x} \phi\right)^{2} + \left(\frac{d}{d y} \phi\right)^{2} + \left(\frac{d}{d z} \phi\right)^{2}\right)}{\left(2 \phi - 1\right)^{2} \left(2 \phi + 1\right)^{2}}$

```python
riccis = [((mu, nu), sympy.simplify(curvature.ricci_tensor_component(mu, nu, g).doit()))
          for mu, nu in itertools.product(range(4), range(4))]
```

```python
sympy.simplify(curvature.ricci_scalar(g).doit()).subs(_subs)
```

$\displaystyle \frac{2 \left(24 \phi^{3} \frac{d^{2}}{d x^{2}} \phi + 24 \phi^{3} \frac{d^{2}}{d y^{2}} \phi + 24 \phi^{3} \frac{d^{2}}{d z^{2}} \phi - 12 \phi^{2} \left(\frac{d}{d x} \phi\right)^{2} + 4 \phi^{2} \frac{d^{2}}{d x^{2}} \phi - 12 \phi^{2} \left(\frac{d}{d y} \phi\right)^{2} + 4 \phi^{2} \frac{d^{2}}{d y^{2}} \phi - 12 \phi^{2} \left(\frac{d}{d z} \phi\right)^{2} + 4 \phi^{2} \frac{d^{2}}{d z^{2}} \phi - 8 \phi \left(\frac{d}{d x} \phi\right)^{2} - 6 \phi \frac{d^{2}}{d x^{2}} \phi - 8 \phi \left(\frac{d}{d y} \phi\right)^{2} - 6 \phi \frac{d^{2}}{d y^{2}} \phi - 8 \phi \left(\frac{d}{d z} \phi\right)^{2} - 6 \phi \frac{d^{2}}{d z^{2}} \phi - 5 \left(\frac{d}{d x} \phi\right)^{2} - \frac{d^{2}}{d x^{2}} \phi - 5 \left(\frac{d}{d y} \phi\right)^{2} - \frac{d^{2}}{d y^{2}} \phi - 5 \left(\frac{d}{d z} \phi\right)^{2} - \frac{d^{2}}{d z^{2}} \phi\right)}{32 \phi^{5} - 16 \phi^{4} - 16 \phi^{3} + 8 \phi^{2} + 2 \phi - 1}$

```python
sympy.simplify(curvature.einstein_tensor_component(0, 0, g).doit()).subs(_subs)
```

$\displaystyle \frac{8 \phi^{2} \frac{d^{2}}{d x^{2}} \phi + 8 \phi^{2} \frac{d^{2}}{d y^{2}} \phi + 8 \phi^{2} \frac{d^{2}}{d z^{2}} \phi - 6 \phi \left(\frac{d}{d x} \phi\right)^{2} - 6 \phi \left(\frac{d}{d y} \phi\right)^{2} - 6 \phi \left(\frac{d}{d z} \phi\right)^{2} - 3 \left(\frac{d}{d x} \phi\right)^{2} - 2 \frac{d^{2}}{d x^{2}} \phi - 3 \left(\frac{d}{d y} \phi\right)^{2} - 2 \frac{d^{2}}{d y^{2}} \phi - 3 \left(\frac{d}{d z} \phi\right)^{2} - 2 \frac{d^{2}}{d z^{2}} \phi}{8 \phi^{3} - 12 \phi^{2} + 6 \phi - 1}$

```python

```

```python
sympy.simplify(g.inverse.matrix[0,0] * curvature.ricci_tensor_component(0, 0, g).doit()).subs(_subs)
```

$\displaystyle \frac{4 \phi^{2} \frac{d^{2}}{d x^{2}} \phi + 4 \phi^{2} \frac{d^{2}}{d y^{2}} \phi + 4 \phi^{2} \frac{d^{2}}{d z^{2}} \phi + 2 \left(\frac{d}{d x} \phi\right)^{2} - \frac{d^{2}}{d x^{2}} \phi + 2 \left(\frac{d}{d y} \phi\right)^{2} - \frac{d^{2}}{d y^{2}} \phi + 2 \left(\frac{d}{d z} \phi\right)^{2} - \frac{d^{2}}{d z^{2}} \phi}{16 \phi^{4} - 8 \phi^{2} + 1}$

```python
curvature.ricci_tensor_component(0, 0, g).doit()
```

$\displaystyle \frac{\frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)}}{1 - 2 \phi{\left(x,y,z \right)}} + \frac{\frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)}}{1 - 2 \phi{\left(x,y,z \right)}} + \frac{\frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)}}{1 - 2 \phi{\left(x,y,z \right)}} + \frac{\left(\frac{\partial}{\partial x} \phi{\left(x,y,z \right)}\right)^{2}}{\left(1 - 2 \phi{\left(x,y,z \right)}\right) \left(- 2 \phi{\left(x,y,z \right)} - 1\right)} + \frac{\left(\frac{\partial}{\partial y} \phi{\left(x,y,z \right)}\right)^{2}}{\left(1 - 2 \phi{\left(x,y,z \right)}\right) \left(- 2 \phi{\left(x,y,z \right)} - 1\right)} + \frac{\left(\frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)^{2}}{\left(1 - 2 \phi{\left(x,y,z \right)}\right) \left(- 2 \phi{\left(x,y,z \right)} - 1\right)} - \frac{\left(\frac{\partial}{\partial x} \phi{\left(x,y,z \right)}\right)^{2}}{\left(1 - 2 \phi{\left(x,y,z \right)}\right)^{2}} - \frac{\left(\frac{\partial}{\partial y} \phi{\left(x,y,z \right)}\right)^{2}}{\left(1 - 2 \phi{\left(x,y,z \right)}\right)^{2}} - \frac{\left(\frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)^{2}}{\left(1 - 2 \phi{\left(x,y,z \right)}\right)^{2}}$

```python
curvature.display_components(riccis)
```

$\begin{split} (\mu=0, \nu=0) &: \frac{- 4 \phi^{2}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} - 4 \phi^{2}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} - 4 \phi^{2}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)} - 2 \left(\frac{\partial}{\partial x} \phi{\left(x,y,z \right)}\right)^{2} + \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} - 2 \left(\frac{\partial}{\partial y} \phi{\left(x,y,z \right)}\right)^{2} + \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} - 2 \left(\frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)^{2} + \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)}}{8 \phi^{3}{\left(x,y,z \right)} - 4 \phi^{2}{\left(x,y,z \right)} - 2 \phi{\left(x,y,z \right)} + 1} \\ (\mu=0, \nu=1) &: 0 \\ (\mu=0, \nu=2) &: 0 \\ (\mu=0, \nu=3) &: 0 \\ (\mu=1, \nu=0) &: 0 \\ (\mu=1, \nu=1) &: \frac{- 24 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} - 8 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} - 8 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)} + 24 \phi^{2}{\left(x,y,z \right)} \left(\frac{\partial}{\partial x} \phi{\left(x,y,z \right)}\right)^{2} - 4 \phi^{2}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} - 4 \phi^{2}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} - 4 \phi^{2}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)} + 12 \phi{\left(x,y,z \right)} \left(\frac{\partial}{\partial x} \phi{\left(x,y,z \right)}\right)^{2} + 6 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} + 4 \phi{\left(x,y,z \right)} \left(\frac{\partial}{\partial y} \phi{\left(x,y,z \right)}\right)^{2} + 2 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} + 4 \phi{\left(x,y,z \right)} \left(\frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)^{2} + 2 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)} + 4 \left(\frac{\partial}{\partial x} \phi{\left(x,y,z \right)}\right)^{2} + \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} + 2 \left(\frac{\partial}{\partial y} \phi{\left(x,y,z \right)}\right)^{2} + \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} + 2 \left(\frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)^{2} + \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)}}{16 \phi^{4}{\left(x,y,z \right)} - 8 \phi^{2}{\left(x,y,z \right)} + 1} \\ (\mu=1, \nu=2) &: \frac{2 \left(- 8 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y\partial x} \phi{\left(x,y,z \right)} + 12 \phi^{2}{\left(x,y,z \right)} \frac{\partial}{\partial x} \phi{\left(x,y,z \right)} \frac{\partial}{\partial y} \phi{\left(x,y,z \right)} + 4 \phi{\left(x,y,z \right)} \frac{\partial}{\partial x} \phi{\left(x,y,z \right)} \frac{\partial}{\partial y} \phi{\left(x,y,z \right)} + 2 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y\partial x} \phi{\left(x,y,z \right)} + \frac{\partial}{\partial x} \phi{\left(x,y,z \right)} \frac{\partial}{\partial y} \phi{\left(x,y,z \right)}\right)}{16 \phi^{4}{\left(x,y,z \right)} - 8 \phi^{2}{\left(x,y,z \right)} + 1} \\ (\mu=1, \nu=3) &: \frac{2 \left(- 8 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z\partial x} \phi{\left(x,y,z \right)} + 12 \phi^{2}{\left(x,y,z \right)} \frac{\partial}{\partial x} \phi{\left(x,y,z \right)} \frac{\partial}{\partial z} \phi{\left(x,y,z \right)} + 4 \phi{\left(x,y,z \right)} \frac{\partial}{\partial x} \phi{\left(x,y,z \right)} \frac{\partial}{\partial z} \phi{\left(x,y,z \right)} + 2 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z\partial x} \phi{\left(x,y,z \right)} + \frac{\partial}{\partial x} \phi{\left(x,y,z \right)} \frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)}{16 \phi^{4}{\left(x,y,z \right)} - 8 \phi^{2}{\left(x,y,z \right)} + 1} \\ (\mu=2, \nu=0) &: 0 \\ (\mu=2, \nu=1) &: \frac{2 \left(- 8 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y\partial x} \phi{\left(x,y,z \right)} + 12 \phi^{2}{\left(x,y,z \right)} \frac{\partial}{\partial x} \phi{\left(x,y,z \right)} \frac{\partial}{\partial y} \phi{\left(x,y,z \right)} + 4 \phi{\left(x,y,z \right)} \frac{\partial}{\partial x} \phi{\left(x,y,z \right)} \frac{\partial}{\partial y} \phi{\left(x,y,z \right)} + 2 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y\partial x} \phi{\left(x,y,z \right)} + \frac{\partial}{\partial x} \phi{\left(x,y,z \right)} \frac{\partial}{\partial y} \phi{\left(x,y,z \right)}\right)}{16 \phi^{4}{\left(x,y,z \right)} - 8 \phi^{2}{\left(x,y,z \right)} + 1} \\ (\mu=2, \nu=2) &: \frac{- 8 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} - 24 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} - 8 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)} - 4 \phi^{2}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} + 24 \phi^{2}{\left(x,y,z \right)} \left(\frac{\partial}{\partial y} \phi{\left(x,y,z \right)}\right)^{2} - 4 \phi^{2}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} - 4 \phi^{2}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)} + 4 \phi{\left(x,y,z \right)} \left(\frac{\partial}{\partial x} \phi{\left(x,y,z \right)}\right)^{2} + 2 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} + 12 \phi{\left(x,y,z \right)} \left(\frac{\partial}{\partial y} \phi{\left(x,y,z \right)}\right)^{2} + 6 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} + 4 \phi{\left(x,y,z \right)} \left(\frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)^{2} + 2 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)} + 2 \left(\frac{\partial}{\partial x} \phi{\left(x,y,z \right)}\right)^{2} + \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} + 4 \left(\frac{\partial}{\partial y} \phi{\left(x,y,z \right)}\right)^{2} + \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} + 2 \left(\frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)^{2} + \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)}}{16 \phi^{4}{\left(x,y,z \right)} - 8 \phi^{2}{\left(x,y,z \right)} + 1} \\ (\mu=2, \nu=3) &: \frac{2 \left(- 8 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z\partial y} \phi{\left(x,y,z \right)} + 12 \phi^{2}{\left(x,y,z \right)} \frac{\partial}{\partial y} \phi{\left(x,y,z \right)} \frac{\partial}{\partial z} \phi{\left(x,y,z \right)} + 4 \phi{\left(x,y,z \right)} \frac{\partial}{\partial y} \phi{\left(x,y,z \right)} \frac{\partial}{\partial z} \phi{\left(x,y,z \right)} + 2 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z\partial y} \phi{\left(x,y,z \right)} + \frac{\partial}{\partial y} \phi{\left(x,y,z \right)} \frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)}{16 \phi^{4}{\left(x,y,z \right)} - 8 \phi^{2}{\left(x,y,z \right)} + 1} \\ (\mu=3, \nu=0) &: 0 \\ (\mu=3, \nu=1) &: \frac{2 \left(- 8 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z\partial x} \phi{\left(x,y,z \right)} + 12 \phi^{2}{\left(x,y,z \right)} \frac{\partial}{\partial x} \phi{\left(x,y,z \right)} \frac{\partial}{\partial z} \phi{\left(x,y,z \right)} + 4 \phi{\left(x,y,z \right)} \frac{\partial}{\partial x} \phi{\left(x,y,z \right)} \frac{\partial}{\partial z} \phi{\left(x,y,z \right)} + 2 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z\partial x} \phi{\left(x,y,z \right)} + \frac{\partial}{\partial x} \phi{\left(x,y,z \right)} \frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)}{16 \phi^{4}{\left(x,y,z \right)} - 8 \phi^{2}{\left(x,y,z \right)} + 1} \\ (\mu=3, \nu=2) &: \frac{2 \left(- 8 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z\partial y} \phi{\left(x,y,z \right)} + 12 \phi^{2}{\left(x,y,z \right)} \frac{\partial}{\partial y} \phi{\left(x,y,z \right)} \frac{\partial}{\partial z} \phi{\left(x,y,z \right)} + 4 \phi{\left(x,y,z \right)} \frac{\partial}{\partial y} \phi{\left(x,y,z \right)} \frac{\partial}{\partial z} \phi{\left(x,y,z \right)} + 2 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z\partial y} \phi{\left(x,y,z \right)} + \frac{\partial}{\partial y} \phi{\left(x,y,z \right)} \frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)}{16 \phi^{4}{\left(x,y,z \right)} - 8 \phi^{2}{\left(x,y,z \right)} + 1} \\ (\mu=3, \nu=3) &: \frac{- 8 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} - 8 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} - 24 \phi^{3}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)} - 4 \phi^{2}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} - 4 \phi^{2}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} + 24 \phi^{2}{\left(x,y,z \right)} \left(\frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)^{2} - 4 \phi^{2}{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)} + 4 \phi{\left(x,y,z \right)} \left(\frac{\partial}{\partial x} \phi{\left(x,y,z \right)}\right)^{2} + 2 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} + 4 \phi{\left(x,y,z \right)} \left(\frac{\partial}{\partial y} \phi{\left(x,y,z \right)}\right)^{2} + 2 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} + 12 \phi{\left(x,y,z \right)} \left(\frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)^{2} + 6 \phi{\left(x,y,z \right)} \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)} + 2 \left(\frac{\partial}{\partial x} \phi{\left(x,y,z \right)}\right)^{2} + \frac{\partial^{2}}{\partial x^{2}} \phi{\left(x,y,z \right)} + 2 \left(\frac{\partial}{\partial y} \phi{\left(x,y,z \right)}\right)^{2} + \frac{\partial^{2}}{\partial y^{2}} \phi{\left(x,y,z \right)} + 4 \left(\frac{\partial}{\partial z} \phi{\left(x,y,z \right)}\right)^{2} + \frac{\partial^{2}}{\partial z^{2}} \phi{\left(x,y,z \right)}}{16 \phi^{4}{\left(x,y,z \right)} - 8 \phi^{2}{\left(x,y,z \right)} + 1} \end{split}$

```python

```

## Problem

### C4

```python
x, y, z = sympy.symbols('x y z')
G, M, c = sympy.symbols('G M c')
```

```python
r = sympy.sqrt(x**2 + y**2 + z**2)
phi = - G * M / (c**2 * r)
phi
```

$\displaystyle - \frac{G M}{c^{2} \sqrt{x^{2} + y^{2} + z^{2}}}$

```python
sympy.Derivative(phi, x).doit()
```

$\displaystyle \frac{G M x}{c^{2} \left(x^{2} + y^{2} + z^{2}\right)^{\frac{3}{2}}}$

```python
sympy.simplify(sympy.Derivative(x / (r ** 2), x).doit())
```

$\displaystyle \frac{- x^{2} + y^{2} + z^{2}}{\left(x^{2} + y^{2} + z^{2}\right)^{2}}$

```python
sympy.simplify(sympy.Derivative(sympy.Derivative(phi, x), x).doit() / phi)
```

$\displaystyle \frac{2 x^{2} - y^{2} - z^{2}}{\left(x^{2} + y^{2} + z^{2}\right)^{2}}$

```python
dx = sympy.Derivative(phi, x).doit()
dy = sympy.Derivative(phi, y).doit()
dz = sympy.Derivative(phi, z).doit()

dxx = sympy.simplify(sympy.Derivative(dx, x).doit())
dxy = sympy.simplify(sympy.Derivative(dy, x).doit())
dxz = sympy.simplify(sympy.Derivative(dz, x).doit())

A_x = sympy.simplify((dxx * dx + dxy * dy + dxz * dz) / (phi ** 2))
A_x
```

$\displaystyle - \frac{2 x}{\left(x^{2} + y^{2} + z^{2}\right)^{2}}$

```python

```

#### Plugin Numbers

```python
G = 6.7e-11
M = 6e24 # kg
R_E = 6.4e6 # m
Alt_Orb = 400e3 # m
R = R_E + Alt_Orb
c = 3e8
```

```python
A_norm = 2 * G**2 * M**2 / (c**4 * R**5)
A_norm
```

    2.7444322835953827e-39

```python
V = 1 / A_norm
V
```

    3.643740842058365e+38

```python
(V * units.meter**3).to(units.AU**3).value #/ (45 ** 3)
```

    108835.66731507442

```python

```

```python
tau = (c * R ** 5 / (2 * G ** 2 * M ** 2)) ** (1/3)
tau
```

    23808.273641190328

```python
tau / (60 * 60)
```

    6.613409344775091

```python

```


# Section 4.7 $\quad$ Homogeneous Systems

Consider the homogeneous system
\begin{equation*}
  A\mathbf{x} = \mathbf{0},
\end{equation*}
where $A$ is $m\times n$, The set of all solutions

### Example 1

Find a basis of the solution space of the linear system
\begin{equation*}
   \left[
    \begin{array}{ccccc}
      1 & 1 & 4 & 1 & 2  \\
      0 & 1 & 2 & 1 & 1  \\
      0 & 0 & 0 & 1 & 2  \\
      1 & -1 &0 & 0 & 2  \\
      2 & 1 & 6 & 0 & 1  \\
    \end{array}
  \right]
  \left[
    \begin{array}{c}
      x_1 \\x_2 \\x_3 \\x_4 \\x_5 \\
    \end{array}
  \right] =
  \left[
    \begin{array}{c}
      0 \\0\\0 \\0 \\0 \\
    \end{array}
  \right]
\end{equation*}

```python
from sympy import *

x1, x2, x3, x4, x5 = symbols('x1 x2 x3 x4 x5');
x = Matrix([x1, x2, x3, x4, x5]);
A = Matrix([[1, 1, 4, 1, 2], [0, 1, 2, 1, 1], [0, 0, 0, 1, 2], [1, -1, 0, 0, 2], [2, 1, 6, 0, 1]]);
Ax = A.dot(x);

solve(Ax)
```

    {x2: -2*x3 + x5, x4: -2*x5, x1: -2*x3 - x5}

**Remark** For the homogeneous system $A\mathbf{x} = \mathbf{0}$, if the reduced row echelon form of $[A~|~\mathbf{0}]$ has $r$ nonzero rows, then
-
-

## Relationship between Homogeneous and Nonhomogeneous Systems

For nonhomogeneous system $A\mathbf{x} = \mathbf{b}$, the set of solutions

### Example 2

Consider the linear system
\begin{equation*}
   \left[
    \begin{array}{ccc}
      1 & 2 & -3 \\
      2 & 4 & -6 \\
      3 & 6 & -9 \\
    \end{array}
  \right]
  \left[
    \begin{array}{c}
      x_1 \\x_2 \\x_3
    \end{array}
  \right] =
  \left[
    \begin{array}{c}
      2 \\4\\6  \\
    \end{array}
  \right]
\end{equation*}


# Gaussian Process (GP) smoothing

This example deals with the case when we want to **smooth** the observed data points $(x_i, y_i)$ of some 1-dimensional function $y=f(x)$, by finding the new values $(x_i, y'_i)$ such that the new data is more "smooth" (see more on the definition of smoothness through allocation of variance in the model description below) when moving along the $x$ axis.

It is important to note that we are **not** dealing with the problem of interpolating the function $y=f(x)$ at the unknown values of $x$. Such problem would be called "regression" not "smoothing", and will be considered in other examples.

If we assume the functional dependency between $x$ and $y$ is **linear** then, by making the independence and normality assumptions about the noise, we can infer a straight line that approximates the dependency between the variables, i.e. perform a linear regression. We can also fit more complex functional dependencies (like quadratic, cubic, etc), if we know the functional form of the dependency in advance.

However, the **functional form** of $y=f(x)$ is **not always known in advance**, and it might be hard to choose which one to fit, given the data. For example, you wouldn't necessarily know which function to use, given the following observed data. Assume you haven't seen the formula that generated it:

```python
%pylab inline
figsize(12, 6);
```

    Populating the interactive namespace from numpy and matplotlib

```python
import numpy as np
import scipy.stats as stats

x = np.linspace(0, 50, 100)
y = (np.exp(1.0 + np.power(x, 0.5) - np.exp(x/15.0)) +
     np.random.normal(scale=1.0, size=x.shape))

plot(x, y);
xlabel("x");
ylabel("y");
title("Observed Data");
```

### Let's try a linear regression first

As humans, we see that there is a non-linear dependency with some noise, and we would like to capture that dependency. If we perform a linear regression, we see that the "smoothed" data is less than satisfactory:

```python
plot(x, y);
xlabel("x");
ylabel("y");

lin = stats.linregress(x, y)
plot(x, lin.intercept + lin.slope * x);
title("Linear Smoothing");
```

### Linear regression model recap

The linear regression assumes there is a linear dependency between the input $x$ and output $y$, sprinkled with some noise around it so that for each observed data point we have:

$$ y_i = a + b\, x_i + \epsilon_i $$

where the observation errors at each data point satisfy:

$$ \epsilon_i \sim N(0, \sigma^2) $$

with the same $\sigma$, and the errors are independent:

$$ cov(\epsilon_i, \epsilon_j) = 0 \: \text{ for } i \neq j $$

The parameters of this model are $a$, $b$, and $\sigma$. It turns out that, under these assumptions, the maximum likelihood estimates of $a$ and $b$ don't depend on $\sigma$. Then $\sigma$ can be estimated separately, after finding the most likely values for $a$ and $b$.

### Gaussian Process smoothing model

This model allows departure from the linear dependency by assuming that the dependency between $x$ and $y$ is a Brownian motion over the domain of $x$. This doesn't go as far as assuming a particular functional dependency between the variables. Instead, by **controlling the standard deviation of the unobserved Brownian motion** we can achieve different levels of smoothness of the recovered functional dependency at the original data points.

The particular model we are going to discuss assumes that the observed data points are **evenly spaced** across the domain of $x$, and therefore can be indexed by $i=1,\dots,N$ without the loss of generality. The model is described as follows:

\begin{equation}
\begin{aligned}
z_i & \sim \mathcal{N}(z_{i-1} + \mu, (1 - \alpha)\cdot\sigma^2) \: \text{ for } i=2,\dots,N \\
z_1 & \sim ImproperFlat(-\infty,\infty) \\
y_i & \sim \mathcal{N}(z_i, \alpha\cdot\sigma^2)
\end{aligned}
\end{equation}

where $z$ is the hidden Brownian motion, $y$ is the observed data, and the total variance $\sigma^2$ of each ovservation is split between the hidden Brownian motion and the noise in proportions of $1 - \alpha$ and $\alpha$ respectively, with parameter $0 < \alpha < 1$ specifying the degree of smoothing.

When we estimate the maximum likelihood values of the hidden process $z_i$ at each of the data points, $i=1,\dots,N$, these values provide an approximation of the functional dependency $y=f(x)$ as $\mathrm{E}\,[f(x_i)] = z_i$ at the original data points $x_i$ only. Therefore, again, the method is called smoothing and not regression.

### Let's describe the above GP-smoothing model in PyMC3

```python
import pymc3 as pm
from theano import shared
from pymc3.distributions.timeseries import GaussianRandomWalk
from scipy import optimize
```

Let's create a model with a shared parameter for specifying different levels of smoothing. We use very wide priors for the "mu" and "tau" parameters of the hidden Brownian motion, which you can adjust according to your application.

```python
LARGE_NUMBER = 1e5

model = pm.Model()
with model:
    smoothing_param = shared(0.9)
    mu = pm.Normal("mu", sd=LARGE_NUMBER)
    tau = pm.Exponential("tau", 1.0/LARGE_NUMBER)
    z = GaussianRandomWalk("z",
                           mu=mu,
                           tau=tau / (1.0 - smoothing_param),
                           shape=y.shape)
    obs = pm.Normal("obs",
                    mu=z,
                    tau=tau / smoothing_param,
                    observed=y)
```

Let's also make a helper function for inferring the most likely values of $z$:

```python
def infer_z(smoothing):
    with model:
        smoothing_param.set_value(smoothing)
        res = pm.find_MAP(vars=[z], fmin=optimize.fmin_l_bfgs_b)
        return res['z']
```

Please note that in this example, we are only looking at the MAP estimate of the unobserved variables. We are not really interested in inferring the posterior distributions. Instead, we have a control parameter $\alpha$ which lets us allocate the variance between the hidden Brownian motion and the noise. Other goals and/or different models may require sampling to obtain the posterior distributions, but for our goal a MAP estimate will suffice.

### Exploring different levels of smoothing

Let's try to allocate 50% variance to the noise, and see if the result matches our expectations.

```python
smoothing = 0.5
z_val = infer_z(smoothing)

plot(x, y);
plot(x, z_val);
title("Smoothing={}".format(smoothing));
```

It appears that the variance is split evenly between the noise and the hidden process, as expected.

Let's try gradually increasing the smoothness parameter to see if we can obtain smoother data:

```python
smoothing = 0.9
z_val = infer_z(smoothing)

plot(x, y);
plot(x, z_val);
title("Smoothing={}".format(smoothing));
```

### Smoothing "to the limits"

By increading the smoothing parameter, we can gradually make the inferred values of the hidden Brownian motion approach the average value of the data. This is because as we increase the smoothing parameter, we allow less and less of the variance to be allocated to the Brownian motion, so eventually it aproaches the process which almost doesn't change over the domain of $x$:

```python
fig, axes = subplots(2, 2)

for ax, smoothing in zip(axes.ravel(), [0.95, 0.99, 0.999, 0.9999]):

    z_val = infer_z(smoothing)

    ax.plot(x, y)
    ax.plot(x, z_val)
    ax.set_title('Smoothing={:05.4f}'.format(smoothing))
```

This example originally contributed by: Andrey Kuzmenko, http://github.com/akuz


### Diagramas de cortante y momento

**Ejemplo 01.** Dibuje los diagramas de cortante y momento para la viga mostrada en la figura.

Por simetría podemos ver que ambas reacciones son de igual magnitud y equivalente a la mitad de la carga aplicada, es decir:

$R_A = R_B = 7\,kN$

Para resolver utilizando SymPy, primero importamos los módulos a utilizar:

```python
from __future__ import division
from sympy import *
from sympy.plotting import *
from sympy.functions.special.singularity_functions import *
init_printing(use_latex="mathjax")
x = symbols("x")
```

Estableciendo la función de carga e integrando para obtener las expresiones para el cortante y momento flexionante:

```python
P = 14e3
RA = P/2
RB = P/2
w = -RA*SingularityFunction(x,0,-1) + P*SingularityFunction(x,1.5,-1) - RB*SingularityFunction(x,3,-1)
V = integrate(-w,x)
M = integrate(V,x)
```

Graficando el diagrama de cortante:

```python
plot(V, (x,0,3), xlabel="L (m)", ylabel="V (N)");
```

Graficando el diagrama de momentos:

```python
plot(M, (x,0,3), xlabel="L (m)", ylabel="M (N*m)");
```


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

### O Metódo de Euler Explicito ou Forward Euler

#### Expansão em Taylor de uma função y

Expansão em Série de Taylor de $y(t)$ centrada em $t_0$ é dada por
$$
y(t) = \sum_{n=0}^{\infty} \frac{y^{(n)}(t_0)}{n!}(t-t_0)^n
$$
##### Expansão de y até a primeira derivada
Seja $h = t_n - t_{n-1}$
$$
y(t_{k+1}) = y(t_k) + y'(t_k)h + \mathcal{O}(h^2)\\
$$
O metódo de Euler explicito é um método recursivo de solução de equações diferenciais ordinárias, e consiste em utilizar a aproximação por Taylor e ignorar o erro $\mathcal{O}(h^2)$, nos dando:

$$
y_{n+1} \approx u_{n+1} = u_n + f(u_n,t_n) \cdot (t_{n+1} - t_n)
$$
Com $y_n = y(t_n)$ a solução analitica no ponto $t_n$ e $u_n$ a aproximação númerica, $f(a,b)$ a derivada de $a$ em $b$

```python
## F: Função derivada da função que queremos encontrar
## t0: tempo inicial
## y0: ponto inicial
## ts: range de tempo
def f_euler(F, y0, ts):
    ys = [y0]
    t = ts[0]
    for tnext in ts:
        ynext = ys[-1] + F(ys[-1],t)*(tnext-t)
        ys.append(ynext)
        t = tnext
    return np.array(ys[:-1])
```

### O metódo de Runge-Kutta de segunda ordem
Enquanto o metódo de Euler aproxima a solução em um ponto andando na tangente daquele ponto, o metódo de Runge-Kutta de segunda ordem aproxima o mesmo ponto andando na média entre a tangente no ponto e a tangente no ponto futuro.

Seja
$$
k_1 = f(u_n,t_n)\\
k_2 = f(u_n + hk_1,t_{n+1})
$$
Então $k_1$ é a derivada no ponto e $k_2$ é a derivada no ponto futuro, aproximando a mesma pelo metódo de Euler

O passo para Runge-Kutta será então a média entre estas duas, e ficará:
$$
y_{n+1} \approx u_{n+1} = u_n + h \frac{k_1 + k_2}{2}
$$

```python
def rk_2(F, y0, ts):
    ys = [y0]
    t = ts[0]
    h = h = ts[1] - ts[0]
    for tnext in ts:
        k1 = F(ys[-1], t)
        k2 = F(ys[-1] + h*k1,tnext)
        ynext = ys[-1] + h * (k1+k2) / 2.0
        ys.append(ynext)
        t = tnext
    return np.array(ys[:-1])
```

Testando para a EDO
$$ \begin{cases} y'(t) = - y(t) + 2\sin(t^2) \\ y(0) = 1.2\end{cases} $$

```python
def F(y,t):
    return -y + 2*np.sin(t**2)
```

```python
## Definindo o dominio
ts = np.linspace(-5,5,500)
y0 = 1.2
## Criando a lista para Runge-Kutta 2nd order
ys = rk_2(F,y0,ts)

## Criando a lista para Euler Explicito
ys2 = f_euler(F,y0,ts)

#ans = f(y,ts)

plt.plot(ts,ys,label='RK')
plt.plot(ts,ys2,label='Explicito')
plt.legend()
plt.show()
```

### O metódo de Euler converge - Um pouco de Análise

### Definições:
Seja $\frac{\mathrm{d}y}{\mathrm{d}t} = f(y,t)$

Seja $t \in \mathbb{N}$ o número de 'tempos' no dominio e $t^*$ o tempo final e $\lfloor \cdot \rfloor$ a função `floor`, que retorna a parte inteira.

Seja $h \in \mathbb{R}, h > 0$, o 'tamanho' de cada partição, ou seja $h = t_{n+1} - t_n$

Podemos então definir $n$, tal que $n$ assume valores no conjunto $\{0, \dots , \lfloor \frac{t^*}{h} \rfloor\}$

Seja $\lVert \cdot \rVert$ uma norma definida no espaço

Seja $y_n$ o valor real (analitico) da função **$y$** no ponto $t_n$, ou seja $y_n = y(t_n)$

Seja $u_n$ o valor númerico aproximado da função $y$ no ponto $t_n$ pelo método de Euler, ou seja $u_{n+1} = u_{n} + f(u_{n},t_{n})\cdot h$

Uma função $f$ é dita Lipschitz se satisfaz a condição de Lipschitz: $\exists M \in \mathbb{R}; \lVert f(x_1) - f(x_2) \rVert \leq M\cdot\lVert x_1 - x_2\rVert$

Um metódo é dito convergente se:
$$
\lim_{h\to 0^+} \max_{n=0, \dots , \lfloor \frac{t^*}{h} \rfloor} \lVert u_n - y_n \rVert = 0
$$

Ou seja, sempre que a malha for refinida, a solução númerica em um ponto se aproxima da solução analitica neste ponto

### Teorema: O metódo de Euler converge

#### Prova

Tomemos $f(y,t)$ analitica, ou seja, pode ser representada pela série de Taylor centrada em um ponto $t_0$ e é Lipschitz.

$f(y,t)$ analitica implica $y$ analitica.

Vamos definir $err_n = u_n - y_n$, nosso erro númerico, então queremos provar
$$
\lim_{h\to 0^+} \max_{n=0, \dots , \lfloor \frac{t^*}{h} \rfloor} \lVert err_n \rVert = 0
$$
Expandindo nossa solução $y$ da equação diferencial por Taylor:
$$
y_{n+1} = y_n + hf(y_n,t_n)+\mathcal{O}(h^2) \tag{1}
$$
Como $y$ é analitica, então sua derivada é contínua, logo pelo `Teorema do Valor Extremo`, dado uma vizinhança em torno de $t_n$ o termo $\mathcal{O}(h^2)$ é limitado $\forall h>0$ e $n \leq \lfloor t^*/h \rfloor$ por $M>0, M \in \mathbb{R}$, e pela propriedade arquimediana do corpo do reais $\exists c \in \mathbb{R}, c>0; c\cdot h^2 \geq M$, portanto podemos limitar $\mathcal{O}(h^2)$ por $ch^2, c>0$.

Agora vamos fazer $err_{n+1} = u_{n+1} - y_{n+1}$ usando a expansão em Taylor $y_{n+1}$ e Euler em $u_{n+1}$
$$
\begin{align}
err_{n+1} &= u_{n+1} - y_{n+1}\\
&= u_n + h(f(u_n,t_n)) - y_n - h(f(yn,tn) + \mathcal{O}(h^2)\\
&= \underbrace{u_n - y_n}_{err_n} + h\left(f(u_n,t_n) - f(y_n,t_n)\right) + \mathcal{O}(h^2)\\
&= err_n + h\left(f(u_n,t_n) - f(y_n,t_n)\right) + \mathcal{O}(h^2)\\
\end{align}
$$
Daqui podemos perceber que o erro no passo seguinte depende também do erro anterior já cometido

E segue do fato de que $\mathcal{O}(h^2)$ é limitada, com uma cota superior $ch^2$ e da desigualdade triangular
$$
\lVert err_{n+1} \rVert \leq \lVert err_n\rVert + \lVert h\left(f(u_n,t_n) - f(y_n,t_n)\right)\rVert + \lVert ch^2 \rVert
$$
E pela condição de Lipschitz
$$
\lVert f(u_n,t_n) - f(y_n,t_n) \rVert \leq \lambda\lVert u_n - y_n \rVert = \lambda\lVert err_n \rVert, \lambda > 0
$$
Então temos
$$
\lVert err_{n+1} \rVert \leq \lVert err_n\rVert + \lVert h\left(f(u_n,t_n) - f(y_n,t_n)\right)\rVert + \lVert ch^2 \rVert \leq \lVert err_n\rVert + \lambda h\lVert err_n \rVert + ch^2\\$$
$\therefore$
$$
\lVert err_{n+1} \rVert \leq  (1+h\lambda)\lVert err_n \rVert + ch^2 \tag{2}
$$

---
Agora vamos propor:
$$
\lVert err_n \rVert \leq \frac{c}{\lambda}h[(1+h\lambda)^n - 1]
$$
#### Demonstração: Indução em n
Para $n = 0$
$$
\lVert err_0 \rVert \leq \frac{c}{\lambda}h[(1+h\lambda)^0 - 1] = \frac{c}{\lambda}h[1 - 1] = 0\\
err_0 = u_0 - y_0 = 0, \text{pois é a condição inicial}
$$
Temos portanto nossa hipotese de indução, vale para $n=k$, vamos para o passo indutivo: $n = k+1$. Da equação 2, temos:
$$
\lVert err_{k+1}\rVert \leq (1+h\lambda)\lVert err_k \rVert + ch^2
$$
E pela hipotese de indução
$$
\lVert err_k \rVert\leq \frac{c}{\lambda}h[(1+h\lambda)^k - 1]
$$
Logo
$$
\lVert err_{k+1} \rVert \leq (1+h\lambda)\frac{c}{\lambda}h[(1+h\lambda)^k - 1] + ch^2
$$
Desenvolvendo o termo da direita:
$$
\begin{align}
(1+h\lambda)\frac{c}{\lambda}h[(1+h\lambda)^k - 1] + ch^2 &= \frac{c}{\lambda}h[(1+h\lambda)^{k+1} - (1+h\lambda)] +ch^2\\
&= \frac{c}{\lambda}h(1+h\lambda)^{k+1} - \frac{c}{\lambda}h(1+h\lambda) +ch^2\\
&= \frac{c}{\lambda}h(1+h\lambda)^{k+1} - \frac{c}{\lambda}h - \frac{c}{\lambda}h^2\lambda + ch^2\\
&= \frac{c}{\lambda}h(1+h\lambda)^{k+1} - \frac{c}{\lambda}h\\
&= \frac{c}{\lambda}h[(1+h\lambda)^{k+1} - 1]
\end{align}
$$
Portanto
$$
\lVert err_{k+1} \rVert \leq \frac{c}{\lambda}h[(1+h\lambda)^{k+1} - 1]
$$
E o passo indutivo vale. Logo pelo principio de indução finita temos:
$$
\lVert err_n \rVert \leq \frac{c}{\lambda}h[(1+h\lambda)^n - 1] \tag{3}
$$

---
Como $h\lambda >0$, então temos $(1+h\lambda) < e^{h\lambda}$ e portanto $(1+h\lambda)^n < e^{nh\lambda}$, e n assume valor máximo em $n = \lfloor t^*/h \rfloor $, portanto:
$$(1+h\lambda)^n < e^{\lfloor t^*/h \rfloor h\lambda} \leq e^{t^*\lambda}$$
Substituindo na inequação 3 para $err_n$, teremos:
$$
\lVert err_n \rVert \leq \frac{c}{\lambda}h[e^{t^*\lambda} - 1]
$$
Passando o limite $h\to 0$, teremos:
$$
\lim_{h\to 0}\lVert err_n \rVert \leq \frac{c}{\lambda}h[e^{t^*\lambda} - 1] = 0\\
\therefore
\lim_{h\to 0}\lVert err_n \rVert = 0
$$
Portanto o Metódo de Euler converge para toda função Lipschitz. Q.E.D.

### Visualizando o teorema

Vamos plotar a solução da equação diferencial $y' = sin(t^2) - y$ com um refinamento da malha cada vez melhor e visualizar a convergência do metódo

Plotaremos também um gráfico com a evolução do erro relativo entre a solução de malha mais fina e todas as soluções anteriores

```python
## Equação Diferencial
def F(y,t):
    return -y + 2*np.sin(t**2)

# Criação dos dominios com vários h diferentes
ts = np.array([np.linspace(-10,10,i) for i in np.arange(50,300,63)])

# Condição inicial
y0 = 1.2
# Preparação da listas para plotagem
ys_e = np.array([f_euler(F,y0,i) for i in ts ])
# Estilo das curvas
lstyle = ['--','-.',':','-']

# Plot do gráfico de solução
plt.figure(figsize=(15,7))
for i in range(len(ts)):
    plt.plot(ts[i],ys_e[i], ls = lstyle[i], label='$h = '+ str("{0:.2f}".format(20.0/len(ts[i])) +'$'))
plt.title('Visualização da convergência do Metódo de Euler')
plt.xlabel('t')
plt.ylabel('y(t)')
plt.legend()
plt.show()

## Criando os arrays de erro
hs = [0.4,0.18,0.11]
ans = [[],[],[]]
for i in range(len(ys_e[:-1])):
    n = np.floor(hs[i]/0.08)
    for j in range(len(ys_e[i])):
        try: ans[i].append(ys_e[-1][n*j])
        except: ans[i].append(ys_e[-1][-1])
for i in range(len(ans)):
    ans[i] = np.array(ans[i])

err = np.array([abs(j - i) for i,j in zip(ys_e,ans)])
plt.figure(figsize=(15,7))
for i in range(len(ts)-1):
    plt.plot(ts[i],err[i], ls = lstyle[i], label='$h = '+ str("{0:.2f}".format(20.0/len(ts[i])) +'$'))
plt.title('Visualização do Erro da solução mais convergida em relação às outras soluções')
plt.xlabel('t')
plt.ylabel('err(y)')
plt.legend()
plt.show()
```

### Gráficos de Convergência de Runge-Kutta

```python
# Preparação da listas para plotagem
ys_rk = np.array([rk_2(F,y0,i) for i in ts ])
# Estilo das curvas
lstyle = ['--','-.',':','-']

# Plot do gráfico de solução
plt.figure(figsize=(15,7))
for i in range(len(ts)):
    plt.plot(ts[i],ys_rk[i], ls = lstyle[i], label='$h = '+ str("{0:.2f}".format(20.0/len(ts[i])) +'$'))
plt.title('Visualização da convergência de Runge-Kutta')
plt.xlabel('t')
plt.ylabel('y(t)')
plt.legend()
plt.show()
```

```python
plt.plot(ts[-1],abs(ys_e[-1]-ys_rk[-1]))
```

##### Teorema: O metódo de Euler converge

###### Prova

Tomemos $f(y,t)$ analitica, ou seja, pode ser representada pela série de Taylor centrada em um ponto $t_0$ e é Lipschitz continua.

$f(y,t)$ analitica implica $y$ analitica.

Vamos definir $err_n = u_n - y_n$, nosso erro númerico, então queremos provar
$$
\lim_{h\to 0^+} \max_{n=0, \dots , \lfloor \frac{t^*}{h} \rfloor} \lVert err_n \rVert = 0
$$
Expandindo nossa solução $y$ da equação diferencial por Taylor:
$$
y_{n+1} = y_n + hf(y_n,t_n)+\mathcal{O}(h^2)
$$
Como $y$ é analitica, então sua derivada é contínua, logo pelo `Teorema do Valor Extremo`, dado uma vizinhança em torno de $t_n$ o termo $\mathcal{O}(h^2)$ é limitado $\forall h>0$ e $n \leq \lfloor t^*/h \rfloor$ por $M>0, M \in \mathbb{R}$, e pela propriedade arquimediana do corpo do reais $\exists c \in \mathbb{R}, c>0; c\cdot h^2 \geq M$, portanto podemos limitar $\mathcal{O}(h^2)$ por $ch^2, c>0$.

Agora vamos fazer $err_{n+1} = u_{n+1} - y_{n+1}$ usando a expansão em Taylor e Euler em $u_n$
$$
\begin{align}
err_{n+1} &= u_{n+1} - y_{n+1}\\
&= u_n + h(f(u_n,t_n)) - y_n - h(f(yn,tn) + \mathcal{O}(h^2)\\
&= \underbrace{u_n - y_n}_{err_n} + h\left(f(u_n,t_n) - f(y_n,t_n)\right) + \mathcal{O}(h^2)\\
&= err_n + h\left(f(u_n,t_n) - f(y_n,t_n)\right) + \mathcal{O}(h^2)\\
\end{align}
$$
Daqui podemos perceber que o erro no passo seguinte depende também do erro anterior já cometido

E segue do limite superior para $\mathcal{O}(h^2)$ e da desigualdade triangular
$$
\lVert err_{n+1} \rVert \leq \lVert err_n\rVert + \lVert h\left(f(u_n,t_n) - f(y_n,t_n)\right)\rVert + \lVert ch^2 \rVert
$$
E pela condição de Lipschitz
$$
\lVert f(u_n,t_n) - f(y_n,t_n) \rVert \leq \lambda\lVert u_n - y_n \rVert = \lambda\lVert err_n \rVert, \lambda > 0
$$
Então temos
$$
\lVert err_{n+1} \rVert \leq \lVert err_n\rVert + \lVert h\left(f(u_n,t_n) - f(y_n,t_n)\right)\rVert + \lVert ch^2 \rVert \leq \lVert err_n\rVert + \lambda h\lVert err_n \rVert + ch^2\\
\therefore
\lVert err_{n+1} \rVert \leq \lVert err_n\rVert + \lVert h\left(f(u_n,t_n) - f(y_n,t_n)\right)\rVert + \lVert ch^2 \rVert \leq  (1+h\lambda)\lVert err_n \rVert + ch^2
$$

Assumiremos (provar mais tarde)
$$
\lVert err_n \rVert \leq \frac{c}{\lambda}h[(1+h\lambda)^n - 1]
$$
Como $h\lambda >0$, então temos $(1+h\lambda) < e^{h\lambda}$ e portanto $(1+h\lambda)^n < e^{nh\lambda}$, e n assume valor máximo em $n = \lfloor t^*/h \rfloor $, portanto: $(1+h\lambda)^n < e^{\lfloor t^*/h \rfloor h\lambda} = e^{t^*\lambda}$

Substituindo na inequação anterior para $err_n$, teremos:
$$
\lVert err_n \rVert \leq \frac{c}{\lambda}h[e^{t^*\lambda} - 1]
$$
Passando o limite $h\to 0$, teremos:
$$
\lim_{h\to 0}\lVert err_n \rVert \leq \frac{c}{\lambda}h[e^{t^*\lambda} - 1] = 0\\
\therefore
\lim_{h\to 0}\lVert err_n \rVert = 0
$$
O Metódo de Euler converger. Q.E.D.

```python

```

```python

```


# Thermo-optic Phase Modulator Theory

```python
import sympy as sy
```

Physics theory behind a thermo-optic phase shifter

\begin{equation}
\frac{d\phi}{dT} = \frac{2 \pi L (\frac{dn}{dT} + n(T)\alpha(T))}{\lambda}
\end{equation}

We know that the thermo-optic coefficient of the material $\beta(T) = \frac{dn}{dT}$ is dependent of the temperature.

Note that if we wanted to understand the dynamic behaviour of the device, we'd need to understand the time differential on the dimensionally proper values that change over time. For this we use SymPy to differentiate.

We must take into account that there are only two variables that really change over time when a heater is turned on and off. The temperature changes over time so all temperature dependent variables are also time dependent in this case: $n$, $\alpha$ and accordingly $\phi$.

```python
lambda_0, L, rho, t, T, w= sy.symbols("lambda_0 L \\rho t T w")
alpha = sy.Function("\\alpha ")(t, T)
phi = sy.Function("\\phi")(t, T)
n = sy.Function("n")(t, T)
```

```python
phi
```

    \phi(t, T)

```python
dphi_dT = 2 * sy.pi * L * ( n.diff(T) + n * alpha) / lambda_0
dphi_dT
```

    2*pi*L*(\alpha (t, T)*n(t, T) + Derivative(n(t, T), T))/lambda_0

So we can integrate this to get the absolute phase $\phi$ for a given temperature.

```python
phi_expression = sy.integrate(dphi_dT, T)
phi_expression
```

    2*pi*L*(Integral(\alpha (t, T)*n(t, T), T) + Integral(Derivative(n(t, T), T), T))/lambda_0

```python
phi_expression.diff(t)
```

    2*pi*L*(Integral(\alpha (t, T)*Derivative(n(t, T), t) + n(t, T)*Derivative(\alpha (t, T), t), T) + Integral(Derivative(n(t, T), T, t), T))/lambda_0

However, in reality what we care about is the phase change, so if we assume that we have discrete changes, we can then write something of the order:

\begin{equation}
\Delta \phi = \frac{2 \pi L\frac{dn}{dT}  \Delta T }{\lambda}
\end{equation}

If we were to consider the inputs and outputs of the system, let's consider the electro-magnetic wave that enters the heated waveguide path. Accroding to the length of the computation, the phase will be different and calculated at each step of the way if desired. The other interesting aspect would be how the temperature changes the properties of the other part of the optical signal. The interesting thing to compute as well would be what if the signal changes midway throughout the waveguide, or if you have an electronic signal changing at similar time-stamps as the optical waveguide - particularly relevant for an RF-modelled design.

So our electronic signal would be somthing of the sort: $v(t)$ and $i(t)$ that is digitally or mixed-signal RF emmitted.

You can in theory perform frequency analysis over these type of electronic signals, but the issue there is that you then need to perform freqeuncy analysis over the electro-optic response. But at least in the case of electro-optic modulators, it is a viable assumption that electronic signal will modulate the optical frequency at given operation frequencies - would you perform frequency analysis on the optical signal too in this case?

We know each thermo-optic heater will have a given resistance of the form, for a given sheet resistance $\rho$.

```python
def resistance_calculator(heater_length=L,
                          heater_width=w,
                          heater_square_resistance = rho):
    return heater_square_resistance * heater_length / heater_width
```

```python
resistance_calculator()
```

    L*\rho/w

Note that this resistance is in the form $\Omega$ and only considers the real electrical resistance assuming DC operation of $V(t) = i(t)R$ Ohm's law, and does not consider parasitic capacitances and impedances. So part of the issue as well is that we're not considering the frequency response of the impedance of the heaters in relation to the input response. We can assume there's an electrically-inputted total power of $P(t) = V(t)i(t)$, and there will be an efficiency fraction $\eta$ that becomes thermal power. Hence, thermal power $P_T(t) = \eta V(t)i(t)$

Let's assume DC impedance is applicable at low frequencies. We then need to understand the electro-thermal dynamic response to the input signal. Assuming the temporal response from:

* https://doi.org/10.1116/1.1914815
* https://www.osapublishing.org/oe/fulltext.cfm?uri=oe-27-8-10456&id=408126
* https://ieeexplore.ieee.org/document/9184872

We could argue there is an electro-thermal time constant of a first order control system of the form:

Note that the first order system model response to a step input is in the form:

\begin{equation}
r(t) = 1 - e^{-t/\tau}
\end{equation}

where $\tau$ is the time constant that determines the speed of the response. A second order system can be equally used to model it in case of oscillations, or in an overdamped case, to understand whatever real effect of damping exists.

With regards to the dynamic characterization, it is known that:

>Figures 10(a) and 10(b) show the measured bandwidth and time-domain switching of the TiN
and N++ Si TOPS. In Fig. 10(a), the pattern generator produces a variable frequency sine wave
with
$V_{\pi}$ /2
amplitude, and the amplitude response is read on the oscilloscope. The 3dB
bandwidth is 33 kHz for the TiN TOPS and 85 kHz for the N++ Si TOPS. In Fig. 10(b), a 5 kHz
square wave with
$V_{\pi}$ / 2
amplitude and the required offset to switch between the two first
extrema of optical transmission is applied to both heaters. The 1/e rise and fall time constants
are measured directly with the oscilloscope, and are
rise
3.7 μs,
fall =
7.2 μs for the TiN
TOPS, and
rise=
2.2 μs,
fall =
2.0 μs for the N++ Si TOPS. For the TiN TOPS, the asymmetric
time constants, i.e., significantly slower fall time, can be explained by the bad thermal
TiN TOPS
Gap: oxide cladding
N++ Si TOPS
Gap: oxide cladding
N++ Si TOPS
Gap: etched oxide
N++ Si TOPS
Gap: deep trench
conductance between the heater and the heat drain [21], itself arising from the relatively thick
oxide layer and air surrounding the metal.

Hence, it is known that the effective electro-thermaml response model depends on the insulation length, insulation distance, and would require two time constants that would need to be embedded into the dynamic effective real response to a certain level of configuration. This can be mathematically done in the form in the response $r(P_T)$ for a given power $P_T$ change:

\begin{equation}
r(P, t) = \left\{
        \begin{array}{ll}
            1 - e^{-t/\tau_1} & \quad \frac{dP_T}{dt} \leq 0 \\
            1 - e^{-t/\tau_2} & \quad \frac{dP_T}{dt} \geq 0
        \end{array}
    \right.
\end{equation}

And this would apply for several asymetric electro-thermal response cases, also in case a second order model is desired, this would be replaced either in the rise or fall edge.

Another factor to consider is the position of the heater respect to the waveguide, and how this power transfers into a temperature position change, and this is very dependent on materials and configuration. This, in particular, may be further explored in Lumerical modelling, but analytically would require some clasical heat transfer 2D modelling that might be better using FEA, although analytical models exist.

So there's a couple of things that can already be observed from this model:

### Inputs:
* Optical signal with a given initial phase.
* Electrical, time dependent signals that give total input electrical power.

## Parameters
* Thermo-optic heater configuration including: materials, geometry, positions, phase change signal, electro-thermal efficiency, thermal parameters change during modulation?

### Assumptions valid for a thermo-optic modulation:
* Electronic modulation frequency << optical modulation frequency, so can be treated as DC. Not valid for RF electro-optic modulation.

### Outputs
* Expected temperature reponse at the waveguide?
* Optical phase change at each point of the waveguide transmission dependent how the heater is time and space calculation stepped.
* Time delay related to thermal effectse even if minimal?

Note that this thermo-optic may be modelled within a single calculation step.
If electrical and optical signals are within the same frequency range as in an electro-optic modulator then this approach does not work as the dynamic changes can occur within the modulator path itself. This also depends on the length of the effect in relation to the modulation wavelength, that is nontrivial in RF. Note you then need to account for group-index and similar compatibility model parameters. The dynamic response will then be determined by further electro-thermal model parameters dependent on the papers. You then need to time and space chop the calculation of the signal as it propagates through the waveguide if you wish to get dynamic responses, and you need to take into account delays and inter-interference effects from dynamics delays when not a pure first order reponse occurs. The simulation method becomes time-stepped within space inside models, rather than time-stepped within models assuming some discrete absolute delay in that path - so may require different simulation methods - ie incompatible.

```python

```

```python

```

```python

```

```python

```


```python
from matplotlib import pyplot as plt
import numpy as np
from nodepy import runge_kutta_method as rk
from nodepy import stability_function
from sympy import symbols, expand
from scipy.special import laguerre
from ipywidgets import interact, FloatSlider
```

```python
def restricted_pade(k,gamma=0):
    coeffs = []
    for m in range(k+1):
        coeffs.append((-1)**k*laguerre(k).deriv(k-m)(1./gamma)*gamma**m)
    numer = np.poly1d(coeffs[::-1])
    z = symbols('z')
    denom = (1-gamma*z)**k
    coeffs = [expand(denom).coeff(z,n) for n in range(k+1)]
    denom = np.poly1d(coeffs[::-1])
    return numer, denom
```

```python
p, q = restricted_pade(3,gamma=1.06857902)
stability_function.plot_order_star(p,q);
```

Here we recreate and elaborate on Figure 6 of the paper "Order Stars and stability theorems" by Wanner, Hairer & Norsett.

```python
L4 = laguerre(4).deriv()
1./L4.r
```

    array([0.1288864 , 0.30253458, 1.06857902])

```python
def hwnplot(k=3,gamma=1.):
    p, q = restricted_pade(k,gamma)
    stability_function.plot_order_star(p,q,bounds=[-4,4,-4,4]);
    plt.show()

interact(hwnplot, gamma=FloatSlider(min=0.12, max=1.07,
                                    step=0.004, value=1.07));
```

    interactive(children=(IntSlider(value=3, description='k', max=9, min=-3), FloatSlider(value=1.07, description=…

```python

```

examples/Stability functions and order stars.ipynb


### Example 3: Diffusion

In this example we will look at the diffusion equation and how to handle second-order derivatives. For this we will introduce Devito's `.laplace` short-hand expression and demonstrate it using the examples from step 7 of the original tutorial.

So, the equation we are now trying to implement is

$$\frac{\partial u}{\partial t} = \nu \frac{\partial ^2 u}{\partial x^2} + \nu \frac{\partial ^2 u}{\partial y^2}$$

To discretize this equation we will use central differences and reorganizing the terms yields

\begin{align}
u_{i,j}^{n+1} = u_{i,j}^n &+ \frac{\nu \Delta t}{\Delta x^2}(u_{i+1,j}^n - 2 u_{i,j}^n + u_{i-1,j}^n) \\
&+ \frac{\nu \Delta t}{\Delta y^2}(u_{i,j+1}^n-2 u_{i,j}^n + u_{i,j-1}^n)
\end{align}

As usual, we establish our baseline experiment by re-creating some of the original example runs. So let's start by defining some parameters.

```python
from examples.cfd import plot_field, init_hat
import numpy as np
%matplotlib inline

# Some variable declarations
nx = 31
ny = 31
nt = 17
nu = .05
dx = 2. / (nx - 1)
dy = 2. / (ny - 1)
sigma = .25
dt = sigma * dx * dy / nu
```

We now set up the diffusion operator as a separate function, so that we can re-use if for several runs.

```python
def diffuse(u, nt):
    for n in range(nt + 1):
        un = u.copy()
        u[1:-1, 1:-1] = (un[1:-1,1:-1] +
                        nu * dt / dx**2 * (un[1:-1, 2:] - 2 * un[1:-1, 1:-1] + un[1:-1, 0:-2]) +
                        nu * dt / dy**2 * (un[2:,1: -1] - 2 * un[1:-1, 1:-1] + un[0:-2, 1:-1]))
        u[0, :] = 1
        u[-1, :] = 1
        u[:, 0] = 1
        u[:, -1] = 1
```

Now let's take this for a spin. In the next two cells we run the same diffusion operator for a varying nuper of timesteps to see our "hat function" dissipate to varying degrees.

```python
#NBVAL_IGNORE_OUTPUT

# Initialise u with hat function
u = np.empty((nx, ny))
init_hat(field=u, dx=dx, dy=dy, value=2.)
plot_field(u, zmax=2.5)

diffuse(u, nt=10)
plot_field(u, zmax=2.5)
```

```python
#NBVAL_IGNORE_OUTPUT

# Initialise u with hat function
u = np.empty((nx, ny))
init_hat(field=u, dx=dx, dy=dy, value=2.)

diffuse(u, nt=50)
plot_field(u, zmax=2.5)
```

Excellent. Now for the Devito part we need to note one important detail to our previous examples: we now have a second-order derivative. So, when creating our `TimeFunction` object we need to tell it about our spatial discretisation by setting `space_order=2`. Only then can we use the shorthand notation `u.dx2` and `u.dx2` to denote second derivatives.

```python
from devito import Grid, TimeFunction, Eq, solve
from sympy.abc import a

# Initialize `u` for space order 2
grid = Grid(shape=(nx, ny), extent=(2., 2.))
u = TimeFunction(name='u', grid=grid, space_order=2)

# Create an equation with second-order derivatives
eq = Eq(u.dt, a * (u.dx2 + u.dy2))
stencil = solve(eq, u.forward)
eq_stencil = Eq(u.forward, stencil)

print(eq_stencil)
```

    Eq(u(t + dt, x, y), -2.0*a*dt*u(t, x, y)/h_y**2 + a*dt*u(t, x, y - h_y)/h_y**2 + a*dt*u(t, x, y + h_y)/h_y**2 - 2.0*a*dt*u(t, x, y)/h_x**2 + a*dt*u(t, x - h_x, y)/h_x**2 + a*dt*u(t, x + h_x, y)/h_x**2 + u(t, x, y))

Now, there is another trick here! Note how the above formulation explicitly uses `u.dx2` and `u.dy2` to denote the laplace operator, which makes this equation dependent on the spatial dimension. We can instead use the notation `u.laplace` to denote all second derivatives in space, allowing us to reuse this code for 2D and 3D examples.

```python
eq = Eq(u.dt, a * u.laplace)
stencil = solve(eq, u.forward)
eq_stencil = Eq(u.forward, stencil)

print(eq_stencil)
```

    Eq(u(t + dt, x, y), -2.0*a*dt*u(t, x, y)/h_y**2 + a*dt*u(t, x, y - h_y)/h_y**2 + a*dt*u(t, x, y + h_y)/h_y**2 - 2.0*a*dt*u(t, x, y)/h_x**2 + a*dt*u(t, x - h_x, y)/h_x**2 + a*dt*u(t, x + h_x, y)/h_x**2 + u(t, x, y))

Great. Now all that is left is to put it all together to build the operator and use it on our examples. For illustration purposes we will do this in one cell, including update equation and boundary conditions.

```python
#NBVAL_IGNORE_OUTPUT
from devito import Operator, Constant, Eq, INTERIOR, solve

# Reset our data field and ICs
init_hat(field=u.data[0], dx=dx, dy=dy, value=2.)

# Create an operator with second-order derivatives
a = Constant(name='a')
eq = Eq(u.dt, a * u.laplace, region=INTERIOR)
stencil = solve(eq, u.forward)
eq_stencil = Eq(u.forward, stencil)

# Create boundary condition expressions
x, y = grid.dimensions
t = grid.stepping_dim
bc = [Eq(u[t+1, 0, y], 1.)]  # left
bc += [Eq(u[t+1, nx-1, y], 1.)]  # right
bc += [Eq(u[t+1, x, ny-1], 1.)]  # top
bc += [Eq(u[t+1, x, 0], 1.)]  # bottom

op = Operator([eq_stencil] + bc)
op(time=10, dt=dt, a=nu)

plot_field(u.data[0], zmax=2.5)
```

And now let's use the same operator again to show the more diffused field. In fact, instead of resetting the previously used object `u`, we can also create a new `TimeFunction` object and tell our operator to use this.

```python
#NBVAL_IGNORE_OUTPUT
u2 = TimeFunction(name='u2', grid=grid, space_order=2)
init_hat(field=u2.data[0], dx=dx, dy=dy, value=2.)

op(u=u2, time=50, dt=dt, a=nu)

plot_field(u2.data[0], zmax=2.5)
```


```python
from ipywidgets import interactive, interact

import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import cvxpy as cvx
import ipyvolume.pylab as p3

import bokeh.plotting as bp
from bokeh.models import HoverTool

bp.output_notebook()

import itertools
""
import holoviews as hv
hv.extension('bokeh')

%matplotlib inline

plt.rc('figure', figsize=(14, 8))
```

```python

```

```python
def surface3d(f, xs, ys=None, show=True, logcolor=False, key=None):
    if ys is None:
        ys = xs
    xs, ys = np.meshgrid(xs, ys)
    zs = f(xs, ys)

    if logcolor:
        znorm = np.log1p(zs - zs.min())
    else:
        znorm = zs
    znorm /= znorm.max()
    color = mpl.cm.plasma(znorm)[..., 0:3]

    fig = p3.figure(key=key)
    p3.plot_surface(xs, ys, zs, color=color)

    if show:
        p3.show()

def isosurface3d(f, xs, ys=None, zs=None, show=True, key=None):
    if ys is None:
        ys = xs
    if zs is none:
        zs = xs

    xs, ys, zs = np.meshgrid(xs, ys, zs)
    density = f(xs, ys, zs)

    fig = p3.figure(key=key)

    if show:
        p3.show()

def line2d(f, xs, fig=None, ax=None, **kwargs):
    xs = np.array(xs)
    if fig is None:
        fig = plt.gcf()
    if ax is None:
        ax = plt.gca()
    ax.plot(xs, f(xs), **kwargs)
    return fig, ax

def scatter2d(f, xs, fig=None, ax=None):
    xs = np.array(xs)
    if fig is None:
        fig = plt.gcf()
    if ax is None:
        ax = plt.gca()
    ax.scatter(xs, f(xs), **kwargs)
    return fig, ax

def scatter3d(xs, ys, zs, show=True, marker='sphere', color='red', key=None):
    p3.scatter(xs, ys, zs, marker=marker, color=color)

def plot_contours(f, x0, x1, levels=[1, 2, 3, 4], title=""):
    xx, yy = np.meshgrid(np.linspace(-2, 2, 50), np.linspace(-2, 2, 50))
    z = f(xx, yy)

    line = interpolate(x0, x1)
    curve = (hv.Curve((line[:, 0], line[:, 1]), label='x0 -> x1')
             .options(color='red', line_width=3, line_dash='dashed'))

    scatter = (hv.Scatter(([x0[0], x1[0]], [x0[1], x1[1]]), extents=(-2, -2, 2, 2))
               .options(color='red', size=10))

    contour = (hv.operation.contours(hv.Image(z, bounds=(-2, -2, 2, 2)), levels=levels)
                .options(line_width=3))

    return (hv.Overlay([contour, scatter, curve],  label=title)
            .options(height=450, width=700, legend_position='top_left', show_title=True))

def plot_convex1d(f, x0, x1, title=""):
    # Prepare data
    theta = np.linspace(0, 1, 50)
    xs = x0 * theta + (x1 * (1 - theta))
    ys = f(xs)
    convex_bounds = f(x0) * theta + f(x1) * (1 - theta)
    data = bp.ColumnDataSource({'x': xs, 'y': ys, 'theta': theta, 'bound': convex_bounds})

    # Plot figure
    hover = HoverTool(mode='vline', names=['f'],
                      tooltips=[("t", "@theta"),
                                ("x", "@x"),
                                ("f(x)", "@y"),
                                ('Convex Bound', "@bound")])
    p = bp.figure(plot_width=700, plot_height=450, tools=[hover], title=title)
    p.line('x', 'y',
           source=data, line_width=5, name='f', legend='f(x0 * t + x1 * (1 - t))')
    p.line('x', 'bound',
           source=data, line_width=5, color='red', line_dash='dotted', legend='f(x0) * t + f(x1) * (1 - t)')
    p.circle([x0, x1], [f(x0), f(x1)], color='red', size=20)
    p.legend.location = 'top_left'
    bp.show(p)
    return p

def interpolate(x0, x1, samples=50):
    x0 = np.array(x0)
    x1 = np.array(x1)
    theta = np.linspace(0, 1, samples)[:, None]
    return theta * x0 + (1 - theta) * x1
```

Convex Optimization for Finance

Scott Sanderson

Email: ssanderson@quantopian.com, GitHub: [@ssanderson](https://github.com/ssanderson), Twitter: [@scottbsanderson](https://twitter.com/scottbsanderson)

# Goals

- Situate Convex Optimization within Broader Landscape
- Build **Geometric** Intuition for Convex Functions and Convex Sets
- Show Applications of Convex Optimization to Finance
- Provide Resources for Further Study

# Outline

- **Optimization**
- **Convex** Optimization
- Convex Optimization **for Finance**

# Optimization

Mathematical optimization is a family of techniques for finding a "best" value from a set of choices.

A well-posed optimization problem has at least two parts:

- A set $S$ of possible choices. Often called the **optimization domain**.

- An scalar-valued **objective function** $f$ defined on $S$, to be **minimized** or **maximized**:
  - **Loss/Cost Function** (minimization)
  - **Utility Function** (maximization)

We often also add a third component, a set of **constraints** on "valid" elements of $S$.

We write optimization problems like this:

\begin{align}
\underset{x \in S}{\text{maximize}}&& f(x)&\\
\text{subject to}&& c_i(x)& = 1&&\\
\end{align}

**In plain English:** *Find the value of $x$ in $S$ that maximizes $f(x)$ while satisfying all constraints $c_i$.*

## Examples

```python
def rosenbrock(x, y):
    """The Rosenbrock function for a = 1, b = 7.5.

    See https://en.wikipedia.org/wiki/Rosenbrock_function.
    """
    a = 1
    b = 5
    return (a - x) ** 2 + b * (y - x ** 2) ** 2

surface3d(rosenbrock, np.linspace(-1.5, 1.5), np.linspace(-0.5, 1.5), logcolor=True)
```

```python
from scipy.optimize import minimize

minimize(lambda p: rosenbrock(*p), x0=[0.0, 0.0], method='nelder-mead')
```

```python

```

```python
def minimize_traced(f, *args, **kwargs):
    """Call scipy.minimize, and store the points it asks us to evaluate.
    """
    trace = []

    def traced_f(point):
        trace.append(tuple(point))
        return f(*point)

    result = minimize(traced_f, *args, **kwargs)
    return result, trace

result, trace = minimize_traced(rosenbrock, x0=[-1., 1.], method='nelder-mead')
trace[:10]
```

```python
def show_optimization_trace(x0, y0, method):
    func = rosenbrock; initial_point = [x0, y0]

    result, trace = minimize_traced(func, x0=initial_point, method=method)

    xs, ys = zip(*trace)
    zs = list(itertools.starmap(func, trace))

    surface3d(func, np.linspace(min(xs) * 1.1, max(xs) * 1.1), np.linspace(min(ys) * 1.1, max(ys) * 1.1), logcolor=True)
    scatter3d(np.array(xs), np.array(ys), np.array(zs), color=np.linspace(0.1, 1, num= 2 * len(xs)))
```

```python
interact(show_optimization_trace, x0=-1.4, y0=1.0, method=['nelder-mead', 'powell']);
```

```python
from scipy.optimize import show_options

show_options()
```

## Convex Optimization

General purpose optimization is **hard**.

In the most extreme cases, it's **impossible**.

Under modest assumptions, it's *merely* computationally infeasible.

### Challenges

- Number of candidates grows exponentially in the dimensionality of the search space.
- Search algorithms can get "trapped" in local minima and/or saddle points.
- Complex feasible regions are hard to navigate.

We can do much better if we can assume that our objective/constraints are "well-behaved".

**Convex Functions** and **Convex Constraints** are a particularly important class of well-behaved inputs.

## Convex Functions

A function $f(x)$ is **convex** if:

$$f(x_0t + x_1(1 - t)) \leq f(x_0)t  + f(x_1)(1 - t)$$

where

$$t \in [0, 1]$$

**Intuition:**

- Left-hand side is the point "t-percent" of the way between $x_0$ and $x_1$.
- Right-hand side is the point "t-percent" of the way between $f(x_0)$ and $f(x_1)$.
- Inequality says that $f$ is changing faster in the output space than the input space.

```python
plot_convex1d(lambda x: x ** 2 + 2 * x - 1, -2, 2, "Convex Function");
```

```python
plot_convex1d(lambda x: x ** 3 - 2 * x ** 2 - x, -5, 5, "Non-Convex Function");
```

## Multivariate Convexity

Convexity generalizes naturally to arbitrary dimensions.

The same inequality applies for $x \in \mathbb{R}^n$:

$$f(x_0t  + x_1(1 - t)) \leq f(x_0)t  + f(x_1)(1 - t) $$

```python
def convex_func(x, y):
    return x ** 2 + 3 * y ** 2

plot_contours(convex_func, (-1, 0), (0, np.sqrt(3) / 3), np.linspace(0.5, 2, 4))
```

```python
def nonconvex_func(x, y):
    return x ** 2 + 4 * y ** 2 - 3 * np.sin(x ** 2 + y ** 2)

plot_contours(nonconvex_func, (-0.5, 0.666), (0.5, 0.666), levels=np.linspace(0.1, 1, 4))
```

## Convex Sets

Closely-related to **convex functions** is the concept of **convex sets**:

Let $C$ be a subset of $\mathbb{R}^n$, and let $x_0, x_1 \in C$.

$C$ is convex iff:

\begin{align}
t x_0 + (1 - t)x_1 \in& C \\
\forall t \in& [0, 1]
\end{align}

**English:** If two points are in a convex set, every point on the line between them is also in the set.

## Useful Facts about Convexity

- The intersection of two convex sets is also convex.

- Level surfaces of convex **functions** are the **boundaries** of **convex sets**.

- In 2 dimensions, a convex set doesn't intersect with **lines** tangent to its boundary
- In 3 dimensions, a convex set doesn't intersect with **planes** tangent to its boundary.

- In general, an N-dimensional convex set doesn't intersect with its **supporting hyperplanes**.

- A scalar function is convex if it has positive second derivative.
- In general, a twice-differentiable function is convex iff its Hessian is positive semi-definite.

## Convex Optimization

Convexity is a very nice property for optimization.

If the objective function $f$ is convex, and the **feasible region** satisfying all constraints is a convex set, then:

- Local extrema are guaranteed to be global extrema.
- There are efficient (polynomial-time) algorithms to find extrema.

## CVX and Disciplined Convex Programming

Most low-level solvers require you to specify a problem in "canonical form" as a collection of matrices.

This can be hard to get right, and hard to debug if you get it wrong.

Modelling languages solve this problem by translating from high-level expressions to low-level solver representations.

## CVX and Disciplined Convex Programming

[CVX](http://cvxr.com/cvx/) (MATLAB) and its siblings [CVXPY](https://github.com/cvxgrp/cvxpy) (Python) and [Convex.jl](https://github.com/JuliaOpt/Convex.jl) (Julia) implement a system called [Disciplined Convex Programming](http://dcp.stanford.edu/home).

DCP allows you to build expressions out of a set of primitives with known curvature and sign. The modeling language translates from expressions into the canonical form of a target backend.

## Convex Optimization for Portfolio Construction

```python
import cvxpy as cvx

expected_returns = np.array([0.001, 0.002, 0.003, 0.004, 0.005])

def maximize_returns(expected_returns):
    """Construct the portfolio that maximizes expected portfolio returns given
    expected security-level returns.
    """
    weights = cvx.Variable(len(expected_returns))
    objective = cvx.Maximize(weights.T * expected_returns)
    problem = cvx.Problem(objective, [cvx.sum_entries(cvx.abs(weights)) <= 1])
    problem.solve()
    return weights.value.round(3).ravel()
```

```python
maximize_returns(expected_returns)
```

```python
def markowitz_portfolio(means, cov, risk_aversion):
    """Generate the optimal fully-invested portfolio for a given risk/returns tradeoff.
    """
    weights = cvx.Variable(len(means))

    expected_return = weights.T * means
    expected_vol = cvx.quad_form(weights, cov)

    utility = expected_return - risk_aversion * expected_vol
    objective = cvx.Maximize(utility)

    constraints = [
        cvx.sum_entries(weights) == 1,  # fully-invested
        weights >= 0,                   # long-only
    ]

    problem = cvx.Problem(objective, constraints)
    problem.solve()

    return np.array(weights.value.flat).round(4), expected_return.value, expected_vol.value
```

```python
expected_rets = np.array([0.001, 0.002, 0.003, 0.004, 0.005])
cov = np.array([[0.02, 0. ,  0.  , 0.  , 0.  ],
                [0.  , 0.02, 0.  , 0.  , 0.  ],
                [0.  , 0.  , 0.02, 0.  , 0.  ],
                [0.  , 0.  , 0.  , 0.02, 0.0 ],
                [0.  , 0.  , 0.  , 0. , 0.02]])

weights, rets, var = markowitz_portfolio(expected_rets, cov, 0.2)
print("Weights:", weights); print("Expected Return:", rets); print("Expected Variance:", var)
```

```python
import bokeh.plotting as bp

def plot_efficient_frontier(means, cov, min_aversion=0, max_aversion=1, nsamples=25):
    """Plot the expected return and variance for different levels of risk aversion.
    """
    samples = np.linspace(min_aversion, max_aversion, nsamples)

    portfolios = []
    returns = []
    stddevs = []
    for aversion in samples:
        portfolio, ret, var = markowitz_portfolio(means, cov, aversion)
        portfolios.append(portfolio)
        returns.append(ret)
        stddevs.append(var)

    fig = bp.figure()
    fig.scatter(stddevs, returns)
    fig.xaxis.axis_label = 'Risk'
    fig.yaxis.axis_label = 'Return'
    return fig
```

```python
bp.show(plot_efficient_frontier(expected_rets, cov))
```

## Example: Constraining Historical Downside Risk

```python
alphas = np.linspace(-2, 2, 8)
rets = np.random.standard_t(df=10, size=(500, 8)) * 0.1
pd.DataFrame(rets).describe()
```

```python
def maximize_alpha_constrain_downside(alphas, returns, percentile, max_loss):
    weights = cvx.Variable(returns.shape[1])

    # Number of worst-case return periods to sample.
    nsamples = round(returns.shape[0] * percentile)

    portfolio_rets = returns * weights
    avg_worst_day = cvx.sum_smallest(portfolio_rets, nsamples) / nsamples

    objective = cvx.Maximize(weights.T * alphas)
    constraints = [avg_worst_day >= max_loss]

    problem = cvx.Problem(objective, constraints)
    problem.solve()

    return weights.value.round(4).ravel()

result = maximize_alpha_constrain_downside(alphas, rets, percentile=0.05, max_loss=-0.05)
print("Portfolio:", result)

portfolio_rets = rets.dot(result)
worst_days = portfolio_rets[portfolio_rets <= np.percentile(portfolio_rets, 5)]
print("Average Bad Day:", worst_days.mean())
```

# Review

- Python has lots of great tools for optimization.
  - `scipy.optimize`
  - `cvxpy`
  - ... lots of other tools not covered.

- Convex functions "curve upward" when traversing lines between points.
- Convex sets always contain lines between points.

- Convex optimization can be used to **efficiently** solve a wide array of interesting problems.
- High-level modeling tools like `cvxpy` or `quantopian.optimize` mean that you don't need to be an expert to productively use convex solvers.

## Questions?

- **GitHub:** https://github.com/ssanderson
- **Twitter:** @scottbsanderson
- **Email:** scott@quantopian.com


# Exercise 2
* Due: 21.11. at noon
* Max points: 8

## General rules
* Send the source code or the written notes of your answers as an attachment to markus.hartikainen@jyu.fi before the due time
* You will get feedback about your answers a week from the due date
* From the second week on, the exercises will be given on the previous Wednesday's lecture

## Exercises

1. **(2 points)** Assume that that you are buying and selling electricity. The more electricity you buy, the better price you get per kwH and the buying prize of water follows function $f(x) = 1-0.01x$, where $0\leq x\leq50$ is the amount of electricity you buy. On the other hand, the price that you get from selling electricity follows function $g(x) = 2-0.01x^2$ with $x$ again the amount of electricity that you sell. Formulate an optimization problem that maximizes the profit.
2. **(2 points)** Use bisection search to optimize problem
$$
\begin{align}
\min & (1-x)^2+x\\
\text{s.t. }&x\in [0,2].
\end{align}
$$
3. **(2 points)** Use golden section search to optimize the problem from above exercise.
4. **(2 points)** Use differentiation to optimize the optimization problem of exercise 1 and to verify the answer of optimization problem 2.

# Solutions

```python
#solutiion 1
#formulate optimization problem
#formulate profit= selling price - buying price
#profit (h(x))=g(x)-f(x)=2-0.01x**2-(1-0.01x) ->1+0.01x-0.01x**2
max (h(x)=1+0.01x-0.01x**2)
s.t x ∈[0,50].

```

```python
#solution 2
#solve the given equation with bisection_line_search method

def f(x):
    return (1-x+pow(x,2))

def bisection_line_search(a,b,f,L,epsilon):
    x = a
    y = b
    while y-x>2*L:
        if f((x+y)/2-epsilon)<f((x+y)/2+epsilon):
            y = (x+y)/2+epsilon
        else:
            x = (x+y)/2-epsilon
    return (x+y)/2

bisection_line_search(0.0,2.0,f,1e-3,1e-4)
```

    0.499561767578125

```python
#solution 3
#solve the above problem with golden_section_line_search method
import math

def f(x):
    return (1-x+pow(x,2))

def golden_section_line_search(a,b,f,L):
    x = a
    y = b
    while y-x>2*L:
        if f(x+(math.sqrt(5.0)-1)/2.0*(y-x))<f(y-(math.sqrt(5.0)-1)/2.0*(y-x)):
            x = y-(math.sqrt(5.0)-1)/2.0*(y-x)
        else:
            y = x+(math.sqrt(5.0)-1)/2.0*(y-x)
    return (x+y)/2

golden_section_line_search(0.0,2.0,f,1e-3)
```

    0.5000865351358561

```python
#solution 4
#solve problem #1 with differentiation approach and verify the answer of problem #2

## verfiying the answer for problem #2
   #max or min value of a function can be calcuated by equating it's first derivative to zero.
    #thus the first derivative of
    f(x)=1-x+pow(x,2) is f'(x)=2x-1
    #then to solve for x we equate
    f'(x)=0 -> 2x-1=0 ->2x=1 -> x=0.5
    #verifying that the answers by both methods (bisection_line_search and golden_section_line_search )
    #give colser guesses
```

```python
#solution 4
#solve problem #1 with differentiation approach
#given profit equation
h(x)=1+0.01x-0.01x**2 and 0<= x <=50
#we can find max of it when we equate h'(x)=0
#h'(x)=0.01-2*001x=0 ->x=1/2=0.5

```


# Convolution

Convolution is the mathematical procedure that defines or represents the action of a linear and time-invariant (LTI) system on an input signal.  Specifically, if a system with an impulse response $h(t)$ is driven by an input signal $x(t)$, then the output is
$$
y(t) = h(t) \ast x(t) = \int_{-\infty}^{\infty} h(\lambda) x(t - \lambda) d\lambda.
$$
Every LTI system has an impulse response $h(t)$, which is the output when the input is the signal $x(t) = \delta(t)$.

The expression above is useful if you have expressions for the signals such as $h(t) = e^{-t} u(t)$ and $x(t) = u(t)$, and a symbolic math package can help in this case.  However, in reality we can't specify the signals we see and care about using simple analytical expressions, particularly for input and output signals.  Yet we still want to work with them.

We saw in a previous lab that if signals are bandlimited then they can be reconstructed from discrete samples.  In other words, the discrete signal $x[n]$ obtained by sampling a continuous-time bandlimited $x(t)$ according to $x[n] = x(nT)$ can be used to represent or reconstruct $x(t)$.  If we have a symbolic expression for the impulse response, which is often the case, then we can use the sampled input to generate samples from the corresponding output.  If all signals involved are represented using samples, then it turns out that we can effectively consider convolution in continuous-time using discrete-time convolution on the samples of the signals involved.  This lab explores all of these concepts and their application.

## Representing a continuous-time signal from samples

A previous lab investigated the topic of reconstructing a continuous-time bandlimited signal $x(t)$ from a sampled representation $x[n]$, with $x[n] = x(nT)$.  The process involves using the discrete signal $x[n]$ to construct a modulated impulse train
$$x_s(t) = \sum_{n=-\infty}^{\infty} x[n] \delta(t-nT).$$
This is an interesting signal that has both continuous-time and discrete-time characteristics:  it is formulated on the continuous-time axis, but contains exactly and only the information contained in the samples $x[n]$.  As such it forms the link between the two domains.  The signal $x_s(t)$ is then filtered with a reconstruction filter $b_T(t)$ to interpolate between the sample points:
$$x(t) = x_s(t) \ast b_T(t).$$
The ideal reconstruction filter is the *sinc* function that corresponds to perfect bandlimiting.

The cell below plots the two example signals mentioned above, which will be used for demonstration.  Also shown is the discrete set of values obtained by sampling the signals with a spacing $T$, starting at $n=0$.  It should be noted that neither of these signals are in fact bandlimited, so there will be errors in the approximation, but these errors will be reduced as $T$ becomes smaller.

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

# Dense set of values for plotting underlying functions
tvp = np.linspace(-1, 5, 2000);
hvp = np.where(tvp<0, 0, np.exp(-tvp));
xvp = np.where(tvp<0, 0, 1);
yvp = np.where(tvp<0, 0, 1-np.exp(-tvp));

# Discrete samples starting from n=0
T = 0.5;  nv = np.arange(0,np.floor(np.max(tvp)/T),1,np.int32);
tv = nv*T;
hv = np.where(tv<0, 0, np.exp(-tv));
xv = np.where(tv<=0, 0, 1);

# Plots
fh = plt.figure;
plt.subplot(211);  plt.plot(tvp,hvp,'r-');  plt.stem(tv,hv);  plt.ylabel('h(t)');
plt.subplot(212);  plt.plot(tvp,xvp,'g-');  plt.stem(tv,xv);  plt.ylabel('x(t)');  plt.xlabel('t');
```

    <IPython.core.display.Javascript object>

## Fully discrete convolution

Suppose now that we want to numerically approximate the convolution
$$y(t) = h(t) \ast x(t).$$
If $h(t)$ and $x(t)$ are bandlimited then we can sample them according to $h[n] = h(nT)$ and $x[n] = x(nT)$, and the following reconstruction formulae hold:
$$x(t) = x_s(t) \ast b_T(t) \qquad \text{and} \qquad
h(t) = h_s(t) \ast b_T(t).$$
The quantity $h_s(t)$ is the modulated impulse train corresponding to $h(t)$, namely
$$h_s(t) = \sum_{k=-\infty}^{\infty} h[k] \delta(t-kT),$$
and $b_T(t)$ is a reconstruction filter.

If we use an ideal lowpass reconstruction filter then $b_T(t) \ast b_T(t) = T b_T(t)$, and the required convolution is given by
$$
y(t) = (x_s(t) \ast b_T(t)) \ast (h_s(t) \ast b_T(t))
= x_s(t) \ast h_s(t) \ast T b_T(t).
$$
If any other reasonable reconstruction filter is used then this expression can be considered approximate.

Since $y(t)$ is the result of a convolution with $b_T(t)$ it is also bandlimited, and can be expressed as $y(t) = y_s(t) \ast b_T(t)$.  We must  therefore have the following:
\begin{align}
  y_s(t) &= T x_s(t) \ast h_s(t) = T \int_{-\infty}^{\infty} x_s(\lambda) h_s(t-\lambda) d\lambda \\
  &= T \int_{-\infty}^{\infty} \sum_{k=-\infty}^{\infty} x[k] \delta(\lambda-kT) \sum_{m=-\infty}^{\infty} h[m] \delta(t-\lambda-mT) d\lambda \\
  &= T \sum_{m=-\infty}^{\infty} \sum_{k=-\infty}^{\infty} x[k] h[m] \int_{-\infty}^{\infty} \delta(\lambda-kT) \delta(t-\lambda-mT) d\lambda \\
  &= T \sum_{m=-\infty}^{\infty} \sum_{k=-\infty}^{\infty} x[k] h[m] \delta(t-kT-mT)
\end{align}

The steps above follow because the order of summation doesn't usually matter (which is probably true for the signals we care about), and the integral can be evaluated using the sifting property of the Dirac delta.

The change of variables $m=n-k$ is useful, and gives the result
\begin{align}
y_s(t) &= T \sum_{n=-\infty}^{\infty} \sum_{k=-\infty}^{\infty} x[k] h[n-k] \delta(t-kT-(n-k)T) \\
  &= \sum_{n=-\infty}^{\infty} \left( T \sum_{k=-\infty}^{\infty} x[k] h[n-k] \right) \delta(t-nT) \\
  &= \sum_{n=-\infty}^{\infty} y[n] \delta(t-nT)
\end{align}
where we have let
$$
y[n] = T \sum_{k=-\infty}^{\infty} x[k] h[n-k].
$$
This is the operation corresponding to discrete-time convolution:  it takes the discrete-time signals $x[n]$ and $h[n]$, and combines them to produce the discrete time output $y[n]$.  We use the same notation as for the continuous-time case, namely $y[n] = T x[n] \ast h[n]$.

There are standard functions implemented for discrete convolution.  They usually take two inputs in the form of vectors representing portions of signals, and produce an output that contains a set of convolution results.  Implicitly the signals are assumed to be zero where they are not specified.  Different variants can select different intervals for the output sets.  The code below implements the desired computation directly, and also shows the result of a full discrete convolution as implemented by standard methods.  This output produces a result for every possible shift that yields a nonzero overlap between the signals.  Evidently all the required convolution values have been calculated in the full convolution, but knowing which portion to extract requires some effort.

```python
# Explicit discrete convolution sum
yv = np.zeros(nv.shape);
for n in nv:
    for k in nv:
        xcurr = 0 if n-k<0 else xv[n-k];
        yv[n] += hv[k]*xcurr;
yv *= T;

# Standard full-length convolution between vectors
yvf = T*np.convolve(hv, xv, 'full');

# Plot discrete convolution results
fh = plt.figure();
plt.subplot(211);  plt.stem(nv,yv);  plt.ylabel('direct')
plt.subplot(212);  plt.stem(yvf);  plt.xlabel('n');  plt.ylabel('"convolve"')
```

    <IPython.core.display.Javascript object>

    <matplotlib.text.Text at 0x119d6e5c0>

## Interpretation

The preceeding discussion is admittedly quite technical, but the result is simple.  Suppose we want to find the convolution
$$y(t) = h(t) \ast x(t).$$
If $h(t)$ and $x(t)$ are bandlimited then by the Nyquist criterion they can be represented by samples taken with a sufficiently small $T$.  All that is required is that they be sampled at a frequency that is more than twice the highest frequency present in either.  We can then perform discrete convolution
$$
y[n] = T h[n] \ast x[n] = T \sum_{k=-\infty}^{\infty} h[k] x[n-k]
$$
on the discretised signals to produce the sampled output $y[n]$.  The corresponding continuous-time output is
$$y(t) = \sum_{n=-\infty}^{\infty} y[n] \delta(t-nT) \ast b_T(t) = y_s(t) \ast b_T(t).$$
The above equation is exact if
$$b_T(t) = \text{sinc}(t/T) = \frac{\sin(\pi t/T)}{\pi t/T}.$$

The code below takes the calculated $y[n]$ and plots the corresponding $y_s(t)$.  The `plot` function applied to the same input effectively 'joins the dots', which as seen before is linear interpolation and corresponds to a low-quality reconstruction filter.  The actual ideal output $y(t)$ calculated analytically is also shown.  Observe that there is some error in the approximation obtained by discrete processing.  This is primarily a consequence of the signals not in fact being bandlimited.  If you decrease $T$ you should observe that the correspondence improves.

```python
# Plot discrete result and continuous-time counterpart
fh = plt.figure(figsize=(6.5,2.5));
plt.plot(tvp,yvp);  plt.stem(tv,yv);  plt.plot(tv,yv);  plt.xlabel('t');  plt.ylabel('y(t)');
```

    <IPython.core.display.Javascript object>

## Approximate impulse input

We cannot formally sample a signal containing an impulse.  However, recall the working definition of the impulse as
$$\delta(t) = \lim_{A \to 0} \frac{1}{A} p_A(t),$$
where $p_A(t)$ is unit pulse with total width $A$ centered on $t=0$.  If we sample the signal with a spacing $T$, and let $T$ become small, then this leads to the approximation
$$\delta(t) \approx \frac{1}{T} p_T(t).$$

The code below demonstrates this concept by using discrete convolution to find the approximate output of the previous system when the input is an impulse at the origin:  $x(t) = \delta(t)$.  The red curve in the first plot shows the finite approximation to the input, along with the samples $x[n] = x(nT)$ starting at $n=0$.  The second plot contains the sample points for the discrete convolution output, along with the linear interpolation of these points (red).  Also shown is the actual impulse response of the system (green), which for this case represents the actual or ideal output.

```python
# Approximation to x(t)=delta(t)
xvip = 1/T*np.where(abs(tvp)<=T/2, 1, 0);

# Explicit discrete convolution sum
xvi = np.zeros(xv.shape);  xvi[0] = 1/T;
yvi = np.zeros(nv.shape);
for n in nv:
    for k in nv:
        xcurr = 0 if n-k<0 else xvi[n-k];
        yvi[n] += hv[k]*xcurr;
yvi *= T;

# Plots
fh = plt.figure();
plt.subplot(211);  plt.plot(tvp,xvip,'r-');  plt.stem(tv,xvi);  plt.ylabel('Approx $\delta(t)$');
plt.subplot(212);  plt.plot(tv,yvi,'r-',label="Approximate");  plt.stem(tv,yvi);
plt.plot(tvp,hvp,'g-',label="Actual");  plt.ylabel('h(t)');  plt.xlabel('t');  plt.legend();
```

    <IPython.core.display.Javascript object>

# Tasks

These tasks involve writing code, or modifying existing code, to meet the objectives described.

1.  The previous code snippets use samples with spacing $T = 0.5$ to approximate the output $y(t)$ from a system with impulse response $h(t) = e^{-t} u(t)$ when the input is $x(t) = u(t)$.  Repeat this with a spacing of $T=0.1$ in the discretisation.  Generate a plot that shows the approximate output $y(t)$ using linear interpolation for this case, and on the same set of axes include the actual output calculated analytically.  The domain of your plot should span $t=-1$ to $t=5$.

2.  Repeat the previous task but using a system with the highpass impulse response $h(t) = \delta(t) - e^{-t} u(t)$.

3.  An underdamped second-order system with transfer function
$$
H(s) = \frac{K \omega_n^2}{s^2 + 2 \zeta \omega_n s + \omega_n^2}
$$
has impulse response
$$
h(t) = \frac{\omega_n}{\sqrt{1-\zeta^2}} e^{-\zeta \omega_n t}
\sin(\omega_n \sqrt{1-\zeta^2} t) u(t)
$$
when $\zeta<1$.  Here $K$ is the *gain*, $\zeta$ is called the *damping ratio*, and $\omega_n$ is the *natural frequency* of the system.  On one set of axes make a plot of this impulse response for $K=1$, $\omega_n = 3$ and the three cases $\zeta = 0.25, 0.5, 0.99$.  Your plot should span the domain $t=-1$ to $t=10$.  Then, using discrete-time convolution with $T=0.1$, find the approximate step responses for each of the three cases.  Plot these responses on one set of coordinate axes over the same domain $t=-1$ to $t=10$.

```python

```


# Cálculos en ingeniería con Sympy y Jupyter Notebooks

## Cuadros de texto con ecuaciones

El editor permite escribir ecuaciones en [$\LaTeX$](). Al dar "`Doble click`", puede ver cómo se escriben tales expresiones.

$$ f(a) = \int_\infty^0 \frac{1}{a+2} \mathrm{d}a$$

## Símbolos especiales en los nombres

Para nombrar una variable con un símbolo especial, use  "`\`" seguido del nombre del símbolo y presione "`tab`".

**Ejemplo**

Escriba `\alpha` y presione "`tab`" para usar α en el código.

```python
α
```

## SymPy

Se importa el módulo [SymPy](http://docs.sympy.org/latest/index.html) para desarrollar operaciones matemáticas de forma simbólica, debe importase. Además se inicia un proceso de "impresión estética" para la visualización de expresiones y uno de "gráficas incorporadas".

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

Se definen símbolos para operarlos matemáticamente.

```python
x = sym.Symbol('x')
```

```python
x
```

Se realizan operaciones matemáticas con los símbolos creados.

```python
polinomio = (x + 2)**2
polinomio.expand()
```

"`polinomio`" se creó como una operación que usa el símbolo "`x`". Después, se usó el método "`.expand()`" para expandir el "`polinomio`".

Puede tener un listado de métodos que puede usar sobre "`polinomio`" escribiendo "`polinomio.`" y presionando "`tab`"

```python
polinomio
```

Puede obtener ayuda de un método con "`?`".

```python
polinomio.expand?
```

También puede obtener ayuda de un método al ubicar el cursor entre el paréntesis y presionar "`Shift+Tab`"

```python
polinomio.factor()
```

## Cálculo

Puede resolver integrales y derivadas.

```python
expresionA = sym.tan(sym.log(x**2 + 1)) # define una expresión
```

```python
expresionA.diff(x) # deriva "expresionA" respecto a "x"
```

```python
polinomio.diff(x) # primera derivada
```

```python
polinomio.diff(x, 2) # segunda derivada
```

```python
polinomio.integrate(x) # integral indefinida sin constante de integración
```

```python
polinomio.integrate((x, 0, 1))  # integral definida. Observe la tupla como argumento
```

## Límites

Puede resolver límites.

```python
expresionB = (2*sym.sin(x) - sym.sin(2*x))/(x - sym.sin(x))
```

```python
expresionB
```

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

```python
lim_a = sym.limit(expresionB, x, a)
lim_a
```

```python
lim_a.subs(sym.sin(a), 1)
```

```python
lim_0 = sym.limit(expresionB, x, 0)
lim_0
```

```python
sym.plot(expresionB)
```

## Aproximaciones

Puede aproximar funciones con la serie de Taylor.

```python
expresionC = sym.sin(x)
sym.series(expresionC, x, 1.5, 2)
# Serie de Taylor en términos de x, al rededor de x=1.5, orden 2.
```

Use `.removeO()`

```python
temp = sym.series(expresionC, x, 1.5, 1)
temp.removeO()
```

SymPy mantiene la representación exacta de los números, aunque pueden aproximarse con "`float`" o "`sympy.N`" si hay variables.

```python
c = sym.sqrt(2)*sym.pi
c
```

```python
float(c)
```

```python
sym.N(c*x)
```

## Solución de ecuaciones

Soluciona ecuaciones con la función "`solve`".

\begin{align}
               2x^2 + 2 &= 4 \\
           2x^2 + 2 - 2 &= 4 - 2 \\
                  2x^2  &= 2 \\
        \frac{2x^2}{2}  &= \frac{2}{2} \\
                    x^2 &= 1 \\
 \end{align}

```python

```

```python
sym.plot(2*x**2 + 2,(x, -2, 2))
```

```python
soluciones = sym.solve(2*x**2 + 2 - 4)
soluciones
```

```python
soluciones[0]
```

Con "`sympy.Eq`" se pueden construir ecuaciones.

```python
eqA = sym.Eq(2*x**2 + 2, 4)
eqA
```

```python
sym.roots(eqA)
```

```python
eqA.lhs  # lado izquierdo de la ecuación
```

```python
eqA.rhs  # lado derecho de la ecuación
```

```python
g1 = sym.plot(eqA.lhs,(x, -2, 2), show = False,line_color='blue')
g2 = sym.plot(eqA.rhs,(x, -2, 2), show = False,line_color='red')
g1.extend(g2)
g1.show()
```

Puede resolverse un sistema de ecuaciones.

```python
x, y = sym.symbols('x, y')
sym.solve([x + y - 2,
           x - y - 0], [x, y])
```

```python
a, b, c = sym.symbols('a, b, c')
sym.solve([a*x + b*y - 2,
           a*x - b*y - c], [x, y])
```


# Lab Manual

## Propagation of Uncertainties
***

### The Sophisticated Approach
We can use calculus to tell us how much a function varies if we change one of its parameters. For the following, we’ll assume that we are trying to compute some value, Y , using the inputs A, B, and C. This could be the perimeter of a rectangle, in which case the perimeter, Y , is the sum of four variables:

$$Y(A, B, C, D) = A + B + C + D$$

In the case of a volume measurement, the volume, Y , would be the product of length, width, and height:

$$Y(A, B, C) = ABC$$

The uncertainties in our measured values will be given by δA, δB, and δC. The uncertainty in the
computed value is, then, δY .

If we have a function which is the sum or difference of a number of variables,
$$Y = A \pm B \pm C$$

Then we can compute the uncertainty in Y with the following formula:

\begin{equation}
(\delta Y)^2 = (\delta A)^2 + (\delta B)^2 + (\delta C)^2
\tag{R1}
\end{equation}

If the function is some kind of product of our three variables,
$$ Y = ABC \quad \textrm{or} \quad Y = \frac{AB}{C} \quad \textrm{or} \quad Y = \frac{A}{BC} \quad \textrm{or} \quad Y = \frac{1}{ABC}$$

Then we can compute the uncertainty in Y via:

\begin{equation}
(\frac{\delta Y}{Y})^2 = (\frac{\delta A}{A})^2 + (\frac{\delta B}{B})^2 + (\frac{\delta C}{C})^2
\tag{R2}
\end{equation}

Finally, if our function is a power of one of our variables (for example, the area of a circle),

$$Y = A^\alpha$$

Then we can compute the uncertainty by
\begin{equation}
\frac{\delta Y}{Y} = |\alpha| \frac{\delta A}{A}
\tag{R3}
\end{equation}

Let’s try our volume example. Because V = xyz, we use ([R2](#mjx-eqn)),

\begin{align*}
\frac{\delta V}{V} = \sqrt{\left(\frac{\delta x}{x}\right)^2 + \left(\frac{\delta y}{y}\right)^2 + \left(\frac{\delta z}{z}\right)^2} \\
= \sqrt{\left(\frac{0.5}{100}\right)^2 + \left(\frac{0.5}{50}\right)^2 + \left(\frac{0.5}{20}\right)^2} \\
\frac{\delta V}{1 * 10^5} = 0.027386 \\
\delta V = 2738.6
\nonumber
\end{align*}

Therefore we may write
$$V = (1.000 \pm 0.027) * 10^5 cm^3 \quad \textrm{or} \quad V = (0.1000 \pm 0.0027) m^3$$

Or, in more modern notation,
$$V = 1.000(27) * 10^5 cm^3 \quad \textrm{or} \quad V = 0.1000(27) m^3$$

Although this method seems complicated, it gives the best estimate of our uncertainties. After you’ve used it a few times, it will become second nature.

### Combining Formulae

What happens if we have something like the volume of a cylinder:
$$ V = \pi r^2 h$$

Here we must work with both ([R3](#mjx-eqn)) and ([R2](#mjx-eqn)). The key is to look at the formula as the product of $r^2$ and *h* ($\pi$ is exact and has no uncertainty). This requires us to use ([R2](#mjx-eqn)). But to get $\delta r^2$, we’ll need ([R3](#mjx-eqn)).

Let’s start with ([R2](#mjx-eqn)):

\begin{equation}
(\frac{\delta V}{V})^2 = (\frac{\delta r^2}{r^2})^2 + (\frac{\delta h}{h})^2
\nonumber
\end{equation}

Now, ([R3](#mjx-eqn)) tells us that

\begin{equation}
\frac{\delta \left(r^2 \right)}{r^2} = 2 \frac{\delta r}{r}
\nonumber
\end{equation}

So we can make the substitution:

\begin{equation}
\left(\frac{\delta V}{V}\right)^2 = \left(2 \frac{\delta r}{r} \right)^2 + \left(\frac{\delta h}{h}\right)^2
\nonumber
\end{equation}

Although it is tempting to multiply through by $V^2 = (\pi r^2 h)^2$ on both sides before we compute, it’s usually easier if we don’t. The fractional errors ($ \delta x/x$) are used in so many places that it makes sense to use those more often than not.