Sources:
 *  [oberlin.edu](http://www2.oberlin.edu/physics/dstyer/QM/Assignments/GaussianWavepacket.pdf)
 *  [Wikipedia](https://en.wikipedia.org/wiki/Wave_packet)

```python
import sympy as sm
import numpy as np
import matplotlib as plt
from IPython.display import display as pprint
sm.init_printing()
```

Useful Definitions
--------------------
We will be using some definitions from the oberlin paper and some custom ones for this notebook:

$$
\begin{align}
\beta(t) &= 1 + i\frac{\hbar}{m\sigma^2}t\\
E(\vec{p}) &= \frac{\vec{p} \cdot \vec{p}}{2m} \\
\end{align}
$$

General Form of the Wave Packet
-------------------------------
A wave packet is an equation of a wave trapped within a certain "width" $\sigma$ travelling with a certain "speed" $\frac{p_0}{m}$.

From oberlin, this wave has an equation of the form:
$$
\begin{equation}
\Psi(\vec{x}, t) = \frac{1}{\sqrt{\sigma \beta \sqrt{\pi}}}
\exp{\left(\frac{i}{\hbar}(\vec{p_0} \cdot \vec{x} - E(\vec{p_0})t)\right)}
\exp{\left(-\frac{1}{2\sigma^2\beta}(\vec{x} - \frac{\vec{p_0}}{m}t)^2\right)}
\end{equation}
$$

However, it is actually more useful to start with the $\Psi(\vec{x},0)$ wave function:
$$
\begin{equation}
\Psi(\vec{x}, 0) = \frac{1}{\sqrt{\sigma \sqrt{\pi}}}
\exp{\left(\frac{i}{\hbar}(\vec{p_0} \cdot \vec{x})\right)}
\exp{\left(-\frac{1}{2\sigma^2}(\vec{x})^2\right)}
\end{equation}
$$
since then we can generate the time steps on the fly via the Schrodinger Equation:
$$
i\hbar\frac{\partial \Psi}{\partial t} = V(\vec{x}, t)\Psi - \frac{\hbar^2}{2m}\nabla^2\Psi
$$

```python
#Constants
sigma, hbar, pi,m = sm.symbols('\sigma, \hbar, \pi, m', positive=True, real=True)

#Initial momentum
p_0 = sm.Matrix([*sm.symbols('p_{0x}, p_{0y}, p_{0z}')])

#Time, distance param; r = sqrt(x_v.dot(x_v))
r = sm.Symbol('|\\vec{x}|', real=True, positive=True)
t = sm.Symbol('t', real=True, positive=True)

#Position parameters
x_v = sm.Matrix([*sm.symbols('x, y, z')])
x, y, z = x_v

#Just for ease of visualisation
PX = sm.Symbol("(\\vec{p_0} \\cdot \\vec{x})")

#Oberlin vars
E_0 = sm.symbols('E_0', positive=True, real=True) #p_0.dot(p_0)/2*m
B = sm.symbols('\\beta')#1 + 1j*hbar*t/(m * sigma^2)

#Potential function
V = sm.symbols('V')

#Split up the declarations for legibility
Psi_coeff = 1/sm.sqrt(sigma * B * sm.sqrt(pi))
Psi_evo = sm.exp(1j/hbar * (p_0.dot(x_v) -E_0 * t))
Psi_pkt = sm.exp(-1/(2 * sigma**2 * B) * (x_v - p_0/m * t).dot(x_v - p_0/m * t))

#Build Psi
Psi = Psi_coeff * Psi_evo * Psi_pkt
Psi_t0 = Psi.subs({B : 1 + 1j*hbar *t/(m * sigma**2), t : 0}).simplify()
pprint(Psi)
pprint(Psi_t0)
```

These are some useful functions to make equations more legible:

```python
def e_rep(elm):
    relm = elm#.expand()

    #E_0 replacement
    epart_trms = [term for term in relm.args if term.has(p_0[0]**2) or term.has(p_0[1]**2) or term.has(p_0[2]**2)]
    if len(epart_trms) == 0:
        return relm
    nepart_trms = [term for term in relm.args if not term in epart_trms]
    epart = sum(epart_trms).factor() \
        .subs({1.0 * p_0.dot(p_0) : E_0 * 2 * m}) \
        .subs({p_0.dot(p_0) : E_0 * 2 * m})
    relm = (epart + sum(nepart_trms)).expand()
    return relm

def r_rep(elm):
    relm = elm.expand()

    #r**2 replacement
    xpart_trms = [term for term in relm.args if term.has(x**2) or term.has(y**2) or term.has(z**2)]
    if len(xpart_trms) == 0:
        return relm
    nxpart_trms = [term for term in relm.args if not term in xpart_trms]
    xpart = sum(xpart_trms).factor() \
        .subs({ x_v.dot(x_v) : r**2}) \
        .subs({ 1.0 * x_v.dot(x_v) : r**2})
    relm = (xpart + sum(nxpart_trms)).expand()
    return relm

def px_rep(elm):
    relm = elm.expand()

    #PX replacement
    pxpart_trms = [term for term in relm.args if term.has(x*p_0[0]) or term.has(y*p_0[1]) or term.has(z*p_0[2])]
    if len(pxpart_trms) == 0:
        return relm
    npxpart_trms = [term for term in relm.args if not term in pxpart_trms]
    pxpart = sum(pxpart_trms).factor() \
        .subs({1.0 * p_0.dot(x_v) : PX}) \
        .subs({p_0.dot(x_v) : PX})

    relm = (pxpart + sum(npxpart_trms)).expand()

    return relm
def b_rep(elm):
    return elm.subs({B : 1 + (1j*hbar*t)/(m * sigma**2)})

def red_exp(elm):
    return b_rep(px_rep(r_rep(e_rep(elm))))

```

Next we build the $t=0$ Laplacian and $KE_{coeff}$ functions to have a good starting point:

```python
lap_t0 = (Psi_t0.diff(x, 2) + Psi_t0.diff(y, 2) + Psi_t0.diff(z, 2)) \
    .subs({x_v.dot(x_v) : r**2, x_v.dot(p_0) : PX, }) \
    .expand()
lap_t0 = red_exp(lap_t0).nsimplify().simplify()
lap_t0
```

Hamiltonian Estimation
----------------------------------
Now that we have the wave at $t=0$, we need to evolve it via the Schrodinger Equation. To do that, we will
create equations to numerically estimate $\Psi(\vec{x}, t+ s)$ from the wave function at time $t$.

First, we will estimate the Hamiltonian of $\Psi$ at constant $t$:

The Taylor expansion for a function $g(a)$ is

$$g(a + \Delta a) = \sum_{k = 0}^{\infty} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)$$

.

Using this a formula can be extracted to determine the $n$th derivative of the function:

$$\begin{align}
g(a + \Delta a) &= \sum_{k = 0}^{\infty} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\\
&= \sum_{k = 0}^{n - 1} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a) +
\frac{(\Delta a)^n}{n!} \frac{d^n g}{da^n}(a) +
\sum_{k = n + 1}^{\infty} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\\
g(a + \Delta a) - \sum_{k = 0}^{n - 1} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a) - \sum_{k = n + 1}^{\infty} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a) &= \frac{(\Delta a)^n}{n!} \frac{d^n g}{da^n}(a)
\\
\frac{n!}{(\Delta a)^n}\left(
g(a + \Delta a) - \sum_{k = 0}^{n - 1} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a) - \sum_{k = n + 1}^{\infty} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\right)
&= \frac{d^n g}{da^n}(a)
\end{align}$$

A similar formula can be generated by replacing $\Delta a$ with $- \Delta a$:

