## Task 20 - Motor Control
### Introduction to modeling and simulation of human movement
https://github.com/BMClab/bmc/blob/master/courses/ModSim2018.md

Desiree Miraldo

* Task (for Lecture 20):

Solve problemas 3 and 4 of the notebook [Optimization (Marcos Duarte)](http://nbviewer.jupyter.org/github/BMClab/bmc/blob/master/notebooks/Optimization.ipynb).

```python
# import Python libraries
import numpy as np
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
import sympy as sym
from sympy.plotting import plot
import pandas as pd
from IPython.display import display
from IPython.core.display import Math
```

## Exercises

1. Find the extrema in the function $f(x)=x^3-7.5x^2+18x-10$ analytically and determine if they are minimum or maximum.
2. Find the minimum in the $f(x)=x^3-7.5x^2+18x-10$ using the gradient descent algorithm.
2. Regarding the distribution problem for the elbow muscles presented in this text:
    a. Test different initial values for the optimization.
    b. Test other values for the elbow angle where the results are likely to change.

3. In an experiment to estimate forces of the elbow flexors, through inverse dynamics it was found an elbow flexor moment of 10 Nm.
Consider the following data for maximum force (F0), moment arm (r), and pcsa (A) of the brachialis, brachioradialis, and biceps brachii muscles: F0 (N): 1000, 250, 700; r (cm): 2, 5, 4; A (cm$^2$): 33, 8, 23, respectively (data from Robertson et al. (2013)).
    a. Use static optimization to estimate the muscle forces.
    b. Test the robustness of the results using different initial values for the muscle forces.
    c. Compare the results for different cost functions.

>##### The distribution problem in biomechanics

>Using the inverse dynamics approach in biomechanics, we can determine the net force and torque acting on a joint if we know the external forces on the segments and the kinematics and inertial properties of the segments. But with this approach we are unable to determine the individual muscles forces that  created such torque, as expressed in the following equation:

>\begin{equation}
M_{total} = M_1 + M_2 + \dots + M_n = r_1F_1 + r_2F_2 + \dots + r_nF_n
\label{}
\end{equation}

>where $r_i$ is the moment arm of the force $F_i$ that generates a torque $M_i$, a parcel of the (known) total torque $M_{total}$.

>Even if we know the moment arm of each muscle (e.g., from cadaveric data or from image analysis), the equation above has $n$ unknowns. Because there is more than one muscle that potentially created such torque, there are more unknowns than equations, and the problem is undetermined. So, the problem is how to find how the torque is distributed among the muscles of that joint.

>One solution is to consider that we (biological systems) optimize our effort in order to minimize energy expenditure, stresses on our tissues, fatigue, etc. The principle of least action, stated in the opening of this text, is an allusion that optimization might be ubiquitous in nature. With this rationale, let's solve the distribution problem in biomechanics using optimization and find the minimum force of each muscle necessary to complete a given task.

>The following cost functions have been proposed to solve the distribution problem in biomechanics:

>\begin{equation}
\begin{array}{l l}
\displaystyle\sum_{i=1}^N F_i \quad &\text{e.g., Seireg and Arkivar (1973)}
\\
\displaystyle\sum_{i=1}^N F_i^2 \quad &
\\
\displaystyle\sum_{i=1}^N \left(\dfrac{F_i}{pcsa_i}\right)^2 \quad &\text{e.g., Crowninshield and Brand (1981)}
\\
\displaystyle\sum_{i=1}^N \left(\dfrac{F_i}{M_{max,i}}\right)^3 \quad &\text{e.g., Herzog (1987)}
\end{array}
\label{}
\end{equation}

>Where $pcsa_i$ is the physiological cross-sectional area of muscle $i$ and $M_{max,i}$ is the maximum torque muscle $i$ can produce.
Each muscle force $F_i$ is a design variable and the following constraints must be satisfied:

>\begin{equation}
\begin{array}{l l}
0 \leq F_i \leq F_{max}
\\
\displaystyle\sum_{i=1}^N r_i \times F_i = M
\end{array}
\label{}
\end{equation}

>Let's apply this concept to solve a distribution problem in biomechanics.

>##### Muscle force estimation

>Consider the following main flexors of the elbow joint (see figure below): biceps long head, biceps short head, and brachialis. Suppose that the elbow net joint torque determined using inverse dynamics is 20 Nm (flexor). How much each of these muscles contributed to the net torque?

>

Figure. A view in OpenSim of the arm26 model showing three elbow flexors (Biceps long and short heads and Brachialis).

>For the optimization, we will need experimental data for the moment arm, maximum moment, and *pcsa* of each muscle. Let's import these data from the OpenSim arm26 model:

```python
# time elbow_flexion BIClong BICshort BRA
r_ef = np.loadtxt('./filesLecture20/r_elbowflexors.mot', skiprows=7)
f_ef = np.loadtxt('./filesLecture20/f_elbowflexors.mot', skiprows=7)
```

>The maximum isometric force of these muscles are defined in the arm26 model as: Biceps long head: 624.3 N, Biceps short head: 435.56 N, and Brachialis: 987.26 N. Let's compute the mamimum torques that each muscle could produce considering a static situation at the different elbow flexion angles:

```python
m_ef = r_ef*1
m_ef[:, 2:] = r_ef[:, 2:]*f_ef[:, 2:]
```

>And let's visualize these data:

```python
labels = ['Biceps long head', 'Biceps short head', 'Brachialis']
fig, ax = plt.subplots(nrows=1, ncols=3, sharex=True, figsize=(10, 4))
ax[0].plot(r_ef[:, 1], r_ef[:, 2:])
#ax[0].set_xlabel('Elbow angle $(\,^o)$')
ax[0].set_title('Moment arm (m)')
ax[1].plot(f_ef[:, 1], f_ef[:, 2:])
ax[1].set_xlabel('Elbow angle $(\,^o)$', fontsize=16)
ax[1].set_title('Maximum force (N)')
ax[2].plot(m_ef[:, 1], m_ef[:, 2:])
#ax[2].set_xlabel('Elbow angle $(\,^o)$')
ax[2].set_title('Maximum torque (Nm)')
ax[2].legend(labels, loc='best', framealpha=.5)
ax[2].set_xlim(np.min(r_ef[:, 1]), np.max(r_ef[:, 1]))
plt.tight_layout()
plt.show()
```

>These data don't have the *pcsa* value of each muscle. We will estimate the *pcsa* considering that the amount of maximum muscle force generated per area is constant and equal to 50N/cm$^2$. Consequently, the *pcsa* (in cm$^2$) for each muscle is:

```python
a_ef = np.array([624.3, 435.56, 987.26])/50  # 50 N/cm2
print(a_ef)
```

    [ 12.486    8.7112  19.7452]

>##### Static versus dynamic optimization

>In the context of biomechanics, we can solve the distribution problem separately for each angle (instant) of the elbow; we will refer to that as static optimization. However, there is no guarantee that when we analyze all these solutions across the range of angles, they will be the best solution overall. One reason is that static optimization ignores the time history of the muscle force. Dynamic optimization refers to the optimization over a period of time. For such, we will need to input a cost function spanning the entire period of time at once. Dynamic optimization usually has a higher computational cost than static optimization. The term 'Dynamic optimization' used in Biomechanics is not related to the broader method known as '[dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming)' in mathematical optimization or computer programming.

>

Figure. Static and dynamic optimization approaches in Biomechanics. Figure taken from Robertson et al. (2013).

>The title 'Static and dynamic optimization solutions for gait are practically equivalent' of an article published by Anderson and Pandy (2001), where they studied normal walking (full gait cycle equals to 1.12 s), summarizes the results of such comparison. The authors reported that the dynamic solution required 1000 times more computation time than the static solution.

>For now, we will solve the present problem using static optimization.

>##### Solution of the optimization problem

>For the present case, we are dealing with a problem of minimization, multidimensional (function of several variables), nonlinear, constrained, and we can't assume that the cost function is convex. Numerical optimization is hardly a simple task. There are many different algorithms and public and commercial software for performing optimization. For instance, look at [NEOS Server](http://www.neos-server.org/neos/), a free internet-based service for solving numerical optimization problems.
We will solve the present problem using the [scipy.optimize](http://docs.scipy.org/doc/scipy/reference/optimize.html#module-scipy.optimize) package which provides several optimization algorithms. We will use the function `minimize`:

>```python
scipy.optimize.minimize(fun, x0, args=(), method=None, jac=None, hess=None, hessp=None, bounds=None, constraints=(), tol=None, callback=None, options=None)
"""Minimization of scalar function of one or more variables."""
```

>Now, let's write Python functions for each cost function:

```python
from scipy.optimize import minimize
```

```python
def cf_f1(x):
    """Cost function: sum of forces."""
    return x[0] + x[1] + x[2]

def cf_f2(x):
    """Cost function: sum of forces squared."""
    return x[0]**2 + x[1]**2 + x[2]**2

def cf_fpcsa2(x, a):
    """Cost function: sum of squared muscle stresses."""
    return (x[0]/a[0])**2 + (x[1]/a[1])**2 + (x[2]/a[2])**2

def cf_fmmax3(x, m):
    """Cost function: sum of cubic forces normalized by moments."""
    return (x[0]/m[0])**3 + (x[1]/m[1])**3 + (x[2]/m[2])**3
```

>Let's also define the Jacobian for each cost function (which is an optional parameter for the optimization):

```python
def cf_f1d(x):
    """Derivative of cost function: sum of forces."""
    dfdx0 = 1
    dfdx1 = 1
    dfdx2 = 1
    return np.array([dfdx0, dfdx1, dfdx2])

def cf_f2d(x):
    """Derivative of cost function: sum of forces squared."""
    dfdx0 = 2*x[0]
    dfdx1 = 2*x[1]
    dfdx2 = 2*x[2]
    return np.array([dfdx0, dfdx1, dfdx2])

def cf_fpcsa2d(x, a):
    """Derivative of cost function: sum of squared muscle stresses."""
    dfdx0 = 2*x[0]/a[0]**2
    dfdx1 = 2*x[1]/a[1]**2
    dfdx2 = 2*x[2]/a[2]**2
    return np.array([dfdx0, dfdx1, dfdx2])

def cf_fmmax3d(x, m):
    """Derivative of cost function: sum of cubic forces normalized by moments."""
    dfdx0 = 3*x[0]**2/m[0]**3
    dfdx1 = 3*x[1]**2/m[1]**3
    dfdx2 = 3*x[2]**2/m[2]**3
    return np.array([dfdx0, dfdx1, dfdx2])
```

>##### Let's define initial values (Exercises):

```python
# ---  Exercise 3 ---

M = 30  # desired torque at the elbow
iang = 45  # which will give the closest value to 90 degrees
r  = r_ef[iang, 2:]
f0 = f_ef[iang, 2:]
a  = a_ef
m  = m_ef[iang, 2:]
x0 = f_ef[iang, 2:]/10  # far from the correct answer for the sum of torques
print('M =', M)
print('x0 =', x0)
print('r * x0 =', np.sum(r*x0))
```

    M = 30
    x0 = [ 62.49103971  42.37048694  97.05571622]
    r * x0 = 5.81241788462

```python
# ---  Exercise 4 ---
'''
    - F0 (N): 1000, 250, 700;
    - r (cm): 2, 5, 4;
    - A (cm$^2$): 33, 8, 23
'''

M = 10  # desired torque at the elbow
iang = 68  # which will give the closest value to 90 degrees
r  = np.array([2, 5, 4])*1e-2
f0 = np.array([1000, 250, 700])
a  = np.array([33, 8, 23])
m  = r*f0
x0 = f0/10  # far from the correct answer for the sum of torques
print('M =', M)
print('x0 =', x0)
print('r * x0 =', np.sum(r*x0))
```

    M = 10
    x0 = [ 100.   25.   70.]
    r * x0 = 6.05

>Inequality constraints (such as boundaries in our problem) can be entered with the parameter `bounds` to the `minimize` function:

```python
bnds = ((0, f0[0]), (0, f0[1]), (0, f0[2]))
```

>Equality constraints (such as the sum of torques should equals the desired torque in our problem), as well as inequality constraints, can be entered with the parameter `constraints` to the `minimize` function (and we can also opt to enter the Jacobian of these constraints):

```python
# use this in combination with the parameter bounds:
cons = ({'type': 'eq',
         'fun' : lambda x, r, f0, M: np.array([r[0]*x[0] + r[1]*x[1] + r[2]*x[2] - M]),
         'jac' : lambda x, r, f0, M: np.array([r[0], r[1], r[2]]), 'args': (r, f0, M)})
```

```python
# to enter everything as constraints:
cons = ({'type': 'eq',
         'fun' : lambda x, r, f0, M: np.array([r[0]*x[0] + r[1]*x[1] + r[2]*x[2] - M]),
         'jac' : lambda x, r, f0, M: np.array([r[0], r[1], r[2]]), 'args': (r, f0, M)},
        {'type': 'ineq', 'fun' : lambda x, r, f0, M: f0[0]-x[0],
         'jac' : lambda x, r, f0, M: np.array([-1, 0, 0]), 'args': (r, f0, M)},
        {'type': 'ineq', 'fun' : lambda x, r, f0, M: f0[1]-x[1],
         'jac' : lambda x, r, f0, M: np.array([0, -1, 0]), 'args': (r, f0, M)},
        {'type': 'ineq', 'fun' : lambda x, r, f0, M: f0[2]-x[2],
         'jac' : lambda x, r, f0, M: np.array([0, 0, -1]), 'args': (r, f0, M)},
        {'type': 'ineq', 'fun' : lambda x, r, f0, M: x[0],
         'jac' : lambda x, r, f0, M: np.array([1, 0, 0]), 'args': (r, f0, M)},
        {'type': 'ineq', 'fun' : lambda x, r, f0, M: x[1],
         'jac' : lambda x, r, f0, M: np.array([0, 1, 0]), 'args': (r, f0, M)},
        {'type': 'ineq', 'fun' : lambda x, r, f0, M: x[2],
         'jac' : lambda x, r, f0, M: np.array([0, 0, 1]), 'args': (r, f0, M)})
```

>Although more verbose, if all the Jacobians of the constraints are also informed, this alternative seems better than informing bounds for the optimization process (less error in the final result and less iterations).

>Given the characteristics of the problem, if we use the function `minimize` we are limited to the SLSQP (Sequential Least SQuares Programming) solver.

>Finally, let's run the optimization for the four different cost functions and find the optimal muscle forces:

```python
f1r = minimize(fun=cf_f1, x0=x0, args=(), jac=cf_f1d,
               constraints=cons, method='SLSQP',
               options={'disp': True})
```

    Optimization terminated successfully.    (Exit mode 0)
                Current function value: 200.0
                Iterations: 12
                Function evaluations: 12
                Gradient evaluations: 12

```python
f2r = minimize(fun=cf_f2, x0=x0, args=(), jac=cf_f2d,
               constraints=cons, method='SLSQP',
               options={'disp': True})
```

    Optimization terminated successfully.    (Exit mode 0)
                Current function value: 22222.2222222
                Iterations: 7
                Function evaluations: 8
                Gradient evaluations: 7

```python
fpcsa2r = minimize(fun=cf_fpcsa2, x0=x0, args=(a,), jac=cf_fpcsa2d,
                   constraints=cons, method='SLSQP',
                   options={'disp': True})
```

    Optimization terminated successfully.    (Exit mode 0)
                Current function value: 69.3481276006
                Iterations: 11
                Function evaluations: 11
                Gradient evaluations: 11

```python
fmmax3r = minimize(fun=cf_fmmax3, x0=x0, args=(m,), jac=cf_fmmax3d,
                   constraints=cons, method='SLSQP',
                   options={'disp': True})
```

    Optimization terminated successfully.    (Exit mode 0)
                Current function value: 267.801389779
                Iterations: 13
                Function evaluations: 13
                Gradient evaluations: 13

>Let's compare the results for the different cost functions:

```python
dat = np.vstack((np.around(r*100,1), np.around(a,1), np.around(f0,0), np.around(m,1)))
opt = np.around(np.vstack((f1r.x, f2r.x, fpcsa2r.x, fmmax3r.x)), 1)
er = ['-', '-', '-', '-',
      np.sum(r*f1r.x)-M, np.sum(r*f2r.x)-M, np.sum(r*fpcsa2r.x)-M, np.sum(r*fmmax3r.x)-M]
data = np.vstack((np.vstack((dat, opt)).T, er)).T

rows = ['$\text{Moment arm}\;[cm]$', '$pcsa\;[cm^2]$', '$F_{max}\;[N]$', '$M_{max}\;[Nm]$',
        '$\sum F_i$', '$\sum F_i^2$', '$\sum(F_i/pcsa_i)^2$', '$\sum(F_i/M_{max,i})^3$']
cols = ['Biceps long head', 'Biceps short head', 'Brachialis', 'Error in M']
df = pd.DataFrame(data, index=rows, columns=cols)
print('\nComparison of different cost functions for solving the distribution problem')
df
```

    Comparison of different cost functions for solving the distribution problem

      Biceps long head
      Biceps short head
      Brachialis
      Error in M

      $\text{Moment arm}\;[cm]$
      2.0
      5.0
      4.0
      -

      $pcsa\;[cm^2]$
      33.0
      8.0
      23.0
      -

      $F_{max}\;[N]$
      1000.0
      250.0
      700.0
      -

      $M_{max}\;[Nm]$
      20.0
      12.5
      28.0
      -

      $\sum F_i$
      -0.0
      200.0
      -0.0
      0.0

      $\sum F_i^2$
      44.4
      111.1
      88.9
      0.0

      $\sum(F_i/pcsa_i)^2$
      151.0
      22.2
      146.7
      0.0

      $\sum(F_i/M_{max,i})^3$
      65.5
      51.1
      153.3
      -1.7763568394e-15

```python

```


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license (c)2014 L.A. Barba, G.F. Forsyth, C.D. Cooper.

# Spreading out

Welcome back! This is the third lesson of the course [Module 4](https://github.com/numerical-mooc/numerical-mooc/tree/master/lessons/04_spreadout), _Spreading out: parabolic PDEs,_ where we study the numerical solution of diffusion problems.

In the first two notebooks, we looked at the 1D heat equation, and solved it numerically using [*explicit*](http://nbviewer.ipython.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/04_spreadout/04_01_Heat_Equation_1D_Explicit.ipynb) and [*implicit*](http://nbviewer.ipython.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/04_spreadout/04_02_Heat_Equation_1D_Implicit.ipynb) schemes. We learned that implicit schemes are unconditionally stable, and we are free to choose any time step. —Wait: _any time step?_ Remember, we still want to capture the physics of the problem accurately. So although stability concerns do not limit the time step, it still has to be small enough to satisfy any accuracy concerns.

We are now ready to graduate to two dimensions! In the remaining lessons of this course module, we will study the 2D heat equation and reaction-diffusion equation. Like before, we start with explicit methods (this lesson) and then move to implicit methods (next lesson). Let's get started.

## 2D Heat conduction

The equation of heat conduction in 2D is:

\begin{equation}
\rho c_p \frac{\partial T}{\partial t} = \frac{\partial}{\partial x} \left( \kappa_x \frac{\partial T}{\partial x} \right) + \frac{\partial}{\partial y} \left(\kappa_y \frac{\partial T}{\partial y} \right)
\end{equation}

where $\rho$ is the density, $c_p$ is the heat capacity and $\kappa$ is the thermal conductivity.

If the thermal conductivity $\kappa$ is constant, then we can take it outside of the spatial derivative and the equation simplifies to:

\begin{equation}
\frac{\partial T}{\partial t} = \alpha \left(\frac{\partial^2 T}{\partial x^2} + \frac{\partial^2 T}{\partial y^2} \right)
\end{equation}

where $\alpha = \frac{\kappa}{\rho c_p}$ is the thermal diffusivity.  The thermal diffusivity describes the ability of a material to conduct heat vs. storing it.

Does that equation have a familiar look to it?  That's because it's the same as the diffusion equation.  There's a reason that $\alpha$ is called the thermal *diffusivity*! We're going to set up an interesting problem where 2D heat conduction is important, and set about to solve it with explicit finite-difference methods.

### Problem statement

Removing heat out of micro-chips is a big problem in the computer industry. We are at a point in technology where computers can't run much faster because the chips might start failing due to the high temperature. This is a big deal! Let's study the problem more closely.

We want to understand how heat is dissipated from the chip with a very simplified model. Say we consider the chip as a 2D plate of size $1{\rm cm}\times 1{\rm cm}$, made of Silicon: $\kappa = 159{\rm W/m C}$, $c_p = 0.712\cdot 10^3 {\rm J/kg C}$, $\rho = 2329{\rm kg/m}^3$, and diffusivity $\alpha \approx 10^{-4}{\rm m}^2{/rm s}$. Silicon melts at $1414{\rm C}$, but chips should of course operate at much smaller temperatures. The maximum temperature allowed depends on the processor make and model; in many cases, the maximum temperature is somewhere between $60{\rm C}$ and $\sim70{\rm C}$, but better CPUs are recommended to operate at a [maximum of $80{\rm C}$](http://www.pugetsystems.com/blog/2009/02/26/intel-core-i7-temperatures/) (like the Intel Core i7, for example).

We're going to set up a somewhat artificial problem, just to demonstrate an interesting numerical solution. Say the chip is in a position where on two edges (top and right) it is in contact with insulating material.  On the other two edges the chip is touching other components that have a constant temperature of $T=100{\rm C}$ when the machine is operating. Initially, the chip is at room temperature $(20{\rm C})$. *How long does it take for the center of the chip to reach $70{\rm C}$?*

#### Figure 1: Simplified microchip problem setup.

Let's use what we have learned to tackle this problem!

## 2D Finite differences

Everything you learned about finite-difference schemes in [Notebook 1 of Module 2](http://nbviewer.ipython.org/github/numerical-mooc/numerical-mooc/blob/master/lessons/02_spacetime/02_01_1DConvection.ipynb) still applies, but now there are two spatial dimensions.  We will need to build a 2D grid of discrete points to compute the solution on.

We will use a 2D Cartesian grid: one that consists of two families of (grid) lines parallel to the two spatial directions. Two lines (of different families) intersect on one and only one grid node (this is called a _structured_ grid). In the $x$ direction, the discretization uses $i=0, \cdots N_x$ lines, and in the $y$ direction we have $j=0, \cdots N_y$ lines. A given node on the grid will now have two spatial coordinates, and we need two indices: for the two lines that intersect at that node.  For example, the middle point in the figure below would be $T_{i,j}$.

#### Figure 2. Nodal coordinates in 2 dimensions

### Explicit scheme in 2D

Recall from above that the 2D heat equation is

$$\frac{\partial T}{\partial t} = \alpha \left(\frac{\partial^2 T}{\partial x^2} + \frac{\partial^2 T}{\partial y^2} \right)$$

Let's write this out discretized using forward difference in time, and central difference in space,  using an explicit scheme. You should be able write this out yourself, without looking—if you need to look, it means you still need to write more difference equations by your own hand!

\begin{equation}
\frac{T^{n+1}_{i,j} - T^n_{i,j}}{\Delta t} = \alpha \left( \frac{T^n_{i+1, j} - 2T^n_{i,j} + T^n_{i-1,j}}{\Delta x^2} + \frac{T^n_{i, j+1} - 2T^n_{i,j} + T^n_{i,j-1}}{\Delta y^2}\right)
\end{equation}

Rearranging the equation to solve for the value at the next time step, $T^{n+1}_{i,j}$, yields

\begin{equation}
T^{n+1}_{i,j}= T^n_{i,j} + \alpha \left( \frac{\Delta t}{\Delta x^2} (T^n_{i+1, j} - 2T^n_{i,j} + T^n_{i-1,j}) + \\\frac{\Delta t}{\Delta y^2} (T^n_{i, j+1} - 2T^n_{i,j} + T^n_{i,j-1})\right)
\end{equation}

That's a little messier than 1D, but still recognizable.

Up until now, we've used stencils to help visualize how a scheme will advance the solution for one timestep.  Stencils in 2D are a little harder to draw, but hopefully the figure below will guide your understanding of this method: we are using five grid points at time step $n$ to obtain the solution on one point at time step $n+1$.

#### Figure 3: 2D Explicit Stencil

Similar to all of the 1D explicit methods we've used, the solution at $T^{n+1}_{i,j}$ is updated using only known values from the current solution at time $n$. This is straightforward to implement in code, but will be subject to stability limitations on the time step that you can choose. We'll study an implicit method in the next lesson.

### Boundary Conditions

Whenever we reach a point that interacts with the boundary, we apply the boundary condition. As in the previous notebook, if the boundary has Dirichlet conditions, we simply impose the prescribed temperature at that point. If the boundary has Neumann conditions, we approximate them with a finite-difference scheme.

Remember, Neumann boundary conditions prescribe the derivative in the normal direction. For example, in the problem described above, we have $\frac{\partial T}{\partial y} = q_y$ in the top boundary and $\frac{\partial T}{\partial x} = q_x$ in the right boundary, with $q_y = q_x = 0$ (insulation).

Thus, at every time step, we need to enforce

\begin{equation}
T_{i,end} = q_y\cdot\Delta y + T_{i,end-1}
\end{equation}

and

\begin{equation}
T_{end,j} = q_x\cdot\Delta x + T_{end-1,j}
\end{equation}

Write the finite-difference discretization of the boundary conditions yourself, and confirm that you can get the expressions above.

### Stability

Before doing any coding, let's revisit stability constraints. We saw in the first notebook of this series that the 1D explicit discretization of the diffusion equation was stable as long as $\alpha \frac{\Delta t}{(\Delta x)^2} \leq \frac{1}{2}$. In 2D, this constraint is even tighter, as we need to add them in both directions:

\begin{equation}
\alpha \frac{\Delta t}{(\Delta x)^2} + \alpha \frac{\Delta t}{(\Delta y)^2} < \frac{1}{2}.
\end{equation}

Say that the mesh has the same spacing in $x$ and $y$, $\Delta x = \Delta y = \delta$. In that case, the stability condition is:

\begin{equation}
\alpha \frac{\Delta t}{\delta^2} < \frac{1}{4}
\end{equation}

## Code implementation

### Array storage

The physical problem has two dimensions, so we also store the temperatures in two dimensions: in a 2D array.

We chose to store it with the $y$ coordinates corresponding to the rows of the array and $x$ coordinates varying with the columns (this is just a code design decision!). If we are consistent with the stencil formula (with $x$ corresponding to index $i$ and $y$ to index $j$), then $T_{i,j}$ will be stored in array format as `T[j,i]`.

This might be a little confusing as most of us are used to writing coordinates in the format $(x,y)$, but our preference is to have the data stored so that it matches the physical orientation of the problem. Then, when we make a plot of the solution, the visualization will make sense to us, with respect to the geometry of our set-up. That's just nicer than to have the plot rotated!

#### Figure 4: Row-column data storage

As you can see on Figure 4 above, if we want to access the value $18$ we would write those coordinates as $(x_2, y_3)$.  You can also see that its location is the 3rd row, 2nd column, so its array address would be `T[3,2]`.

Again, this is a design decision.  However you can choose to manipulate and store your data however you like; just remember to be consistent!

### Code time!

Now, to some coding! First, we have a little function that will advance the solution in time with a forward-time, centered-space scheme, and will monitor the center of the plate to tell us when it reaches $70{\rm C}$. Let's start by setting up our Python compute environment.

```python
import numpy
from matplotlib import pyplot
%matplotlib inline
from matplotlib import rcParams, cm
rcParams['font.family'] = 'serif'
rcParams['font.size'] = 16
```

```python
def ftcs(T, nt, alpha, dt, dx, dy):

    #force j_mid and i_mid to be integers so we can use them as indices
    #for the array T
    j_mid = int((numpy.shape(T)[0])/2)
    i_mid = int((numpy.shape(T)[1])/2)

    for n in range(nt):
        Tn = T.copy()
        T[1:-1,1:-1] = Tn[1:-1,1:-1] + alpha *\
            (dt/dy**2 * (Tn[2:,1:-1] - 2*Tn[1:-1,1:-1] + Tn[:-2,1:-1]) +\
             dt/dx**2 * (Tn[1:-1,2:] - 2*Tn[1:-1,1:-1] + Tn[1:-1,:-2]))

        # Enforce Neumann BCs
        T[-1,:] = T[-2,:]
        T[:,-1] = T[:,-2]

        # Check if we reached T=70C
        if T[j_mid, i_mid] >= 70:
            print ("Center of plate reached 70C at time {0:.2f}s.".format(dt*n))
            break

    if T[j_mid, i_mid]<70:
        print ("Center has not reached 70C yet, it is only {0:.2f}C.".format(T[j_mid, i_mid]))

    return T
```

See the [`break`](https://docs.python.org/2/tutorial/controlflow.html) statement? It exits the `for` loop at the closest time iteration when the plate reaches $70{\rm C}$.

In the code cell below, we define our initial conditions according to the problem set up, and choose the discretization parameters. We start with only 20 spatial steps in each coordinate direction and advance for 500 time steps. You should later experiments with these parameters at your leisure!

```python
L = 1.0e-2
H = 1.0e-2

nx = 21
ny = 21
nt = 500

dx = L/(nx-1)
dy = H/(ny-1)

x = numpy.linspace(0,L,nx)
y = numpy.linspace(0,H,ny)

alpha = 1e-4

Ti = numpy.ones((ny, nx))*20
Ti[0,:]= 100
Ti[:,0] = 100
```

We don't want our solution blowing up, so let's find a time step with $\frac{\alpha \Delta t}{\Delta x^2} = \frac{\alpha \Delta t}{\Delta y^2} = \frac{1}{4}$.

```python
sigma = 0.25
dt = sigma * min(dx, dy)**2 / alpha
T = ftcs(Ti.copy(), nt, alpha, dt, dx, dy)
```

    Center of plate reached 70C at time 0.16s.

### Visualize the results

By now, you're no doubt *very* familiar with the `pyplot.plot` command.  It's great for line plots, scatter plots, etc., but what about when we have two spatial dimensions and another value (temperature) to display?

Are you thinking contour plot?  We're thinking contour plot.  Check out the documentation on [`pyplot.contourf`](http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.contour) (the 'f' denotes "filled" contours).

```python
pyplot.figure(figsize=(8,5))
pyplot.contourf(x,y,T,20,cmap=cm.viridis)
pyplot.xlabel('$x$')
pyplot.ylabel('$y$')
pyplot.colorbar();
```

##### Note
This plotting function uses *Viridis*, a new (and _awesome_) colormap available in Matplotlib versions 1.5 and greater.  If you see an error when you try to plot using <tt>cm.viridis</tt>, just update Matplotlib using <tt>conda</tt> or <tt>pip</tt>.

That looks pretty cool!  Note that in the call to `pyplot.contourf` you can specify the number of contour levels to display (we chose 20).  Look at that visualization: does it make physical sense to you, considering that the upper and right sides of the chip are insulated, in our problem?

##### Dig deeper

In the problem we just demonstrated, the chip reaches a temperature of $70{\rm C}$ at a given time, but will it keep increasing? That spells trouble.

Imagine that you have a heat sink instead of an insulator acting on the upper and right sides. What should be the heat flux that the heat sink achieves there, so that the temperature does not exceed $70{\rm C}$ at the center of the chip?

---
###### The cell below loads the style of the notebook

```python
from IPython.core.display import HTML
css_file = '../../styles/numericalmoocstyle.css'
HTML(open(css_file, "r").read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Source+Code+Pro' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 750px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
    margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Alegreya Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:600px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Nixie One', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 45pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h2 {
    font-family: 'Nixie One', serif;
    font-weight: 400;
    font-size: 30pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Nixie One', serif;
    margin-top:16px;
    font-size: 22pt;
    font-weight: 600;
    margin-bottom: 3px;
    font-style: regular;
    color: rgb(102,102,0);
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Nixie One', serif;
    font-size: 14pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Nixie One', sans-serif;
    font-weight: 400;
    font-size: 16pt;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
    font-family: "Source Code Pro";
    font-size: 90%;
}

.alert-box {
    padding:10px 10px 10px 36px;
    margin:5px;
}

.success {
    color:#666600;
    background:rgb(240,242,229);
}
</style>


# GPyTorch Regression Tutorial

## Introduction

In this notebook, we demonstrate many of the design features of GPyTorch using the simplest example, training an RBF kernel Gaussian process on a simple function. We'll be modeling the function

\begin{align}
y &= \sin(2\pi x) + \epsilon \\
  \epsilon &\sim \mathcal{N}(0, 0.04)
\end{align}

with 100 training examples, and testing on 51 test examples.

**Note:** this notebook is not necessarily intended to teach the mathematical background of Gaussian processes, but rather how to train a simple one and make predictions in GPyTorch. For a mathematical treatment, Chapter 2 of Gaussian Processes for Machine Learning provides a very thorough introduction to GP regression (this entire text is highly recommended): http://www.gaussianprocess.org/gpml/chapters/RW2.pdf

```python
# COMMENT this if not used in colab
!pip3 install gpytorch
```

    Collecting gpytorch
      Downloading https://files.pythonhosted.org/packages/9c/5f/ce79e35c1a36deb25a0eac0f67bfe85fb8350eb8e19223950c3d615e5e9a/gpytorch-1.0.1.tar.gz (229kB)
         |████████████████████████████████| 235kB 2.8MB/s
    Building wheels for collected packages: gpytorch
      Building wheel for gpytorch (setup.py) ... done
      Created wheel for gpytorch: filename=gpytorch-1.0.1-py2.py3-none-any.whl size=390441 sha256=4c1c86a4228d2a6b7a30cf734f9a1b61de06c54de6177c0c84e34ae7d7b02939
      Stored in directory: /root/.cache/pip/wheels/10/2f/7a/3328e5713d796daeec2ce8ded141d5f3837253fc3c2a5c62e0
    Successfully built gpytorch
    Installing collected packages: gpytorch
    Successfully installed gpytorch-1.0.1

```python
import math
import torch
import gpytorch
from matplotlib import pyplot as plt

%matplotlib inline
%load_ext autoreload
%autoreload 2
```

### Set up training data

In the next cell, we set up the training data for this example. We'll be using 100 regularly spaced points on [0,1] which we evaluate the function on and add Gaussian noise to get the training labels.

```python
# Training data is 100 points in [0,1] inclusive regularly spaced
train_x = torch.linspace(0, 1, 100)
# True function is sin(2*pi*x) with Gaussian noise
train_y = torch.sin(train_x * (2 * math.pi)) + torch.randn(train_x.size()) * math.sqrt(0.04)
```

```python
plt.plot(train_x.numpy(), train_y.numpy())
plt.show()
```

## Setting up the model

The next cell demonstrates the most critical features of a user-defined Gaussian process model in GPyTorch. Building a GP model in GPyTorch is different in a number of ways.

First in contrast to many existing GP packages, we do not provide full GP models for the user. Rather, we provide *the tools necessary to quickly construct one*. This is because we believe, analogous to building a neural network in standard PyTorch, it is important to have the flexibility to include whatever components are necessary. As can be seen in more complicated examples, this allows the user great flexibility in designing custom models.

For most GP regression models, you will need to construct the following GPyTorch objects:

1. A **GP Model** (`gpytorch.models.ExactGP`) -  This handles most of the inference.
1. A **Likelihood** (`gpytorch.likelihoods.GaussianLikelihood`) - This is the most common likelihood used for GP regression.
1. A **Mean** - This defines the prior mean of the GP.(If you don't know which mean to use, a `gpytorch.means.ConstantMean()` is a good place to start.)
1. A **Kernel** - This defines the prior covariance of the GP.(If you don't know which kernel to use, a `gpytorch.kernels.ScaleKernel(gpytorch.kernels.RBFKernel())` is a good place to start).
1. A **MultivariateNormal** Distribution (`gpytorch.distributions.MultivariateNormal`) - This is the object used to represent multivariate normal distributions.

### The GP Model

The components of a user built (Exact, i.e. non-variational) GP model in GPyTorch are, broadly speaking:

1. An `__init__` method that takes the training data and a likelihood, and constructs whatever objects are necessary for the model's `forward` method. This will most commonly include things like a mean module and a kernel module.

2. A `forward` method that takes in some $n \times d$ data `x` and returns a `MultivariateNormal` with the *prior* mean and covariance evaluated at `x`. In other words, we return the vector $\mu(x)$ and the $n \times n$ matrix $K_{xx}$ representing the prior mean and covariance matrix of the GP.

This specification leaves a large amount of flexibility when defining a model. For example, to compose two kernels via addition, you can either add the kernel modules directly:

```python
self.covar_module = ScaleKernel(RBFKernel() + WhiteNoiseKernel())
```

Or you can add the outputs of the kernel in the forward method:

```python
covar_x = self.rbf_kernel_module(x) + self.white_noise_module(x)
```

```python
# We will use the simplest form of GP model, exact inference
class ExactGPModel(gpytorch.models.ExactGP):
    def __init__(self, train_x, train_y, likelihood):
        super(ExactGPModel, self).__init__(train_x, train_y, likelihood)
        self.mean_module = gpytorch.means.ConstantMean()
        self.covar_module = gpytorch.kernels.ScaleKernel(gpytorch.kernels.RBFKernel())

    def forward(self, x):
        mean_x = self.mean_module(x)
        covar_x = self.covar_module(x)
        return gpytorch.distributions.MultivariateNormal(mean_x, covar_x)

# initialize likelihood and model
likelihood = gpytorch.likelihoods.GaussianLikelihood()
model = ExactGPModel(train_x, train_y, likelihood)
```

### Plot before optimizing the hyperparamters

Let's take a look at the model parameters. More information is here.

```python
model.state_dict()
```

    OrderedDict([('likelihood.noise_covar.raw_noise', tensor([0.])),
                 ('mean_module.constant', tensor([0.])),
                 ('covar_module.raw_outputscale', tensor(0.)),
                 ('covar_module.base_kernel.raw_lengthscale', tensor([[0.]]))])

Goes to run [Make predictions with the model](https://colab.research.google.com/github/jwangjie/gpytorch/blob/master/examples/01_Exact_GPs/Simple_GP_Regression.ipynb#scrollTo=S1gMlb1TCM7i), and then run `Plot the model fit`.

```python
# Get into evaluation (predictive posterior) mode
model.eval()
likelihood.eval()

# Test points are regularly spaced along [0,1]
# Make predictions by feeding model through likelihood
with torch.no_grad(), gpytorch.settings.fast_pred_var():
    test_x = torch.linspace(0, 1, 51)
    observed_pred = likelihood(model(test_x))

with torch.no_grad():
    # Initialize plot
    f, ax = plt.subplots(1, 1, figsize=(8, 6))

    # Get upper and lower confidence bounds
    lower, upper = observed_pred.confidence_region()
    # Plot training data as black stars
    ax.plot(train_x.numpy(), train_y.numpy(), 'k*')
    # Plot predictive means as blue line
    ax.plot(test_x.numpy(), observed_pred.mean.numpy(), 'r')
    # Shade between the lower and upper confidence bounds
    ax.fill_between(test_x.numpy(), lower.numpy(), upper.numpy(), alpha=0.3)
    ax.set_ylim([-3, 3])
    ax.legend(['Observed Data', 'Mean', 'Confidence'])
```

### Model modes

Like most PyTorch modules, the `ExactGP` has a `.train()` and `.eval()` mode.
- `.train()` mode is for optimizing model hyperameters.
- `.eval()` mode is for computing predictions through the model posterior.

## Training the model

In the next cell, we handle using Type-II MLE to train the hyperparameters of the Gaussian process.

The most obvious difference here compared to many other GP implementations is that, as in standard PyTorch, the core training loop is written by the user. In GPyTorch, we make use of the standard PyTorch optimizers as from `torch.optim`, and all trainable parameters of the model should be of type `torch.nn.Parameter`. Because GP models directly extend `torch.nn.Module`, calls to methods like `model.parameters()` or `model.named_parameters()` function as you might expect coming from PyTorch.

In most cases, the boilerplate code below will work well. It has the same basic components as the standard PyTorch training loop:

1. Zero all parameter gradients
2. Call the model and compute the loss
3. Call backward on the loss to fill in gradients
4. Take a step on the optimizer

However, defining custom training loops allows for greater flexibility. For example, it is easy to save the parameters at each step of training, or use different learning rates for different parameters (which may be useful in deep kernel learning for example).

```python
# this is for running the notebook in our testing framework
import os
smoke_test = ('CI' in os.environ)
training_iter = 2 if smoke_test else 50

# Find optimal model hyperparameters
model.train()
likelihood.train()

# Use the adam optimizer
optimizer = torch.optim.Adam([
    {'params': model.parameters()},  # Includes GaussianLikelihood parameters
], lr=0.1)

# "Loss" for GPs - the marginal log likelihood
mll = gpytorch.mlls.ExactMarginalLogLikelihood(likelihood, model)

for i in range(training_iter):
    # Zero gradients from previous iteration
    optimizer.zero_grad()
    # Output from model
    output = model(train_x)
    # Calc loss and backprop gradients
    loss = -mll(output, train_y)
    loss.backward()
    print('Iter %d/%d - Loss: %.3f   lengthscale: %.3f   noise: %.3f' % (
        i + 1, training_iter, loss.item(),
        model.covar_module.base_kernel.lengthscale.item(),
        model.likelihood.noise.item()
    ))
    optimizer.step()
```

    Iter 1/50 - Loss: 0.906   lengthscale: 0.693   noise: 0.693
    Iter 2/50 - Loss: 0.873   lengthscale: 0.644   noise: 0.644
    Iter 3/50 - Loss: 0.839   lengthscale: 0.598   noise: 0.598
    Iter 4/50 - Loss: 0.802   lengthscale: 0.554   noise: 0.554
    Iter 5/50 - Loss: 0.762   lengthscale: 0.513   noise: 0.513
    Iter 6/50 - Loss: 0.717   lengthscale: 0.475   noise: 0.474
    Iter 7/50 - Loss: 0.671   lengthscale: 0.438   noise: 0.437
    Iter 8/50 - Loss: 0.624   lengthscale: 0.404   noise: 0.402
    Iter 9/50 - Loss: 0.579   lengthscale: 0.371   noise: 0.370
    Iter 10/50 - Loss: 0.537   lengthscale: 0.341   noise: 0.339
    Iter 11/50 - Loss: 0.498   lengthscale: 0.315   noise: 0.311
    Iter 12/50 - Loss: 0.462   lengthscale: 0.292   noise: 0.284
    Iter 13/50 - Loss: 0.427   lengthscale: 0.273   noise: 0.260
    Iter 14/50 - Loss: 0.393   lengthscale: 0.258   noise: 0.237
    Iter 15/50 - Loss: 0.359   lengthscale: 0.247   noise: 0.216
    Iter 16/50 - Loss: 0.325   lengthscale: 0.238   noise: 0.197
    Iter 17/50 - Loss: 0.291   lengthscale: 0.233   noise: 0.180
    Iter 18/50 - Loss: 0.257   lengthscale: 0.229   noise: 0.163
    Iter 19/50 - Loss: 0.223   lengthscale: 0.228   noise: 0.149
    Iter 20/50 - Loss: 0.190   lengthscale: 0.229   noise: 0.135
    Iter 21/50 - Loss: 0.157   lengthscale: 0.232   noise: 0.123
    Iter 22/50 - Loss: 0.126   lengthscale: 0.236   noise: 0.112
    Iter 23/50 - Loss: 0.096   lengthscale: 0.241   noise: 0.102
    Iter 24/50 - Loss: 0.067   lengthscale: 0.248   noise: 0.093
    Iter 25/50 - Loss: 0.040   lengthscale: 0.256   noise: 0.084
    Iter 26/50 - Loss: 0.016   lengthscale: 0.264   noise: 0.077
    Iter 27/50 - Loss: -0.005   lengthscale: 0.274   noise: 0.070
    Iter 28/50 - Loss: -0.024   lengthscale: 0.283   noise: 0.064
    Iter 29/50 - Loss: -0.039   lengthscale: 0.292   noise: 0.059
    Iter 30/50 - Loss: -0.051   lengthscale: 0.300   noise: 0.054
    Iter 31/50 - Loss: -0.059   lengthscale: 0.307   noise: 0.050
    Iter 32/50 - Loss: -0.065   lengthscale: 0.311   noise: 0.046
    Iter 33/50 - Loss: -0.069   lengthscale: 0.312   noise: 0.043
    Iter 34/50 - Loss: -0.071   lengthscale: 0.310   noise: 0.040
    Iter 35/50 - Loss: -0.072   lengthscale: 0.306   noise: 0.038
    Iter 36/50 - Loss: -0.071   lengthscale: 0.300   noise: 0.036
    Iter 37/50 - Loss: -0.069   lengthscale: 0.293   noise: 0.034
    Iter 38/50 - Loss: -0.066   lengthscale: 0.285   noise: 0.033
    Iter 39/50 - Loss: -0.063   lengthscale: 0.278   noise: 0.032
    Iter 40/50 - Loss: -0.059   lengthscale: 0.271   noise: 0.031
    Iter 41/50 - Loss: -0.056   lengthscale: 0.265   noise: 0.030
    Iter 42/50 - Loss: -0.054   lengthscale: 0.260   noise: 0.030
    Iter 43/50 - Loss: -0.053   lengthscale: 0.256   noise: 0.029
    Iter 44/50 - Loss: -0.053   lengthscale: 0.253   noise: 0.029
    Iter 45/50 - Loss: -0.054   lengthscale: 0.251   noise: 0.030
    Iter 46/50 - Loss: -0.056   lengthscale: 0.250   noise: 0.030
    Iter 47/50 - Loss: -0.058   lengthscale: 0.250   noise: 0.031
    Iter 48/50 - Loss: -0.061   lengthscale: 0.251   noise: 0.031
    Iter 49/50 - Loss: -0.064   lengthscale: 0.252   noise: 0.032
    Iter 50/50 - Loss: -0.066   lengthscale: 0.253   noise: 0.033

```python
model.state_dict()
```

    OrderedDict([('likelihood.noise_covar.raw_noise', tensor([-3.3824])),
                 ('mean_module.constant', tensor([0.0588])),
                 ('covar_module.raw_outputscale', tensor(-0.4108)),
                 ('covar_module.base_kernel.raw_lengthscale',
                  tensor([[-1.2372]]))])

## Make predictions with the model

In the next cell, we make predictions with the model. To do this, we simply put the model and likelihood in eval mode, and call both modules on the test data.

Just as a user defined GP model returns a `MultivariateNormal` containing the prior mean and covariance from forward, a trained GP model in eval mode returns a `MultivariateNormal` containing the posterior mean and covariance. Thus, getting the predictive mean and variance, and then sampling functions from the GP at the given test points could be accomplished with calls like:

```python
f_preds = model(test_x)
y_preds = likelihood(model(test_x))

f_mean = f_preds.mean
f_var = f_preds.variance
f_covar = f_preds.covariance_matrix
f_samples = f_preds.sample(sample_shape=torch.Size(1000,))
```

The `gpytorch.settings.fast_pred_var` context is not needed, but here we are giving a preview of using one of our cool features, getting faster predictive distributions using [LOVE](https://arxiv.org/abs/1803.06058).

```python
# Get into evaluation (predictive posterior) mode
model.eval()
likelihood.eval()

# Test points are regularly spaced along [0,1]
# Make predictions by feeding model through likelihood
with torch.no_grad(), gpytorch.settings.fast_pred_var():
    test_x = torch.linspace(0, 1, 51)
    observed_pred = likelihood(model(test_x))
```

## Plot the model fit

In the next cell, we plot the mean and confidence region of the Gaussian process model. The `confidence_region` method is a helper method that returns 2 standard deviations above and below the mean.

```python
with torch.no_grad():
    # Initialize plot
    f, ax = plt.subplots(1, 1, figsize=(8, 6))

    # Get upper and lower confidence bounds
    lower, upper = observed_pred.confidence_region()
    # Plot training data as black stars
    ax.plot(train_x.numpy(), train_y.numpy(), 'k*')
    # Plot predictive means as blue line
    ax.plot(test_x.numpy(), observed_pred.mean.numpy(), 'r')
    # Shade between the lower and upper confidence bounds
    ax.fill_between(test_x.numpy(), lower.numpy(), upper.numpy(), alpha=0.3)
    ax.set_ylim([-3, 3])
    ax.legend(['Observed Data', 'Mean', 'Confidence'])
```

```python

```


## подготовка:

```python
import numpy as np
from numpy.linalg import *
rg = matrix_rank
from IPython.display import display, Math, Latex, Markdown

from sympy import *

pr = lambda s: display(Markdown('$'+str(latex(s))+'$'))

def pmatrix(a, intro='',ending='',row=False):
    if len(a.shape) > 2:
        raise ValueError('pmatrix can at most display two dimensions')
    lines = str(a).replace('[', '').replace(']', '').splitlines()
    rv = [r'\begin{pmatrix}']
    rv += ['  ' + ' & '.join(l.split()) + r'\\' for l in lines]
    rv +=  [r'\end{pmatrix}']
    if row:
        return(intro+'\n'.join(rv)+ending)
    else:
        display(Latex('$$'+intro+'\n'.join(rv)+ending+'$$'))
```

# Задача 7
## 1) доно:

```python
C = np.array([[1,2],
              [0,1]])
pmatrix(C, intro=r'C_{2\times 2}=')

D = np.array([[3,1],
              [1,0]])
pmatrix(D, intro=r'D_{2\times 2}=')

B = np.array([[5,1],
              [5,2]])
pmatrix(B, intro=r'B_{2\times 2}=')
```

$$C_{2\times 2}=\begin{pmatrix}
  1 & 2\\
  0 & 1\\
\end{pmatrix}$$

$$D_{2\times 2}=\begin{pmatrix}
  3 & 1\\
  1 & 0\\
\end{pmatrix}$$

$$B_{2\times 2}=\begin{pmatrix}
  5 & 1\\
  5 & 2\\
\end{pmatrix}$$

```python
A = np.array([[5,6],
              [3,4]])

pmatrix(rg(A), intro=r'rg(A)=')
```

$$rg(A)=\begin{pmatrix}
  2\\
\end{pmatrix}$$

```python

pmatrix(inv(C), intro=r'C^{-1}=')

pmatrix(B.T, intro=r'C^{T}=')

pmatrix(B.dot(C), intro=r'BC=')
9
pmatrix(rg(B), intro=r'rg(C)=')

pmatrix(det(B), intro=r'det(C)=')

```

$$C^{-1}=\begin{pmatrix}
  1. & -2.\\
  0. & 1.\\
\end{pmatrix}$$

$$C^{T}=\begin{pmatrix}
  5 & 5\\
  1 & 2\\
\end{pmatrix}$$

$$BC=\begin{pmatrix}
  5 & 11\\
  5 & 12\\
\end{pmatrix}$$

$$rg(C)=\begin{pmatrix}
  2\\
\end{pmatrix}$$

$$det(C)=\begin{pmatrix}
  5.0\\
\end{pmatrix}$$

```python
A = np.array([[2,6],
              [1,3]])
#pmatrix(rg(B), intro=r'rg(B)=')
pmatrix(rg(A), intro=r'rg(A)=')
#pmatrix(rg(A.dot(B)), intro=r'rg(AB)=')

```

$$rg(A)=\begin{pmatrix}
  1\\
\end{pmatrix}$$

# 3 пункт

## примерчик

```python
a1 = Symbol('a_{12}')
b1 = Symbol('b_{11}')
c1 = Symbol('c_{22}')
d1 = Symbol('d_{21}')

X =np.array([[a1,b1],
             [c1,d1]])

B = np.array([[5,1],
              [5,2]])

C1 = np.array([[1,1],
               [1,2]])
D1 = np.array([[2,1],
               [1,0]])

C2 = np.array([[1,-1],
               [0,1]])
D2 = np.array([[1,1],
               [0,1]])
pmatrix(B.reshape((4, 1)), intro="X=")
```

$$X=\begin{pmatrix}
  5\\
  1\\
  5\\
  2\\
\end{pmatrix}$$

```python
pmatrix( (C1.dot(X)).dot(D1))
A = (C1.dot(X)).dot(D1) + (C2.dot(X)).dot(D2)
pmatrix(A)
F = np.array([[3,1,1,1],
              [2,1,0,-1],
              [2,1,5,2],
              [1,0,3,1]])

pmatrix(F, ending=pmatrix(X.reshape((4, 1)),row=True)+"="+pmatrix(B.reshape((4, 1)),row=True))
pmatrix(rg(F), intro=r'rg(F)=')
print("Зничит есть нормальное решение!)")
```

$$\begin{pmatrix}
  2*a1 & + & b1 & + & 2*c1 & + & d1 & a1 & + & c1\\
  2*a1 & + & b1 & + & 4*c1 & + & 2*d1 & a1 & + & 2*c1\\
\end{pmatrix}$$

$$\begin{pmatrix}
  3*a1 & + & b1 & + & c1 & + & d1 & 2*a1 & + & b1 & - & d1\\
  2*a1 & + & b1 & + & 5*c1 & + & 2*d1 & a1 & + & 3*c1 & + & d1\\
\end{pmatrix}$$

$$\begin{pmatrix}
  3 & 1 & 1 & 1\\
  2 & 1 & 0 & -1\\
  2 & 1 & 5 & 2\\
  1 & 0 & 3 & 1\\
\end{pmatrix}\begin{pmatrix}
  a1\\
  b1\\
  c1\\
  d1\\
\end{pmatrix}=\begin{pmatrix}
  5\\
  1\\
  5\\
  2\\
\end{pmatrix}$$

$$rg(F)=\begin{pmatrix}
  4\\
\end{pmatrix}$$

    Зничит есть нормальное решение!)

# Решаем этоЁ!!!

```python
from sympy import Matrix, solve_linear_system
from sympy.abc import a,b,c,d

```

Examlpe:
   x + 4 y ==  2
-2 x +   y == 14

>from sympy import Matrix, solve_linear_system
>from sympy.abc import x, y

>system = Matrix(( (1, 4, 2), (-2, 1, 14)))
>solve_linear_system(system, x, y)

```python
system = Matrix(( (3,1,1,1,5), (2,1,0,-1,1), (2,1,5,2,5),(1,0,3,1,2) ))
x = solve_linear_system(system, a,b,c,d)
X =np.array([[x[a],x[b]],[x[c],x[d]] ])
```

```python
pmatrix(X,intro="X=")
```

$$X=\begin{pmatrix}
  10/11 & 9/11\\
  -2/11 & 18/11\\
\end{pmatrix}$$

```python

```

```python

x = Symbol('x')
y = Symbol('y')
pr(integrate(sqrt(4*x-x**2), x))
```

$\int \sqrt{- x^{2} + 4 x}\, dx$

```python

```


**Brief Honor Code**. Do the homework on your own. You may discuss ideas with your classmates, but DO NOT copy the solutions from someone else or the Internet. If stuck, discuss with TA.

**1**. (20 points)

Consider the linear transformation $f(x)$ on $\mathbb{R}^3$ that takes the standard basis $\left\{e_1,e_2,e_3\right\}$ to $\left\{v_1,v_2,v_3\right\}$ where

$$v_1=\left(\begin{matrix}10\\-10\\16\end{matrix}\right), v_2=\left(\begin{matrix}2\\-5\\20\end{matrix}\right) \textrm {and } v_3=\left(\begin{matrix}1\\-4\\13\end{matrix}\right)$$

1. Write a matrix $A$ that represents the same linear transformation. (4 points)

2. Compute the rank of $A$ using two different methods (do not use `matrix_rank`!). (4 points)

3. Find the eigenvalues and eigenvectors of $A$. (4 points)

4. What is the matrix representation of $f$ with respect to the eigenbasis? (8 points)

Write a matrix  AA  that represents the same linear transformation. (4 points)

```python
import numpy as np
```

```python
v=np.array([[10,2,1],[-10,-5,-4],[16,20,13]])
e=np.array([[1,0,0],[0,1,0],[0,0,1]])
```

```python
# since A @ e = v, then A = v @ inv(e)
A = v @ np.linalg.inv(e)
A
```

    array([[ 10.,   2.,   1.],
           [-10.,  -5.,  -4.],
           [ 16.,  20.,  13.]])

Compute the rank of  A  using two different methods (do not use matrix_rank!). (4 points)

```python
### method 1
# n = rank + null
from sympy import Matrix
A1=Matrix(A)
rank=A1.shape[1]-len(A1.nullspace())
print(rank)
```

    3

```python
### method 2
u, s, v = np.linalg.svd(A)
rank = np.sum(s > 1e-10)
print(rank)
```

    3

```python
### method 3
# rank = num of none zero eigenvalus
W,V=np.linalg.eig(A)
rank = np.sum(W > 1e-10)
print(rank)
```

    3

```python
### check results with np.linalg.matrix_rank
np.linalg.matrix_rank(A)
```

    3

Find the eigenvalues and eigenvectors of  AA . (4 points)

```python
W,V=np.linalg.eig(A)
print('eigenvalues = ',W)
```

    eigenvalues =  [ 9.  3.  6.]

```python
## eigenvectors
V
```

    array([[  5.77350269e-01,   6.31950024e-16,  -1.20385853e-01],
           [ -5.77350269e-01,  -4.47213595e-01,  -2.40771706e-01],
           [  5.77350269e-01,   8.94427191e-01,   9.63086825e-01]])

```python
v1=V[:,0]
v2=V[:,1]
v3=V[:,2]
print('eigenvalue:',W[0],'; eigenvector:',v1)
print('eigenvalue:',W[1],'; eigenvector:',v2)
print('eigenvalue:',W[2],'; eigenvector:',v3)
```

    eigenvalue: 9.0 ; eigenvector: [ 0.57735027 -0.57735027  0.57735027]
    eigenvalue: 3.0 ; eigenvector: [  6.31950024e-16  -4.47213595e-01   8.94427191e-01]
    eigenvalue: 6.0 ; eigenvector: [-0.12038585 -0.24077171  0.96308682]

What is the matrix representation of  ff  with respect to the eigenbasis? (8 points)

```python
mat = np.linalg.solve(np.linalg.eig(A)[1],A)
mat.T
```

    array([[ 20.78460969, -13.41640786,  16.61324773],
           [ 10.39230485, -20.1246118 ,  33.22649545],
           [  5.19615242,  -6.70820393,  16.61324773]])

```python
print('the matrix representaion of f(x) with respect to the eigenbasis can be expressed as:')
print(mat[0,:].reshape(3,1), "* w1 ")
print('+')
print(mat[1,:].reshape(3,1), "* w2 ")
print('+')
print(mat[2,:].reshape(3,1), "* w3 ")
```

    the matrix representaion of f(x) with respect to the eigenbasis can be expressed as:
    [[ 20.78460969]
     [ 10.39230485]
     [  5.19615242]] * w1
    +
    [[-13.41640786]
     [-20.1246118 ]
     [ -6.70820393]] * w2
    +
    [[ 16.61324773]
     [ 33.22649545]
     [ 16.61324773]] * w3

```python

```

**2**. (20 points)

You are given the following x-y coordinates (first column is x, second is y)

```
array([[  0.        ,   4.12306991],
       [  3.        , -15.47355729],
       [  4.        , -11.68725507],
       [  3.        , -20.33756693],
       [  5.        ,  -6.06401989],
       [  6.        ,  32.79353057],
       [  8.        ,  82.48658405],
       [  9.        ,  84.02971858],
       [  4.        ,  -1.30587276],
       [  8.        ,  68.59409878]])
```

- Find the coefficients $(a, b, c)$ of the least-squares fit of a quadratic function $y = a + bx + cx^2$ to the data.
- Plot the data and fitted curve using `matplotlib`.

```python
xs = np.array([
    [  0.        ,   4.12306991],
    [  3.        , -15.47355729],
    [  4.        , -11.68725507],
    [  3.        , -20.33756693],
    [  5.        ,  -6.06401989],
    [  6.        ,  32.79353057],
    [  8.        ,  82.48658405],
    [  9.        ,  84.02971858],
    [  4.        ,  -1.30587276],
    [  8.        ,  68.59409878]])
```

```python
def func(x, a, b, c):
    return a + b*x + c*x*x
```

```python
xdata=xs[:,0]
ydata=xs[:,1]
```

```python
from scipy.optimize import curve_fit
popt, pcov = curve_fit(func, xdata, ydata)
popt # the estimated a, b, c
```

    array([ -0.35762896, -11.78531232,   2.53125199])

```python
yfit=func(xdata, *popt)
```

```python
import matplotlib.pyplot as plt
# the red points are points in the raw data
# the green points are fitted points
# the blue line is fitted line
plt.scatter(xdata, ydata, c='red', alpha=0.5,label='raw data')
plt.scatter(xdata, yfit, c='green', alpha=0.5,label='fitted data')
xx = np.linspace(0,9,100)
yy = popt[2]*xx**2 + popt[1]*xx + popt[0]
plt.plot(xx,yy)
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.show()
```

**3**. (20 points)

Use the `svd` function to solve the least squares problem above, and repeat the same plot. Calculate the residual error $\lvert y - X\beta \rvert$.

```python
A=np.array(list(np.ones(len(xdata)))+list(xdata)+list(xdata**2)).reshape(3,len(xdata))
A=A.T
A
```

    array([[  1.,   0.,   0.],
           [  1.,   3.,   9.],
           [  1.,   4.,  16.],
           [  1.,   3.,   9.],
           [  1.,   5.,  25.],
           [  1.,   6.,  36.],
           [  1.,   8.,  64.],
           [  1.,   9.,  81.],
           [  1.,   4.,  16.],
           [  1.,   8.,  64.]])

```python
u, s, v = np.linalg.svd(A)
beta = v.T @ np.c_[np.linalg.inv(np.diag(s)), np.zeros(7*3).reshape(3,7)] @ u.T @ ydata
beta # the estimated a, b, c
```

    array([ -0.35762896, -11.78531232,   2.53125199])

```python
import matplotlib.pyplot as plt
# the red points are points in the raw data
# the blue line is fitted line
plt.scatter(xdata, ydata, c='red', alpha=0.5,label='raw data')
xx = np.linspace(0,9,100)
yy = beta[2]*xx**2 + beta[1]*xx + beta[0]
plt.plot(xx,yy)
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.show()
```

```python
np.abs(A @ beta - ydata)
```

    array([  4.48069887,   2.54125925,   4.68840861,   7.40526889,
            10.06112898,  12.73796195,  15.12658447,  14.57625244,
             5.6929737 ,   1.2340992 ])

**4**. (20 points)

Avoiding catastrophic cancellation.

Read the Wikipedia entry on [loss of significance](https://en.wikipedia.org/wiki/Loss_of_significance). Then answer the following problem:

The tail of the standard logistic distribution is given by $1 - F(t) = 1 - (1+e^{-t})^{-1}$.

- Define a function `f1` to calculate the tail probability of the logistic distribution using the formula given above
- Use [`sympy`](http://docs.sympy.org/latest/index.html) to find the exact value of the tail distribution (using the same symbolic formula) to 20 decimal digits
- Calculate the *relative error* of `f1` when $t = 25$ (The relative error is given by `abs(exact - approximate)/exact`)
- Rewrite the expression for the tail of the logistic distribution using simple algebra so that there is no risk of cancellation, and write a function `f2` using this formula. Calculate the *relative error* of `f2` when $t = 25$.
- How much more accurate is `f2` compared with `f1` in terms of the relative error?

```python
import sympy
```

Q1: Define a function f1 to calculate the tail probability of the logistic distribution using the formula given above

```python
def f1(t):
    f1=1-1/(1+np.exp(-t))
    return(f1)
```

Q2: Use sympy to find the exact value of the tail distribution (using the same symbolic formula) to 20 decimal digits

```python
def f0(t):
    f0=sympy.N(1-1/(1+sympy.exp(-t)),20)
    return(f0)
```

Q3: Calculate the relative error of f1 when  t=25t=25  (The relative error is given by abs(exact - approximate)/exact)

```python
exact=f0(25)
approx=f1(25)
relative_error=np.abs((exact-approx)/exact)
relative_error
```

    4.1759147665982646285e-6

Q4: Rewrite the expression for the tail of the logistic distribution using simple algebra so that there is no risk of cancellation, and write a function f2 using this formula. Calculate the relative error of f2 when  t=25t=25 .

```python
import numpy as np
1/(np.exp(25)+1)
```

    1.3887943864771144e-11

```python
def f2(t):
    f2=np.exp(-t)/(1+np.exp(-t))
    return(f2)
```

```python
f2(25)
```

    1.3887943864771146e-11

```python
exact=f0(25)
approx2=f2(25)
relative_error2=np.abs((exact-approx2)/exact)
relative_error2
```

Q5: How much more accurate is f2 compared with f1 in terms of the relative error?

```python
relative_error/relative_error2
```

    1806875123599.7465438

```python

```

```python

```

**5**. (20 points)

Read in `figs/elephant.jpg` as a gray-scale image. The image has $1066 \times 1600$ values. Using SVD, recreate the image with a relative error of less than 0.5%. What is the relative size of the compressed image as a percentage?

```python
from skimage import io
img = io.imread('figs/elephant.jpg', as_grey=True)
```

```python
import scipy.sparse.linalg
import matplotlib.pyplot as plt
```

```python
k = 1
a=1
while a:
    u, s, v = scipy.sparse.linalg.svds(img, k)
    A=u @ np.diag(s) @ v
    if np.var(A) > (np.var(img) * 0.995):
        a=0
    k += 1
```

```python
k=k-1
k
```

    158

```python
u, s, v = scipy.sparse.linalg.svds(img, 158)
A=u @ np.diag(s) @ v
```

```python
A
```

    array([[ 0.1751828 ,  0.17532793,  0.17936893, ...,  0.21370902,
             0.21522053,  0.21177994],
           [ 0.17520919,  0.17591909,  0.17964196, ...,  0.21431063,
             0.21547293,  0.21162661],
           [ 0.17458847,  0.17589472,  0.17935701, ...,  0.21586281,
             0.21584101,  0.21163815],
           ...,
           [ 0.64574139,  0.634374  ,  0.6352618 , ...,  0.60306447,
             0.59627018,  0.58810847],
           [ 0.65323139,  0.65108152,  0.65401417, ...,  0.60050738,
             0.58508802,  0.57334184],
           [ 0.64943004,  0.65001985,  0.65542358, ...,  0.58759695,
             0.56924365,  0.56179656]])

```python
plt.imshow(A, cmap='Greys_r')
plt.show()
```

the relative size of the compressed image as a percentage

```python
(u.shape[0]*u.shape[1] + v.shape[0]*v.shape[1] + len(s)) / (img.shape[0]*img.shape[1])
```

    0.24706027204502815

```python

```


In this homework, you will practice how to compute word embeddings and use them for sentiment analysis.

By completing this homework you will:

- Train word vectors from scratch.
- Learn how to create batches of data.
- Understand how backpropagation works.
- Plot and visualize your learned word vectors.

Knowing how to train these models will give you a better understanding of word vectors, which are building blocks to many applications in natural language processing.

```python
# Import Python libraries and helper functions (in utils2)
import nltk
import numpy as np
from IPython.core.display import display

from utils import get_batches, compute_pca, get_dict
```

```python
# Download sentence tokenizer
nltk.data.path.append('.')
```

```python
# Load, tokenize and process the data
import re                                                           #  Load the Regex-modul
with open('data/shakespeare.txt') as f:
    data = f.read()                                                 #  Read in the data
data = re.sub(r'[,!?;-]', '.',data)                                 #  Punktuations are replaced by .
data = nltk.word_tokenize(data)                                     #  Tokenize string to words
data = [ ch.lower() for ch in data if ch.isalpha() or ch == '.']    #  Lower case and drop non-alphabetical tokens
print("Number of tokens:", len(data),'\n', data[:15])               #  print data sample
```

    Number of tokens: 60996
     ['o', 'for', 'a', 'muse', 'of', 'fire', '.', 'that', 'would', 'ascend', 'the', 'brightest', 'heaven', 'of', 'invention']

```python
# Compute the frequency distribution of the words in the dataset (vocabulary)
fdist = nltk.FreqDist(word for word in data)
print("Size of vocabulary: ",len(fdist) )
print("Most frequent tokens: ",fdist.most_common(20) ) # print the 20 most frequent words and their freq.
```

    Size of vocabulary:  5778
    Most frequent tokens:  [('.', 9630), ('the', 1521), ('and', 1394), ('i', 1257), ('to', 1159), ('of', 1093), ('my', 857), ('that', 781), ('in', 770), ('a', 752), ('you', 748), ('is', 630), ('not', 559), ('for', 467), ('it', 460), ('with', 441), ('his', 434), ('but', 417), ('me', 417), ('your', 397)]

#### Mapping words to indices and indices to words
We provide a helper function to create a dictionary that maps words to indices and indices to words.

```python
# get_dict creates two dictionaries, converting words to indices and viceversa.
word2Ind, Ind2word = get_dict(data)
V = len(word2Ind)
print("Size of vocabulary: ", V)
```

    Size of vocabulary:  5778

```python
# example of word to index mapping
print("Index of the word 'king' :  ",word2Ind['king'] )
print("Word which has index 2743:  ",Ind2word[2743] )
```

    Index of the word 'king' :   2745
    Word which has index 2743:   kindness

# 2 Training the Model

###  Initializing the model

You will now initialize two matrices and two vectors.
- The first matrix ($W_1$) is of dimension $N \times V$, where $V$ is the number of words in your vocabulary and $N$ is the dimension of your word vector.
- The second matrix ($W_2$) is of dimension $V \times N$.
- Vector $b_1$ has dimensions $N\times 1$
- Vector $b_2$ has dimensions  $V\times 1$.
- $b_1$ and $b_2$ are the bias vectors of the linear layers from matrices $W_1$ and $W_2$.

The overall structure of the model will look as in Figure 1, but at this stage we are just initializing the parameters.

### Exercise 01
Please use [numpy.random.rand](https://numpy.org/doc/stable/reference/random/generated/numpy.random.rand.html) to generate matrices that are initialized with random values from a uniform distribution, ranging between 0 and 1.

**Note:** In the next cell you will encounter a random seed. Please **DO NOT** modify this seed so your solution can be tested correctly.

```python
# UNQ_C1 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
# GRADED FUNCTION: initialize_model
def initialize_model(N,V, random_seed=1):
    '''
    Inputs:
        N:  dimension of hidden vector
        V:  dimension of vocabulary
        random_seed: random seed for consistent results in the unit tests
     Outputs:
        W1, W2, b1, b2: initialized weights and biases
    '''

    np.random.seed(random_seed)

    ### START CODE HERE (Replace instances of 'None' with your code) ###
    # W1 has shape (N,V)
    W1 = np.random.rand(N, V)
    # W2 has shape (V,N)
    W2 = np.random.rand(V, N)
    # b1 has shape (N,1)
    b1 = np.random.rand(N, 1)
    # b2 has shape (V,1)
    b2 = np.random.rand(V, 1)
    ### END CODE HERE ###

    return W1, W2, b1, b2
```

```python
# Test your function example.
tmp_N = 4
tmp_V = 10
tmp_W1, tmp_W2, tmp_b1, tmp_b2 = initialize_model(tmp_N,tmp_V)
assert tmp_W1.shape == ((tmp_N,tmp_V))
assert tmp_W2.shape == ((tmp_V,tmp_N))
print(f"tmp_W1.shape: {tmp_W1.shape}")
print(f"tmp_W2.shape: {tmp_W2.shape}")
print(f"tmp_b1.shape: {tmp_b1.shape}")
print(f"tmp_b2.shape: {tmp_b2.shape}")
```

    tmp_W1.shape: (4, 10)
    tmp_W2.shape: (10, 4)
    tmp_b1.shape: (4, 1)
    tmp_b2.shape: (10, 1)

##### Expected Output

```CPP
tmp_W1.shape: (4, 10)
tmp_W2.shape: (10, 4)
tmp_b1.shape: (4, 1)
tmp_b2.shape: (10, 1)
```

### 2.1 Softmax
Before we can start training the model, we need to implement the softmax function as defined in equation 5:

$$ \text{softmax}(z_i) = \frac{e^{z_i} }{\sum_{i=0}^{V-1} e^{z_i} }  \tag{5} $$

- Array indexing in code starts at 0.
- $V$ is the number of words in the vocabulary (which is also the number of rows of $z$).
- $i$ goes from 0 to |V| - 1.

### Exercise 02
**Instructions**: Implement the softmax function below.

- Assume that the input $z$ to `softmax` is a 2D array
- Each training example is represented by a column of shape (V, 1) in this 2D array.
- There may be more than one column, in the 2D array, because you can put in a batch of examples to increase efficiency.  Let's call the batch size lowercase $m$, so the $z$ array has shape (V, m)
- When taking the sum from $i=1 \cdots V-1$, take the sum for each column (each example) separately.

Please use
- numpy.exp
- numpy.sum (set the axis so that you take the sum of each column in z)

```python
# UNQ_C2 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
# GRADED FUNCTION: softmax
def softmax(z):
    """
    Inputs:
        z: output scores from the hidden layer
    Outputs:
        yhat: prediction (estimate of y)
    """

    ### START CODE HERE (Replace instances of 'None' with your own code) ###

    # Calculate yhat (softmax)
    e_z = np.exp(z)
    yhat = e_z / e_z.sum(axis=0)
    ### END CODE HERE ###

    return yhat
```

```python
# Test the function
tmp = np.array([[1,2,3],
                [1,1,1]
               ])
tmp_sm = softmax(tmp)
display(tmp_sm)
```

    array([[0.5       , 0.73105858, 0.88079708],
           [0.5       , 0.26894142, 0.11920292]])

##### Expected Ouput

```CPP
array([[0.5       , 0.73105858, 0.88079708],
       [0.5       , 0.26894142, 0.11920292]])
```

### 2.2 Forward propagation

### Exercise 03
Implement the forward propagation $z$ according to equations (1) to (3).

\begin{align}
 h &= W_1 \  X + b_1  \tag{1} \\
 a &= ReLU(h)  \tag{2} \\
 z &= W_2 \  a + b_2   \tag{3} \\
\end{align}

For that, you will use as activation the Rectified Linear Unit (ReLU) given by:

$$f(h)=\max (0,h) \tag{6}$$

    Hints

You can use numpy.maximum(x1,x2) to get the maximum of two values

Use numpy.dot(A,B) to matrix multiply A and B

```python
# UNQ_C3 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
# GRADED FUNCTION: forward_prop
def forward_prop(x, W1, W2, b1, b2):
    """
    Inputs:
        x:  average one hot vector for the context
        W1, W2, b1, b2:  matrices and biases to be learned
     Outputs:
        z:  output score vector
    """

    ### START CODE HERE (Replace instances of 'None' with your own code) ###

    # Calculate h
    h = np.dot(W1, x) + b1

    # Apply the relu on h (store result in h(a))
    h = np.maximum(0, h)

    # Calculate z
    z = np.dot(W2, h) + b2

    ### END CODE HERE ###

    return z, h
```

```python
# Test the function

# Create some inputs
tmp_N = 2
tmp_V = 3
tmp_x = np.array([[0,1,0]]).T
tmp_W1, tmp_W2, tmp_b1, tmp_b2 = initialize_model(N=tmp_N,V=tmp_V, random_seed=1)

print(f"x has shape {tmp_x.shape}")
print(f"N is {tmp_N} and vocabulary size V is {tmp_V}")

# call function
tmp_z, tmp_h = forward_prop(tmp_x, tmp_W1, tmp_W2, tmp_b1, tmp_b2)
print("call forward_prop")
print()
# Look at output
print(f"z has shape {tmp_z.shape}")
print("z has values:")
print(tmp_z)

print()

print(f"h has shape {tmp_h.shape}")
print("h has values:")
print(tmp_h)
```

    x has shape (3, 1)
    N is 2 and vocabulary size V is 3
    call forward_prop

    z has shape (3, 1)
    z has values:
    [[0.55379268]
     [1.58960774]
     [1.50722933]]

    h has shape (2, 1)
    h has values:
    [[0.92477674]
     [1.02487333]]

##### Expected output
```CPP
x has shape (3, 1)
N is 2 and vocabulary size V is 3
call forward_prop

z has shape (3, 1)
z has values:
[[0.55379268]
 [1.58960774]
 [1.50722933]]

h has shape (2, 1)
h has values:
[[0.92477674]
 [1.02487333]]
```

## 2.3 Cost function

- We have implemented the *cross-entropy* cost function for you.

```python
# compute_cost: cross-entropy cost functioN
def compute_cost(y, yhat, batch_size):
    # cost function
    logprobs = np.multiply(np.log(yhat),y) + np.multiply(np.log(1 - yhat), 1 - y)
    cost = - 1/batch_size * np.sum(logprobs)
    cost = np.squeeze(cost)
    return cost
```

```python
# Test the function
tmp_C = 2
tmp_N = 50
tmp_batch_size = 4
tmp_word2Ind, tmp_Ind2word = get_dict(data)
tmp_V = len(word2Ind)

tmp_x, tmp_y = next(get_batches(data, tmp_word2Ind, tmp_V,tmp_C, tmp_batch_size))

print(f"tmp_x.shape {tmp_x.shape}")
print(f"tmp_y.shape {tmp_y.shape}")

tmp_W1, tmp_W2, tmp_b1, tmp_b2 = initialize_model(tmp_N,tmp_V)

print(f"tmp_W1.shape {tmp_W1.shape}")
print(f"tmp_W2.shape {tmp_W2.shape}")
print(f"tmp_b1.shape {tmp_b1.shape}")
print(f"tmp_b2.shape {tmp_b2.shape}")

tmp_z, tmp_h = forward_prop(tmp_x, tmp_W1, tmp_W2, tmp_b1, tmp_b2)
print(f"tmp_z.shape: {tmp_z.shape}")
print(f"tmp_h.shape: {tmp_h.shape}")

tmp_yhat = softmax(tmp_z)
print(f"tmp_yhat.shape: {tmp_yhat.shape}")

tmp_cost = compute_cost(tmp_y, tmp_yhat, tmp_batch_size)
print("call compute_cost")
print(f"tmp_cost {tmp_cost:.4f}")
```

    tmp_x.shape (5778, 4)
    tmp_y.shape (5778, 4)
    tmp_W1.shape (50, 5778)
    tmp_W2.shape (5778, 50)
    tmp_b1.shape (50, 1)
    tmp_b2.shape (5778, 1)
    tmp_z.shape: (5778, 4)
    tmp_h.shape: (50, 4)
    tmp_yhat.shape: (5778, 4)
    call compute_cost
    tmp_cost 9.9560

##### Expected output

```CPP
tmp_x.shape (5778, 4)
tmp_y.shape (5778, 4)
tmp_W1.shape (50, 5778)
tmp_W2.shape (5778, 50)
tmp_b1.shape (50, 1)
tmp_b2.shape (5778, 1)
tmp_z.shape: (5778, 4)
tmp_h.shape: (50, 4)
tmp_yhat.shape: (5778, 4)
call compute_cost
tmp_cost 9.9560
```

## 2.4 Training the Model - Backpropagation

### Exercise 04
Now that you have understood how the CBOW model works, you will train it.

You created a function for the forward propagation. Now you will implement a function that computes the gradients to backpropagate the errors.

```python
# UNQ_C4 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
# GRADED FUNCTION: back_prop
def back_prop(x, yhat, y, h, W1, W2, b1, b2, batch_size):
    """
    Inputs:
        x:  average one hot vector for the context
        yhat: prediction (estimate of y)
        y:  target vector
        h:  hidden vector (see eq. 1)
        W1, W2, b1, b2:  matrices and biases
        batch_size: batch size
     Outputs:
        grad_W1, grad_W2, grad_b1, grad_b2:  gradients of matrices and biases
    """
    ### START CODE HERE (Replace instanes of 'None' with your code) ###

    # Compute l1 as W2^T (Yhat - Y)
    # Re-use it whenever you see W2^T (Yhat - Y) used to compute a gradient
    l1 = np.dot(W2.T, (yhat - y))
    # Apply relu to l1
    l1 = np.maximum(0, l1)
    # Compute the gradient of W1
    grad_W1 = 1 /batch_size * np.dot(l1, x.T)
    # Compute the gradient of W2
    grad_W2 = 1 /batch_size * np.dot(yhat - y, h.T)
    # Compute the gradient of b1
    grad_b1 = 1 /batch_size * np.sum(l1, axis=1, keepdims=True)
    # Compute the gradient of b2
    grad_b2 = 1 /batch_size * np.sum(yhat - y, axis=1, keepdims=True)
    ### END CODE HERE ###

    return grad_W1, grad_W2, grad_b1, grad_b2
```

```python
# Test the function
tmp_C = 2
tmp_N = 50
tmp_batch_size = 4
tmp_word2Ind, tmp_Ind2word = get_dict(data)
tmp_V = len(word2Ind)

# get a batch of data
tmp_x, tmp_y = next(get_batches(data, tmp_word2Ind, tmp_V,tmp_C, tmp_batch_size))

print("get a batch of data")
print(f"tmp_x.shape {tmp_x.shape}")
print(f"tmp_y.shape {tmp_y.shape}")

print()
print("Initialize weights and biases")
tmp_W1, tmp_W2, tmp_b1, tmp_b2 = initialize_model(tmp_N,tmp_V)

print(f"tmp_W1.shape {tmp_W1.shape}")
print(f"tmp_W2.shape {tmp_W2.shape}")
print(f"tmp_b1.shape {tmp_b1.shape}")
print(f"tmp_b2.shape {tmp_b2.shape}")

print()
print("Forwad prop to get z and h")
tmp_z, tmp_h = forward_prop(tmp_x, tmp_W1, tmp_W2, tmp_b1, tmp_b2)
print(f"tmp_z.shape: {tmp_z.shape}")
print(f"tmp_h.shape: {tmp_h.shape}")

print()
print("Get yhat by calling softmax")
tmp_yhat = softmax(tmp_z)
print(f"tmp_yhat.shape: {tmp_yhat.shape}")

tmp_m = (2*tmp_C)
tmp_grad_W1, tmp_grad_W2, tmp_grad_b1, tmp_grad_b2 = back_prop(tmp_x, tmp_yhat, tmp_y, tmp_h, tmp_W1, tmp_W2, tmp_b1, tmp_b2, tmp_batch_size)

print()
print("call back_prop")
print(f"tmp_grad_W1.shape {tmp_grad_W1.shape}")
print(f"tmp_grad_W2.shape {tmp_grad_W2.shape}")
print(f"tmp_grad_b1.shape {tmp_grad_b1.shape}")
print(f"tmp_grad_b2.shape {tmp_grad_b2.shape}")
```

    get a batch of data
    tmp_x.shape (5778, 4)
    tmp_y.shape (5778, 4)

    Initialize weights and biases
    tmp_W1.shape (50, 5778)
    tmp_W2.shape (5778, 50)
    tmp_b1.shape (50, 1)
    tmp_b2.shape (5778, 1)

    Forwad prop to get z and h
    tmp_z.shape: (5778, 4)
    tmp_h.shape: (50, 4)

    Get yhat by calling softmax
    tmp_yhat.shape: (5778, 4)

    call back_prop
    tmp_grad_W1.shape (50, 5778)
    tmp_grad_W2.shape (5778, 50)
    tmp_grad_b1.shape (50, 1)
    tmp_grad_b2.shape (5778, 1)

##### Expected output

```CPP
get a batch of data
tmp_x.shape (5778, 4)
tmp_y.shape (5778, 4)

Initialize weights and biases
tmp_W1.shape (50, 5778)
tmp_W2.shape (5778, 50)
tmp_b1.shape (50, 1)
tmp_b2.shape (5778, 1)

Forwad prop to get z and h
tmp_z.shape: (5778, 4)
tmp_h.shape: (50, 4)

Get yhat by calling softmax
tmp_yhat.shape: (5778, 4)

call back_prop
tmp_grad_W1.shape (50, 5778)
tmp_grad_W2.shape (5778, 50)
tmp_grad_b1.shape (50, 1)
tmp_grad_b2.shape (5778, 1)
```

## Gradient Descent

### Exercise 05
Now that you have implemented a function to compute the gradients, you will implement batch gradient descent over your training set.

**Hint:** For that, you will use `initialize_model` and the `back_prop` functions which you just created (and the `compute_cost` function). You can also use the provided `get_batches` helper function:

```for x, y in get_batches(data, word2Ind, V, C, batch_size):```

```...```

Also: print the cost after each batch is processed (use batch size = 128)

```python
# UNQ_C5 (UNIQUE CELL IDENTIFIER, DO NOT EDIT)
# GRADED FUNCTION: gradient_descent
def gradient_descent(data, word2Ind, N, V, num_iters, alpha=0.03):
    """
    This is the gradient_descent function

      Inputs:
        data:      text
        word2Ind:  words to Indices
        N:         dimension of hidden vector
        V:         dimension of vocabulary
        num_iters: number of iterations
     Outputs:
        W1, W2, b1, b2:  updated matrices and biases
    """
    W1, W2, b1, b2 = initialize_model(N,V, random_seed=282)
    batch_size = 128
    iters = 0
    C = 2
    for x, y in get_batches(data, word2Ind, V, C, batch_size):
        ### START CODE HERE (Replace instances of 'None' with your own code) ###
        # Get z and h
        z, h = forward_prop(x, W1, W2, b1, b2)
        # Get yhat
        yhat = softmax(z)
        # Get cost
        cost = compute_cost(y, yhat, batch_size)

        if (iters+1) % 10 == 0:
            print(f"iters: {iters + 1} cost: {cost:.6f}")
        # Get gradients
        grad_W1, grad_W2, grad_b1, grad_b2 = back_prop(x, yhat, y, h, W1, W2, b1, b2, batch_size)

        # Update weights and biases
        W1 -= alpha * grad_W1
        W2 -= alpha * grad_W2
        b1 -= alpha * grad_b1
        b2 -= alpha * grad_b2

        ### END CODE HERE ###

        iters += 1
        if iters == num_iters:
            break
        if iters % 100 == 0:
            alpha *= 0.66

    return W1, W2, b1, b2
```

```python
# test your function
C = 2
N = 50
word2Ind, Ind2word = get_dict(data)
V = len(word2Ind)
num_iters = 150
print("Call gradient_descent")
W1, W2, b1, b2 = gradient_descent(data, word2Ind, N, V, num_iters)
```

    Call gradient_descent
    iters: 10 cost: 0.789141
    iters: 20 cost: 0.105543
    iters: 30 cost: 0.056008
    iters: 40 cost: 0.038101
    iters: 50 cost: 0.028868
    iters: 60 cost: 0.023237
    iters: 70 cost: 0.019444
    iters: 80 cost: 0.016716
    iters: 90 cost: 0.014660
    iters: 100 cost: 0.013054
    iters: 110 cost: 0.012133
    iters: 120 cost: 0.011370
    iters: 130 cost: 0.010698
    iters: 140 cost: 0.010100
    iters: 150 cost: 0.009566

##### Expected Output

```CPP
iters: 10 cost: 0.789141
iters: 20 cost: 0.105543
iters: 30 cost: 0.056008
iters: 40 cost: 0.038101
iters: 50 cost: 0.028868
iters: 60 cost: 0.023237
iters: 70 cost: 0.019444
iters: 80 cost: 0.016716
iters: 90 cost: 0.014660
iters: 100 cost: 0.013054
iters: 110 cost: 0.012133
iters: 120 cost: 0.011370
iters: 130 cost: 0.010698
iters: 140 cost: 0.010100
iters: 150 cost: 0.009566
```

Your numbers may differ a bit depending on which version of Python you're using.

## 3.0 Visualizing the word vectors

In this part you will visualize the word vectors trained using the function you just coded above.

```python
# visualizing the word vectors here
from matplotlib import pyplot
%config InlineBackend.figure_format = 'svg'
words = ['king', 'queen','lord','man', 'woman','dog','horse',
         'rich','happy','sad']

embs = (W1.T + W2) / 2.0

# given a list of words and the embeddings, it returns a matrix with all the embeddings
idx = [word2Ind[word] for word in words]
X = embs[idx, :]
print(X.shape, idx)  # X.shape:  Number of words of dimension N each
```

    (10, 50) [2745, 3951, 2961, 3023, 5675, 1452, 2472, 4191, 2316, 4278]

```python
result= compute_pca(X, 2)
pyplot.scatter(result[:, 0], result[:, 1])
for i, word in enumerate(words):
    pyplot.annotate(word, xy=(result[i, 0], result[i, 1]))
pyplot.show()
```

You can see that woman and queen are next to each other. However, we have to be careful with the interpretation of this projected word vectors, since the PCA depends on the projection -- as shown in the following illustration.

```python
result= compute_pca(X, 4)
pyplot.scatter(result[:, 3], result[:, 1])
for i, word in enumerate(words):
    pyplot.annotate(word, xy=(result[i, 3], result[i, 1]))
pyplot.show()
```


#  ¿Cómo se mueve un péndulo?

> Calificaciones: https://docs.google.com/spreadsheets/d/1X8sAHmrIErYgoAjTocclAFS0Mx_EA8BDWlp6DgYyBVo/edit?usp=sharing

> Se dice que un sistema cualquiera, mecánico, eléctrico, neumático, etc., es un oscilador armónico si, cuando se deja en libertad fuera de su posición de equilibrio, vuelve hacia ella describiendo oscilaciones sinusoidales, o sinusoidales amortiguadas en torno a dicha posición estable.
- https://es.wikipedia.org/wiki/Oscilador_armónico

Referencias:
   - http://matplotlib.org
   - https://seaborn.pydata.org
   - http://www.numpy.org
   - http://ipywidgets.readthedocs.io/en/latest/index.html

**En realidad esto es el estudio de oscilaciones. **
___

```python
from IPython.display import YouTubeVideo
YouTubeVideo('k5yTVHr6V14')
```

Los sistemas mas sencillos a estudiar en oscilaciones son el sistema ` masa-resorte` y el `péndulo simple`.

\begin{align}
\frac{d^2 x}{dt^2} +  \omega_{0}^2 x &= 0, \quad \omega_{0} = \sqrt{\frac{k}{m}}\notag\\
\frac{d^2 \theta}{dt^2} + \omega_{0}^{2}\, \theta &= 0, \quad\mbox{donde}\quad \omega_{0}^2 = \frac{g}{l}
\end{align}

___
## Sistema `masa-resorte`

La solución a este sistema `masa-resorte` se explica en términos de la segunda ley de Newton. Para este caso, si la masa permanece constante y solo consideramos la dirección en $x$. Entonces,
\begin{equation}
F = m \frac{d^2x}{dt^2}.
\end{equation}

¿Cuál es la fuerza? **Ley de Hooke!**
\begin{equation}
F = -k x, \quad k > 0.
\end{equation}

Vemos que la fuerza se opone al desplazamiento y su intensidad es proporcional al mismo.  Y $k$ es la constante elástica o recuperadora del resorte.

Entonces, un modelo del sistema `masa-resorte` está descrito por la siguiente **ecuación diferencial**:

\begin{equation}
\frac{d^2x}{dt^2} + \frac{k}{m}x = 0,
\end{equation}

cuya solución se escribe como
\begin{equation}
x(t) = A \cos(\omega_{o} t) + B \sin(\omega_{o} t)
\end{equation}
Y su primera derivada (velocidad) sería
\begin{equation}
\frac{dx(t)}{dt} = \omega_{0}[- A \sin(\omega_{0} t) + B\cos(\omega_{0}t)]
\end{equation}

 Ver en el tablero que significa solución de la ecuación diferencial.

### **¿Cómo se ven las gráficas de $x$ vs $t$ y $\frac{dx}{dt}$ vs $t$?**

_Esta instrucción es para que las gráficas aparezcan dentro de este entorno._

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

```python
import matplotlib as mpl
label_size = 14
mpl.rcParams['xtick.labelsize'] = label_size
mpl.rcParams['ytick.labelsize'] = label_size
```

```python
import numpy as np
```

```python
# Definición de funciones a graficar
A, B, w0 = .5, .1, .5                            # Parámetros
t = np.linspace(0, 50, 100)                      # Creamos vector de tiempo de 0 a 50 con 100 puntos
x = A * np.cos(w0 * t) + B * np.sin(w0 * t)      # Función de posición
dx = w0 * (-A * np.sin(w0 * t) + B * np.cos(w0 * t))  # Función de velocidad

# Gráfico
plt.figure(figsize=(7, 4))                     # Ventana de gráfica con tamaño
plt.plot(t, x, '-', lw=1, ms=1,
         label='$x(t)$')                       # Explicación
plt.plot(t, dx, 'ro-', lw=1, ms=4,
         label=r'$\dot{x}(t)$')
plt.legend(loc='best')
plt.xlabel('$t$', fontsize=14)                 # Etiqueta eje x
plt.show()
```

```python
# Colores, etiquetas y otros formatos
plt.figure(figsize = (7, 4))
plt.scatter(t, x, lw=0, c = 'red',
            label = '$x(t)$')           # Gráfica con puntos
plt.plot(t, x, 'r-', lw = 1)            # Grafica normal
plt.scatter(t, dx, lw = 0, c = 'b',
            label = r'$\frac{dx}{dt}$') # Con la r, los backslash se tratan como un literal, no como un escape
plt.plot(t, dx, 'b-', lw = 1)
plt.xlabel('$t$', fontsize = 20)
plt.legend(loc = 'best')                # Leyenda con las etiquetas de las gráficas
plt.show()
```

Y si consideramos un conjunto de frecuencias de oscilación, entonces

```python
frecuencias = np.array([.1, .2 , .5, .6])   # Vector de diferentes frecuencias
plt.figure(figsize = (7, 4))                # Ventana de gráfica con tamaño

# Graficamos para cada frecuencia
for w0 in frecuencias:
    x = A * np.cos(w0 * t) + B * np.sin(w0 * t)
    plt.plot(t, x, 'D-', label=f'Frecuencia $w_0$={w0}')
plt.xlabel('$t$', fontsize = 16)            # Etiqueta eje x
plt.ylabel('$x(t)$', fontsize = 16)         # Etiqueta eje y
plt.title('Oscilaciones', fontsize = 16)    # Título de la gráfica
plt.legend(loc='upper left', bbox_to_anchor=(1.05, 1))
plt.show()
```

Si queremos manipular un poco mas las cosas, hacemos uso de lo siguiente:

```python
from ipywidgets import *
```

```python
def masa_resorte(t = 0):
    A, B, w0 = .5, .1, .5                            # Parámetros
    x = A*np.cos(w0*t)+B*np.sin(w0*t)                # Función de posición

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)
    ax.plot(x, [0],  'ko', ms = 10)
    ax.set_xlim(xmin = -0.6, xmax = .6)
    ax.axvline(x=0, color = 'r')
    ax.axhline(y=0, color = 'grey', lw = 1)
    fig.canvas.draw()
```

```python
interact(masa_resorte, t = (0, 50,.01));
```

    interactive(children=(FloatSlider(value=0.0, description='t', max=50.0, step=0.01), Output()), _dom_classes=('…

La opción de arriba generalmente será lenta, así que lo recomendable es usar `interact_manual`.

```python
def masa_resorte(t = 0):
    A, B, w0 = .5, .1, .5                            # Parámetros
    x = A*np.cos(w0*t)+B*np.sin(w0*t)                # Función de posición

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)
    ax.plot(x, [0],  'ko', ms = 10)
    ax.set_xlim(xmin = -0.6, xmax = .6)
    ax.axvline(x=0, color = 'r')
    ax.axhline(y=0, color = 'grey', lw = 1)
    fig.canvas.draw()
```

```python
interact_manual(masa_resorte, t = (0, 50,.01));
```

    interactive(children=(FloatSlider(value=0.0, description='t', max=50.0, step=0.01), Button(description='Run In…

___
## Péndulo simple
Ahora, si fijamos nuestra atención al movimiento de un péndulo simple _(oscilaciones pequeñas)_, la ecuación diferencial a resolver tiene la misma forma:

\begin{equation}
\frac{d^2 \theta}{dt^2} + \omega_{0}^{2}\, \theta = 0, \quad\mbox{donde}\quad \omega_{0}^2 = \frac{g}{l}.
\end{equation}

La diferencia más evidente es como hemos definido a $\omega_{0}$. Esto quiere decir que,

\begin{equation}
\theta(t) = A\cos(\omega_{0} t) + B\sin(\omega_{0}t)
\end{equation}

Si graficamos la ecuación de arriba vamos a encontrar un comportamiento muy similar al ya discutido anteriormente. Es por ello que ahora veremos el movimiento en el plano $xy$. Es decir,

\begin{align}
x &= l \sin(\theta), \quad
y = l \cos(\theta)
\end{align}

```python
# Podemos definir una función que nos entregue theta dados los parámetros y el tiempo
def theta_t(a, b, g, l, t):
    omega_0 = np.sqrt(g / l)
    return a * np.cos(omega_0 * t) + b * np.sin(omega_0 * t)
```

```python
# Hacemos un gráfico interactivo del péndulo
def pendulo_simple(t = 0):
    fig = plt.figure(figsize = (5,5))
    ax = fig.add_subplot(1, 1, 1)
    l = 2
    g = 9.8
    x = l * np.sin(theta_t(.4, .6, g, l, t))
    y =  - l * np.cos(theta_t(.4, .6, g, l, t))
    ax.plot(x, y, 'ko', ms = 10)
    ax.plot([0], [0], 'rD')
    ax.plot([0, x ], [0, y], 'k-', lw = 1)
    ax.set_xlim(xmin = -2.2, xmax = 2.2)
    ax.set_ylim(ymin = -2.2, ymax = .2)
    fig.canvas.draw()
```

```python
interact_manual(pendulo_simple, t = (0, 10,.01));
```

    interactive(children=(FloatSlider(value=0.0, description='t', max=10.0, step=0.01), Button(description='Run In…

### Condiciones iniciales

Realmente lo que se tiene que resolver es,

\begin{equation}
\theta(t) = \theta(0) \cos(\omega_{0} t) + \frac{\dot{\theta}(0)}{\omega_{0}} \sin(\omega_{0} t)
\end{equation}

> **Actividad.** Modificar el programa anterior para incorporar las condiciones iniciales.

```python
# Solución:
def theta_t(theta0, dtheta0, g, l, t):
    omega_0 = np.sqrt(g / l)
    a = theta0
    b = dtheta0 / omega_0
    return a * np.cos(omega_0 * t) + b * np.sin(omega_0 * t)
```

```python
def pendulo_simple(t = 0):
    fig = plt.figure(figsize = (5,5))
    ax = fig.add_subplot(1, 1, 1)
    l = 2
    g = 9.8
    theta0 = np.pi / 4
    dtheta0 = 0
    x = l * np.sin(theta_t(np.pi / 4, 0, 9.8, 2, t))
    y =  - l * np.cos(theta_t(np.pi / 4, 0, 9.8, 2, t))
    ax.plot(x, y, 'ko', ms = 10)
    ax.plot([0], [0], 'rD')
    ax.plot([0, x ], [0, y], 'k-', lw = 1)
    ax.set_xlim(xmin = -2.2, xmax = 2.2)
    ax.set_ylim(ymin = -2.2, ymax = .2)
    fig.canvas.draw()

interact_manual(pendulo_simple, t = (0, 10,.01));
```

    interactive(children=(FloatSlider(value=0.0, description='t', max=10.0, step=0.01), Button(description='Run In…

### Plano fase  $(x, \frac{dx}{dt})$

La posición y velocidad para el sistema `masa-resorte` se escriben como:

\begin{align}
x(t) &= x(0) \cos(\omega_{o} t) + \frac{\dot{x}(0)}{\omega_{0}} \sin(\omega_{o} t)\\
\dot{x}(t) &= -\omega_{0}x(0) \sin(\omega_{0} t) + \dot{x}(0)\cos(\omega_{0}t)]
\end{align}

```python
k = 3  #constante elástica [N]/[m]
m = 1 # [kg]
omega_0 = np.sqrt(k/m)
x_0 = .5
dx_0 = .1
```

```python
t = np.linspace(0, 15, 300)
```

```python
x_t = x_0 * np.cos(omega_0 * t) + (dx_0 / omega_0) * np.sin(omega_0 * t)
dx_t = -omega_0 * x_0 * np.sin(omega_0 * t) + dx_0 * np.cos(omega_0 * t)
```

```python
plt.figure(figsize = (7, 4))
plt.plot(t, x_t, label = '$x(t)$', lw = 4)
#plt.plot(t, dx_t, label = '$\dot{x}(t)$', lw = 1)
plt.plot(t, dx_t/omega_0, label = '$\dot{x}(t)$', lw = 4)  # Mostrar que al escalar, la amplitud queda igual
plt.legend(loc='center left', bbox_to_anchor=(1.01, 0.5), prop={'size': 14})
plt.xlabel('$t$', fontsize = 18)
plt.show()
```

```python
plt.figure(figsize = (5, 5))
plt.plot(x_t, dx_t/omega_0, 'ro', ms = 2)
plt.xlabel('$x(t)$', fontsize = 18)
plt.ylabel('$\dot{x}(t)/\omega_0$', fontsize = 18)
plt.show()
```

```python
plt.figure(figsize = (5, 5))
plt.scatter(x_t, dx_t/omega_0, cmap = 'viridis', c = dx_t, s = 8, lw = 0)
plt.xlabel('$x(t)$', fontsize = 18)
plt.ylabel('$\dot{x}(t)/\omega_0$', fontsize = 18)
plt.show()
```

#### Multiples condiciones iniciales

```python
k = 3  #constante elástica [N]/[m]
m = 1 # [kg]
omega_0 = np.sqrt(k/m)
```

```python
t = np.linspace(0, 50, 50)
```

```python
x_0s = np.array([.7, .5, .25, .1])
dx_0s = np.array([.2, .1, .05, .01])
cmaps = np.array(['viridis', 'inferno', 'magma', 'plasma'])
```

```python
plt.figure(figsize = (6, 6))
for indx, x_0 in enumerate(x_0s):
    x_t = x_0 *np.cos(omega_0 *t) + (dx_0s[indx]/omega_0) * np.sin(omega_0 *t)
    dx_t = -omega_0 * x_0 * np.sin(omega_0 * t) + dx_0s[indx] * np.cos(omega_0 * t)
    plt.scatter(x_t,  dx_t/omega_0, cmap = cmaps[indx],
                c = dx_t, s = 10,
                lw = 0)
    plt.xlabel('$x(t)$', fontsize = 18)
    plt.ylabel('$\dot{x}(t)/\omega_0$', fontsize = 18)
    #plt.legend(loc='center left', bbox_to_anchor=(1.05, 0.5))
```

Trayectorias del oscilador armónico simple en el espacio fase $(x,\, \dot{x}\,/\omega_0)$ para diferentes valores de la energía.

# Anuncios parroquiales

## 1. Examen final: lo habilito el miércoles 5 de mayo, entregan el viernes 7 de mayo.

## 2. Proyecto tercer módulo para el viernes 14 de mayo.

## 3. Quiz la próxima clase

Created with Jupyter by Lázaro Alonso. Modified by Esteban Jiménez Rodríguez