$$\begin{align}
g(a - \Delta a) &= \sum_{k = 0}^{\infty} \frac{(-\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\\
&= \sum_{k = 0}^{n - 1} \frac{(-\Delta a)^k}{k!} \frac{d^k g}{da^k}(a) +
\frac{(-\Delta a)^n}{n!} \frac{d^n g}{da^n}(a) +
\sum_{k = n + 1}^{\infty} \frac{(-\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\\
g(a - \Delta a) - \sum_{k = 0}^{n - 1} \frac{(-\Delta a)^k}{k!} \frac{d^k g}{da^k}(a) - \sum_{k = n + 1}^{\infty} \frac{(-\Delta a)^k}{k!} \frac{d^k g}{da^k}(a) &= \frac{(-\Delta a)^n}{n!} \frac{d^n g}{da^n}(a)
\\
\frac{n!}{(-\Delta a)^n}\left(
g(a - \Delta a) - \sum_{k = 0}^{n - 1} \frac{(-\Delta a)^k}{k!} \frac{d^k g}{da^k}(a) - \sum_{k = n + 1}^{\infty} \frac{(-\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\right)
&= \frac{d^n g}{da^n}(a)
\end{align}$$

This is useful because, depending on $n$, they can be combined into a more accurate result. We only care about the cases $n=1$ and $n=2$ though, for the components of the Schrodinger equation. For $n=2$:

$$\begin{align}
g(a + \Delta a) &= \sum_{k = 0}^{\infty} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\\
&= g(a) +
\Delta a \frac{d g}{da}(a) +
\frac{(\Delta a)^2}{2} \frac{d^2 g}{da^2}(a) +
\sum_{k = 3}^{\infty} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\\
g(a + \Delta a) - g(a) - \Delta a \frac{d g}{da}(a) - \sum_{k = 3}^{\infty} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a) &= \frac{(\Delta a)^2}{2} \frac{d^2 g}{da^2}(a)
\\
\frac{2}{(\Delta a)^2}\left(
g(a + \Delta a) - g(a) - \Delta a \frac{d g}{da}(a) - \sum_{k = 3}^{\infty} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\right)
&= \frac{d^2 g}{da^2}(a)
\end{align}$$

$$\begin{align}
g(a - \Delta a) &= \sum_{k = 0}^{\infty} \frac{(-\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\\
&= g(a) -
\Delta a \frac{d g}{da}(a) +
\frac{(\Delta a)^2}{2} \frac{d^2 g}{da^2}(a) +
\sum_{k = 3}^{\infty} \frac{(-\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\\
g(a - \Delta a) - g(a) + \Delta a \frac{d g}{da}(a) - \sum_{k = 3}^{\infty} \frac{(-\Delta a)^k}{k!} \frac{d^k g}{da^k}(a) &= \frac{(\Delta a)^2}{2} \frac{d^2 g}{da^2}(a)
\\
\frac{2}{(\Delta a)^2}\left(
g(a - \Delta a) - g(a) + \Delta a \frac{d g}{da}(a) - \sum_{k = 3}^{\infty} \frac{(-\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\right)
&= \frac{d^2 g}{da^2}(a)
\end{align}$$

Add the 2 equations:

$$\begin{align}
2 \frac{d^2 g}{da^2}(a) &=
\frac{2}{(\Delta a)^2}\left(
g(a - \Delta a) - g(a) + \Delta a \frac{d g}{da}(a) - \sum_{k = 3}^{\infty} \frac{(-\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
+
g(a + \Delta a) - g(a) - \Delta a \frac{d g}{da}(a) - \sum_{k = 3}^{\infty} \frac{(\Delta a)^k}{k!} \frac{d^k g}{da^k}(a)
\right)
\\
 \frac{d^2 g}{da^2}(a) &=
\frac{1}{(\Delta a)^2}\left(
g(a - \Delta a) + g(a + \Delta a)  - 2g(a)  - \sum_{k = 1}^{\infty} \frac{2(\Delta a)^{2k+1}}{(2k+1)!} \frac{d^{2k+1}g}{da^{2k+1}}(a)
\right)
\end{align}$$

We can then use this to estimate the laplacian of $\Psi$, using a constant step value $d$:

$$\begin{align}
\nabla^2\Psi(\vec{a}, t) &= \sum_{j=0}^{Dims} \frac{\partial^2 \Psi}{\partial x_j^2} \\
&\approx \sum_j \frac{1}{d^2} \left(
\Psi(\vec{a} + d{\vec{x_j}}, t) + \Psi(\vec{a} - d{\vec{x_j}}, t) - 2\Psi(\vec{a}, t)
\right)
\\
&= \frac{-2D}{d^2}\Psi(\vec{a}, t) + \frac{1}{d^2} \sum_j  \left(
\Psi(\vec{a} + d{\vec{x_j}}, t) + \Psi(\vec{a} - d{\vec{x_j}}, t)
\right)
\end{align}$$

Finally we construct our estimation for $\hat{H}\Psi$:

$$\begin{align}
\hat{H}\Psi(\vec{x}, t) &= -\frac{\hbar^2}{2m}\nabla^2\Psi(\vec{x}, t) + V\Psi(\vec{x}, t) \\
&= \left(V(\vec{x}, t) + \frac{D\hbar^2}{md^2}\right)\Psi(\vec{x}, t) - \frac{\hbar^2}{2md^2}\sum_j \left(\Psi(\vec{x} + d\vec{x}_j, t) + \Psi(\vec{x} - d\vec{x}_j, t)\right)
\end{align}$$

Time Step Estimation
--------------------------
Next we will find an estimate for the time evolution, $\Psi(\vec{x}, t + s)$.

The partial Taylor expation of $\Psi$ for $t$ is

$$
\Psi(\vec{x}, t + s) =  \sum_{k = 0}^{\infty} \frac{s^k}{k!} \frac{\partial^k \Psi}{\partial a^k}(\vec{x}, t)
$$

Since we already know that $\frac{\partial \Psi}{\partial t}(\vec{x}, t) = \frac{\hat{H}\Psi(\vec{x}, t)}{i\hbar}$, we can expand the first 2 terms:

$$\begin{align}
\Psi(\vec{x}, t + s) &=  \sum_{k = 0}^{\infty} \frac{s^k}{k!} \frac{\partial^k \Psi}{\partial t^k}(\vec{x}, t) \\
&=  \Psi(\vec{x}, t) + s\frac{\partial \Psi}{\partial t}(\vec{x}, t) + \sum_{k = 2}^{\infty} \frac{s^k}{k!} \frac{\partial^k \Psi}{\partial t^k}(\vec{x}, t) \\
\end{align}$$

We again flip the step variable to reduce the error:

$$\begin{align}
\Psi(\vec{x}, t - s) &=  \sum_{k = 0}^{\infty} \frac{(-s)^k}{k!} \frac{\partial^k \Psi}{\partial t^k}(\vec{x}, t)
\\
&=  \Psi(\vec{x}, t) - s\frac{\partial \Psi}{\partial t}(\vec{x}, t) + \sum_{k = 2}^{\infty} \frac{(-s)^k}{k!} \frac{\partial^k \Psi}{\partial t^k}(\vec{x}, t)
\\
\end{align}$$

Combining the 2 produces:

$$
\Psi(\vec{x}, t+s) = \Psi(\vec{x}, t - s) + 2s\frac{\partial \Psi}{\partial t}(\vec{x}, t) + \sum_{k = 1}^\infty \frac{2s^{2k+1}}{(2k+1)!} \frac{\partial ^{2k+1}\Psi}{\partial t^{2k+1}}(\vec{x}, t)
\\
\approx \Psi(\vec{x}, t - s) + 2s\frac{\partial \Psi}{\partial t}(\vec{x}, t)
$$

Final Equation
----------------
Finally we can combine everything into a single equation:

$\Psi(\vec{x}, t+s) = \Psi(\vec{x}, t - s) + \frac{2s}{i\hbar}\left(
(V(\vec{x}, t) + \frac{D\hbar^2}{md^2})\Psi(\vec{x}, t) - \frac{\hbar^2}{2md^2}\sum_j \left(\Psi(\vec{x} + d\vec{x}_j, t) + \Psi(\vec{x} - d\vec{x}_j, t)\right)
\right)$

Example Calculations
------------------------
We will exclusively be performing the simulations in a 2-D environment so we can use a colormap.

We will also construct an object to hold and run simulations using our new equations:

```python
class ExperimentRunner(object):

    """Constructs a new experiment holder.
    V is a potential function of type (x, y, t) -> double.
    m is the mass.
    d is the position step value.
    s is the time step value.
    xmin/xmax/ymin/ymax specify the rectangular borders.
    hbar is the value we will use to estimate the reduced Planck constant.
    res is a pre-loaded results board."""
    def __init__(self, V, m, d, s, xmin, xmax, ymin, ymax, hbar, res):
        self.V = V
        self.d = d
        self.s = s
        self.res = res
        self.xmin = xmin
        self.xmax = xmax
        self.ymin = ymin
        self.ymax = ymax
        self.boundschecks = {}

        self.Hcoeff = 2*s/(1j * hbar)
        self.lap_Psi_coeff = hbar**2/(m * d**2)
        self.lap_offPsi_coeff = -hbar**2/(2 * m * d**2)
        assert self.Hcoeff != 0
        assert self.lap_Psi_coeff != 0
        assert self.lap_offPsi_coeff != 0

    """Gets a previous Psi result. The numbers passed are in index form for speed."""
    def getPsiRes(self, xind, yind, tind):
        if xind < 0 or xind + 1 > len(self.res[0]) or yind < 0 or yind + 1 > len(self.res[0][0]) and not '{} | {} | {}'.format(tind, xind, yind) in self.boundschecks:
            self.boundschecks['{} | {} | {}'.format(tind, xind, yind)] = 'getRes;'
            return 0
        assert tind <= len(self.res), "Can't just predict from nothing!"
        if tind < 0:
            tind = 0
        return self.res[tind][xind][yind]

    """Calculates the value of Psi(x, y, ts) from previous results."""
    def calcNextPsi(self, args):

        x, y, ts = args

        #Current t
        t = ts - self.s

        #Border conditions
        if x > self.xmax or x < self.xmin or y > self.ymax or y < self.ymin and not '{} | {} | {}'.format(tind, xind, yind) in self.boundschecks:
            self.boundschecks['{} | {} | {}'.format(tind, xind, yind)] = 'calcNext;'
            return 0

        #Get the indices in the results list
        tind = int(round(t/self.s))
        xind = int(round((x - self.xmin)/self.d))
        yind = int(round((y - self.ymin)/self.d))

        #Err if we didn't yet make a new res board
        assert len(self.res) -1 >= tind+1, "Results dimension err: wanted {} but got {}!\nRes is:\n{}".format(tind + 1, len(self.res)-1, str(self.res))

        #Err if we already calced here
        assert self.res[tind+1][xind][yind] == 0, "Already calced at {}, {}, {}!" %(tind+1, xind, yind)

        #Psi here, now
        Psi_t = self.getPsiRes(xind, yind, tind)
        #Psi here, before
        Psi_t_s = self.getPsiRes(xind, yind, tind - 1)
        #Psi to the +x
        Psi_xs = self.getPsiRes(xind+1, yind, tind)
        #Psi to the -x
        Psi_x_s = self.getPsiRes(xind-1, yind, tind)
        #Psi to the +y
        Psi_ys = self.getPsiRes(xind, yind+1, tind)
        #Psi to the -y
        Psi_y_s = self.getPsiRes(xind, yind-1, tind)

        #The sum of the positional offsets
        offPsi = Psi_xs + Psi_x_s + Psi_ys + Psi_y_s

        if not offPsi and not '{} | {} | {}'.format(tind, xind, yind) in self.boundschecks:
            self.boundschecks['{} | {} | {}'.format(tind, xind, yind)] = 'offPsi;'

        #The current hamiltonian
        HPsi = (self.V(x, y, t) + self.lap_Psi_coeff) * Psi_t + self.lap_offPsi_coeff * offPsi

        if not HPsi and not '{} | {} | {}'.format(tind, xind, yind) in self.boundschecks:
            self.boundschecks['{} | {} | {}'.format(tind, xind, yind)] = 'HPsi;'

        Psi_ts = Psi_t_s + self.Hcoeff * HPsi

        if not Psi_ts and not '{} | {} | {}'.format(tind, xind, yind) in self.boundschecks:
            self.boundschecks['{} | {} | {}'.format(tind, xind, yind)] = 'Sum;'

        self.res[tind + 1][xind][yind] = Psi_ts

        if not self.res[tind + 1][xind][yind]:
            print(self.boundschecks)
            assert False, "Fuck"
        return Psi_ts

    """Constructs and adds an empty results board to self.res."""
    def makeBoard(self):
        startlen = len(self.res)
        xdim = int(round((self.xmax - self.xmin)/self.d))
        ydim = int(round((self.ymax - self.ymin)/self.d))
        self.res.append(np.zeros((xdim, ydim), dtype=complex))
        assert len(self.res) == startlen + 1

```

For our example potential, we will construct a square container from -1 to 1 on each side with a potential "dome" of the form $V(x, y, t) = -\sqrt{x^2 + y^2}$:

```python
def V_f(x, y, t):
    if x >= 1 or x <= -1 or y >= 1 or y <= -1:
        return 9e99
    r = np.sqrt(x**2 + y**2)
    if r <= 1:
        return r
    else:
        return 0
```

Instead of starting with a wave packet, we will use a Dirac delta wave at $t=0$ and $d=s=10^{-3}$:

```python
ymin = xmin = -1
ymax = xmax = 1
d_v = 10**-3
s_v = 10**-3
res0 = [np.zeros((2000, 2000), dtype=complex)]
res0[0][1000][1000] = 1.0 + 0j
```

```python
exp1_runner = ExperimentRunner(V_f, 1, d_v, s_v, xmin, xmax, ymin, ymax, 1, res=res0)
len(exp1_runner.res)
```

```python
import multiprocessing as parl

threads = parl.Pool(processes=1)

for tind in range(1, 10):
    exp1_runner.makeBoard()
    for xind in range(0, 2000):
        curArgs = map(lambda yind: (xind * d_v + xmin, yind * d_v + ymin, s_v * tind), range(0, 2000))
        threads.map(ewxp1_runner.calcNextPsi, curArgs)
        assert np.any(exp1_runner.res[tind]), "Wave disappeared at index "+str(tind)
    print('Finished calcing for tind '+str(tind))

```


```python
from IPython.display import display, HTML
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
#InteractiveShell.ast_node_interactivity = "last"
```

```python
# Load inputs
import pandas as pd
df = pd.read_pickle("bau.pickle")

# Already have OAT, no need for wx.
#wx = pd.read_pickle("wxhydro2018.pickle")
#df = dfIn.merge(wx,how='outer',left_index=True,right_index=True)
#df = pd.DataFrame(df[df['OAT']<16]['OAT'].dropna())
#df.describe()
```

# Introduction

### Programmable Stat
My house is occupied 24/7, but everyone sleeps on the same schedule, so we could consider implementing a night setback program.  NSB in a fancy commercial BAS builds a lookup table so it know's each zone's thermal response speed to temperature perturbations, but most residential programmable stats only let you modify the RTSP on a schedule.

The hard way to estimate savings from a reduction in interior temperature is to model the heating system's behaviour.  It's hard because it requires a solid heat transfer model of the entire house, including ventilation, to carefully determine the heat loss, solving the differential equations to account for the exponential decay in transfer rates as temperatures approach steady state, and then trying to figure out how the heating system will compensate.

But fortunately, we already know how much heat is coming through the walls, and for a first-order approximation, heat loss is proportional to the delta-T between inside and outside. Our savings will be the time-weighted change in delta-T that we can produce with scheduling.

In my house, if I turn the heat off overnight with the OAT at around 10C, it will cool from 22 to 19 in about 8 hours.  It takes about three hours to warm back up from 18 no matter what the OAT.

Applying Newton's law of cooling $T(t)=OAT+(RT-OAT)e^{-kt}$, we find

\begin{align*}
T(8)=19 & =10+12e^{-8k}\\
ln(\frac{9}{12}) & =-8k\\
k & =0.036\\
\end{align*}

Heating dominates over losses, so the warmup rate is quite linear:

$$WARMUP\:RATE = 0.5\frac{^{\circ}C}{hr}$$

Bedtime's around 10PM and reveille around 7AM, so let's use a NSB schedule of 8PM-4AM.  We can tolerate things cooling off a bit just before bedtime, and the 4AM start guarantees that we'll have hit our "occupied" RTSP by 7AM on all but very cold nights.

```python
# RTSP = 22 Occupied, 18 Night
#df = pd.DataFrame(bau['20170901':'20180301']['OAT'].dropna())
df.loc[(df.index.hour>=4)&(df.index.hour<20),'nrtsp']=22
df.loc[(df.index.hour<4)|(df.index.hour>=20),'nrtsp']=18
```

```python

```

```python
# Now calculate the sum of delta-T for BAU vs NSB (heating only)
df['bauhd']=df.rt-df.OAT
df.loc[df['bauhd']<0,'bauhd']=0
df['nsbhd']=df.nrt-df.OAT
df.loc[df['nsbhd']<0,'nsbhd']=0
#project savings
heatDTsaved = (df.bauhd.sum() - df.nsbhd.sum())
heatfracsaved = heatDTsaved/df.bauhd.sum()
print("Heating savings: %.2f%%"%(100*heatfracsaved))
#df.nsbhd.describe()
#df.bauhd.describe()
```

    Heating savings: 7.61%

```python
df.nsbhd.describe()
df.bauhd.describe()
df.nsbhd.sum() / df.bauhd.sum()
```

    count    8760.000000
    mean       10.333060
    std         4.850989
    min         0.000000
    25%         6.171439
    50%        10.998450
    75%        14.100000
    max        23.900000
    Name: nsbhd, dtype: float64

    count    8760.000000
    mean       11.183767
    std         5.257502
    min         0.000000
    25%         6.500000
    50%        12.000000
    75%        15.500000
    max        25.000000
    Name: bauhd, dtype: float64

    0.9239337241165206

The following chart illustrates how much more quickly things cool down when it's cold out.  The green squiggle shows the sum of reduced $\Delta T$ values (RT vs OAT) over two periods of three days, as compared to a constant RT of 22C.

```python
import matplotlib.pyplot as plt
fig, axes = plt.subplots(nrows=2, ncols=1,figsize=(7,12),sharey=True)
#fig.suptitle("Typical NSB HDH Savings")

dd=pd.DataFrame(df['2013-05-10':'2013-05-13'])
dd['SavHDH'] = (dd.rt-dd.nrt).cumsum()
a=0

tit="Warm OAT ~ %d°C"%dd.OAT.mean()
ax=dd[['nrt','OAT','SavHDH']].plot(ax=axes[a],kind='line',title=tit,secondary_y='SavHDH')
dd[['nrtsp']].plot(ax=axes[a],kind='line',drawstyle="steps-post",alpha=.3)
lines = ax.get_lines() + ax.right_ax.get_lines() # Need to add "lines" from right ax to label it.
#ax.legend(lines,['RT','OAT','SavHDH','RTSP'])
ax.set_ylabel('Degrees C')

dd=pd.DataFrame(df['2013-12-6':'2013-12-9'])
dd['SavHDH'] = (dd.rt-dd.nrt).cumsum()
a=1
tit="Cold OAT ~ %d°C"%dd.OAT.mean()
ax=dd[['nrt','OAT','SavHDH']].plot(ax=axes[a],kind='line',title=tit,secondary_y='SavHDH')
dd[['nrtsp']].plot(ax=axes[a],kind='line',drawstyle="steps-post",alpha=.3)
lines = ax.get_lines() + ax.right_ax.get_lines() # Need to add "lines" from right ax to label it.
#ax.legend(lines,['RT','OAT','SavHDH','RTSP'])
ax.right_ax.set_ylabel('Sum of saved degree-hours')

plt.show()

```

## NSB Hourly Consumption
The average annual savings number is nice to know, but what we *really* want to know is the new hourly consumption -- otherwise we have no way of knowing how the NSB will interact with other measures.

We can get fairly close with the assumptions we've already made:

  - Cooldown: hourly heating load is 0
  - Steady state: same as BAU, but with savings proportional to the reduction in $\Delta T$ vs OAT.
  - Total heating savings $BAU \rightarrow NSB$ are known from the approximation above
  - We can now solve for the sum of heating loads during warmup periods
    - distribute this sum between all warmup periods
    - pro rate such that hours with a fractional warmup get a fractional share.

\begin{align}
\sum H_{NSB}	=\sum H_{BAU}\times(1-Savings\sim6.5\%)\\
H_{NSB}	=H_{cooldown}+H_{SS}+H_{warmup}\\
\begin{aligned}H_{cool} &=0\\
H_{SS} & =H_{BAU}\times\left(1-\frac{\Delta T_{NSB}}{\Delta T_{BAU}}\right)\\
\end{aligned}\\
\sum H_{warmup}	=\sum H_{BAU}(0.935)-\sum H_{SS}
\end{align}

Distribute as follows:

\begin{align}
\sum Hours_{warm}=\sum f(t) \begin{cases}
1 & \Delta T(t)\ge1C\\
\Delta T(t) & \Delta T(t)<1C
\end{cases}\\
H(t)=\frac{\sum H}{\sum Hours} \times f(t)\\
\end{align}

```python
# This is a row operation to calculate the fractional savings.
# No longer necessary, calculated above.
def saveDT(row):
    RTN=row.nrt
    RTB=row.rt
    OAT=row.OAT
    # Computes the fractional reduction in delta T for NSB as compared to BAU
    DTN=RTN-OAT
    DTB=RTB-OAT
    if(DTB<=0): return 0 # There are no savings possible if the BAU DT indicates no heating.
    if(DTN<=0): DTN=0 # Best we can do is turn off the heat and save 100%
    return (DTB-DTN)/DTB

# Categorize periods and calculate new heating load for each period
h = df[(df.hload!=0)].dropna()
# Set default values for cooling
h['cooldown']= h.nrt > h.nrtsp
h.loc[h.cooldown,'savefrac'] = 1
h.loc[h.cooldown,'nhload'] = 0

#compute savings fraction and hload for the Steady State periods
# I think there's an error here.
h['ss']= h.nrt == h.nrtsp
#h['savefrac'] =
h.loc[h.ss,'savefrac']=h[h.ss].apply(saveDT,axis=1)
h.loc[h.ss,'nhload']=h[h.ss]['hload'] * (1-h[h.ss]['savefrac'])

#now get ready for the warmup rows
h['warmup']= h.nrt < h.nrtsp
```

```python

```

```python

```

```python
# calculate the expected total NSB heating load, and the number of hours over which it should be distributed
hw = pd.DataFrame(h[h.warmup])

# Count the whole and fractional hours where warmup occurs
hw['whrs'] = hw.nrtsp - hw.nrt # This is correct where DT<=1
hw.loc[hw.whrs>1,'whrs'] = 1 # Because whrs is 1 where DT>1
whrs = hw.whrs.sum()

# Total Warmup heating is expected NSB total, less expected NSB at SS.
expectedHnsb = h.hload.sum() * (1-heatfracsaved)
expectedHss = h[h.ss].hload.sum()
expectHwarmup = expectedHnsb - expectedHss

# Now distribute the warmup load
HwarmPerWhr = expectHwarmup / whrs
h.loc[hw.index,'nhload'] = (hw.whrs * HwarmPerWhr)

# There is an error here:
h.nhload.sum() / h.hload.sum()

```

    0.9188191831627831

```python
# These should match
#expectedHss
#h.nhload[h.ss].sum()

# So should these
#expectHwarmup
#h.nhload[h.warmup].sum()

h.nhload.sum()
```

    6369.993951481103

```python
h.hload.sum()
```

    6932.8047

```python

```

```python

```


# Characterization of Discrete Systems in the Time Domain

*This Jupyter notebook is part of a [collection of notebooks](../index.ipynb) in the bachelors module Signals and Systems, Comunications Engineering, Universität Rostock. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Difference Equations

The relation between input $x[k]$ and output $y[k] = \mathcal{H} \{ x[k] \}$ of a discrete system can be described by relating actual and past samples of the input and output signal to each other. When restricting the relation to a linear superposition of these, this results in a [linear difference equation](https://en.wikipedia.org/wiki/Linear_difference_equation). The interconnection of difference equations to [linear time-invariant (LTI) systems](https://en.wikipedia.org/wiki/LTI_system_theory) is discussed in the following.

### Linear Difference Equations with Constant Coefficients

The relation between the input $x[k] \in \mathbb{C}$ with $k \in \mathbb{Z}$ and output $y[k] \in \mathbb{C}$ of a system in terms of a linear difference equation with constant coefficients $a_n, b_m \in \mathbb{C}$ reads

\begin{equation}
\sum_{n = 0}^{N} a_n \; y[k - n] = \sum_{m = 0}^{M} b_m \; x[k -m]
\end{equation}

with $a_N \neq 0$. The order of the difference equation is given by $N$. For a unique solution of the difference equation, $N$ initial conditions are required. Under the assumption that the input signal of the system $x[k] = 0$ for $k < 0$ these are given by $y[0], y[-1], \dots, y[-N + 1]$. Other cases can be handled accordingly.

### Linear Time-Invariant Systems

LTI systems are an important class of systems. In order to show that linear difference equations with constant coefficients represent LTI systems, the linearity and time-invariance property has to be checked. A system is linear when the superposition principle holds

\begin{equation}
\mathcal{H} \{ A x_1[k] + B x_2[k] \} = A \cdot \mathcal{H} \{ x_1[k] \} + B \cdot \mathcal{H} \{ x_2[k] \}
\end{equation}

for arbitraty signals $x_1[k], x_2[k] \in \mathbb{C}$ and coefficients $A, B \in \mathbb{C}$. Above condition is fulfilled by a linear difference equation, since the [summation](https://en.wikipedia.org/wiki/Summation) on left- and right-hand side is a linear operation. A system is time-invariant when

\begin{equation}
y[k - \kappa] = \mathcal{H} \{ x[k - \kappa] \}
\end{equation}

with $y[k] = \mathcal{H} \{ x[k] \}$ holds for $\kappa \in \mathbb{Z}$. Substituting $k$ by $k -\kappa$ in above linear difference equation shows that the time-invariance is fulfilled.

### Computation of the Output Signal

The solution of above difference equation is derived by extracting $y[k]$ from the sum on the left-hand-side and rearranging terms. This results in

\begin{equation}
y[k] = \frac{1}{a_0} \left( \sum_{m=0}^{M} b_m \; x[k-m] - \sum_{n=1}^{N} a_n \; y[k-n] \right)
\end{equation}

for $a_0 \neq 0$. The output signal is given by a weighted superposition of the actual and past samples of the input signal $x[k]$ and past samples of the output signal $y[k]$. The second sum is known as *recursive* part since it can be interpreted as a feedback of past output samples. The dependency on the input samples (the first sum) is known as *non-recursive* part.

It can be concluded that linear difference equations with constant coefficients represent LTI systems. The coefficients together with the initial conditions characterize an LTI system fully. However, not all LTI systems can be described by linear difference equations of finite order $N$. Idealized systems may result in a linear difference equation of infinite order, for instance.

### Recursive and Non-Recursive Systems

Above analysis of the solution of a linear difference equation revealed two contributions to the output signal $y[k]$: a non-recursive part which depends only on the current and past input samples $x[k]$ and a recursive part which depends on past output samples $y[k]$. A system is said to be

* non-recursive if its output depends only on the input signal $x[k]$. This implies that $a_n = 0$ for $n > 0$.
* recursive if its output depends on the input signal $x[k]$ and past output samples $y[k]$. This implies that at least one $a_n \neq 0$ for $n > 0$.

The classification into non-recursive and recursive systems has far reaching implications on the theory and practical realization of recursive systems. The feedback of past output samples in recursive systems for instance, may lead to instabilities and numerical issues. The former are discussed in detail later.

### Examples

Some examples for discrete systems defined by their input/output relation formulated in terms of a linear difference equation are given in the following.

#### Delay

The output signal $y[k] = \mathcal{H} \{ x[k] \}$ of a discrete system which delays the input $x[k]$ by $\kappa \in \mathbb{N}$ samples is given as

\begin{equation}
y[k] = x[k - \kappa]
\end{equation}

The order of the system is $N=0$. The coefficients of the linear difference equation read

\begin{align}
a_0 &= 1 \\
b_m &= \begin{cases}
1 & \text{for } m=\kappa \\
0 & \text{otherwise}
\end{cases}
\end{align}

The system is non-recursive as can be deduced from its coefficients.

The output signal $y[k]$ for a cosine signal at the input $x[k] = \cos[\Omega k]$ is computed by solving the difference equation for zero initial conditions and $\kappa = 5$. The Python module [`scipy.signal`](http://docs.scipy.org/doc/scipy/reference/signal.html) offers various functionality for continuous and discrete systems. The function [`scipy.signal.filter`](http://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.lfilter.html#scipy.signal.lfilter) implements above formula for the computation of the output signal $y[k]$ of a discrete LTI system given its coefficients and initial conditions. The coefficients $a_n$ and $b_m$ of the system are stored in the vectors `a` and `b`.

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

a = 1.0
b = [0.0, 0.0, 0.0, 0.0, 1.0]

k = np.arange(30)
x = np.cos(2*np.pi/15 * k)
y = signal.lfilter(b, a, x)
```

The input $x[k]$ and output signal $y[k]$ are plotted for illustration.

```python
plt.figure(figsize=(10, 4))
plt.subplot(121)
plt.stem(k, x)
plt.xlabel('$k$')
plt.ylabel(r'$x[k]$')
plt.ylim([-1.1, 1.1])

plt.subplot(122)
plt.stem(k, y)
plt.xlabel('$k$')
plt.ylabel('$y[k]$')
plt.ylim([-1.1, 1.1])
plt.tight_layout()
```

**Exercise**

* Change the delay $\kappa$ of the system and rerun the example.

#### Moving Average

The [moving average](https://en.wikipedia.org/wiki/Moving_average) of a time-series is frequently used to smooth out short-term fluctuations in order to emphasize longer-term trends. A system which calculates the moving average of its input signal $x[k]$ is represented by

\begin{equation}
y[k] = \frac{1}{M} \sum_{\kappa = 0}^{M-1} x[k - \kappa]
\end{equation}

The order of the system is $N=0$. The coefficients of the linear difference equation read

\begin{align}
a_0 &= 1 \\
b_m &= \begin{cases}
\frac{1}{M} & \text{for } 0 \leq m < M \\
0 & \text{otherwise}
\end{cases}
\end{align}

The system is non-recursive as can be deduced from its coefficients. The output of the system for a cosine signal at the input superimposed with noise is computed as illustration of the functionality of the moving average.

```python
np.random.seed(seed=0)

M = 10
a = [1.0]
b = 1/M * np.ones(M)

k = np.arange(0, 60)
x = np.cos(2*np.pi/30 * k) + .2 * np.random.normal(size=(len(k)))
y = signal.lfilter(b, a, x)

plt.figure(figsize=(10, 4))
plt.subplot(121)
plt.stem(k, x)
plt.xlabel('$k$')
plt.ylabel(r'$x[k]$')
plt.ylim([-1.5, 1.5])

plt.subplot(122)
plt.stem(k, y)
plt.xlabel('$k$')
plt.ylabel('$y[k]$')
plt.ylim([-1.5, 1.5])
plt.tight_layout()
```

**Exercise**

* Change the length `M` of the moving average. How does the output of the system change?

#### Second-Order System

The recursive LTI system with the following linear difference equation is given

\begin{equation}
y[k] - y[k-1] + \frac{1}{2} y[k-2] = x[k]
\end{equation}

The order of the system is $N=2$. The coefficients of the system can be derived from its difference equation as $a_0 = 1$, $a_1 = -1$, $a_2 = \frac{1}{2}$ and $b_0 = 1$. The output signal for a rectangular signal at the input $x[k] = \text{rect}_{20}[k]$ is computed and plotted for zero initial conditions.

```python
def rect(k, N):
    return np.where((k >= 0) & (k < N), 1.0, 0.0)

a = [1.0, -1.0, 1/2]
b = [1.0]

k = np.arange(0, 40)
x = rect(k, 20)
y = signal.lfilter(b, a, x)

plt.figure(figsize=(10, 4))
plt.subplot(121)
plt.stem(k, x)
plt.xlabel('$k$')
plt.ylabel(r'$x[k]$')
plt.ylim([-0.7, 2.6])

plt.subplot(122)
plt.stem(k, y)
plt.xlabel('$k$')
plt.ylabel('$y[k]$')
plt.ylim([-0.7, 2.6])
plt.tight_layout()
```

**Copyright**

This notebook is provided as [Open Educational Resource](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebook for your own purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Sascha Spors, Continuous- and Discrete-Time Signals and Systems - Theory and Computational Examples*.


# Taller de Python Capítulo 2:  Numérico y Laboratorio

## _Bibliotecas_
Ya hemos visto la vez anterior que python tiene varias funciones que vienen "de fábrica" como ```help()```, ```print()``` así también como operaciones básicas entre números como sumar, restar, etc; también vimos que nosotros podemos crear nustras propias funciones para que hagan lo que necesitemos usando la palabra clave ```def nombre_funcion```. Si uno necesita siempre realizar las mismas operaciones, reutilizará la misma función en todos sus códigos.
Por ejemplo: Supongamos que uno quiere calcular ```cos(x)```. Python no viene por defecto con esa operación, uno debería crear un algoritmo (es decir, una serie de acciones) que calcule el valor del cos de x (cosa que puede ser no trivial), pero es _obvio_ que alguien ya lo hizo antes, alguien ya pensó el algoritmo, lo escribió, y lo utiliza diariamente, si esta persona subió su código a internet, todos podemos aprovechar y utilizarlo sin preocuparnos en cómo hizo esta persona!! Solamente hay que decirle a Python _donde_ es que está guardado esta función. **Esta posibilidad de usar algoritmos de otros es fundamental en la programación, porque es lo que permite que nuestro problema se limite solamente a entender cómo llamar a estos algoritmos ya pensados y no tener que pensarlos cada vez**.

Vamos entonces a decirle a python que, además de sus operaciones de fábrica, queremos ampliar nuestro abanico de operaciones matemáticas en todas las opciones que aparecen dentro de la biblioteca "math" (una biblioteca es, tal como sugiere el nombre, un archivo con un montón de funciones, objetos y demás).

```python
import math #Importo a mi programa todo lo que este contenido dentro del archivo "math"
```

Con esta linea Python entiende que queremos que traiga todo lo que está dentro del archivo "math"

###### OJO: No todas las bibliotecas vienen instaladas por defecto, si queremos usar una muy rara es probable que tengamos que instalarla nosotros. Las que vamos a utilizar en el curso ya fueron instaladas automaticamente por Anaconda.

Macanudo, ahora que Python trajo esa biblioteca, nosotros podemos acceder a su contenido usando la sintaxis ```math.lo_que_haya_dentro_de_math``` por ejemplo, ```math.cos()```

```python
math.cos(0)
```

Hay una infinidad de bibliotecas o librerías dando vueltas por internet. Muchas veces el problema que queremos solucionar se reduce simplemente a encontrar la librería que tenga la funcionalidad correcta.

Ahora continuaremos usando una muy utilizada en nuestro ámbito, la querídisima NumPy.

## _NumPy_ : vectores, matrices y tablas de datos

NumPy (NUMeric PYthon) es **LA** biblioteca para operar sobre vectores de números. Además de contener un nuevo [tipo de dato](http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html) que nos va a ser muy útil para representar vectores y matrices, nos provee de un arsenal de [funciones de todo tipo](https://docs.scipy.org/doc/numpy/reference/routines.html).

Vamos a empezar por importar la bliblioteca _numpy_. La sintaxis típica de eso era `import biblio as nombre`:

```python
import numpy as np # con eso voy a poder acceder a las funciones de numpy a través de np.función()

# en ejemplo
print('El numero e = ', np.e)
#print(f"El numero e = {np.e}")
#print("El numero e = {}".format(np.e))

print('O el numero Pi = ', np.pi)
#print(f"O el numero Pi = {np.pi}")
#print("O el numero Pi = {}".format(np.pi))

# Podemos calcular senos y cosenos de numeros igual que con math!
print(np.sin(np.pi)) # casi cero! guarda con los floats!
```

    El numero e =  2.718281828459045
    O el numero Pi =  3.141592653589793
    1.2246467991473532e-16

Todo eso está muy bien, pero lo importante de _numpy_ son los arrays numéricos, que van a ser como una lista de números pero con muchos esteroides. Los arrays numéricos nos van a servir para representar vectores (el objeto matemático de "la tira de números", no el físico) o columnas/tablas de datos (el objeto oriyinezco o de laboratorio).

La idea es que es parecido a una lista: son muchos números juntos en la misma variable y están indexados (los puedo llamar de a uno dando la posición dentro de la variable). La gran diferencia con las listas de Python es que los arrays de _numpy_ operan de la forma que todos queremos:
1. Si sumamos o restamos dos arrays, se suman componente a componente.
2. Si multiplicamos o dividimos dos arrays, se multiplican o dividen componente a componente.

Veamos ejemplos usando la función `np.array` para crear arrays básicos.

```python
array_1 = np.array([1, 2, 3, 4]) # array toma como argumento un vector_like (lista, tupla, otro array...vector_like)
array_2 = np.array([5, 6, 7, 8])

# Se acuerdan como antes pasabamos de un objeto tipo 'range' a un objeto tipo 'list' porque eran "parecidos"?
# Aca hacemos lo mismo. Pasamos de un objeto tipo 'list' a un objeto tipo 'ndarray'

print("ARRAYS:")
print(f"{array_1} + {array_2} =  {array_1 + array_2}") # aca los sumo
print(f"{array_1} * {array_2} = {array_1*array_2}") # acá los multiplico

# en el caso de las listas esto era muy molesto
lista_1 = [1, 2, 3, 4]
lista_2 = [5, 6, 7, 8]
print("LISTAS:")
print(f"{lista_2} + {lista_1} = {lista_1 + lista_2}") # sumar concatena
# print(l1 * l2) # esto ni siquiera se puede hacer!
```

    ARRAYS:
    [1 2 3 4] + [5 6 7 8] =  [ 6  8 10 12]
    [1 2 3 4] * [5 6 7 8] = [ 5 12 21 32]
    LISTAS:
    [5, 6, 7, 8] + [1, 2, 3, 4] = [1, 2, 3, 4, 5, 6, 7, 8]

Y al igual que con las listas, uno puede acceder a elementos específicos de un array con su índice:

```python
print(array_1[0], array_1[1], array_1[2], array_1[3]) # son 4 elementos, los indices van del 0 al 3

# y más o menos vale todo lo que valía con listas
print(array_2[-1]) # agarro al último elemento de b
print(array_2[0:3]) # desde el primero hasta el 3 (no incluido el final, nunca se incluye)
```

    1 2 3 4
    8
    [5 6 7]

También podemos iterar sobre ellos con _for_ , de las mismas formas que habíamos visto para iterar listas.

```python
print("array_1:")
for num in array_1:  # Aca itero sobre los ELEMENTOS de array_1
    print(num)

print('\n') # Esto es solo para que agregue una linea nueva y no queden pegados

print("array_2:")
for j in range(len(array_2)): # Aca itero sobre los INDICES de array_2
    print(array_2[j])

```

    array_1:
    1
    2
    3
    4

    array_2:
    5
    6
    7
    8

Para crear *arrays* de dos dimensiones (o más), podemos aplicar la función `np.array` sobre una *lista de listas*:

```python
c = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) # cada lista corresponde a una fila de la matriz
print(c)

# Aca hacer c[0] dara la primer FILA de la matrix.
# Si queremos sacar la primer componente habra que hacer c[0, 0]
```

    [[1 2 3]
     [4 5 6]
     [7 8 9]]

Para facilitar la vida del usuario _numpy_ viene con un montón de rutinas de creación de arrays típicos. En particular, matrices típicas como las identidades o de todos elementos iguales a 1 o 0 y arrays con cierta cantidad de elementos entre dos números (muy útil para crear dominios para gráficos).

Veamos ejemplos de esos:

```python
# Equiespaciados >> Elijo CANTIDAD DE PUNTOS en un dado intervalo [a, b]
equi_linspace = np.linspace(0, 1, 10) # 9 números equiespaciados linealmente entre 0 y 1 (LINear SPACE = linspace)
print('Numero de puntos fijo:', equi_linspace) # N-1 puntos entre [0, 1] = {0, 1/n, 2/n, 3/n, ... , n/n = 1}

print('\n')
# Equiespaciados >> Elijo EL PASO entre los puntos en un dado intervalo [a, b)
equi_arange = np.arange(0, 1, 1./10) # como el range de Python pero puede venir con un paso en coma flotante
print('Paso entre puntos fijo:', equi_arange) # {0, paso, 2*paso, ... n*paso} siempre que n*paso<1

print('\n Identidad de 3×3')
identidad = np.identity(3)
print(identidad)

#otros para que prueben ustedes
ceros = np.zeros((4, 4)) # todos ceros, matriz de 4x4
unos = np.ones((2,3)) # todos unos, matriz de 2x3
ojos = np.eye(5, k=0) # unos en la diagonal, como identidad
ojos2 = np.eye(5, k=2) # qué pasó acá?

print()
print(ceros)
print()
print(unos)
print()
print(ojos)
print()
print(ojos2)
```

    Numero de puntos fijo: [0.         0.11111111 0.22222222 0.33333333 0.44444444 0.55555556
     0.66666667 0.77777778 0.88888889 1.        ]

    Paso entre puntos fijo: [0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9]

     Identidad de 3×3
    [[1. 0. 0.]
     [0. 1. 0.]
     [0. 0. 1.]]

    [[0. 0. 0. 0.]
     [0. 0. 0. 0.]
     [0. 0. 0. 0.]
     [0. 0. 0. 0.]]

    [[1. 1. 1.]
     [1. 1. 1.]]

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

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

Y antes de seguir, algo que siempre puede ser útil: los arrays tienen ciertas propiedades como su _shape_ (de cuánto por cuánto) y el _dtype_ (qué tipo de cosas tiene adentro). Podemos acceder a estos datos de la siguiente manera:

```python
x = np.linspace(0, 10, 1000) # ese array tiene 1000 elementos, andar printeando es poco práctico!

print(x.dtype) # Esto nos dice qué tipo de elemento es x (tener en cuenta que acá no van paréntesis)

ceros = np.zeros((100, 100)) # matriz de 100x100
print(ceros.shape) # np.shape nos dice cuántas filas y columnas tiene el array
# obs: esto es equivalente a:
# print(np.shape(ceros))

# Prueben qué pasa cuando le piden el shape a un array con una sola fila o columna como el x
```

    float64
    (100, 100)

---

### Ejercicio 1

Creen un vector con 20 valores entre -5 y 5 llamado ```dominio```, y un vector llamado ```imagen``` que contenga los valores de la función $x^2$ en el mismo intervalo.

**Ejercicio 1 _bis_ :** creen otro vector que calcule el valor absoluto de ```dominio``` (Ayuda: googlear "numpy valor absoluto")

---

### Ejercicio 2

La idea de este ejercicio es crear una matriz de 16x16 con los números del 1 al 256 siguiendo los pasos:

1) Crear un array que contenga los valores 1, 2, ..., 256

2) Modificar las dimensiones del array de 1×256 a 16×16

*Ayuda*: siempre que puedan utilicen las funciones de creación de arrays de numpy.

*Ayuda* bis: la función `np.reshape` hace lo que promete (reshapear) y puede tomar tuplas como argumento. Vean como usarla ya sea con su documentacion ```help(np.reshape)``` o con google.

---
## Graficar funciones

A lo largo de nuestras carreras, y de la vida, nos encontramos con información que queremos **graficar**. Vamos a ver que con la ayuda de la biblioteca [```matplotlib```](https://matplotlib.org/) esto no es nada complicado.

Esta librería es muy grande, por lo que tiene divididas sus funciones en distintos módulos. Para lo que vamos a ver en esta segunda parte nos alcanza con la sublibrería ```pyplot```. Una de las formas de importar esta librería es:
```python
import matplotlib.pyplot as plt
```

Podemos usar esta librería en conjunto con ```numpy``` para graficar las funciones que querramos.

Veamos un ejemplo, grafiquemos la función $f(x) = \sin(x)\sin(20x)$ entre $0$ y $2\pi$ (no tienen por qué entender la función, es solo el logo de [Arctic Monkeys](https://c1.staticflickr.com/7/6126/5933658807_601e0b1751_b.jpg))

```python
import numpy as np
import matplotlib.pyplot as plt
# La siguiente linea va si estás usando un Notebook
%matplotlib inline

# Definimos la función
def f(x): return np.sin(x)*np.sin(20*x)

# Definimos el dominio y la imagen
x = np.linspace(0, 2*np.pi, 500)
y = f(x)

# Graficamos la función
plt.plot(x, y)
plt.show() # Esta linea va si no estás usando un Notenook
```

Desglosemos el código que acabamos de escribir:
* Primero escribimos la función que queremos graficar.
* Luego, definimos nuestro dominio ```x``` y nuestra imagen ```y```.
* Y por último usamos la función de ```plot``` de ```matplotlib.pyplot``` para graficar ```y``` en función de ```x```.

**Aclaración 1: plt.show() vs %matplotlib:**
En este ejemplo pusimos estas dos lineas, pero son redudantes, solo una de ellas es necesaria. Si están trabajando en un Notebook, o en algo que use un Notebook de fondo, este puede trabajar con gráficos de forma interactiva, por lo que necesitan de la linea ```%matplotlib inline```. En el caso contrario, si están trabajando en un IDLE más usual, o ejecutando Python desde la terminal, necesitan usar la función ```plt.show()``` para mostrar los gráficos. No se vuelvan locos/as con esto, simplemente prueben cuál les funcina! (En Spyder puede funcionar cualquiera de las dos dependiendo de la configuración, pero por defecto se usa ```plt.show()```.)

**Aclaración 2:** ```y``` no es una función. Sino que es un _array_ que contiene los números que resultan de aplicarle ```f``` a cada uno de los valores de ```x```. Lo que vemos arriba parece ser una _función continua_ pero en realidad es un conjunto de punto unidos con una línea. Para visualizar mejor esto podemos hacer un gráfico que muestre explícitamente los puntos que forman el gráfico:

```python
plt.plot(x, y, '.-');
```

#### Qué hace plt.plot()?
Esta función que parece mágica lo único que hace es tomar los vectores que le damos como inputs y graficar un punto en cada par de coordenadas $(x,y)$. Además, por defecto une estos puntos con lineas rectas. Por lo tanto, para que la función pueda hacer su trabajo, es necesario que los _array_ ```x``` e ```y``` tengan la misma longitud. Para dejar claro este concepto veamos un ejemplo con pocos puntos escritos a mano:

```python
coords_en_x = np.array( [0, 2, 6, 8,  10] )
coords_en_y = np.array( [2, 4, 6, 14, 14] )

plt.plot(coords_en_x, coords_en_y, 'o:r')
plt.grid() # Esto me deja poner una grilla detrás!
```

Vemos que los puntos que se grafican son los $(x,y)$ = $\{(0,2);\,(2,4);\,(6,6);\,(8,14);\,(10,14)\}$

### Dar formato y estética a los gráficos
En los gráficos anteriores cuando usamos ```plt.plot``` agregamos un término opcional para cambiar el color y la forma en la que se muestran los puntos que graficamos. Si usamos ```help(plt.plot)``` vemos que este argumento opcional es el _[fmt]_ y que toma distintos _strings_ con los que se puede cambiar la forma en que se ve el gráfico. La estructura general es
```
fmt = '[color][marker][line]'
```
donde _marker_ hace referencia a la forma de los puntitos y _line_ hace referencia a la forma de la linea.

Los _strings_ válidos son un montón y los pueden ver en la documentación usando el _help_ o en la [documentación online](https://matplotlib.org/3.2.1/api/_as_gen/matplotlib.pyplot.plot.html) de la función.

Acá dejamos algunas de las opciones para que se den una idea:
* **color:** 'r' (rojo), 'b' (azul), 'g' (verde), 'm' (magenta), etc
* **marker:** '.' (puntito chiquito), 'o' (punto grande), '^' (triángulo), '*' (estrella), etc
* **line:** '-' (sólida), '--' (de a rayas), ':' (de a puntos)

#### Título, labels y grid
Veamos también algunas funciones que nos permiten agregar información a la figura sobre la que estamos trabajando. Veamos un ejemplo con la función de los [Arctic Monkeys](https://qph.fs.quoracdn.net/main-qimg-69d69b537bb5e7e3f21e44c36781a10d).

```python
import numpy as np
import matplotlib.pyplot as plt
# La siguiente linea solo es necesaria si estás usando Jupyter Notebook
%matplotlib inline

# Definimos la función
def f(x): return np.sin(x)*np.sin(20*x)

# Definimos el dominio y la imagen
x = np.linspace(0, 2*np.pi, 500)
y = f(x)

# Graficamos la función con linea sólida y le ponemos una etiqueta
plt.plot(x, y, '-', label='Arctic Monkeys')

# Agregamos título y etiquetamos los ejes
plt.title('Logo de los Arctic Monkeys', fontsize=16)
plt.xlabel('Eje de $\hat x$')
plt.ylabel('Eje de $\hat y$')

plt.grid()
plt.legend()
plt.show()
```

La mayoría de las funciones son bastante descriptivas. Veamos solo algunas aclaraciones:
* El _fontsize_ en el ```plt.title``` es un argumento opcional y también se puede utilizar en el ```plt.xlabel``` o ```plt.ylabel``` por ejemplo.
* El ```plt.grid()``` agrega una grid si no exite y la elimina si existe. Si se quiere forzar a que aparezca se puede utilizar ```plt.grid(True)```
* El ```plt.legend()``` es el que permite que se muestren los labels asociados a las lineas (en este caso es el "Arctic Monkeys"). Esta función tiene algunos parámetros opcionales que pueden probar como el _fontsize_ o el _loc_. (ver ```help(plt.legend)```)
* Podemos usar $LaTeX$ en los labels!

---
### Ejercicio 3

* Graficar la función $f(x) = x^2$ entre $-5$ y $5$ (pueden
reutilizar el código del Ejercicio 1)
* Etiquetar la función, los ejes y poner
un título (pueden usar Latex si conocen la syntaxis)

---

### Graficar información guardada y ajustes

#### Llevando los datos a Python
Muchas veces estamos acostumbrados a guardar los datos en hojas de Excel o en las Hoja de Cálculo de Drive. Python puede leer estas hojas utilizando las librerías adecuadas, pero es más sencillo, y es mejor práctica, exportar estos archivos en CSV para luego leerlos desde ahí.

##### Por qué CSV
* El formato **CSV (Comma Separated Values)** es abierto y un estándar a la hora de guardar datos de tipo fila o columna. Guardar los datos en Excel o Drive no está mal, pero se dificulta a la hora de pasarle los archivos a un/a colega, los CSV los puede abrir cualquiera! Inclusive, se pueden abrir Excel y Drive si se busca visualizarlo mejor.
* Con pocos datos no hay problemas de _lag_ o *crasheos*, pero si son muchos datos: _"Microsoft Excel dejó de responder"_ .$^1$

Acá una imagen de cómo descargar los archivos de Drive en CSV y de cómo se ven una vez en la compu. Es solo un archivo de texto con valores separados con comas, nada místico.

$\scriptsize\text{1. Para archivos muchos muuuchos datos inclusive los CSV se pueden quedar corto, y para estos casos existen formatos específicos que dependen de la naturaleza de los datos.}$

Una vez tenemos el archivo que descargamos podemos leerlo desde Python utilizando numpy. En este caso, la linea es
```python
data = np.loadtxt('data_exp.csv', delimiter=',', skiprows=1, unpack=True)
```

Desglosemos esto por argumentos:
1. El primer argumento es ubicación del archivo con la ruta respecto del archivo de Python. En este caso tenemos ambos archivos en la misma carpeta por lo que es solo el nombre.
2. El segundo argumento, _delimiter_, recibe un _string_ que le indica a Python cómo están separados los valores en nuestro archivo. En este caso es por comas porque son archivos CSV.
3. El tercer argumento es más opcional, y nos permite evitar que Python lea algunas lineas del archivo. En nuestro caso pusimos *1* ya que queremos evitar que lea la primera linea, que dice _"VAR 1, VAR 2"_.
4. Por qué ```unpack=True```

    ```np.loadtxt()``` por defecto te da los valores exactamente como están en el CSV, por lo que en este caso nos daría 2 columnas de datos con 20 filas. Utilizando ```unpack=True``` trasponemos los datos para pasar las columnas a filas, de modo que la primera fila (```data[0]```) sean los datos de _VAR 1_ y la segunda fila (```data[1]```) sean los valores de _VAR 2_. Acá el código utilizando ```unpack=True``` y sin utilizarlo

    También podríamos importarlo sin usar el ```unpack=True``` y luego usar ```data = np.transpose(data)```

```python
data = np.loadtxt('data_exp.csv', delimiter=',', skiprows=1)
print(f"Data sin unpack:\n {data}")

data = np.loadtxt('data_exp.csv', delimiter=',', skiprows=1, unpack=True)
print(f"\nData con unpack:\n {data}")
```

    Data sin unpack:
     [[ 0.    1.93]
     [ 1.58  1.67]
     [ 3.16  1.3 ]
     [ 4.74  0.63]
     [ 6.32  1.26]
     [ 7.89  2.38]
     [ 9.47  2.91]
     [11.05  2.36]
     [12.63  3.93]
     [14.21  4.8 ]
     [15.79  4.83]
     [17.37  5.17]
     [18.95  6.21]
     [20.53  7.94]
     [22.11  9.12]
     [23.68 11.09]
     [25.26 12.57]
     [26.84 14.28]
     [28.42 17.1 ]
     [30.   20.56]]

    Data con unpack:
     [[ 0.    1.58  3.16  4.74  6.32  7.89  9.47 11.05 12.63 14.21 15.79 17.37
      18.95 20.53 22.11 23.68 25.26 26.84 28.42 30.  ]
     [ 1.93  1.67  1.3   0.63  1.26  2.38  2.91  2.36  3.93  4.8   4.83  5.17
       6.21  7.94  9.12 11.09 12.57 14.28 17.1  20.56]]

### Graficar datos y hacer ajustes
Una vez tenemos los datos en Python podemos graficarlos igual que lo hacíamos antes. Por ahora vamos a graficarlo sin agregar labels para que quede el código más cortito.

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

# Importamos los datos
data = np.loadtxt('data_exp.csv', delimiter=',', skiprows=1, unpack=True)
var_x, var_y = data # es lo mismo que poner en dos lineas x = data[0] e y = data[1]

# Graficamos los datos crudos
plt.plot(var_x, var_y, 'o-k', label='Datos')

plt.legend()
plt.show()
```

Los datos parecen tener un comportamiento exponencial. Para realizar un ajuste podemos usar la función ```curve_fit()``` que nos ofrece la librería/sublibrería ```scipy.optimize```. Veamos cómo funciona con un ejemplo:

```python
# Importo la función desde la librería
from scipy.optimize import curve_fit

# Declaro la función con la que quiero ajustar con parámetros genéricos
def f_ajuste(x, A, C): return A*np.exp(C*x)

# Utilizo curve_fit() para el ajuste
popt, pcov = curve_fit( f_ajuste, var_x, var_y )

# Imprimo en pantalla los valores de popt y pcov
print(f'Parámetros óptimos para A y C (popt): {popt}')
print(f'\nMatriz de covariancia de popt (pcov):\n{pcov}')

# Graficamos los datos crudos
plt.plot(var_x, var_y, 'o-k', label='Datos')

# Graficamos el ajuste
A, C = popt
var_x_ajuste = var_x
var_y_ajuste = f_ajuste(var_x, A, C)
plt.plot(var_x_ajuste, var_y_ajuste, '-r', label='Ajuste')

# Legend y show
plt.legend()
plt.show()
```

##### Vemos que la función recibe tres inputs (obligatorios):
* **El primer argumento es la función _modelo_ con la que queremos ajustar.**

    La función modelo tiene que tener como primer argumento la variable que corresponde al eje $\hat x$. Luego se usan los otros argumentos para poner las constantes que queremos hallar (en nuestro caso A y C).

* **Los otros dos argumentos son nuestros datos**

    Estos son los datos crudos, los que usamos para visualizarlos normalmente.

##### Outputs del ```curve_fit()```
* **popt**

    Vemos que ```curve_fit()``` nos devuelve como primer parámetro una lista con los valores óptimos para nuestro ajuste. En este caso son dos porque en la función modelo pusimos dos constantes (A y C). Estos valores vienen ordenados del mismo modo que están en la función modelo por lo que ```popt[0]``` es el A óptimo y ```popt[1]``` es el C óptimo.

* **pcov**

    Esta es la matriz de covarianza de los resultados que nos da _popt_. Esta matriz nos da una idea de cuál es el error de este ajuste y de cuán ligados están estos errores entre sí. No es el objetivo del curso dar una clase de estadísitica, pero si conocemos que las variables son independientes podemos obtener el error de nuestros parámetros de ajuste como la raíz cuadrada de la covarianza. Para este ejemplo sería
    ```python
    err_A = np.sqrt(pcov[0,0])
    err_C = np.sqrt(pcov[1,1])
    ```
    o
    ```python
    err_A, err_C = np.sqrt(np.diag(pcov))
    ```

**Una cosita más:** En este caso pusimos ```var_x_ajuste = var_x```, lo que implica que el dominio para graficar nuestro ajuste tiene 20 puntos (los mismos que var_x), pero como conocemos la función podríamos hacer un linspace del estilo ```np.linspace( min(var_x), max(var_x), 1000 )``` para tener una mejor densidad de puntos. Esto esta bueno para hacer que los gráficos se vean menos acartonados, pero SOLAMENTE se puede usar a la hora de graficar.

#### Error y barras de error
* Si ven la [documentación](https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html) de ```curve_fit()``` ven que como parámetro opcional también se le puede agregar el error de nuestros datos.
* Si queremos poner barras de error a nuestro gráfico podemos usar ```plt.errorbar()```, que funciona de forma muy similar a ```plt.plot()``` pero nos permite dar un array con los errores o un error constante para todos.

#### Guardar un gráfico
Una vez tenemos un gráfico podemos guardarlo en el mismo directorio donde tenemos el archivo con el que estamos trabajando utilizando la linea
```python
plt.savefig('nombre_del_archivo.png')
```

#### Ejemplo final
Perfecto, utilizando todo lo aprendido el código para gráfico completo y exportado sería:

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit

# Importamos los datos
var_x, var_y = np.loadtxt('data_exp.csv', delimiter=',', skiprows=1, unpack=True)
err_var_y = 1.1 * np.ones(len(var_y)) # ponemos que todos los var_y tiene 1.1 de error a modo de ej

# Graficamos los datos crudos con su error
plt.errorbar(var_x, var_y, yerr=err_var_y, fmt='o-k', label='Datos')

# Calculamos el ajuste
def f_ajuste(x, A, C): return A*np.exp(C*x)
popt, pcov = curve_fit(f_ajuste, var_x, var_y, sigma=err_var_y)
A, C = popt

# Declaramos nuestro nuevo dominio e imagen y graficamos el ajuste
new_var_x = np.linspace(min(var_x), max(var_x), 1000)
new_var_y = f_ajuste(new_var_x, A, C)
plt.plot(new_var_x, new_var_y, '-r', label='Ajuste exponencial')

# Títuo y labels
plt.title('Datos y Ajuste Exponencial', fontsize=16)
plt.xlabel('Variable 1', fontsize=14)
plt.ylabel('Variable 2', fontsize=14)

# Grid, legend, save y show
plt.grid(True)
plt.legend()
plt.savefig('ajuste_exponencial.png')
plt.show()

# Printiamos en pantalla los parámetros óptimos con sus errores
err_A, err_C = np.sqrt(np.diag(pcov))
print(f'A: {A} ± {err_A}')
print(f'C: {C} ± {err_C}')
```

---
### Ejercicio 4
La idea ahora es que hagan un _script_ similar al de arriba para un set de datos lineal. Dejamos una pequeña guía.

1. Descargar el archivo en este [link](https://raw.githubusercontent.com/fifabsas/talleresfifabsas/master/python/2_Numerico/data_lineal.csv). Pueden hacerlo con click derecho -> "Guardar como..."
2. Importen el archivo data_lineal.csv a Python (miren el archivo para ver cuántas filas tienen que saltarse)
3. Grafiquen los datos crudos
4. Realicen un ajuste lineal de los datos (tengan en cuenta que la f_ajuste ahora va a ser otra!)
5. Grafiquen los datos crudos junto a el ajuste.
6. Decoren el gráfico tanto como les guste y guárdenlo en la computadora.

Este ejercicio es el más largo y complicado hasta ahora. Hay muchas cosas! No se preocupen si no llegan, después en su casa pueden ir paso a paso leyendo la guía, probando y preguntando cosas por el [canal de Discord](https://discord.gg/bN2KeTu).

---
---

### Acá termina nuestra clase.

Si aún hay tiempo abajo tenemos armado algunos párrafos sobre distintas cosas piolas que podemos hacer. La elección esta en ustedes. (Si, se convirtió en un elija su propia aventura, no se la esperaba nadie esa)

* Pandas y DataFrames
* Histogramas
* Sistemas de ecuaciones diferenciales
* Algebra Lineal
* Calculo Simbólico
* Integración Numérica
* Funciones Locas

---
# Pandas y DataFrames

[Pandas](https://pandas.pydata.org/) es una librería bastante utilizada para Análisis de Datos y tiene [demasiadas](https://pandas.pydata.org/docs/user_guide/index.html) cosas disponibles, demasiadas. Aca en el curso no vamos a cubrir casi nada, sólo veremos algunas cosas.

Pandas, tal como NumPy y SymPy, trae un nuevo tipo de dato muy genial, los DataFrames. Recomendado es el [tutorial](https://pandas.pydata.org/docs/getting_started/index.html) de 10 minutos que aporta la misma página de Pandas, en los que hace un paneo sobre las cosas básicas de la librería. Hay un [ejemplo práctico](https://github.com/fifabsas/talleresfifabsas/blob/master/python/Extras/Pandas_ejemplo.ipynb) hecho por la FIFA en el que se trabaja un poco los datos de inscripción de una jornada anterior del cursito este.

Los DataFrames, el nuevo tipo de dato introducido, son básicamente tablas de datos donde cada columna tiene un nombre descriptivo. Podemos operar con las columnas por su nombre, y también elegir filas como haciamos con arrays antes.

```python
import pandas as pd

df = pd.DataFrame(np.random.randn(8, 3), columns=['A', 'B', 'Columna'])

print(df) # Esto lo muestra mas "crudamente"

print()

df  # Si solo escribimos esto la Notebook lo printea re cheto
    # pero solo la notebook (Y quizas la terminal de spyder)
```

              A         B   Columna
    0  1.306630 -2.291737 -0.569453
    1 -1.152830  0.732933  0.321360
    2 -0.764704  0.196400 -0.936674
    3  1.058185 -0.324949 -1.742166
    4  0.330806  0.085614  0.438102
    5  1.626899  0.383054  0.628802
    6  1.001783 -0.187721  1.806652
    7  0.802827 -0.100067  0.662701

      A
      B
      Columna

      0
      1.306630
      -2.291737
      -0.569453

      1
      -1.152830
      0.732933
      0.321360

      2
      -0.764704
      0.196400
      -0.936674

      3
      1.058185
      -0.324949
      -1.742166

      4
      0.330806
      0.085614
      0.438102

      5
      1.626899
      0.383054
      0.628802

      6
      1.001783
      -0.187721
      1.806652

      7
      0.802827
      -0.100067
      0.662701

Además podemos acceder a cada columna con su nombre! Genial. De yapa nos dice el tipo de dato que esa columna contiene.

```python
print(df.A)
print()
print(df.Columna)
```

    0    1.306630
    1   -1.152830
    2   -0.764704
    3    1.058185
    4    0.330806
    5    1.626899
    6    1.001783
    7    0.802827
    Name: A, dtype: float64

    0   -0.569453
    1    0.321360
    2   -0.936674
    3   -1.742166
    4    0.438102
    5    0.628802
    6    1.806652
    7    0.662701
    Name: Columna, dtype: float64

Ya que está podemos indexear de otra forma y no con números del 0-7. Abajo mostramos una forma para indexear con fechas consecutivas. Además podemos tomar cachos del DataFrame igual que como hacíamos con vectores antes.

```python
dates = pd.date_range('20200404', periods=8)
df = pd.DataFrame({'Columna': np.random.randint(0,5,8),
                  'B': np.random.rand(8)},
                  index=dates)
print(df)
print()
print(df.loc[:, ['B']])
print()
print(df[0:4])
print()
print(df.sort_values(by='Columna'))
```

                Columna         B
    2020-04-04        1  0.876051
    2020-04-05        1  0.783758
    2020-04-06        2  0.881846
    2020-04-07        0  0.303063
    2020-04-08        4  0.192155
    2020-04-09        2  0.913549
    2020-04-10        2  0.879073
    2020-04-11        2  0.583418

                       B
    2020-04-04  0.876051
    2020-04-05  0.783758
    2020-04-06  0.881846
    2020-04-07  0.303063
    2020-04-08  0.192155
    2020-04-09  0.913549
    2020-04-10  0.879073
    2020-04-11  0.583418

                Columna         B
    2020-04-04        1  0.876051
    2020-04-05        1  0.783758
    2020-04-06        2  0.881846
    2020-04-07        0  0.303063

                Columna         B
    2020-04-07        0  0.303063
    2020-04-04        1  0.876051
    2020-04-05        1  0.783758
    2020-04-06        2  0.881846
    2020-04-09        2  0.913549
    2020-04-10        2  0.879073
    2020-04-11        2  0.583418
    2020-04-08        4  0.192155

La librería también viene con una forma bastante poderosa para levantar archivos de un [montón](https://pandas.pydata.org/docs/user_guide/io.html#csv-text-files) de tipos distintos (incluidas cosas tipo excel, json, html, etc...), vamos a usar ```read_csv``` para levantar el archivo que vimos arriba de la exponencial.

```python
pd.read_csv("data_exp.csv")

#
# Escrito asi nomas dejamos que Pandas trate de leer el archivo lo mejor que pueda
# pero hay muchas opciones para levantar archivos mas "conflictivos", como para
# definir el separador entre datos, las lineas iniciales, tipo de datos, etc...
# esto puede verse desde la documentación de la librería o google.
#
```

      VAR 1
      VAR 2

      0
      0.00
      1.93

      1
      1.58
      1.67

      2
      3.16
      1.30

      3
      4.74
      0.63

      4
      6.32
      1.26

      5
      7.89
      2.38

      6
      9.47
      2.91

      7
      11.05
      2.36

      8
      12.63
      3.93

      9
      14.21
      4.80

      10
      15.79
      4.83

      11
      17.37
      5.17

      12
      18.95
      6.21

      13
      20.53
      7.94

      14
      22.11
      9.12

      15
      23.68
      11.09

      16
      25.26
      12.57

      17
      26.84
      14.28

      18
      28.42
      17.10

      19
      30.00
      20.56

---

# Histogramas

Una cosita más que nos va a ser útil a la hora de dejar el Oriyin sin instalar es poder hacer histogramas. Con _pyplot_ eso lo podemos obtener de la función _hist_.

Recordemos que en un histograma dividimos una serie de datos en rangos y contamos cuántos de nuestros datos caen en cada rango. A esos rangos se los llama _bins_.

_hist_ toma como argumentos un array de números, en cuántos _bins_ queremos dividir a nuestro eje x y algunas otras opciones de color como constante de normalización y color de las barras.

Hagamos un histograma simple de un set gaussiano. Para eso, creemos datos sintéticos usando ```random.normal``` de _NumPy_ . Esto de crear datos lo hacemos acá a modo de ejemplo, en la vida real uno importaria algun dataset de las formas que ya hemos visto.

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

# Distribución gaussiana centrada en 100, con 15 de desviación estándar
data = np.random.normal(100, 15, 2000)

n, bins, patches = plt.hist(data, bins=20, edgecolor='black', facecolor='royalblue', alpha=0.75)
# n la variable n se encuentran los datos del histograma
# bins es un vector con los bordes de los rangos de datos
# patches no nos interesa en general

print(f'n:\n{n}\n')
print(f'bins:\n{bins}')
```

```python
# Si lo quisieramos normalizar el area hay que agregar una opcion mas que es density=True como para
# que entienda que queremos ver la "densidad de probabilidad", forma cheta para decir: normalizar el area.

n, bins, patches = plt.hist(data, bins=20, edgecolor='black', facecolor='royalblue', alpha=0.75, density=True)
```

Y ya que estamos, para concientizar acerca de los peligros a la hora de la elección de *bins*, graficamos algunos histogramas superpuestos.

```python
n, bins, patches = plt.hist(data, bins=100, density=True, edgecolor='black', facecolor='royalblue', alpha=0.75)
n, bins, patches = plt.hist(data, bins=20 , density=True, edgecolor='black', facecolor='purple'  , alpha=0.5)
n, bins, patches = plt.hist(data, bins=2 , density=True, edgecolor='black', facecolor='indianred'  , alpha=0.3)
```

### Ejercicio 6

La función `randn` que usamos nos brinda números aleatorios distribuidos de manera gaussiana (distribución normal).
1. Haga el mismo ejercicio de recién pero con números aleatorios distribuidos de manera uniforme
2. Lo mismo pero con números con una distribución pareto

_Info:_ https://docs.scipy.org/doc/numpy-1.14.0/reference/routines.random.html

---
## Resolución de un sistema de ecuaciones diferenciales ordinarias (ODE)

Vamos con un caso simple y conocido por la mayoría: el infaltable problema del péndulo. Arrancamos desde donde sabemos todos. (Donde sabemos todos? Si no saben de donde salió esto no se hagan problema, es solo algo físico, no lo podemos evitar)

$$ \frac{d^2\theta}{dt^2} + \omega^2 \theta = 0$$

con $\omega^2 = \frac{g}{l}$

Para resolver numéricamente este problema, proponemos utilizar la función `odeint` de la biblioteca `scipy.integrate` (Otra opción podría ser la función `solve_ivp`, para problemas de valores iniciales), y esa función utiliza un método no taaaaan distinto al [método de Euler](https://es.wikipedia.org/wiki/M%C3%A9todo_de_Euler) para la solución de ODE's, (inserte conocimientos de Cálculo Numérico aquí, sí, dale, cursala antes de recibirte) por lo que, para un problema con derivadas de segundo orden debemos armar un sistema de ecuaciones de primer orden.

Sea $\phi = \dot{\theta} \rightarrow \dot{\phi} = \ddot{\theta}$

Nos queda entonces

$$
\begin{align}
\dot{\theta} = \phi \\
\dot{\phi} = -\omega^2 \theta
\end{align}
$$

o bien

$$
\begin{equation}
\begin{pmatrix}
\dot{\theta} \\
\dot{\phi}
\end{pmatrix}
=
 \begin{pmatrix}
  0 & 1  \\
  -\omega^2 & 0
 \end{pmatrix}
\begin{pmatrix}
\theta \\
\phi
\end{pmatrix}
\end{equation}
$$

o también

$$ \dot{\vec{X}} = A \vec{X} $$

Escencialmente, nuestro `odeint` va a intentar resolver ese sistema para distintos $t$ mientras le hayamos dado un valor inicial de donde comenzar. Nada demasiado extraño. Así que lo que la función va a necesitar es una función que tome como primer argumento $\vec{X}$, segundo argumento $t$ (por ser la variable independiente) y luego los demás parametros que necesite (en este caso sean $g$ y $l$) y opere para obtener $\dot{\vec{X}}$.

```python
from scipy.integrate import odeint
import numpy as np
```

```python
def ecdif(X, t, g, l):
    theta, phi = X
    omega2 = g/l
    return [phi, -omega2 * theta]

g = 9.8
l = 2
X0 = [np.pi/4, 0] # Inicio a 45 grados con velocidad = 0
t = np.linspace(0, 10, 101)
solucion = odeint(ecdif, X0, t, args = (g,l))

plt.plot(t,solucion[:,0], label = 'theta')
plt.plot(t,solucion[:,1], label = 'phi')
plt.title('Resolucion del pendulo')
plt.xlabel('Tiempo')
plt.ylabel('Valores')
plt.grid(True)
plt.legend(loc = 'best')
plt.show()
```

Si quieren ver otro ejemplo que esto, recomendamos el [resuelto](https://github.com/fifabsas/talleresfifabsas/blob/master/python/Extras/Fisica2/oscilador_forzado.py) que hizo un Ex-FIFA del oscilador con forzante sin aproximación y comparado con la solución analítica (sí, esa que sin aproximación no buscó nadie). También recomendamos, a cuento de esto, un pasito más en resolución de ODE's que es una [simulación](https://github.com/fifabsas/talleresfifabsas/blob/master/python/Extras/Fisica1/simulacion.ipynb) de uno de los ejercicios de F1, donde se arma una animación y una visualización más interactiva con la biblioteca `ipywidgets` de tantas otras posibles.

### Ejercicio
1. Resuelvan el oscilador armónico amortiguado con el parámetro $\gamma$
2. Vuelvan a todas esas guías que nadie terminó, busquen los ejercicios con asterisco, métanlos en Python y aprecien el poder del cálculo numérico.

---

## Un poco de algebra lineal con *NumPy*

NumPy trae muchas funciones para resolver problemas típicos de algebra lineal usando a los arrays como vectores. El que nos interesa en general es el de autovalores y autovectores y el sistema de ecuaciones lineales, pero empecemos con un ejemplo más fácil:

```python
# cargamos el módulo de algebra lineal
from numpy import linalg
v = np.array([1, 1, 1])
w = np.array([2, 2, 2])
z = np.array([1, 0, 1])

norma =linalg.norm(v) # la norma 2 / módulo del vector v
print(norma)
print(np.sqrt(v[0]**2 + v[1]**2 + v[2]**2)) # calculado a mano == sqrt(3)
print(norma == np.sqrt(3)) # y numpy sabe que son lo mismo
```

    1.7320508075688772
    1.7320508075688772
    True

Ahora si, usemos los vectores que creamos recién para crear una matriz y digamosle a _NumPy_ que calcule los autovectoresy autovalores de esa matriz:

```python
matriz = np.array([v, w, z], dtype=np.float64)
#eig devuelve una tupla de arrays con los autovalores en un array 1D y los autovec en un array 2D
eigens = linalg.eig(matriz)

autvals, autvecs = eigens

print('Los autovalores:', autvals)
print()
print('Los autovectores:', autvecs)

#se terminó el problema
```

    Los autovalores: [ 3.41421356e+00 -1.23150120e-16  5.85786438e-01]

    Los autovectores: [[ 4.39732612e-01  7.07106781e-01 -3.03890631e-01]
     [ 8.79465224e-01 -5.45825614e-17 -6.07781262e-01]
     [ 1.82143212e-01 -7.07106781e-01  7.33656883e-01]]

Y para un sistema de ecuaciones del tipo $Ax = b$:

```python
mat = np.array([[1, 2, 5], [2, 5, 8], [4, 0, 8]], dtype=np.float64)
b = np.array([1, 2, 3])
x = linalg.solve(mat, b) #resuelve el sistema A*x = b
print(x)

#se terminó el problema
```

    [0.67857143 0.07142857 0.03571429]

Por supuesto, también se puede hacer producto matriz con vector, y... oh si, se pueden calcular *inversas*.

```python
print(np.dot(mat,x))
print(linalg.inv(mat))
```

    [1. 2. 3.]
    [[-1.42857143  0.57142857  0.32142857]
     [-0.57142857  0.42857143 -0.07142857]
     [ 0.71428571 -0.28571429 -0.03571429]]

---

### Ejercicio

Parecen incrédulos. Fabriquen entonces la matriz
$$
\begin{equation}
A =
\begin{pmatrix}
  0 & 1 \\
  1 & 0
\end{pmatrix}
\end{equation}
$$

cuyos autovalores son $\lambda_1 = 1$ y $\lambda_2 = -1 $, hallen sus autovalores y autovectores (a mano y con Python) y calculen $Ax$ con
$$
\begin{equation}
x =
\begin{pmatrix}
1\\0
\end{pmatrix}
\end{equation}
$$

---
### Ejercicio

Inventen una matriz de 5x5 (con el método que quieran y ¡que no sea la identidad!) y averigüen si es invertible. Si están prestando atención, saben que para resolver este problema les conviene ver la documentación de numpy.linalg.

*Ayuda*: ¿Es necesario calcular la inversa? ¿Se acuerdan algo de álgebra lineal?

---

# Cálculo simbólico

Ahora no sólo nos vamos a emancipar del *Oriyin*, sino también del bendito *Wolfram Alpha* (Disclaimer: si bien la librería que se usa es muy buena, todavía no llega a tener el nivel de software como Mathematica, Maple, etc).
Así es, Python nos va a permitir hacer cálculos simbólicos, como las integrales que nunca supimos calcular. Todo eso y más, en el paquete **SymPy**:

Hemos dicho que en general no es una buena práctica, pero este es uno de los pocos casos para los cuales se justifica importar toda la librería.

```python
from sympy import *
from sympy.abc import *
```

Les daremos ahora un breve tour por algunas de las funciones que nos ofrece *SymPy*. Así como *numpy* nos introdujo el array como tipo de variable, las expresiones algebraicas en *sympy* son del tipo *symbols*. Estas pueden representar números enteros, reales, funciones, etc.

A diferencia de las librerías anteriores, en las cuales usualmente escribimos los comandos dentro de un archivo a ejectutar todo junto (*script*); a modo de demostración utilizaremos *SymPy* de forma *interactiva*. Esta es posiblemente la manera en la que usaron *Wolfram Alpha* o *Mathematica* (para quienes lo hayan hecho).

Una forma conveniente para empezar es utilizando el siguiente comando:

```python
init_session(use_latex='matplotlib') #el argumento nos permite renderear los resultados con el latex de matplotlib
```

    IPython console for SymPy 1.5.1 (Python 3.7.3-64-bit) (ground types: gmpy)

    These commands were executed:
    >>> from __future__ import division
    >>> from sympy import *
    >>> x, y, z, t = symbols('x y z t')
    >>> k, m, n = symbols('k m n', integer=True)
    >>> f, g, h = symbols('f g h', cls=Function)
    >>> init_printing()

    Documentation can be found at https://docs.sympy.org/1.5.1/

Vimos aquí que una cantidad de variables se definieron como *symbols* de distintos tipos.
Veamos ahora algunas de las posibilidades que tenemos:

```python
expand( (x + y)**5 )
```

$\displaystyle x^{5} + 5 x^{4} y + 10 x^{3} y^{2} + 10 x^{2} y^{3} + 5 x y^{4} + y^{5}$

```python
factor( x**6 - 1 )
```

$\displaystyle \left(x - 1\right) \left(x + 1\right) \left(x^{2} - x + 1\right) \left(x^{2} + x + 1\right)$

Utilizando variables simbólicas de tipo integer, podemos hacer algunas sumatorias, por ejemplo, la famosa $$\sum_{k=0}^{m}k$$

```python
Sum(k, (k,0,m) ).doit().factor()  #el comando .doit() evalúa la suma
```

$\displaystyle \frac{m \left(m + 1\right)}{2}$

O incluso algunas series, como $$ \sum_{n=1}^{\infty}\frac{1}{n^2} $$

```python
Sum(1/n**2, (n, 1, oo)).doit() #el infinito se escribe como oo (dos o minúscula)
```

$\displaystyle \frac{\pi^{2}}{6}$

---
Posiblemente si queremos algo de cálculo simbólico, sea para calcular derivadas e integrales que nos molesten. Veamos algunos ejemplos $\dfrac{\text{d}x^n}{\text{d}x}$

```python
diff(x**n , x).simplify()
```

$\displaystyle n x^{n - 1}$

$$ \frac{d}{dx}\big(\ \sin(\tan(8x))\ \big)$$

```python
diff(sin(tan(8*x)), x)
```

$\displaystyle \left(8 \tan^{2}{\left(8 x \right)} + 8\right) \cos{\left(\tan{\left(8 x \right)} \right)}$

```python
out = diff(sin(tan(8*x)), x).simplify()
```

```python
# Podemos hacer sustituciones de simbolos por otros simbolos!!
out.subs(x, sqrt(y))
```

$\displaystyle \frac{8 \cos{\left(\tan{\left(8 \sqrt{y} \right)} \right)}}{\cos^{2}{\left(8 \sqrt{y} \right)}}$

```python
# Hasta podemos sustituir por valores numéricos y evaluar la funcion!!
print(f"{out.subs(x, 8)} = {out.subs(x, 8).evalf()}")
```

    8*cos(tan(64))/cos(64)**2 = -36.5316624851842

Si alguno desconfia, puede verlo desde [WolframAlfa](https://www.wolframalpha.com/input/?i=%288+%2B+8*tan%2864%29**2%29*log%288%29*cos%28tan%2864%29%29+%2B+sin%28tan%2864%29%29%2F8)

---
Otro ejemplo! Derivadas parciales: $\dfrac{\partial^2}{\partial y \partial x}\left( x\sin{y} \right)$

```python
diff(x*sin(y), x, y) #qué pasa si en lugar de x,y ponemos y,y?
```

$\displaystyle \cos{\left(y \right)}$

También se pueden hacer integrales indefinidas o definidas con dominio infinito/acotado, obviamente luego se podría reemplazar en la expresión final por algun valor numérico, si así se quisiera
$$\int \dfrac{1}{1+x^2}\text{d}x$$

```python
integrate(1/(1+x**2), x)
```

$\displaystyle \operatorname{atan}{\left(x \right)}$

$$\int_{-\infty}^{+\infty} \dfrac{\sin(x)}{x}\text{d}x$$

```python
integrate( sin(x)/x, (x,-oo,oo) )
```

$\displaystyle \pi$

---
Algo importantisimo y fabuloso de Sympy (algo que sorprendentemente NO tiene Mathematica ni Matlab) es que a cualquier expresión, podemos envolverla en un `latex()` para que devuelva la misma expresión que renderizó, pero en el formato de LaTeX. Derecho para mandar al informe o trabajo sin tipearla.

```python
out = diff(sin(tan(8*x))*log(x), x)
out
```

$\displaystyle \left(8 \tan^{2}{\left(8 x \right)} + 8\right) \log{\left(x \right)} \cos{\left(\tan{\left(8 x \right)} \right)} + \frac{\sin{\left(\tan{\left(8 x \right)} \right)}}{x}$

```python
print(f"Normal: {out}")
print(f"LaTeX: {latex(out)}")
```

    Normal: (8*tan(8*x)**2 + 8)*log(x)*cos(tan(8*x)) + sin(tan(8*x))/x
    LaTeX: \left(8 \tan^{2}{\left(8 x \right)} + 8\right) \log{\left(x \right)} \cos{\left(\tan{\left(8 x \right)} \right)} + \frac{\sin{\left(\tan{\left(8 x \right)} \right)}}{x}

### Ejercicio 9

Calculen la siguiente integral:  $$ \int_{-\infty}^{+\infty} e^{-x^2} \text{d}x $$ (opcional: verificar el resultado a mano!)

---
# Integración numérica
Siempre es útil tener a mano una rutina de integración numérica. Puede ser para el caso en el cual la integral analítica sea muy complicada o no estándar. También es fundamental para poder integrar datos obtenidos experimentalmente, sin asumir alguna función que los modele. Para ambos casos, el paquete relevante será `scipy.integrate`

### A partir de una función predefinida
En este caso, la función a importar es `quad`, porque usa un método de [cuadraturas](https://en.wikipedia.org/wiki/Numerical_integration#Quadrature_rules_based_on_interpolating_functions)

```python
from scipy.integrate import quad
```

Definamos una función para ser integrada. A modo de ejemplo, calcularemos

$$\int_a^b \dfrac{\text{e}^{-x^2 / 2}}{\sqrt{2\pi}}\text{d}x$$

```python
def integrando(x): return np.exp(-x**2 / 2)/np.sqrt(2*np.pi)
```

Ahora, llamamos a `quad` para integrar esta función entre $a=-1$ y $b=1$

```python
a = -1
b = 1
integral, error = quad(integrando, a, b)
print(integral, error)
```

    0.682689492137086 7.579375928402476e-15

Podemos, con este método, incluso obtener la primitiva numéricamente. Por ejemplo

$$F(x) = \int_a^x \dfrac{\text{e}^{-t^2 / 2}}{\sqrt{2\pi}}\text{d}t$$

```python
# Definimos este dominio
x = np.linspace(-5,5,100)

# Ahora obtenemos la primitiva
prim = np.zeros(x.size)

for i in range(x.size):
    prim[i], error = quad(integrando, a, x[i])

```

Veamos la gráfica para este dominio

```python
plt.figure()

plt.plot(x, integrando(x), 'b', label = 'integrando')
plt.plot(x, prim, 'r', label = 'primitiva')
plt.grid(True)
plt.legend()
```

---
# Funciones especiales

Tal vez en algún momento de la vida nos encontremos con funciones más exóticas que el seno, coseno y exponenciales, funciones que las conocemos de nombre pero de graficarlas ni hablemos. Por suerte, la biblioteca *scipy* cuenta con abanico muy grande de estas funciones con nombre propio. En la documentación (bloque de ayuda del spyder) podemos encontrar la lista completa de funciones, buscando `scipy.special`

```python
import scipy.special as sp
```

Empecemos graficando un ejemplo común en probabilidad, la *función error*:

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

x = np.linspace(-5,5,100) #generamos el dominio
y = sp.erf(x) #llama a la función error de la librería

plt.plot(x,y, label='función error')
plt.grid(True)
plt.legend(loc='best')
plt.show()
```

Dentro de las muchas funciones que nos ofrece la librería, otro ejemplo importante en la física son las *funciones de Bessel* $J_{\nu}(x)$. Hay una función para cada valor del índice $\nu$ (que llamamos orden). En la librería, se llaman con el comando `jv(i,x)`, donde la primer entrada corresponde al orden $\nu$.
Como demostración, veamos ahora algunos de sus gráficos:

```python
num = 500
r = np.linspace(0,10,num)

for i in range(5):
    y_i = sp.jv(i,r) # para cada i, grafica la función de orden i
    plt.plot(r,y_i,label='orden {}'.format(i))
    plt.legend(loc='best')
plt.grid(True)
plt.xlim(0, 10)
plt.title('Funciones de Bessel')
```

### Ejercicio 8

1. Busquen en la documentación la función *gamma*, y grafiquenla en el intervalo $\left[-5,5\right]$
2. (Opcional) Googleen a ver qué es lo tan importante de esa función.


# Time-varying frame

> Renato Naville Watanabe, Marcos Duarte
> [Laboratory of Biomechanics and Motor Control](http://pesquisa.ufabc.edu.br/bmclab)
> Federal University of ABC, Brazil

## Cartesian coordinate system

As we perceive the surrounding space as three-dimensional, a convenient coordinate system is the [Cartesian coordinate system](http://en.wikipedia.org/wiki/Cartesian_coordinate_system) in the [Euclidean space](http://en.wikipedia.org/wiki/Euclidean_space) with three orthogonal axes as shown below. The axes directions are commonly defined by the [right-hand rule](http://en.wikipedia.org/wiki/Right-hand_rule) and attributed the letters X, Y, Z. The orthogonality of the Cartesian coordinate system is convenient for its use in classical mechanics, most of the times the structure of space is assumed having the [Euclidean geometry](http://en.wikipedia.org/wiki/Euclidean_geometry) and as consequence, the motion in different directions are independent of each other.

Figure. Representation of a point and its position vector in a Cartesian coordinate system.

## Determination of a coordinate system

In Biomechanics, we may use different coordinate systems for convenience and refer to them as global, laboratory, local, anatomical, or technical reference frames or coordinate systems.

From [linear algebra](http://en.wikipedia.org/wiki/Linear_algebra), a set of unit linearly independent vectors (orthogonal in the Euclidean space and each with norm (length) equals to one) that can represent any vector via [linear combination](http://en.wikipedia.org/wiki/Linear_combination) is called a basis (or **orthonormal basis**). The figure below shows a point and its position vector in the Cartesian coordinate system and the corresponding versors (**unit vectors**) of the basis for this coordinate system. See the notebook [Scalar and vector](http://nbviewer.ipython.org/github/demotu/BMC/blob/master/notebooks/ScalarVector.ipynb) for a description on vectors.

Figure. Representation of a point $\mathbf{P}$ and its position vector $\overrightarrow{\mathbf{r}}$ in a Cartesian coordinate system. The versors $\hat{\mathbf{i}},\, \hat{\mathbf{j}},\, \hat{\mathbf{k}}\,$  form a basis for this coordinate system and are usually represented in the color sequence RGB (red, green, blue) for easier visualization.

One can see that the versors of the basis shown in the figure above have the following coordinates in the Cartesian coordinate system:

\begin{equation}
\hat{\mathbf{i}} = \begin{bmatrix}1\\0\\0 \end{bmatrix}, \quad \hat{\mathbf{j}} = \begin{bmatrix}0\\1\\0 \end{bmatrix}, \quad \hat{\mathbf{k}} = \begin{bmatrix} 0 \\ 0 \\ 1 \end{bmatrix}
\label{eq_1}
\end{equation}

Using the notation described in the figure above, the position vector $\overrightarrow{\mathbf{r}}$  can be expressed as:

\begin{equation}
\overrightarrow{\mathbf{r}} = x\hat{\mathbf{i}} + y\hat{\mathbf{j}} + z\hat{\mathbf{k}}
\label{eq_2}
\end{equation}

However, to use a fixed basis can lead to very complex expressions.

## Time varying basis

Consider that we have the position vector of a particle, moving in the path described by the parametric curve $s(t)$, described in a fixed reference frame as:

\begin{equation}
{\bf\hat{r}}(t) = {x}{\bf\hat{i}}+{y}{\bf\hat{j}} + {z}{\bf\hat{k}}
\label{eq_3}
\end{equation}

Figure. Position vector of a moving particle in relation to a coordinate system.

### Tangential versor

Often we describe all the kinematic variables in this fixed reference frame. However, it is often useful to define a time-varying basis, attached to some point of interest. In this case, what is usually done is to choose as one of the basis vector a unitary vector in the direction of the velocity of the particle. Defining this vector as:

\begin{equation}
{\bf\hat{e}_t} = \frac{{\bf\vec{v}}}{\Vert{\bf\vec{v}}\Vert}
\label{eq_4}
\end{equation}

### Normal versor

For the second vector of the basis, we define first a vector of curvature of the path (the meaning of this curvature vector will be seeing in another notebook):

\begin{equation}
{\bf\vec{C}} = \frac{d{\bf\hat{e}_t}}{ds}
\label{eq_5}
\end{equation}

Note that $\bf\hat{e}_t$ is a function of the path $s(t)$. So, by the chain rule:

\begin{equation}
\frac{d{\bf\hat{e}_t}}{dt} = \frac{d{\bf\hat{e}_t}}{ds}\frac{ds}{dt} \longrightarrow \frac{d{\bf\hat{e}_t}}{ds} = \frac{\frac{d{\bf\hat{e}_t}}{dt}}{\frac{ds}{dt}} \longrightarrow {\bf\vec{C}} = \frac{\frac{d{\bf\hat{e}_t}}{dt}}{\frac{ds}{dt}}\longrightarrow {\bf\vec{C}} = \frac{\frac{d{\bf\hat{e}_t}}{dt}}{\Vert{\bf\vec{v}}\Vert}
\label{eq_6}
\end{equation}

Now we can define the second vector of the basis, ${\bf\hat{e}_n}$:

\begin{equation}
{\bf\hat{e}_n} = \frac{{\bf\vec{C}}}{\Vert{\bf\vec{C}}\Vert}
\label{eq_7}
\end{equation}

Figure. A moving particle and a corresponding time varying basis.

### Binormal versor

The third vector of the basis is obtained by the cross product between ${\bf\hat{e}_n}$ and ${\bf\hat{e}_t}$.

\begin{equation}
{\bf\hat{e}_b} = {\bf\hat{e}_t} \times {\bf\hat{e}_n}
\label{eq_8}
\end{equation}

Note that the vectors ${\bf\hat{e}_t}$, ${\bf\hat{e}_n}$ and ${\bf\hat{e}_b}$ vary together with the particle movement.

## Velocity and Acceleration in a time-varying frame

### Velocity

Given the expression of $r(t)$ in a fixed frame we can write the velocity ${\bf\vec{v}(t)}$ as a function of the fixed frame of reference ${\bf\hat{i}}$, ${\bf\hat{j}}$ and ${\bf\hat{k}}$ (see http://nbviewer.jupyter.org/github/BMClab/bmc/blob/master/notebooks/KinematicsParticle.ipynb)).

\begin{equation}
{\bf\vec{v}}(t) = \dot{x}{\bf\hat{i}}+\dot{y}{\bf\hat{j}}+\dot{z}{\bf\hat{k}}
\label{eq_9}
\end{equation}

However, this can lead to very complex functions. So it is useful to use the basis find previously ${\bf\hat{e}_t}$, ${\bf\hat{e}_n}$ and ${\bf\hat{e}_b}$.

The velocity ${\bf\vec{v}}$ of the particle is, by the definition of ${\bf\hat{e}_t}$, in the direction of ${\bf\hat{e}_t}$:

\begin{equation}
{\bf\vec{v}}={\Vert\bf\vec{v}\Vert}.{\bf\hat{e}_t}
\label{eq_10}
\end{equation}

### Acceleration

The acceleration can be written in the fixed frame of reference as:

\begin{equation}
{\bf\vec{a}}(t) = \ddot{x}{\bf\hat{i}}+\ddot{y}{\bf\hat{j}}+\ddot{z}{\bf\hat{k}}
\label{eq_11}
\end{equation}

But for the same reasons of the velocity vector, it is useful to describe the acceleration vector in the time varying basis. We know that the acceleration is the time derivative of the velocity:

\begin{align}
{\bf\vec{a}} =& \frac{{d\bf\vec{v}}}{dt}=\\
    =&\frac{{d({\Vert\bf\vec{v}\Vert}{\bf\hat{e}_t}})}{dt}=\\
    =&\dot{\Vert\bf\vec{v}\Vert}{\bf\hat{e}_t}+{\Vert\bf\vec{v}\Vert}\dot{{\bf\hat{e}_t}}=\\
    =&\dot{\Vert\bf\vec{v}\Vert}{\bf\hat{e}_t}+{\Vert\bf\vec{v}\Vert}\frac{d{\bf\hat{e}_t}}{ds}\frac{ds}{dt}=\\
    =&\dot{\Vert\bf\vec{v}\Vert}{\bf\hat{e}_t}+{\Vert\bf\vec{v}\Vert}^2\frac{d{\bf\hat{e}_t}}{ds}=\\
    =&\dot{\Vert\bf\vec{v}\Vert}{\bf\hat{e}_t}+{\Vert\bf\vec{v}\Vert}^2\Vert{\bf\vec{C}} \Vert {\bf\hat{e}_n}
\label{eq_12}
\end{align}

## Example
For example, consider that a particle follows the path described by the parametric curve below:

\begin{equation}
\vec{r}(t) = (10t+100){\bf{\hat{i}}} + \left(-\frac{9,81}{2}t^2+50t+100\right){\bf{\hat{j}}}
\label{eq_13}
\end{equation}

This curve could be, for example, from a projectile motion. See http://nbviewer.jupyter.org/github/BMClab/bmc/blob/master/notebooks/ProjectileMotion.ipynb for an explanation on projectile motion.

```python
import numpy as np
import sympy as sym
from sympy.vector import CoordSys3D
import matplotlib.pyplot as plt
sym.init_printing()
from sympy.plotting import plot_parametric
from sympy.physics.mechanics import ReferenceFrame, Vector, dot
```

### Solving numerically

Now we will obtain the time-varying basis numerically. This method is useful when it is not available a mathematical expression of the path. This often happens when you have available data collected experimentally (most of the cases in Biomechanics).

First, data will be obtained from the expression of $r(t)$. This is done to replicate the example above. You could use data collected experimentally, for example.

```python
t = np.linspace(0, 10, 1000).reshape(-1,1)
r = np.hstack((10*t + 100, -9.81/2*t**2 + 50*t + 100))
```

Now, to obtain the $\bf{\hat{e_t}}$ versor, we can use Equation (4).

```python
dt = t[1]
v = np.diff(r,axis=0)/dt
vNorm  = np.linalg.norm(v, axis=1, keepdims=True)

et = v/vNorm
```

And to obtain the versor $\bf{\hat{e_n}}$, we can use Equation (8).

```python
C = np.diff(et,axis=0)/dt
C = C/vNorm[1:]

CNorm = np.linalg.norm(C, axis=1, keepdims=True)
en = C/CNorm
```

```python
from matplotlib.patches import FancyArrowPatch
plt.rcParams['figure.figsize'] = (8,8)
fig = plt.figure()
plt.plot(r[:,0],r[:,1],'.')
ax = fig.add_axes([0,0,1,1])
time = np.linspace(0,10,10)
for i in np.arange(0, len(t)-2, 50):
    vec1 = FancyArrowPatch(r[i,:],r[i,:]+10*et[i,:],mutation_scale=20,color='r')
    vec2 = FancyArrowPatch(r[i,:],r[i,:]+10*en[i,:],mutation_scale=20,color='g')
    ax.add_artist(vec1)
    ax.add_artist(vec2)
plt.xlim((80,250))
plt.ylim((80,250))
plt.legend([vec1, vec2],[r'$\vec{e_t}$', r'$\vec{e_{n}}$'])
plt.show()
```

```python
v  = vNorm*et
vNormDot  = np.diff(vNorm, axis=0)/dt

a = vNormDot*et[1:,:] + CNorm*en*vNorm[1:]**2
```

```python
from matplotlib.patches import FancyArrowPatch
%matplotlib inline
plt.rcParams['figure.figsize'] = (8,8)
fig = plt.figure()
plt.plot(r[:,0],r[:,1],'.')
ax = fig.add_axes([0,0,1,1])
for i in range(0,len(t)-2,50):
    vec1 = FancyArrowPatch(r[i,:],r[i,:]+v[i,:],mutation_scale=10,color='r')
    vec2 = FancyArrowPatch(r[i,:],r[i,:]+a[i,:],mutation_scale=10,color='g')
    ax.add_artist(vec1)
    ax.add_artist(vec2)
plt.xlim((80,250))
plt.ylim((80,250))
plt.legend([vec1, vec2],[r'$\vec{v}$', r'$\vec{a}$'])
plt.show()
```

### Symbolic solution (extra reading)

The computation here will be performed symbolically, with the symbolic math package of Python, Sympy. Below,a reference frame, called O,  and a varible for time (t) are defined.

```python
O = sym.vector.CoordSys3D(' ')
t = sym.symbols('t')
```

Below the vector $r(t)$ is defined symbolically.

```python
r = (10*t+100)*O.i + (-9.81/2*t**2+50*t+100)*O.j+0*O.k
r
```

```python
plot_parametric(r.dot(O.i), r.dot(O.j), (t, 0, 10))
```

```python
v = sym.diff(r)
v
```

```python
et = v/sym.sqrt(v.dot(v))
et
```

```python
C = sym.diff(et)/sym.sqrt(v.dot(v))
C
```

```python
en = C/(sym.sqrt(C.dot(C)))
sym.simplify(en)
```

```python
from matplotlib.patches import FancyArrowPatch
plt.rcParams['figure.figsize'] = (8, 8)
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
ax.axis("on")
time = np.linspace(0,10,30)
for instant in time:
    vt = FancyArrowPatch([float(r.dot(O.i).subs(t,instant)),float(r.dot(O.j).subs(t,instant))],
                         [float(r.dot(O.i).subs(t,instant))+10*float(et.dot(O.i).subs(t,instant)),
                          float(r.dot(O.j).subs(t, instant))+10*float(et.dot(O.j).subs(t,instant))],
                         mutation_scale=20,
                         arrowstyle="->",color="r",label='${\hat{e_t}}$')
    vn = FancyArrowPatch([float(r.dot(O.i).subs(t, instant)),float(r.dot(O.j).subs(t,instant))],
                         [float(r.dot(O.i).subs(t, instant))+10*float(en.dot(O.i).subs(t, instant)),
                          float(r.dot(O.j).subs(t, instant))+10*float(en.dot(O.j).subs(t, instant))],
                         mutation_scale=20,
                         arrowstyle="->",color="g",label='${\hat{e_n}}$')
    ax.add_artist(vn)
    ax.add_artist(vt)
plt.xlim((90,250))
plt.ylim((90,250))
plt.xlabel('x')
plt.legend(handles=[vt,vn],fontsize=20)
plt.show()
```

Now we can find the vectors ${\bf\vec{v}}$ and ${\bf\vec{a}}$ described in the time varying frame.

```python
v = sym.sqrt(v.dot(v))*et
```

```python
a = sym.diff(sym.sqrt(v.dot(v)))*et+v.dot(v)*sym.sqrt(C.dot(C))*en
sym.simplify(sym.simplify(a))
```

```python
from matplotlib.patches import FancyArrowPatch
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
ax.axis("on")
time = np.linspace(0, 10, 10)
for instant in time:
    vt = FancyArrowPatch([float(r.dot(O.i).subs(t,instant)),float(r.dot(O.j).subs(t,instant))],
                         [float(r.dot(O.i).subs(t,instant))+float(v.dot(O.i).subs(t,instant)),
                          float(r.dot(O.j).subs(t, instant))+float(v.dot(O.j).subs(t,instant))],
                         mutation_scale=20,
                         arrowstyle="->",color="r",label='${{v}}$')
    vn = FancyArrowPatch([float(r.dot(O.i).subs(t, instant)),float(r.dot(O.j).subs(t,instant))],
                         [float(r.dot(O.i).subs(t, instant))+float(a.dot(O.i).subs(t, instant)),
                          float(r.dot(O.j).subs(t, instant))+float(a.dot(O.j).subs(t, instant))],
                         mutation_scale=20,
                         arrowstyle="->",color="g",label='${{a}}$')
    ax.add_artist(vn)
    ax.add_artist(vt)
plt.xlim((60, 250))
plt.ylim((60, 250))
plt.legend(handles=[vt, vn], fontsize=20)
plt.show()
```

## Problems

1. Obtain the vectors $\hat{e_n}$ and $\hat{e_t}$ for the problem 17.1.1 from Ruina and Rudra's book.
2. Solve the problem 17.1.9 from Ruina and Rudra's book.
3. Write a Python program to solve the problem 17.1.10 (only the part of $\hat{e_n}$ and $\hat{e_t}$).

## References

+ Ruina A, Rudra P (2019) [Introduction to Statics and Dynamics](http://ruina.tam.cornell.edu/Book/index.html). Oxford University Press.