**Machine Learning Fundamentals**
© Deep Learning Indaba. Apache License 2.0.

Introducción
El objetivo de esta practica es entender la idea de clasificación (clasificación de cosas en categorías) utilizando un modelo de ML. Jugar un poco con la relación entre los parámetros de un clasificador y el límite de decisión (una línea que separa las categorías) también idea de una función de pérdida o cost function. Y luego empezar a usar Tensorflow.

Objetivos de aprendizaje:

*Comprender la idea de clasificación

*Comprender el concepto de separabilidad (lineal) de un conjunto de datos

*Comprender cuáles son los parámetros de un clasificador y cómo se relacionan con el límite de decisión

*Poder explicar brevemente qué es Tensorflow

**Importar librerías**

```
!pip install -q moviepy > /dev/null 2>&1
!pip install -q imageio > /dev/null 2>&1
!pip install -q tensorflow==2.0.0-beta0 > /dev/null 2>&1

import tensorflow as tf
import numpy as np                 # Numpy is an efficient linear algebra library.
import matplotlib.pyplot as plt    # Matplotlib is used to generate plots of data.
from matplotlib import animation, rc

from IPython import display

try:
  tf.executing_eagerly()
  print("TensorFlow executing eagerly: {}".format(tf.executing_eagerly()))
except ValueError:
  print('Already running eagerly')
```

    TensorFlow executing eagerly: True

**Outline:** La clasificación en el aprendizaje automático implica aprender a etiquetar ejemplos en una (o más) categorías discretas. Esto difiere de otra tarea común en el aprendizaje automático llamada regresión, que implica aprender una asignación de entradas a una salida de valor continuo.

1.Comenzamos presentando un conjunto de datos de puntos rojos y azules que queremos separar.

2.Introducimos y exploramos la idea de la separabilidad lineal.

3.Definimos una pérdida como una medida de cuán bueno de un separador es una línea particular

4.Presentamos brevemente TensorFlow y mostramos cómo se puede usar para encontrar automáticamente el mínimo de una función de pérdida.

**Dear data**

Ejecuta el código en la celda a continuación y observa el diagrama resultante. Debe producir un conjunto de datos 2D simple que consta de 2 clases de puntos, las clases están representadas por los colores azul y rojo.

Nuestra tarea es construir un clasificador binario que pueda distinguir entre los puntos rojo y azul (al rojo y al azul se les conoce como las clases de los puntos), utilizando solo las coordenadas 2-D de un punto.

En otras palabras, queremos una función que tome como entrada un vector 2-D que represente las coordenadas de un punto y devuelva un valor de 1 o 0 que indica si el punto es rojo o azul. Aquí hemos codificado los colores rojo y azul en los números 1 y 0 (¡lo que facilita el trabajo en matemáticas y código!)

Nota: hems codificado arbitrariamente rojo como 1 y azul como 0, ¡también puedes hacerlo al revés siempre que seas coherente!

**Helper functions (run me)**

```
# this is a helper function to assist with plotting the dataset below
def plot_dataset(inputs, labels):
  # Plot the given 2D inputs and labels using Matplotlib.
  plt.scatter(
      inputs[:, 0], inputs[:, 1],
      c=['red' if label > 0 else 'blue' for label in labels])

  plt.axis('equal')

  plt.xlabel('x1')
  plt.ylabel('x2')
```

**Data**

```
#@title Generate the Dataset  {run: "auto"}
# Define the centre(s) of the points
centre = 1.8    #@param {type:"slider", min:0, max:2, step:0.1}

points_in_class = 20  # How many points we want per class

# A fixed random seed is a common "trick" used in ML that allows us to recreate
# the same data when there is a random element involved.
np.random.seed(0)

# Generate random points in the "red" class
red_inputs = np.random.normal(loc=centre, scale=1.0, size=[points_in_class, 2])
# Generate random points in the "blue" class
blue_inputs = np.random.normal(loc=-centre, scale=1.0, size=[points_in_class, 2])
# Put these together
inputs = np.concatenate((red_inputs, blue_inputs), axis=0)

# The class (label) is 1 for red or 0 for blue
red_labels = np.ones(points_in_class)
blue_labels = np.zeros(points_in_class)
labels = np.concatenate((red_labels, blue_labels), axis=0)

# num_data_points is the total data set size
num_data_points = 2 * points_in_class

plot_dataset(inputs, labels)
```

**¿Cómo se ven los datos?**

Las entradas son vectores bidimensionales (puntos en un espacio bidimensional). Aquí están las coordenadas de 4 puntos, que hemos elegido deliberadamente para que los puntos 1 y 2 sean "rojos" y los puntos 3 y 4 sean "azules".

```
print('Input 1:\t', inputs[0])
print('Input 2:\t', inputs[1])

print('Input 3:\t', inputs[-1])
print('Input 4:\t', inputs[-2])
```

    ('Input 1:\t', array([3.56405235, 2.20015721]))
    ('Input 2:\t', array([2.77873798, 4.0408932 ]))
    ('Input 3:\t', array([-2.11155253, -1.74383466]))
    ('Input 4:\t', array([-2.67079715, -2.37884966]))

Las etiquetas son 0 (azul) o 1 (rojo). Aquí están las etiquetas correspondientes a los puntos anteriores:

```
print('Label 1:\t', labels[0])
print('Label 2:\t', labels[1])

print('Label 3:\t', labels[-1])
print('Label 4:\t', labels[-2])
```

    ('Label 1:\t', 1.0)
    ('Label 2:\t', 1.0)
    ('Label 3:\t', 0.0)
    ('Label 4:\t', 0.0)

**Separabilidad lineal**

Para un conjunto de datos bidimensional con 2 clases, podemos decir que el conjunto de datos es linealmente separable si es posible dibujar una línea (1-D) que separe los ejemplos de cada clase.

En este caso, querríamos una línea entre los puntos rojo y azul de modo que todos los puntos rojos se encuentren en un lado de la línea y todos los puntos azules en el otro. La separabilidad lineal de un conjunto de datos D-dimensional con 2 clases significa que existe un solo plano (D-1) -dimensional (hiper-) que separa las clases (un hiperplano es una generalización de una línea recta a muchas dimensiones).

**Tarea exploratoria**

En la celda de código bajo el encabezado "Los datos", cambia el control deslizante para el valor central. Esto actualizará automáticamente el valor en el código y volverá a dibujar el gráfico.

¿A qué valor de centro el conjunto de datos se vuelve linealmente separable?

**Pregunta para discusión**

¿Puedes pensar en algunos conjuntos de datos 2D, de 2 clases, similares al anterior, que son separables (los puntos de las 2 clases no se superponen entre sí), pero no son linealmente separables?

Dibuja algunos ejemplos en papel o complétalos con Matplotlib y mira qué pasa

**Drawing the line**
Si te acuerdas de la escuela, una ecuación $w_1x_1 + w_2x_2 = 0$ puede representar una línea en 2 dimensiones, con ejes de coordenadas $x_1$ and $x_2$,que pasa por el origen (0, 0).

También podemos escribir esto en forma de vector como: $\mathbf{w}^T\mathbf{x} = 0$, donde $\mathbf{w}^T = [w_1, w_2]$ y $\mathbf{x}^T = [x_1, x_2]$.

**Nota**: Nuestra línea de arriba pasará por el origen (0,0). Si queremos describir una línea que no pasa por el origen, necesitamos agregar un término de sesgo o bias.

Cuando una línea (o más generalmente, un hiperplano) se define de esta manera, llamamos a los **parámetros** $\mathbf{w} = (w_1, w_2)$ un **vector normal** para la línea. El vector normal es ortogonal (perpendicular) a la línea. Queremos construir una línea que separe los puntos rojo y azul, lo que llamaremos un **límite de decisión**. El nombre "límite de decisión" proviene del hecho de que la línea describe el límite en el espacio de entrada donde la salida (decisión) del modelo cambia de una clase a otra.

En la siguiente celda, trazamos nuestro conjunto de datos junto con un vector normal $\mathbf{w}$ y un límite de decisión. Puede ajustar los valores de  $w_1$ y  $w_2$ utilizando los controles deslizantes de la derecha. Observa el efecto que tienen los valores en el vector normal dibujado en rojo y el límite de decisión en negro. Ajusta los valores para que la línea negra separe los puntos azul y rojo (es decir, puntos rojos en un lado y azules en el otro).

Tu línea también debe tener el vector normal apuntando en la dirección de los puntos rojos. La razón por la cual la dirección es significativa es que queremos clasificar eventualmente los puntos en un lado de la línea (límite de decisión) como rojos y el otro como azules.

¿Es posible encontrar una línea a través del origen que separe perfectamente los puntos?

**Nota:** Cada una de nuestras entradas es un vector 2-D, compuesto por dos valores de coordenadas. Nos referimos a estos 2 ejes de coordenadas como $x_1$ y $x_2$. Por ejemplo, si tenemos una entrada  $(1, 2)$, entonces diríamos$x_1 = 1$ y $x_2 = 2$ para ese punto.

```
#@title Effect of parameters {run: "auto"}

# Define the parameters
w1 = 0.8 #@param { type: "slider", min: -5, max: 5, step: 0.1 }
w2 = 1 #@param { type: "slider", min: -5, max: 5, step: 0.1 }

plot_dataset(inputs, labels)

# Add the weight vector to the plot. We plot it in red, as it has to "point"
# in the direction of the red points.
ax = plt.axes()
ax.arrow(0, 0, w1, w2, head_width=0.3, head_length=0.3, fc='r', ec='r')

# Plot part of the decision boundary in black. It is orthogonal to the weight
# vector.
t = 2 # this is how long the line should be
plt.plot([-t * w2, t * w2], [t * w1, -t * w1], 'k-')

plt.xlim([-4, 4])
plt.ylim([-4, 4])

plt.show()
```

**Clasificación**

Dado un vector normal $w$, podemos evaluar a qué lado del límite de decisión se encuentra un punto particular $x_i = (x_i, 1, x_i, 2)$ evaluando $\mathbf{w^Tx_i}$. Si $\mathbf{w^Tx_i}> 0$, el punto $x_i$ se encuentra a un lado del límite (en la dirección del vector normal), y podemos clasificar ese punto como perteneciente a la clase $1$ (en nuestro caso, "rojo").

Si $\mathbf{w^Tx_i}> 0$,  el punto se encuentra en el otro lado y se puede clasificar como clase $0$ (en nuestro caso, "azul"). Finalmente, si $\mathbf{w^Tx_i} = 0$, el punto se encuentra en el límite de decisión y podemos decidir si clasificarlo como $0$ o $1$, o ignorarlo.

**¿Qué tan "buena" es la línea?**

Si jugaste con el código anterior, es posible que hayas desarrollado cierta intuición sobre cómo las diferentes configuraciones de los parámetros influyen en la ubicación final del límite de decisión. ¡El propósito del machine learning es ajustar automáticamente los valores de $w_1$ y $w_2$ para encontrar un límite de decisión adecuado! Pero para hacer esto, necesitamos especificar matemáticamente alguna pérdida o función objetiva.

La pérdida es una función de los parámetros $w_1$ y $w_2$ y nos dice cuán buena es una determinada configuración de los valores de los parámetros para clasificar los datos. Esta función se define de tal manera que alcanza su configuración óptima cuando se minimiza, es decir, cuanto menor sea su valor, mejor será la separación entre las clases. Una propiedad adicional que puede tener una función de pérdida que a menudo es crucial para el aprendizaje automático es ser diferenciable. Una función de pérdida diferenciable nos permite utilizar la optimización basada en gradiente para encontrar su valor mínimo y los valores óptimos correspondientes de $w_1$ y $w_2$.

Para este problema de clasificación, consideramos la función de pérdida de entropía cruzada binaria o binary cross-entropy  para medir qué tan buenas son las predicciones del modelo. Esta función de pérdida compara la predicción del modelo para cada ejemplo, $x_i$, con el objetivo verdadero $y_i$ (a menudo nos referimos a la etiqueta verdadera asociada con una entrada como "objetivo"). Luego aplica la función de registro no lineal para penalizar al modelo por estar más lejos de la clase verdadera.

La función de entropía cruzada binaria utiliza una operación llamada función sigmoidea. Esta función permite que nuestro clasificador genere cualquier valor real. La función de pérdida de entropía cruzada binaria, sin embargo, espera que las predicciones hechas por un clasificador estén entre 0 y 1. La función sigmoidea "aplasta" cualquier entrada de número real para ubicarse en el intervalo $(0,1)$.

Para los que quieren entender esto de una manera más profunda o sea matemática, la ecuación para la pérdida de entropía cruzada binaria, en un conjunto de datos con $N$ puntos, se define de la siguiente manera:

\begin{align}
l(\mathbf{w}; \mathbf{\hat{y}}, \mathbf{y}) = -\frac{1}{N}\sum_{i=1}^N y_i log(\hat{y}_i) + (1-y_i)log(1-\hat{y}_i)
\end{align}

Dónde:

 $\hat{y}_i = \operatorname{sigmoid}(\mathbf{w}^T\mathbf{x_i})$ y el $\operatorname{sigmoid}$ se define como:

 $$
\mathrm{sigmoid}(a) = \frac{1}{1 + e^{-a}} .
$$

Puedes consultar este blog para más información:
(https://towardsdatascience.com/understanding-binary-cross-entropy-log-loss-a-visual-explanation-a3ac6025181a).

Ahora volvamos a programar para cualquier $W_1$ y $W_2$

```
def compute_loss(w1, w2, inputs, labels):

  total_log_likelihood = 0
  N = len(inputs)

  # Add the contribution of each datapoint to the loss
  for (x1, x2), target in zip(inputs, labels):
    # As our targets are 0 or 1, our prediction function must output a value between 0 and 1.
    # The sigmoid function 'squashes' any value to lie between 0 and 1:
    prediction = tf.sigmoid(w1*x1 + w2*x2)

    # Compute the local loss term
    # We add 1e-10 to make the log operations numerically stable (i.e. avoid taking the log of 0.)
    log_likelihood = target * tf.math.log(prediction + 1e-10) + (1.-target)*tf.math.log(1.-prediction + 1e-10)
    total_log_likelihood += log_likelihood

  loss = -total_log_likelihood
  average_loss = loss / N
  return average_loss
```

**Optional section: More on the sigmoid function**

**Pregunta:**

Qué crees que es importante al diseñar una buena función de pérdida, es decir, ¿qué debe hacer la función de pérdida cuando el modelo produce una predicción buena / mala?

Si deseas, puedes revisar con matemáticas, muestra cómo la función sigmoidea (y la función binaria de entropía cruzada) es buena para evaluar la calidad de las predicciones utilizando las siguientes preguntas:

```
ax = plt.axes()
xs = np.arange(-10,10)
ax.plot(xs, 1 / (1 + np.exp(-xs)))
```

Como se señaló anteriormente, la función sigmoidea se define como
$$
\mathrm{sigmoid}(a) = \frac{1}{1 + e^{-a}} .
$$
¿Puedes mostrar eso?
$$
1 - \mathrm{sigmoid}(a) = \frac{1}{1 + e^{a}} ,
$$
y dibujar ambos en una hoja de papel?

* ¿Cuál es su valor cuando $a = \mathbf{w}^{T}\mathbf{x}$ es positivo? ¿negativo? y cero?
* ¿Qué sucede con su valor cuando$a = \mathbf{w}^{T}\mathbf{x}$ se hace más grande?
* ¿Cuál es el valor de $\mathrm{sigmoid}(\mathbf{w^Tx})$  cuando $\mathbf{w}^T\mathbf{x} = 0$? ¿Cómo cambia esto cómo clasificamos los puntos a ambos lados del límite de decisión?

** SUGERENCIA **: Recuerda que la idea de la función de pérdida es devolver valores pequeños cuando el clasificador hace buenas predicciones y valores grandes cuando el clasificador hace malas predicciones.

* Si no te siente cómodo con las matemáticas *, recuerde que el objetivo principal de esta pregunta es ** resaltar la importancia de la pérdida binaria de entropía cruzada ** y ** NO ** las matemáticas, ¡así que  a concéntrarse en los conceptos!

**Pregunta extra**

Derivamos la función compute_loss () anterior basada en minimizar la pérdida logarítmica del error de predicción. Esto está relacionado con un concepto llamado 'entropía cruzada'. Pero otra forma de derivar exactamente la misma función de pérdida es maximizando la probabilidad de los datos bajo el modelo $P(y | x, w_1, w_2)$. Si está familiarizado con este concepto (por ejemplo, a partir de estadísticas), vea si también puede derivarlo de esta manera.

###Lectura adicional opcional
Más información  [cross-entropy loss](http://ml-cheatsheet.readthedocs.io/en/latest/loss_functions.html) y otra info interesante [information theory](https://rdipietro.github.io/friendly-intro-to-cross-entropy-loss/).

## Loss value for your chosen $w_1$ and $w_2$

La siguiente línea de código calcula el valor de pérdida para los valores elegidos de $w_1$ y $w_2$. Intenta cambiar los valores de $w_1$ y $w_2$ con los controles deslizantes.

¿Puedes ver cómo una mejor separación resulta en una pérdida menor?

**Nota:** Si has usado una versión anterior de TensorFlow anteriormente, ¡puede ser confuso cómo funciona esta celda de código!

```
#@title Effect of parameters on the loss {run: "auto"}

# Define the parameters
w1 = -1 #@param { type: "slider", min: -5, max: 5, step: 0.1 }
w2 = 1 #@param { type: "slider", min: -5, max: 5, step: 0.1 }

plot_dataset(inputs, labels)

# Add the weight vector to the plot. We plot it in red, as it has to "point"
# in the direction of the red points.
ax = plt.axes()
ax.arrow(0, 0, w1, w2, head_width=0.3, head_length=0.3, fc='r', ec='r')

# Plot part of the decision boundary in black. It is orthogonal to the weight
# vector.
t = 2 # this is how long the line should be
plt.plot([-t * w2, t * w2], [t * w1, -t * w1], 'k-')

plt.xlim([-4, 4])
plt.ylim([-4, 4])

plt.show()

compute_loss(w1, w2, inputs, labels).numpy()
```

## Visualising the loss function

Podemos visualizar la función de pérdida para nuestro conjunto de datos trazando su valor en cada punto de una cuadrícula completa de valores de parámetros $w_1$ y $w-2$. Hacemos esto usando un diagrama de contorno, que es una técnica para visualizar una función tridimensional en un diagrama bidimensional dejando que el color represente la tercera dimensión. Todos los puntos con el mismo color tienen el mismo valor de pérdida.

```
# We define a function so we can re-use this code later
def plot_contours():
  # Generate a whole bunch of (w1, w2) points in a grid
  ind = np.linspace(-5, 5, 50)
  w1grid, w2grid = np.meshgrid(ind, ind)

  # Compute the loss for each point in the grid
  losses = []
  for w1s, w2s in zip(w1grid, w2grid):
    loss = compute_loss(w1s, w2s, inputs, labels)
    losses.append(loss)

  # Pack the loss values for every value of w1 & w2 into one (50,50) array
  losses_array = np.concatenate(losses).reshape(50,50)

  # Now plot the resulting loss function as a contour plot over the whole grid of (w1, w2) values.
  fig = plt.figure()
  plt.contourf(w1grid, w2grid, losses_array, 20, cmap=plt.cm.jet)
  cbar = plt.colorbar()
  cbar.ax.set_ylabel('Binary cross-entropy loss value')
  plt.xlabel('w1 value')
  plt.ylabel('w2 value')
  plt.title('Total loss for different values of w1 and w2')

plot_contours()
```

En la barra de la derecha, a medida que el color va de rojo a azul, el valor de pérdida disminuye y disminuye hasta alcanzar el valor más pequeño (azul oscuro).

Dado que queremos que la pérdida sea lo más pequeña posible, queremos que nuestros valores de $w_1$ y $w_2$ produzcan una pérdida en el área azul oscuro de la gráfica de contorno. Esto se logra dentro de un cierto rango de valores para $w_1$ y $w_2$.

Pregunta: ¿Puedes leer qué valores de $w_1$ y $w_2$ te darán la menor pérdida?

¿Son estos valores similares a los que encontró anteriormente para separar linealmente los datos?

**Pregunta**: Dependiendo del punto central que elijas, el valor más pequeño para la pérdida puede no ser 0. ¿En qué circunstancias es este el caso?

##Optimizando la pérdida usando TensorFlow

Ahora que tenemos una función que nos da la pérdida para diferentes valores de $ w_1 $ y $ w_2 $, queremos un método automatizado para encontrar los valores que minimicen la función de pérdida. Aquí es donde entra en juego la optimización por **descenso de gradiente**. La idea es que para cada (batch of) puntos de datos, calculemos la pérdida utilizando los valores actuales de $ w_1 $ y $ w_2 $ en los datos. Luego calculamos el **gradiente** (o derivado) de la función de pérdida en los valores actuales de $ w_1 $ y $ w_2 $. El negativo del gradiente apunta en la dirección del *steepest descent* a lo largo de la función de pérdida. Al ajustar los valores de $ w_1 $ y $ w_2 $ en la dirección del gradiente negativo, nos acercamos al mínimo de la función de pérdida (siempre que la función de pérdida tenga un "buen comportamiento"). El gran paso que damos está mediado por la **tasa de aprendizaje**. Para hacer esto más fácilmente, usaremos TensorFlow.

##Optional extra reading: TensorFlow

*TensorFlow* (TF) es una biblioteca de software de código abierto para el cálculo numérico que utiliza el concepto de tensores. Puedes pensar en los tensores como una generalización de matrices a dimensiones superiores, o más o menos equivalentes a matrices multidimensionales. Los escalares son tensores de 0 dimensiones, los vectores son de 1 dimensión, las matrices estándar son de 2 dimensiones y los tensores de dimensiones superiores tienen 3 o más dimensiones. Puedes pensar que las dimensiones representan grupos de números que significan lo mismo. Por ejemplo, para las imágenes, a menudo utilizamos tensores tridimensionales donde la primera dimensión representa los canales de color rojo, verde y azul de la imagen, y los dos siguientes son las columnas y filas de píxeles de la imagen.

**Nota:** No te confundas cuando las personas digan "vector 2-D" o "vector 3-D", que se refiere a un tensor unidimensional que tiene tamaño 2 o 3.

La principal ventaja de usar TensorFlow es que puede derivar automáticamente los gradientes de muchas expresiones matemáticas que involucran tensores. Lo logra a través de un proceso llamado "diferenciación automática". Tensorflow también admite múltiples "núcleos", lo que le permite ejecutar fácilmente su código en procesadores normales (CPU), tarjetas gráficas (GPU) y otros aceleradores de hardware más exóticos como las Unidades de procesamiento de tensor (TPU) de Google

Tensorflow en realidad proporciona dos modos de operación, el primero, llamado "modo gráfico", crea un gráfico de cálculo por adelantado y luego introduce datos en el gráfico. Al construir el gráfico por adelantado, Tensorflow puede aplicar optimizaciones al gráfico que le permiten extraer el máximo rendimiento del hardware que está ejecutando. El segundo modo, llamado "Eager-mode", es mucho más nuevo y evalúa las operaciones de Tensor de manera imperativa (en el orden en que las escribe), similar a NumPy y PyTorch. Eager-mode es un poco menos eficiente pero mucho más intuitivo, especialmente si nunca antes ha usado un estilo de programación "definir y ejecutar" (como el modo gráfico).

### Usando Tensorflow para optimizar la pérdida
Utilizamos TensorFlow para optimizar los parámetros del modelo con descenso de gradiente. Recorremos el conjunto de datos varias veces (llamadas "epochs") y trazamos el límite de decisión final junto con un diagrama que muestra cómo los parámetros y la pérdida cambiaron durante las épocas.

**Nota**: TensorFlow probablemente es excesivo para este ejemplo, porque el gradiente es muy fácil de calcular.

```
lr = 0.33  # The learning rate

# Initialise Tensorflow variables representing our parameters.
# We need to use TensorFlow variables here rather than Numpy or Python ones so
# that TensorFlow is able to compute gradients.
w1 = tf.Variable(-2.0, dtype=tf.float64)
w2 = tf.Variable(-4.0,  dtype=tf.float64)
print(w1.dtype)

plot_contours()

# Loop over the dataset multiple times
parameter_values = []
for epoch in range(100):
  plt.scatter(w1.numpy(), w2.numpy(), marker='o', color='black')

  # The GradientTape is how TF keeps track of gradients in Eager-mode
  with tf.GradientTape() as tape:
    loss = compute_loss(w1, w2, inputs, labels)

  # Now we take a step in parameter space in the direction of the gradient to move the parameters closer (hopefully!) to their optimum
  dw1, dw2 = tape.gradient(loss, [w1, w2])

  # Step 'lr units' in the direction of the negative gradient
  # We achieve this by subtracting lr * dw1 and lr * dw2 from the w1 and w2 variables
  w1.assign_sub(lr*dw1)
  w2.assign_sub(lr*dw2)

print('Finished optimisation, the final values of w1 and w2 are:')
print(w1.numpy(), w2.numpy(), loss)

# Plot the final point on the loss surface.
plt.scatter(w1.numpy(), w2.numpy(), marker='x', color='red')
plt.show()

# Plot the final decision boundary
plot_dataset(inputs, labels)
ax = plt.axes()
ax.arrow(0, 0, w1.numpy(), w2.numpy(), head_width=0.3, head_length=0.3, fc='r', ec='r')
plt.plot([-2 * w2.numpy(), 2 * w2.numpy()], [2 * w1.numpy(), -2 * w1.numpy()], 'k-')

plt.xlim([-4, 4])
plt.ylim([-4, 4])

plt.show()
```

¿Puedes ver cómo el modelo atraviesa el diagrama de contorno hasta que minimiza la pérdida?

¿Cómo se corresponden los valores finales de $w_1$ y $w_2$ encontrados por Tensorflow con los valores que encontró manualmente? Si no lo son, ¿puedes explicar por qué?

## Tareas opcionales
Si has trabajado en esta práctica, has respondido todas las preguntas y sientes que comprendes bien lo que está sucediendo, intenta las siguientes tareas:

1. Agregue un parámetro **sesgo** a la ecuación para el límite de decisión y visualiza cómo eso cambia el límite de decisión, la pérdida y la solución final encontrada por Tensorflow.
2. Agregue un **regularizador**, por ejemplo, el regularizador L2 (consulta el apéndice a continuación para obtener más información): ¿cómo afecta el diagrama de contorno de los parámetros frente a la pérdida? ¿Cómo afecta la pérdida el cambio en la fuerza de la regularización?

# Appendix

Dos de los métodos de regularización más simples son la regularización L1 y L2 (o mejor conocidos como _Lasso_ y _Ridge_ regresión si has utilizado la regresión lineal antes). Ambos métodos regularizan el modelo agregando un término a la pérdida que penaliza el modelo si se vuelve demasiado complejo.
La regularización L1 agrega un término basado en la norma L1:

$loss_{L1} = loss + \lambda \sum_i |w_i|$

donde $\lambda$ es un parámetro que controla la cantidad de regularización, y $w_i$ son los parámetros del modelo. La regularización de L1 tiene el efecto de obligar a algunos parámetros a reducirse a 0, eliminándolos efectivamente del modelo.

La regularización L2 agrega de manera similar un término basado en la norma L2:

$loss_{L2} = loss + \lambda \sum_i w_i^2$.

La regularización de L2 tiene el efecto de evitar que cualquiera de los parámetros se vuelva demasiado grande y sobrecargar a los demás.

En algunos casos, puede funcionar bien usar tanto la regularización L1 como la L2.

Para obtener más información, (http://enhancedatascience.com/2017/07/04/machine-learning-explained-regularization/) y  (https://towardsdatascience.com/l1-and- l2-regularization-method-ce25e7fc831c).

```

```


**This notebook is an exercise in the [Computer Vision](https://www.kaggle.com/learn/computer-vision) course.  You can reference the tutorial at [this link](https://www.kaggle.com/ryanholbrook/convolution-and-relu).**

---

# Introduction #

In this exercise, you'll work on building some intuition around feature extraction. First, we'll walk through the example we did in the tutorial again, but this time, with a kernel you choose yourself. We've mostly been working with images in this course, but what's behind all of the operations we're learning about is mathematics. So, we'll also take a look at how these feature maps can be represented instead as arrays of numbers and what effect convolution with a kernel will have on them.

Run the cell below to get started!

```python
# Setup feedback system
from learntools.core import binder
binder.bind(globals())
from learntools.computer_vision.ex2 import *

import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt

plt.rc('figure', autolayout=True)
plt.rc('axes', labelweight='bold', labelsize='large',
       titleweight='bold', titlesize=18, titlepad=10)
plt.rc('image', cmap='magma')

tf.config.run_functions_eagerly(True)
```

# Apply Transformations #

The next few exercises walk through feature extraction just like the example in the tutorial. Run the following cell to load an image we'll use for the next few exercises.

```python
image_path = '../input/computer-vision-resources/car_illus.jpg'
image = tf.io.read_file(image_path)
image = tf.io.decode_jpeg(image, channels=1)
image = tf.image.resize(image, size=[400, 400])

img = tf.squeeze(image).numpy()
plt.figure(figsize=(6, 6))
plt.imshow(img, cmap='gray')
plt.axis('off')
plt.show();
```

You can run this cell to see some standard kernels used in image processing.

```python
import learntools.computer_vision.visiontools as visiontools
from learntools.computer_vision.visiontools import edge, bottom_sobel, emboss, sharpen

kernels = [edge, bottom_sobel, emboss, sharpen]
names = ["Edge Detect", "Bottom Sobel", "Emboss", "Sharpen"]

plt.figure(figsize=(12, 12))
for i, (kernel, name) in enumerate(zip(kernels, names)):
    plt.subplot(1, 4, i+1)
    visiontools.show_kernel(kernel)
    plt.title(name)
plt.tight_layout()
```

# 1) Define Kernel #

Use the next code cell to define a kernel. You have your choice of what kind of kernel to apply. One thing to keep in mind is that the *sum* of the numbers in the kernel determines how bright the final image is. Generally, you should try to keep the sum of the numbers between 0 and 1 (though that's not required for a correct answer).

In general, a kernel can have any number of rows and columns. For this exercise, let's use a $3 \times 3$ kernel, which often gives the best results. Define a kernel with `tf.constant`.

```python
# YOUR CODE HERE: Define a kernel with 3 rows and 3 columns.
kernel = tf.constant([
    [-2, -1, 0],
    [-1, 1, 1],
    [0,1,2],
])
# Uncomment to view kernel
visiontools.show_kernel(kernel)

# Check your answer
q_1.check()
```

```python
# Lines below will give you a hint or solution code
#q_1.hint()
#q_1.solution()
```

Now we'll do the first step of feature extraction, the filtering step. First run this cell to do some reformatting for TensorFlow.

```python
# Reformat for batch compatibility.
image = tf.image.convert_image_dtype(image, dtype=tf.float32)
image = tf.expand_dims(image, axis=0)
kernel = tf.reshape(kernel, [*kernel.shape, 1, 1])
kernel = tf.cast(kernel, dtype=tf.float32)
```

# 2) Apply Convolution #

Now we'll apply the kernel to the image by a convolution. The *layer* in Keras that does this is `layers.Conv2D`. What is the *backend function* in TensorFlow that performs the same operation?

```python
# YOUR CODE HERE: Give the TensorFlow convolution function (without arguments)
conv_fn = tf.nn.conv2d

# Check your answer
q_2.check()
```

    <IPython.core.display.Javascript object>

Correct

```python
# Lines below will give you a hint or solution code
#q_2.hint()
#q_2.solution()
```

Once you've got the correct answer, run this next cell to execute the convolution and see the result!

```python
image_filter = conv_fn(
    input=image,
    filters=kernel,
    strides=1, # or (1, 1)
    padding='SAME',
)

plt.imshow(
    # Reformat for plotting
    tf.squeeze(image_filter)
)
plt.axis('off')
plt.show();
```

Can you see how the kernel you chose relates to the feature map it produced?

# 3) Apply ReLU #

Now detect the feature with the ReLU function. In Keras, you'll usually use this as the activation function in a `Conv2D` layer. What is the *backend function* in TensorFlow that does the same thing?

```python
# YOUR CODE HERE: Give the TensorFlow ReLU function (without arguments)
relu_fn = tf.nn.relu

# Check your answer
q_3.check()
```

    <IPython.core.display.Javascript object>

Correct

```python
# Lines below will give you a hint or solution code
#q_3.hint()
#q_3.solution()
```

Once you've got the solution, run this cell to detect the feature with ReLU and see the result!

The image you see below is the feature map produced by the kernel you chose. If you like, experiment with some of the other suggested kernels above, or, try to invent one that will extract a certain kind of feature.

```python
image_detect = relu_fn(image_filter)

plt.imshow(
    # Reformat for plotting
    tf.squeeze(image_detect)
)
plt.axis('off')
plt.show();
```

In the tutorial, our discussion of kernels and feature maps was mainly visual. We saw the effect of `Conv2D` and `ReLU` by observing how they transformed some example images.

But the operations in a convolutional network (like in all neural networks) are usually defined through mathematical functions, through a computation on numbers. In the next exercise, we'll take a moment to explore this point of view.

Let's start by defining a simple array to act as an image, and another array to act as the kernel. Run the following cell to see these arrays.

```python
# Sympy is a python library for symbolic mathematics. It has a nice
# pretty printer for matrices, which is all we'll use it for.
import sympy
sympy.init_printing()
from IPython.display import display

image = np.array([
    [0, 1, 0, 0, 0, 0],
    [0, 1, 0, 0, 0, 0],
    [0, 1, 0, 0, 0, 0],
    [0, 1, 0, 0, 0, 0],
    [0, 1, 0, 1, 1, 1],
    [0, 1, 0, 0, 0, 0],
])

kernel = np.array([
    [1, -1],
    [1, -1],
])

display(sympy.Matrix(image))
display(sympy.Matrix(kernel))
# Reformat for Tensorflow
image = tf.cast(image, dtype=tf.float32)
image = tf.reshape(image, [1, *image.shape, 1])
kernel = tf.reshape(kernel, [*kernel.shape, 1, 1])
kernel = tf.cast(kernel, dtype=tf.float32)
```

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

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

# 4) Observe Convolution on a Numerical Matrix #

What do you see? The image is simply a long vertical line on the left and a short horizontal line on the lower right. What about the kernel? What effect do you think it will have on this image? After you've thought about it, run the next cell for the answer.

```python
# View the solution (Run this code cell to receive credit!)
q_4.check()
```

    <IPython.core.display.Javascript object>

Correct:

In the tutorial, we talked about how the pattern of positive numbers will tell you the kind of features the kernel will extract. This kernel has a vertical column of 1's, and so we would expect it to return features of vertical lines.

Now let's try it out. Run the next cell to apply convolution and ReLU to the image and display the result.

```python
image_filter = tf.nn.conv2d(
    input=image,
    filters=kernel,
    strides=1,
    padding='VALID',
)
image_detect = tf.nn.relu(image_filter)

# The first matrix is the image after convolution, and the second is
# the image after ReLU.
display(sympy.Matrix(tf.squeeze(image_filter).numpy()))
display(sympy.Matrix(tf.squeeze(image_detect).numpy()))
```

$\displaystyle \left[\begin{matrix}-2.0 & 2.0 & 0 & 0 & 0\\-2.0 & 2.0 & 0 & 0 & 0\\-2.0 & 2.0 & 0 & 0 & 0\\-2.0 & 2.0 & -1.0 & 0 & 0\\-2.0 & 2.0 & -1.0 & 0 & 0\end{matrix}\right]$

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

Is the result what you expected?

# Conclusion #

In this lesson, you learned about the first two operations a convolutional classifier uses for feature extraction: **filtering** an image with a **convolution** and **detecting** the feature with the **rectified linear unit**.

# Keep Going #

Move on to [**Lesson 3**](https://www.kaggle.com/ryanholbrook/maximum-pooling) to learn the final operation: **condensing** the feature map with **maximum pooling**!

---

*Have questions or comments? Visit the [course discussion forum](https://www.kaggle.com/learn/computer-vision/discussion) to chat with other learners.*


```python
import math
from sympy import *
init_printing(use_latex='mathjax')
```

# Definitions and Functions

```python
## Define symbols
x, y, z = symbols('mu gamma psi')

### NOTE: THE CODE BELOW IS NOT BEING USED IN THE FINAL EXAMPLE ###
cx, sx = symbols('cos(x) sin(x)')
cy, sy = symbols('cos(y) sin(y)')
cz, sz = symbols('cos(z) sin(z)')

## Elementary rotation matrices:

#C1
Rx = Matrix([
    [1, 0, 0],
    [0, cx, sx],
    [0, -sx, cx]])
#C2
Ry = Matrix([
    [cy, 0, -sy],
    [0,  1, 0],
    [sy, 0, cy]])
#C3
Rz = Matrix([
    [cz, sz, 0],
    [-sz,  cz, 0],
    [0,    0, 1]])

```

```python
## Elementary rotation matrices Functions:

def C1(angle):
    x = symbols('x')
    Rx = Matrix([
        [1, 0, 0],
        [0, cos(x), sin(x)],
        [0, -sin(x), cos(x)]])
    return Rx.subs(x, angle)

def C2(angle):
    y = symbols('y')
    Ry = Matrix([
        [cos(y), 0, -sin(y)],
        [0,  1, 0],
        [sin(y), 0, cos(y)]])
    return Ry.subs(y, angle)

def C3(angle):
    z = symbols('z')
    Rz = Matrix([
        [cos(z), sin(z), 0],
        [-sin(z),  cos(z), 0],
        [0,    0, 1]])
    return Rz.subs(z, angle)
```

```python
class IJKReferenceFrame(ReferenceFrame):
    def __init__(self, name):
        super().__init__(name, latexs=['\mathbf{%s}_{%s}' % (
            idx, name) for idx in ("i", "j", "k")])
        self.i = self.x
        self.j = self.y
        self.k = self.z
```

# Examples
We can compute the matrix Rzyx by multiplying matrixes corresponding to each consecutive rotation, e.g.
Rzyx(θx, θy, θz) = Rz(θz)∗Ry(θy)∗Rx(θx)

In this file, we will use the SymPy to compute algebraic expressions for euler angle matrices. Using these expressions, we will be able to derive formulas for converting from matrices to euler angles.

x = spin angle ($\mu$)

y = nutation angle ($\gamma$)

z = precession angle ($\psi$)

```python
# 3-1-3 Euler angles rotation matrices
# Ctot(x,y,z) = C3(x) * C1(y) * C3(z)

C3_x = C3(x)
C1_y = C1(y)
C3_z = C3(z)

R_zxz = C3_x * C1_y * C3_z
R_zxz

```

$\displaystyle \left[\begin{matrix}- \sin{\left(\mu \right)} \sin{\left(\psi \right)} \cos{\left(\gamma \right)} + \cos{\left(\mu \right)} \cos{\left(\psi \right)} & \sin{\left(\mu \right)} \cos{\left(\gamma \right)} \cos{\left(\psi \right)} + \sin{\left(\psi \right)} \cos{\left(\mu \right)} & \sin{\left(\gamma \right)} \sin{\left(\mu \right)}\\- \sin{\left(\mu \right)} \cos{\left(\psi \right)} - \sin{\left(\psi \right)} \cos{\left(\gamma \right)} \cos{\left(\mu \right)} & - \sin{\left(\mu \right)} \sin{\left(\psi \right)} + \cos{\left(\gamma \right)} \cos{\left(\mu \right)} \cos{\left(\psi \right)} & \sin{\left(\gamma \right)} \cos{\left(\mu \right)}\\\sin{\left(\gamma \right)} \sin{\left(\psi \right)} & - \sin{\left(\gamma \right)} \cos{\left(\psi \right)} & \cos{\left(\gamma \right)}\end{matrix}\right]$

```python
# 3-2-1 Euler angles rotation matrices
# Ctot(x,y,z) = C1(x) * C2(y) * C3(z)

C1_x = C1(x)
C2_y = C2(y)
C3_z = C3(z)

R_zyx = C1_x * C2_y * C3_z
R_zyx

```

$\displaystyle \left[\begin{matrix}\cos{\left(\gamma \right)} \cos{\left(\psi \right)} & \sin{\left(\psi \right)} \cos{\left(\gamma \right)} & - \sin{\left(\gamma \right)}\\\sin{\left(\gamma \right)} \sin{\left(\mu \right)} \cos{\left(\psi \right)} - \sin{\left(\psi \right)} \cos{\left(\mu \right)} & \sin{\left(\gamma \right)} \sin{\left(\mu \right)} \sin{\left(\psi \right)} + \cos{\left(\mu \right)} \cos{\left(\psi \right)} & \sin{\left(\mu \right)} \cos{\left(\gamma \right)}\\\sin{\left(\gamma \right)} \cos{\left(\mu \right)} \cos{\left(\psi \right)} + \sin{\left(\mu \right)} \sin{\left(\psi \right)} & \sin{\left(\gamma \right)} \sin{\left(\psi \right)} \cos{\left(\mu \right)} - \sin{\left(\mu \right)} \cos{\left(\psi \right)} & \cos{\left(\gamma \right)} \cos{\left(\mu \right)}\end{matrix}\right]$

```python
# 3-2-3 Euler angles rotation matrices

# Ctot(x,y,z) = C3(x) * C2(y) * C3(z)

C3_x = C3(x)
C2_y = C2(y)
C3_z = C3(z)

R_zyz = C3_z * C2_y * C3_x
R_zyz

```

$\displaystyle \left[\begin{matrix}- \sin{\left(\mu \right)} \sin{\left(\psi \right)} + \cos{\left(\gamma \right)} \cos{\left(\mu \right)} \cos{\left(\psi \right)} & \sin{\left(\mu \right)} \cos{\left(\gamma \right)} \cos{\left(\psi \right)} + \sin{\left(\psi \right)} \cos{\left(\mu \right)} & - \sin{\left(\gamma \right)} \cos{\left(\psi \right)}\\- \sin{\left(\mu \right)} \cos{\left(\psi \right)} - \sin{\left(\psi \right)} \cos{\left(\gamma \right)} \cos{\left(\mu \right)} & - \sin{\left(\mu \right)} \sin{\left(\psi \right)} \cos{\left(\gamma \right)} + \cos{\left(\mu \right)} \cos{\left(\psi \right)} & \sin{\left(\gamma \right)} \sin{\left(\psi \right)}\\\sin{\left(\gamma \right)} \cos{\left(\mu \right)} & \sin{\left(\gamma \right)} \sin{\left(\mu \right)} & \cos{\left(\gamma \right)}\end{matrix}\right]$

### Using the physics.mechanics library
I will now compare **two methods of obtaining the same rotation matrix**.

One uses the .orient method implemented in the sympy.physics.mechanics and the other the functions stated on the beginning of this notebook.

```python
from sympy.physics.mechanics import *
```

```python
# First let's define the dynamic symbols, as these are a function of time
x, y, z = dynamicsymbols('mu gamma psi')
```

#### As done earlier:

```python
# (-2)-3-1 Euler angle rotation matrix

C3_x = C3(z)
C2_y = C2(-y)
C1_x = C1(x)

R_y_neg_zx = C1_x*C3_x*C2_y
R_y_neg_zx

```

$\displaystyle \left[\begin{matrix}\cos{\left(\gamma{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)} & \sin{\left(\psi{\left(t \right)} \right)} & \sin{\left(\gamma{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)}\\- \sin{\left(\gamma{\left(t \right)} \right)} \sin{\left(\mu{\left(t \right)} \right)} - \sin{\left(\psi{\left(t \right)} \right)} \cos{\left(\gamma{\left(t \right)} \right)} \cos{\left(\mu{\left(t \right)} \right)} & \cos{\left(\mu{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)} & - \sin{\left(\gamma{\left(t \right)} \right)} \sin{\left(\psi{\left(t \right)} \right)} \cos{\left(\mu{\left(t \right)} \right)} + \sin{\left(\mu{\left(t \right)} \right)} \cos{\left(\gamma{\left(t \right)} \right)}\\- \sin{\left(\gamma{\left(t \right)} \right)} \cos{\left(\mu{\left(t \right)} \right)} + \sin{\left(\mu{\left(t \right)} \right)} \sin{\left(\psi{\left(t \right)} \right)} \cos{\left(\gamma{\left(t \right)} \right)} & - \sin{\left(\mu{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)} & \sin{\left(\gamma{\left(t \right)} \right)} \sin{\left(\mu{\left(t \right)} \right)} \sin{\left(\psi{\left(t \right)} \right)} + \cos{\left(\gamma{\left(t \right)} \right)} \cos{\left(\mu{\left(t \right)} \right)}\end{matrix}\right]$

```python
# 3-1 Euler angle rotation matrix

C1_x*C3_x
```

$\displaystyle \left[\begin{matrix}\cos{\left(\psi{\left(t \right)} \right)} & \sin{\left(\psi{\left(t \right)} \right)} & 0\\- \sin{\left(\psi{\left(t \right)} \right)} \cos{\left(\mu{\left(t \right)} \right)} & \cos{\left(\mu{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)} & \sin{\left(\mu{\left(t \right)} \right)}\\\sin{\left(\mu{\left(t \right)} \right)} \sin{\left(\psi{\left(t \right)} \right)} & - \sin{\left(\mu{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)} & \cos{\left(\mu{\left(t \right)} \right)}\end{matrix}\right]$

#### Using built-in functions

```python
A = IJKReferenceFrame("A")
```

```python
A1 = IJKReferenceFrame("A1")
psi = dynamicsymbols('psi')
A1.orient(A, 'Axis', [psi, A.z])
A1.dcm(A)  # T_{A1A}
```

$\displaystyle \left[\begin{matrix}\cos{\left(\psi{\left(t \right)} \right)} & \sin{\left(\psi{\left(t \right)} \right)} & 0\\- \sin{\left(\psi{\left(t \right)} \right)} & \cos{\left(\psi{\left(t \right)} \right)} & 0\\0 & 0 & 1\end{matrix}\right]$

```python
A2 = IJKReferenceFrame("A2")
gamma = dynamicsymbols('gamma')
A2.orient(A1, 'Axis', [gamma, -A1.y])
A2.dcm(A1)  # T_{A2A1}

```

$\displaystyle \left[\begin{matrix}\cos{\left(\gamma{\left(t \right)} \right)} & 0 & \sin{\left(\gamma{\left(t \right)} \right)}\\0 & 1 & 0\\- \sin{\left(\gamma{\left(t \right)} \right)} & 0 & \cos{\left(\gamma{\left(t \right)} \right)}\end{matrix}\right]$

```python
A3 = IJKReferenceFrame("A3")
#zeta = dynamicsymbols('zeta')
A3.orient(A2, 'Axis', [psi, A2.z])
A3.dcm(A1)  # T_{A3A1}
```

$\displaystyle \left[\begin{matrix}\cos{\left(\gamma{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)} & \sin{\left(\psi{\left(t \right)} \right)} & \sin{\left(\gamma{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)}\\- \sin{\left(\psi{\left(t \right)} \right)} \cos{\left(\gamma{\left(t \right)} \right)} & \cos{\left(\psi{\left(t \right)} \right)} & - \sin{\left(\gamma{\left(t \right)} \right)} \sin{\left(\psi{\left(t \right)} \right)}\\- \sin{\left(\gamma{\left(t \right)} \right)} & 0 & \cos{\left(\gamma{\left(t \right)} \right)}\end{matrix}\right]$

```python
B = IJKReferenceFrame("B")
mu = dynamicsymbols('mu')
B.orient(A3, 'Axis', [mu, A3.x])
B.dcm(A3)  # T_{BA3}
```

$\displaystyle \left[\begin{matrix}1 & 0 & 0\\0 & \cos{\left(\mu{\left(t \right)} \right)} & \sin{\left(\mu{\left(t \right)} \right)}\\0 & - \sin{\left(\mu{\left(t \right)} \right)} & \cos{\left(\mu{\left(t \right)} \right)}\end{matrix}\right]$

```python
B.dcm(A2)
```

$\displaystyle \left[\begin{matrix}\cos{\left(\psi{\left(t \right)} \right)} & \sin{\left(\psi{\left(t \right)} \right)} & 0\\- \sin{\left(\psi{\left(t \right)} \right)} \cos{\left(\mu{\left(t \right)} \right)} & \cos{\left(\mu{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)} & \sin{\left(\mu{\left(t \right)} \right)}\\\sin{\left(\mu{\left(t \right)} \right)} \sin{\left(\psi{\left(t \right)} \right)} & - \sin{\left(\mu{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)} & \cos{\left(\mu{\left(t \right)} \right)}\end{matrix}\right]$

```python
B.dcm(A1)
```

$\displaystyle \left[\begin{matrix}\cos{\left(\gamma{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)} & \sin{\left(\psi{\left(t \right)} \right)} & \sin{\left(\gamma{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)}\\- \sin{\left(\gamma{\left(t \right)} \right)} \sin{\left(\mu{\left(t \right)} \right)} - \sin{\left(\psi{\left(t \right)} \right)} \cos{\left(\gamma{\left(t \right)} \right)} \cos{\left(\mu{\left(t \right)} \right)} & \cos{\left(\mu{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)} & - \sin{\left(\gamma{\left(t \right)} \right)} \sin{\left(\psi{\left(t \right)} \right)} \cos{\left(\mu{\left(t \right)} \right)} + \sin{\left(\mu{\left(t \right)} \right)} \cos{\left(\gamma{\left(t \right)} \right)}\\- \sin{\left(\gamma{\left(t \right)} \right)} \cos{\left(\mu{\left(t \right)} \right)} + \sin{\left(\mu{\left(t \right)} \right)} \sin{\left(\psi{\left(t \right)} \right)} \cos{\left(\gamma{\left(t \right)} \right)} & - \sin{\left(\mu{\left(t \right)} \right)} \cos{\left(\psi{\left(t \right)} \right)} & \sin{\left(\gamma{\left(t \right)} \right)} \sin{\left(\mu{\left(t \right)} \right)} \sin{\left(\psi{\left(t \right)} \right)} + \cos{\left(\gamma{\left(t \right)} \right)} \cos{\left(\mu{\left(t \right)} \right)}\end{matrix}\right]$

# Other interesting methods

### DCM

Examples from: https://docs.sympy.org/latest/modules/physics/vector/api/classes.html

```python
# Define the reference frames
N = ReferenceFrame('N')
q1 = symbols('q1')
# orientnew = Returns a new reference frame oriented with respect to this reference frame.
A = N.orientnew('A', 'Axis', (q1, N.x))

# DCM between A and N reference frames
A.dcm(N), N.dcm(A)

```

$\displaystyle \left( \left[\begin{matrix}1 & 0 & 0\\0 & \cos{\left(q_{1} \right)} & \sin{\left(q_{1} \right)}\\0 & - \sin{\left(q_{1} \right)} & \cos{\left(q_{1} \right)}\end{matrix}\right], \  \left[\begin{matrix}1 & 0 & 0\\0 & \cos{\left(q_{1} \right)} & - \sin{\left(q_{1} \right)}\\0 & \sin{\left(q_{1} \right)} & \cos{\left(q_{1} \right)}\end{matrix}\right]\right)$

```python
q1 = symbols('q1')
N = ReferenceFrame('N')
B = ReferenceFrame('B')

B.orient_axis(N, N.x, q1)

# The orient_axis() method generates a direction cosine matrix and its transpose which
# defines the orientation of B relative to N and vice versa. Once orient is called,
# dcm() outputs the appropriate direction cosine matrix:

B.dcm(N), N.dcm(B)
```

$\displaystyle \left( \left[\begin{matrix}1 & 0 & 0\\0 & \cos{\left(q_{1} \right)} & \sin{\left(q_{1} \right)}\\0 & - \sin{\left(q_{1} \right)} & \cos{\left(q_{1} \right)}\end{matrix}\right], \  \left[\begin{matrix}1 & 0 & 0\\0 & \cos{\left(q_{1} \right)} & - \sin{\left(q_{1} \right)}\\0 & \sin{\left(q_{1} \right)} & \cos{\left(q_{1} \right)}\end{matrix}\right]\right)$

### Kinematic Equations

```python
from sympy.physics.vector import ReferenceFrame, get_motion_params, dynamicsymbols, init_vprinting

```

Returns the three motion parameters - (acceleration, velocity, and position) as vectorial functions of time in the given frame.

If a higher order differential function is provided, the lower order functions are used as boundary conditions. For example, given the acceleration, the velocity and position parameters are taken as boundary conditions.

The values of time at which the boundary conditions are specified are taken from timevalue1(for position boundary condition) and timevalue2(for velocity boundary condition).

If any of the boundary conditions are not provided, they are taken to be zero by default (zero vectors, in case of vectorial inputs). If the boundary conditions are also functions of time, they are converted to constants by substituting the time values in the dynamicsymbols._t time Symbol.

This function can also be used for calculating rotational motion parameters. Have a look at the Parameters and Examples for more clarity.

```python
R = ReferenceFrame('R')
v1, v2, v3 = dynamicsymbols('v1 v2 v3')
v = v1*R.x + v2*R.y + v3*R.z
get_motion_params(R, position=v)
a, b, c = symbols('a b c')
v = a*R.x + b*R.y + c*R.z
get_motion_params(R, velocity=v)
parameters = get_motion_params(R, acceleration=v)
parameters[1], parameters[2]
```

$\displaystyle \left( a t\mathbf{\hat{r}_x} + b t\mathbf{\hat{r}_y} + c t\mathbf{\hat{r}_z}, \  \frac{a t^{2}}{2}\mathbf{\hat{r}_x} + \frac{b t^{2}}{2}\mathbf{\hat{r}_y} + \frac{c t^{2}}{2}\mathbf{\hat{r}_z}\right)$

Gives equations relating the qdot’s to u’s for a rotation type.

Supply rotation type and order as in orient. Speeds are assumed to be body-fixed; if we are defining the orientation of B in A using by rot_type, the angular velocity of B in A is assumed to be in the form: speed[0]*B.x + speed[1]*B.y + speed[2]*B.z

```python
u1, u2, u3 = dynamicsymbols('u1 u2 u3')
q1, q2, q3 = dynamicsymbols('q1 q2 q3')
k_Eq = kinematic_equations([u1, u2, u3], [q1, q2, q3], 'body', '313')
k_Eq

```

$\displaystyle \left[ - \frac{\operatorname{u_{1}}{\left(t \right)} \sin{\left(\operatorname{q_{3}}{\left(t \right)} \right)} + \operatorname{u_{2}}{\left(t \right)} \cos{\left(\operatorname{q_{3}}{\left(t \right)} \right)}}{\sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)}} + \frac{d}{d t} \operatorname{q_{1}}{\left(t \right)}, \  - \operatorname{u_{1}}{\left(t \right)} \cos{\left(\operatorname{q_{3}}{\left(t \right)} \right)} + \operatorname{u_{2}}{\left(t \right)} \sin{\left(\operatorname{q_{3}}{\left(t \right)} \right)} + \frac{d}{d t} \operatorname{q_{2}}{\left(t \right)}, \  \frac{\left(\operatorname{u_{1}}{\left(t \right)} \sin{\left(\operatorname{q_{3}}{\left(t \right)} \right)} + \operatorname{u_{2}}{\left(t \right)} \cos{\left(\operatorname{q_{3}}{\left(t \right)} \right)}\right) \cos{\left(\operatorname{q_{2}}{\left(t \right)} \right)}}{\sin{\left(\operatorname{q_{2}}{\left(t \right)} \right)}} - \operatorname{u_{3}}{\left(t \right)} + \frac{d}{d t} \operatorname{q_{3}}{\left(t \right)}\right]$

```python
u1, u2, u3 = symbols('mu gamma psi')
q1, q2, q3 = symbols('\dot{\mu} \dot{\gamma} \dot{\psi}')

K_eq = Matrix(kinematic_equations([q1, q2, q3], [u1, u2, u3], 'body', '323'))
K_eq
# Check this site http://man.hubwiz.com/docset/SymPy.docset/Contents/Resources/Documents/_modules/sympy/physics/vector/functions.html

```

$\displaystyle \left[\begin{matrix}- \frac{\dot{\gamma} \sin{\left(\psi \right)} - \dot{\mu} \cos{\left(\psi \right)}}{\sin{\left(\gamma \right)}}\\- \dot{\gamma} \cos{\left(\psi \right)} - \dot{\mu} \sin{\left(\psi \right)}\\- \dot{\psi} - \frac{\left(- \dot{\gamma} \sin{\left(\psi \right)} + \dot{\mu} \cos{\left(\psi \right)}\right) \cos{\left(\gamma \right)}}{\sin{\left(\gamma \right)}}\end{matrix}\right]$


The chain rule is a powerful and useful derivation technique that allows the derivation of functions that would not be straightforward or possible with the only the previously discussed rules at our disposal. The rule takes advantage of the "compositeness" of a function. For example, consider the function:

$$ f(x) = \sin{4x} $$

This function can be broken into a composite function $f \circ g$ by observing that:

$$ y = f(u) = \sin{u} \space \text{and} \space u = g(x) = 4x $$

Therefore we can reinterpret the original function as:

$$ F(x) = f(g(x)), \space \text{or} \space F = f \circ g $$

The chain rule gives us the ability the find the derivatives of $f$ and $g$ using the tools we previously discussed. We can state the chain rule more precisely as:

Assuming $g$ is differentiable at $x$ and the derivative of $f(g(x))$ exists, then we can state the composite function $F = f \circ g$ as $F(x) = f(g(x))$ and $F^\prime$ is given by:

$$ F^\prime (x) = f^\prime (g(x)) \circ g^\prime(x) $$

In Leibniz notation, assuming $y = f(u)$ and $u = g(x)$ have derivatives, the chain rule can be expressed as:

$$ \frac{dy}{dx} = \frac{dy}{du} \space \frac{du}{dx} $$

With the chain rule, we can now find the derivative of the function $f(x) = \sin{4x}$.

$$ u = 4x, \qquad y = \sin{u} $$

$$ \frac{du}{dx} = 4, \qquad \frac{dy}{du} = \cos{u} $$

$$ \frac{dy}{dx} = 4 \cos{4x} $$

We can check our answer using [SymPy](http://www.sympy.org/en/index.html)

```python
from sympy import symbols, diff, sin, cos, sqrt, simplify, init_printing
```

```python
init_printing()
x = symbols('x')
```

```python
diff(sin(4 * x))
```

## Examples

These and the previous example are taken from James Stewart's *Calculus Early Transcendentals* (Section 3.4, pp. 203)

Example 1: Find the derivative of $f(x) = (1 - x^2)^{10}$

$$ u = 1 - x^2, \qquad y = u^{10} $$

$$ \frac{dy}{du} = 10u^9, \qquad \frac{du}{dx} = -2x $$

$$ \frac{dy}{dx} = 10(1 - x^2)^9 * - 2x = -20x (1 - x^2)^9 $$

```python
diff((1 - x ** 2) ** 10)
```

Example 2: Find the derivative of $f(x) = e^{\sqrt{x}}$

$$ y = e^{\sqrt{u}}, \qquad u = \sqrt{x} $$

$$ \frac{dy}{du} = e^u, \qquad \frac{du}{dx} = \frac{1}{2}x^{-\frac{1}{2}} = \frac{1}{2\sqrt{x}} $$

$$ \frac{dy}{dx} = e^{\sqrt{x}} * \frac{1}{2\sqrt{x}} = \frac{e^{\sqrt{x}}}{2\sqrt{x}} $$

Import the constant `e` from the `mpmath` library for SymPy to calculate the derivative.

```python
from mpmath import e
```

```python
diff(e ** sqrt(x))
```

Example 3: Find the derivative of $f(x) = \sqrt[4]{1 + 2x + x^3}$

$$ u = 1 + 2x + x^3, \qquad y = u^\frac{1}{4} $$

$$ \frac{du}{dx} = 3x^2 + 2, \qquad \frac{dy}{du} = \frac{1}{4} u^{-\frac{3}{4}} =  \frac{3}{4\sqrt{u}} $$

$$ \frac{dy}{dx} = \frac{3x + 2}{4 (1 + 2x + x^3)^\frac{3}{4}} $$

```python
diff((1 + 2 * x + x ** 3) ** (1/4))
```

Example 4: Find the derivative of $f(x) = \frac{1}{(t^4 + 1)^3}$

$$ u = t^4 + 1, \qquad y = \frac{1}{u^3} = u^{-3} $$

$$ \frac{du}{dt} = 4t^3, \qquad \frac{dy}{du} = u^{-3} = -3u^{-4} = -\frac{3}{u^4} $$

$$ \frac{dy}{dx} = 4t^3 * -\frac{3}{(t^4 + 1)^4} = -\frac{12t^3}{(t^4 + 1)^4} $$

```python
diff(1 / (x ** 4 + 1) ** 3)
```

Example 5: Find the derivative of $f(x) = \cos{(a^3 + x^3)}$

$$ u = a^3 + x^3, \qquad y = \cos{u} $$

$$ \frac{du}{dx} = 3x^2, \qquad \frac{dy}{du} = -\sin{u} $$

$$ \frac{dy}{dx} = -3x^2 \sin{a^3 + x^3} $$

```python
a = symbols('a') # define a variable that we'll treat as constant
```

Because there is more than variable, must specify which we're interested in for SymPy to compute the derivative.

```python
diff(cos(a ** 3 + x ** 3), x)
```

## References

[Stewart, J. (2007). Essential calculus: Early transcendentals. Belmont, CA: Thomson Higher Education.](https://amzn.to/38dnRV0)

[Strang, G. (2010). Calculus. Wellesley, MA: Wellesley-Cambridge.](https://amzn.to/2vVY0SZ)


Massimo Nocentini

Abstract

In this document we collect a naive type system based on sets.

```python
from itertools import repeat
from sympy import *
#from type_system import *
```

```python
%run ../../src/commons.py
```

```python
%run ./type-system.py
```

---

```python
init_printing()
```

```python
x,y,m,n,t,z = symbols('x y m n t z', commutative=True)
alpha, beta, gamma, eta = symbols(r'\alpha \beta \gamma \eta', commutative=True)
f,g = Function('f'), Function('g')
```

# lists

```python
lst_of_alpha_gf, = lst(tyvar(alpha)[z]).gf()
lst_of_alpha_gf
```

```python
occupancy(lst_of_alpha_gf, syms=[alpha], objects='unlike', boxes='unlike').series(z)
```

```python
occupancy(lst_of_alpha_gf, syms=[alpha], objects='unlike', boxes='like').series(z)
```

```python
occupancy(lst_of_alpha_gf, syms=[alpha], objects='like', boxes='unlike').series(z)
```

```python
occupancy(lst_of_alpha_gf, syms=[alpha], objects='like', boxes='like').series(z)
```

---

```python
lst_of_maybe_of_alpha_gf, = lst(maybe(truth)[z]).gf()
lst_of_maybe_of_alpha_gf
```

```python
occupancy(lst_of_maybe_of_alpha_gf, syms=[w[1]], objects='unlike', boxes='unlike').series(z, n=5)
```

```python
occupancy(lst_of_maybe_of_alpha_gf, syms=[w[1]], objects='unlike', boxes='like').series(z, n=5)
```

```python
occupancy(lst_of_maybe_of_alpha_gf, syms=[w[1]], objects='like', boxes='unlike').series(z, n=5)
```

```python
occupancy(lst_of_maybe_of_alpha_gf, syms=[w[1]], objects='like', boxes='like').series(z,n=5)
```

---

```python
nats = (lst(truth[z]) * lst(falsehood[z]))
nats_gf, = nats.gf()
nats_gf
```

```python
occupancy(nats_gf, syms=[w[1],w[0]], objects='unlike', boxes='unlike').series(z)
```

```python
occupancy(nats_gf, syms=[w[1],w[0]], objects='unlike', boxes='like').series(z)
```

```python
occupancy(nats_gf, syms=[w[1],w[0]], objects='like', boxes='unlike').series(z)
```

```python
occupancy(nats_gf, syms=[w[1],w[0]], objects='like', boxes='like').series(z)
```

---

```python
lst_of_boolean_gf, = lst(boolean[z]).gf()
lst_of_boolean_gf
```

```python
occupancy(lst_of_boolean_gf, syms=[w[1],w[0]], objects='unlike', boxes='unlike').series(z, n=5)
```

```python
occupancy(lst_of_boolean_gf, syms=[w[1],w[0]], objects='unlike', boxes='like').series(z, n=5)
```

```python
occupancy(lst_of_boolean_gf, syms=[w[1],w[0]], objects='like', boxes='unlike').series(z, n=5)
```

```python
occupancy(lst_of_boolean_gf, syms=[w[1],w[0]], objects='like', boxes='like').series(z, n=5)
```

---

```python
dollar_exchange, = (lst(tyvar(u[1]*x)) * lst(tyvar(u[5]*x**5)) * lst(tyvar(u[10]*x**10)) *
                    lst(tyvar(u[25]*x**25)) * lst(tyvar(u[50]*x**50))).gf()
dollar_exchange
```

```python
coins = dollar_exchange.series(x, n=101)
```

```python
coins.rhs.rhs.coeff(x,42)
```

```python
assert 292 == coins.rhs.rhs.coeff(x,100).subs( # number of ways to change *one* dollar
    {u[1]:1, u[5]:1, u[10]:1, u[25]:1, u[50]:1})
```

# non-empty lists

```python
nnlst_of_alpha_gf, = nnlst(tyvar(alpha*z)).gf()
nnlst_of_alpha_gf
```

```python
nnlst_of_alpha_gf.series(z, n=10)
```

```python
nnlst_gf, = nnlst(nnlst(truth[z])*falsehood).gf()
nnlst_gf
```

```python
occupancy(nnlst_gf, syms=[w[1],w[0]], objects='unlike', boxes='unlike').series(z, n=5)
```

```python
occupancy(nnlst_gf, syms=[w[1],w[0]], objects='unlike', boxes='like').series(z, n=5)
```

---

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.


Solving for Orbits around Black-Holes using Numerical Integrators

## Introduction

Imagine a particle, located at a distance $r$ from a black-hole of mass $M$ (see picture below). In this module, we will explore the motion of this particle as it orbits around the black-hole, and how this motion can be used to test our understanding of gravity! Along the way, we will learn about *numerical integration*, an important tool that will allow us to use computers to solve equations that are too complicated to solve by hand.

We will start with what we know of the laws of gravity (what Newton discovered). Then three basics are needed to understand numerical integration before we apply it to the motion of a particle around a black hole. That has us  exploring numerical integration with respect first to a ball at constant velocity and then to a ball with changing velocity. Then we will explore numerical integration with respect to a ball on a spring. With that information we will be able to see whether Newton's law of gravity (e.g., the gravity we know) will help determine a particle's motion around a black hole or whether the gravitational law of general relativity would be better suited and why.
___

# Table of Contents

* [How to Use This Notebook](#How-to-Use-This-Notebook)
* [Pre-Activity Setup](#Pre-Activity-Setup)
* [Activity 1: Newton's Law of Universal Gravitation](#Activity-1:-Newton's-Law-of-Universal-Gravitation)
* [Activity 2: Intro to Numerical Integration](#Activity-2:-Intro-to-Numerical-Integration)
    * [Part 2.1: To start with](#Part-2.1:-To-start-with)
    * [Part 2.2: Ball with Changing Velocity](#Part-2.2:-Ball-with-Changing-Velocity)
    * [Part 2.3: Ball on a Spring](#Part-2.3:-Ball-on-a-Spring)
* [Activity 3: The Gravity We Know: Newton's Law, Integrated](#Activity-3:-The-Gravity-We-Know:-Newton's-Law,-Integrated)
* [Activity 4: The Gravity Near a Black Hole: Orbits in General Relativity](#Activity-4:-The-Gravity-Near-a-Black-Hole:-Orbits-in-General-Relativity)

___

# How to Use This Notebook

The webpage you are in is actually an app called a Jupyter Notebook — much like the ones on your phone. This app consists of cells.

An *input* cell looks like a light grey box with an `In [ ]:` on its left. Input cells each contain code — instructions to make the computer do something.

To activate or select a cell, click anywhere inside of it.

    Select the cell below and read its contents.

```python
# Text that follows a "#" is a comment.
# Comments do not affect your code in any way.
# Always read the comments at the top of each cell you interact with.
# Comments will be used to describe what the cell's code is actually doing.
```

To execute or run a selected cell, click the little play button or hit `[Shift + Enter]` on your keyboard.

    Select the cell below and read its contents. Then, run the cell.

 If a warning appears, just click "Run Anyway", this code is safe ;)

 Also, if you want to save your progress, click the "Copy to Drive" button at the top.

```python
# Text that DOESN'T follow a "#" is considered code.
# Lines of code are instructions given to your computer.
# The line of code below is a "print" statement.
# A print statement literally prints out the text between its quotes.

print("Congrats! You have successfully run your first cell!")
```

Running a cell creates an *output* directly below it. An output can be some text, a graph, an interactive slider, or even nothing at all! When you have run a cell a number appears between the brackets, for instance [1].

You can learn more about how Jupyter Notebooks work at https://try.jupyter.org/
___

# Pre-Activity Setup

In order for any of the activities to work properly, you must import the libraries needed for the code in this notebook.

    Select and run the cell below.

```python
#Here, you are importing the libraries needed for this notebook.
#These libraries set up the plotting environment in your browser.

import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
from ipywidgets import *

print('Done! You have successfully imported the libraries.')
```

___

# Activity 1: Newton's Law of Universal Gravitation

### The kind of gravity we deal with every day...

The first mathematical theory of gravity was *Newton's law of universal gravitation*, or *Newtonian gravity* for short. Named after its discoverer, Sir Isaac Newton (1643-1727), Newtonian gravity was our most accurate theory of gravity for more than 330 years.

In Newtonian gravity, our particle feels the gravitational acceleration $a$ whose magnitude (or strength) is given by,

\begin{equation}
a(r) = -\frac{G M}{r^2} \; ,
\end{equation}

where $G$ is the Newtonian gravitational constant (i.e. just a fixed number), $M$ is the mass of the black hole and $r$ is the distance between the black hole and the particle. Equations like these are called *equations of motion*, and different theories of gravity prescribe different equations of motions.

In addition to its acceleration, the particle also possesses a velocity, whose magnitude (or strength) is given by $v$, which describes the speed of the particle as it orbits the black-hole. While the acceleration is always pointing towards the black hole, the particle can have a velocity that points in arbitrary directions. What do we mean by that? Let's give an example. While you stand on the Earth, gravity, $a$, is always pulling you down toward the center of the Earth. But you might be running forward with a velocity $v$ on the surface of the Earth. The result is similar to what is happening in the image below.

Given the mass of the black-hole in solar masses (e.g., how many times the mass of our Sun), and the distance $r$ in AU (e.g, how many times the distance between our Earth and the Sun), the following code computes the magnitude of the accleration due to Newtonian gravity. Note that $Msol$ is the mass of our Sun (called Sol).

```python
def Newton(M,r):
    """ Returns the magnitude of the Newtonian acceleration given mass M in Solar Masses and distance r in AU. """
    Msol = 2.0e30       # Mass of the Sun in kilograms (kg)
    AU = 1.5e11         # The "astronomical unit", i.e. the Sun-Earth distance in meters (m)
    G = 6.673e-11       # Gravitational constant [N (m/kg)^2]
    return G*(M*Msol)/(r*AU)**2.

# Please note that Msol and AU are unit conversions in the above equation.
print("You have defined the Newton function. Nice Job!")
```

**NOTE:** Throughout this module, the mathematical functions like the one in the code above (called **Newton**) will be boldfaced.

        Question 1: What is the magnitude of the Newtonian acceleration 1 AU away from Sagittarius A*, the black-hole at the center of the Milky Way Galaxy?

(Remember, 1 AU  or astronomical unit is the distance between the Sun and the Earth.)

Since Sagittarius A\* has 4.3 million times the mass of the Sun, we can plug $ M = 4.3 \times 10^6 $ (also written as 4.3e6) into the **Newton** function:

$$
a(1 AU) = -\frac{G \times 4.3 \times 10^6 Msol}{(1 AU)^2} \;
$$

```python
Newton(4.3e6,1) # The two numbers in the parentheses (M,r) represent mass (M) in units of solar masses and radius (r) in astronomical units (AU).
```

**NOTE:** Although the **Newton** function only returned numbers, we need to remember the units: *meters per second per second*, or $m/s^2$. These are the units of *acceleration* $a$, which is the the change in velocity as time passes.

Question 2a: How much greater is the gravitational acceleration or pull of an object orbiting a black hole at a distance of 1 AU than the acceleration of the Earth due to the Sun (at 1 AU)?

...Here is a couple of calculations to figure it out. First, we can use the **Newton** function, now with M=1:

$$
a(1 AU) = -\frac{G \times 1 Msol}{(1 AU)^2} \;
$$

```python
Newton(1,1)
```

Then, a comparison of two quantities often means taking a ratio or dividing one by the other. Upon taking the ratio of the two equations above, we see that at the same distance, the gravitational acceleration due to Sagittarius A\* is 4.3 million times larger than that due to the Sun.

$$
-\frac{G \times 4.3 \times 10^6 Msol}{(1 AU)^2} \;
$$

divided by

$$
-\frac{G \times 1 Msol}{(1 AU)^2} \;
$$

will give you the ratio of 4.3 $\times$ 10$^6$ solar masses to 1 solar mass or just:

```python
Newton(4.3e6,1)/Newton(1,1)
```

A mathematical short cut is to notice the "like" terms in the top and bottom parts of the ratio can be divided out, which only leaves the number relating to how many solar masses the black hole is.

Question 2b: If you double the mass of the black hole, what happens to the value for the acceleration?

Now, we want to solve for the orbit of our particle around the black hole. While this is doable by hand, we will instead use computers to do this task, using a technique called *numerical integration*. The importance of numerical integration cannot be overstated. This technique is ubiquitous (e.g., used everywhere) in the physics literature as professional physicists often have to work with equations that are too complex to be solved by pen-and-paper.
___

# Activity 2: Intro to Numerical Integration

## Part 2.1: To start with

Let's consider a much simpler problem: a ball starts at position $x=0$, and possess a constant velocity $v$.

        Question 3: What is the distance travelled by the ball at time $\tau$ later?

We can solve this by performing the multiplication,

$$
x(\tau) = v\tau \; .
$$

We can easily write this up in code:

```python
def ConstantBall(v,tau):
    ''' Integrates the motion of a ball moving with constant velocity v for time tau'''

    return v*tau

print("You have defined the ConstantBall function. Nice Job!")
```

Try it out, with $v$=1 and $\tau$=1 or other small values. Enter the numbers in the next cell.

**NOTE:** Physicists when creating computer programs, often leave numbers unitless in their calculations. As long all of the parameters of the equation use the same units on both sides of the equal sign, that is considered the norm. So for instance, if the velocity $v$ is 1 and $\tau$ is 1, they could be in units for the speed of light (300,000 kilometers per second) and time (or $\tau$) in seconds, respectively. Your answer for distance could be in units of kilometers or perhaps a fraction of the speed of light within a time $\tau$.

```python
ConstantBall(1,1) # The two numbers in the parentheses (𝑣, 𝜏) represent the velocity (𝑣) of the ball and the time (𝜏)
# the ball is moving at constant velocity (𝑣). The function "ConstantBall" multiplies the two numbers to give you
# the distance the ball has travelled.
```

## Part 2.2: Ball with Changing Velocity

What if the ball's velocity changes with position (we notate this with $v=v(x)$)? This is the case, for example, with a ball rolling down a hill. It's velocity initially is smaller than its velocity at a later time as it rolls downhill.

In short, in this instance, the further the ball travels, the faster it goes ($v(x)$ increases with time and distance).

        Question 4: In what direction is $a$, the gravitational acceleration (or gravity), pointed in the above diagram?

There are instances where, depending on what the $v(x)$ function is, the equation for $v(x)$ can be very complicated to solve by hand. One example of this would be solving the equation for $v(x)$ for a ball on a spring. In cases like that, the equation can be solved *numerically* by making the approximation that for a small chunk of time $T$, the ball's velocity is approximately constant. To get an understanding of this, let's first picture this process:

At $t=0$, the ball is at $x=0$, moving with some velocity $v(0)$. At $t=T$, the ball moves a distance $d_1=v(0)T$. This is the approximation that the velocity is constant for a small amount of time $T$.

At $t=2T$, the ball moves a further distance $d_2=v(d_1)T$.

By breaking up the calculation into N *timesteps*, each of length $T$, we can approximate the motion for the entire length of time $\tau=\text{N}T$ as

$$
x(\tau) \approx v(0)T + v(d_1)T + \ldots + v(d_{(\text{N}-1)})T \; .
$$

This technique is referred to as *numerical integration*. This can be tricky to understand at first; take your time to go over the explanation multiple times before you proceed. Answer the following question:

        Question 5: When is it appropriate to use this technique?

Let's look at this more closely. We can write this numerical integration as code.

First, we write a function for the velocity, which we will call **velocity**:

```python
def velocity(x):
    ''' Returns the velocity of the ball, as a function of position, x. '''
    return x

print("You have defined the velocity function. Nice Job!")
```

This next function returns a value for $x$; so the velocity is $v(x)=x$.

Next, we write up our numerical integral:

```python
def MyFirstIntegrator(x0,T,N):

    ''' Integrates the motion of a ball moving with velocity v(x) for time NT. '''

    # Generates an array of N zeros, the nth entry of this array represents the position of the ball at time t=nT
    xlist = np.zeros(N)

    # Initial condition: the ball starts at x=x0, so the 0th entry of xlist is x0
    xlist[0] = x0

    # Use a for loop to integrate N times, each integral in the loop is done over time T
    for n in range(N-1):
        xlist[n+1] = xlist[n] + T*velocity(xlist[n]) # Here we call the velocity function that we defined earlier

    # Plot the resulting motion
    plt.figure()
    # Note the x-axis: np.arrange(N) produces the array (0,1,2,...,N). We multiply this by T because each timestep is of length T.
    plt.plot(np.arange(N)*T,xlist, 'g')
    plt.xlabel(r"$t$")
    plt.ylabel(r"$x$")
    plt.grid()

    # Returns the array of x's: position of the ball as a function of time.
    return xlist

print("You have defined the MyFirstIntegrator function. Nice Job!")
```

I have added a new parameter, $x_0$ (written as x0, without subscript, in Python). This is the position of the ball at $t=0$. There is no reason why we should restrict our integrator to a ball that starts at $x=0$!

Let's see what happens if I set
$$
v(x) = x \; ,
$$
i.e. the ball moves faster the further away it is.

        Question 6: What is the ball's position as a function of time?

See the "widget" with the slider and interactive plot below.

        Question 7: Play around with x0 and N. How do these parameters change the plot?

What happens if you set the ball's starting position, x0, to 0? Why?

```python
x0       = FloatSlider(min=-1, max=1, step=0.1, value=0.1)
N        = IntSlider(min=1000, max=100000, step=1000, value = 100000)#, disabled=True)

interactive(MyFirstIntegrator, x0=x0, T=fixed(0.0001), N=N)
```

## Part 2.3: Ball on a Spring

Let's use our numerical integrator to solve the ball-on-a-spring problem.

The ball accelerates because it is attached to the spring, and the further away the ball is, the stronger the acceleration. The *acceleration* function is

$$
a(x) = -x \; ,
$$

        Question 8: Why is there a negative sign?

Hint: Think of what direction you are pulling the ball with respect to the spring.

The following code computes the motion of the ball on a spring system:

```python
def acceleration(x,v):

    ''' Returns the acceleration of the ball, as a function of position, x and velocity, v. '''

    return -x

def AcceleratingBall(x0,v0,T=0.1,N=1000):

    ''' Integrates the motion of a ball moving with acceleration a(x,v) for time NT. '''

    # Generates an array of N zeros, the nth entry of this array represents the position of the ball at time t=nT
    xlist = np.zeros(N)
    # Generates an array of N zeros, the nth entry of this array represents the velocity of the ball at time t=nT
    vlist = np.zeros(N)

    # Initial condition: the ball starts at x=x0, so the 0th entry of xlist is x0
    xlist[0] = x0
    # Initial condition: the ball starts at v=v0, so the 0th entry of xlist is v0
    vlist[0] = v0

    # Use a for loop to integrate N times, each integral in the loop is done over time T
    for n in range(N-1):
        xlist[n+1] = xlist[n] + T*vlist[n]
        # At timestep n+1, the position is advanced by the velocity at timestep n multiplied by T.
        vlist[n+1] = vlist[n] + T*acceleration(xlist[n],vlist[n])
        # Here we call the acceleration function that we defined earlier

    # Plot the resulting motion
    plt.figure()
    # Note the x-axis: np.arrange(N) produces the array (0,1,2,...,N).
    # We multiply this by T because each timestep is of length T.
    plt.plot(np.arange(N)*T,xlist, 'g')
    plt.xlabel(r"$t$")
    plt.ylabel(r"$x$")
    plt.grid()

    # Returns the array of x's: position of the ball as a function of time.
    return xlist
```

        Question 9: What is the motion of a ball with this acceleration?

See the "widget" with the slider and interactive plot below.

**Play with x0 (the starting location of the ball), v0 (the starting velocity of the ball), and N (how many times we make the calculation or how many intervals of T there are).**

What do you notice?

```python
x0       = FloatSlider(min=0, max=10, step=0.1, value=0, disabled=False)
v0       = FloatSlider(min=0, max=10, step=0.1, value=1, disabled=False)
N        = IntSlider(min=1000, max=100000, step=1000, value = 10000, disabled=False)

interactive(AcceleratingBall, x0=x0, v0=v0, T=fixed(0.01), N=N)
```

As time progresses, the amplitude of the oscillation increases, i.e. **the energy of the system increases with time**. In reality, this should not happen in reality, because physics dictates that energy should always be conserved! Therefore there is an error in our numerical integration scheme. The point here was to show that there are instances where simple numerical integration do not always work perfectly. Fortunately, the more complex integration schemes do not suffer from this error.
___

# Activity 3: The Gravity We Know: Newton's Law, Integrated

Now, we are ready to numerically integrate Newton's laws,

$$
a(r) = -\frac{G M}{r^2} \; ,
$$

or pictorially,

Again, $G$ is the Newton's constant (i.e. just a number), while $r$ is the distance between the black hole and the particle.

We can use the numerical integration technique we learned in the previous sections to perform this calculation. First, let us define a function that computes both the velocity and acceleration at once:

```python
def v_and_a(x, y, vx, vy):

    ''' Returns the accelerations and velocities of the system. '''

    rho = np.sqrt(x**2 + y**2)
    ax = -0.5*x/rho**3
    ay = -0.5*y/rho**3

    # Here we return an array that contains both velocities, vx & vy, as well as both accelerations, ax & ay
    return (vx, vy, ax, ay)
```

This code below performs the calculation in much the same way as we did for the ball-on-a-spring:

```python
def Orbit(x0,y0,vx0,vy0,T,N):

    ''' Integrates the motion of a particle moving around a black-hole with Newtonian gravity. Integration is done over
     time NT. '''

    # Generates four arrays of N zeros, the nth entry of this array represents the x, y, vx, and vy at time t=nT
    xlist = np.zeros(N)
    ylist = np.zeros(N)
    vxlist = np.zeros(N)
    vylist = np.zeros(N)

    # Initial condition: the particle starts at x=x0, y=y0, vx=vx0, vy=vy0
    xlist[0] = x0
    ylist[0] = y0
    vxlist[0] = vx0
    vylist[0] = vy0

    # Use a for loop to integrate N times, each integral in the loop is done over time T
    for n in range(N-1):
        # Here we call the v_and_a function that we defined earlier to give us the velocity and acceleration of the particle at that time
        xlist[n+1] = xlist[n] + T*v_and_a(xlist[n], ylist[n], vxlist[n], vylist[n])[0]
        ylist[n+1] = ylist[n] + T*v_and_a(xlist[n], ylist[n], vxlist[n], vylist[n])[1]
        vxlist[n+1] = vxlist[n] + T*v_and_a(xlist[n], ylist[n], vxlist[n], vylist[n])[2]
        vylist[n+1] = vylist[n] + T*v_and_a(xlist[n], ylist[n], vxlist[n], vylist[n])[3]

    # Plot the resulting motion
    plt.figure()
    plt.plot(xlist, ylist, 'g', [0], [0], 'ro')
    plt.xlabel(r"$x$")
    plt.ylabel(r"$y$")
    plt.grid()
```

Play around with the parameters of **Orbit** using the "widget" with the slider and interactive plot below. In the previous section, we showed that an error in our integration scheme causes energy to increase. **Do we get the same error here?** Note that x0 and y0 refer to the particle's starting position in 2-dimension, vx0 and vy0 refer to the particle's starting velocities in the x and y directions, and again the parameter $N$ refers to how many times we make the calculations.

```python
x0       = FloatSlider(min=3, max=10, step=1, value=10)
vx0      = FloatSlider(min=0, max=0.1, step=0.01, value=0)
vy0      = FloatSlider(min=0, max=0.2, step=0.01, value=0.19)

interactive(Orbit, x0=x0, y0=fixed(0), vx0=vx0, vy0=vy0, T=fixed(0.01), N=fixed(20000))
```

___

# Activity 4: The Gravity Near a Black Hole: Orbits in General Relativity

In the first half of the 20th century, Albert Einstein (1879-1955) devised a theory of gravity called general relativity. Since then, general relativity has passed a battery of experimental tests and is now touted as our most accurate theory of gravity.

Using Einstein's theory of general relativity, Karl Schwarzschild (1873-1916) derived a correction to the Newtonian gravitational equation of motion (while he was serving in the frontline during World War I!). This new equation for the magnitude of the accleration of a particle in general relativity, $a_{GR}$, is

\begin{align}
a_{GR} &= a_N + a_{\textrm{correction}} \; ,
\end{align}

where $a_N$ is the magnitude of the gravitational acceleration in Newton's theory, i.e. our old friend,

$$
a(r) = -\frac{G M}{r^2} \; ,
$$

and $a_{\textrm{correction}}$ is a correction term that involves things like the particle's angular momentum.

(See a one-minute explanation of angular momentum at https://www.youtube.com/watch?v=iWSu6U0Ujs8.)

Angular momentum is the angular velocity multiplied by something called the moment of inertia. The moment of inertia in this case is the mass times the distance squared (e.g., the mass of the black hole and the distance of the particle to the black hole).

Let us modify the code **v_and_a** to return the general relativistic acceleration if a new variable, called Einstein, is set to 1:

```python
def v_and_a(x, y, u, v, Einstein):
    """ Returns the accelerations and velocities of the system. If Einstein=0, returns the Newtonian gravitational
    acceleration. If Einstein=1, returns the general relativistic expression instead."""

    rho = np.sqrt(x**2 + y**2)
    velocity_x = u
    velocity_y = v

    if Einstein == 1:
        # If Einstein=1, returns the general relativistic expressions
        l = (x*v - y*u)
        acceleration_x = (-0.5*x/rho**3)*(1. + 3.*l**2/rho**2)
        acceleration_y = (-0.5*y/rho**3)*(1. + 3.*l**2/rho**2)
    elif Einstein == 0:
        # If Einstein=0, returns the Newtonian expressions
        acceleration_x = -0.5*x/rho**3
        acceleration_y = -0.5*y/rho**3

    # Here we return an array that contains both velocities, vx & vy, as well as both accelerations, ax & ay
    return (velocity_x, velocity_y, acceleration_x, acceleration_y)
```

Now, all we have to do is to modify following function, **Orbit**, to accept this new parameter. The function, **Orbit**, integrates the motion of a particle moving around a black-hole with Newtonian gravity over time NT.

```python
def Orbit(x0,y0,vx0,vy0,T,N,Einstein):

    ''' Integrates the motion of a particle moving around a black-hole with Newtonian gravity. Integration is done over
     time NT. '''

    # Generates four arrays of N zeros, the nth entry of this array represents the x, y, vx, and vy at time t=nT
    xlist = np.zeros(N)
    ylist = np.zeros(N)
    vxlist = np.zeros(N)
    vylist = np.zeros(N)

    # Initial condition: the particle starts at x=x0, y=y0, vx=vx0, vy=vy0
    xlist[0] = x0
    ylist[0] = y0
    vxlist[0] = vx0
    vylist[0] = vy0

    # Use a for loop to integrate N times, each integral in the loop is done over time T
    for n in range(N-1):
        # Here we call the v_and_a function that we defined earlier to give us the velocity and acceleration of the particle at that time
        xlist[n+1] = xlist[n] + T*v_and_a(xlist[n], ylist[n], vxlist[n], vylist[n], Einstein)[0]
        ylist[n+1] = ylist[n] + T*v_and_a(xlist[n], ylist[n], vxlist[n], vylist[n], Einstein)[1]
        vxlist[n+1] = vxlist[n] + T*v_and_a(xlist[n], ylist[n], vxlist[n], vylist[n], Einstein)[2]
        vylist[n+1] = vylist[n] + T*v_and_a(xlist[n], ylist[n], vxlist[n], vylist[n], Einstein)[3]

    # Plot the resulting motion
    plt.figure()
    plt.plot(xlist, ylist, 'g', [0], [0], 'ro')
    # Title the plots, if Einstein=0 as "Newtonian Orbit" and if Einstein=1 as "Einsteinian Orbit"
    if Einstein == 0:
        plt.title("Newtonian Orbit")
    elif Einstein == 1:
        plt.title("Einsteinian Orbit")
    plt.xlabel(r"$x$")
    plt.ylabel(r"$y$")
    plt.grid()
```

Now try out various values of the parameters x0, y0, vx0, and vy0 using the "widget" with the slider and interactive plot below.

        Question 10: Can you make qualitative statements on the differences between Newtonian and Einsteinian orbits?

        Question 11: How far do you need to go from the black hole for the Einsteinian orbit to look like the Newtonian one?

```python
x0       = FloatSlider(min=10, max=20, step=1, value=10)#, disabled=True)
vx0      = FloatSlider(min=0, max=0.2, step=0.01, value=0)
vy0      = FloatSlider(min=0, max=0.2, step=0.01, value=0.19)
Einstein = Checkbox()

interactive(Orbit, x0=x0, y0=fixed(0), vx0=vx0, vy0=vy0, T=fixed(0.01), N=fixed(50000), Einstein=Einstein)
```

___

### Final Challenge

Suppose you have a near-infinite budget, using what you know about the orbits in these two theories of gravity, **devise an experiment to test whether Newton or Einstein is correct**!
___

```python

```

_**Written by Pierre Christian, Steward Observatory**_


# Solución de ecuaciones

Python en Jupyter Notebook

Creado por Giancarlo Ortiz para el curso de Métodos Numéricos

## Raíces de ecuaciones
Las raíces o ceros de una función continua, son los valores de $\color{#a78a4d}{x}$ tal que $\color{#a78a4d}{f(x) = 0}$; lo que es equivalente a resolver ecuaciones del tipo $\color{#a78a4d}{g(x)=h(x)}$, ya que esta ecuación puede reescribirse como $\color{#a78a4d}{f(x)=h(x)-g(x)=0}$. Los métodos numéricos de búsqueda de raíces son algoritmos que iteran sobre una solución aproximada, produciendo una secuencia de números que, con suerte, convergen hacia la raíz como límite.

## Agenda
1. Tipos de ecuaciones
1. Linealidad
1. Puntos periódicos
1. Solución de ecuaciones no lineales

```python
# Importar módulos al cuaderno de jupyter
import math as m
import numpy as np
import pylab as plt
```

## 1. Tipos de Ecuaciones
---
En matemáticas una ecuación es una igualdad entre dos expresiones, separadas por el signo igual; tal que en las expresiones miembro aparecen variables independientes relacionadas mediante operaciones matemáticas.

\begin{align}
\tag{1} f(x_1, x_2, ... , x_n) &= g(x_1, x_2, ... , x_n) \\
\tag{2} f(x_1, x_2, ... , x_n) &= 0 \\
\end{align}

### a. Ecuaciones de una variable
Una simplificación de $(2)$ sucede cuando solo existe una variable independiente y la ecuación se puede reescribir como:

\begin{align}
\tag{3} f(x) & = 0 \\
\end{align}

### b. Ecuaciones algebraicas o polinómicas
Una simplificación adicional de de $(2)$ sucede cuando sus miembros incluyen unicamente sumas y multiplicaciones de la variable independiente; en cuyo caso la expresión se conoce como polinomio y se puede reescribir como la suma de $\color{#a78a4d}{n+1}$ monomios cuyos coeficientes o parámetros $\color{#a78a4d}{a_0, a_1, a_2, ... , a_n}$ son constantes en el dominio de los reales:

\begin{align}
\tag{4} f(x) &= 0 \\
\tag{5} f(x) &= \sum_{1}^n a_i x^i \\
\tag{6} a_0 + a_1 x + a_2 x^2 + ... + a_n x^n & = 0, \quad a_n \neq 0 \\

\end{align}

> **NOTA:** El número n se denomina grado de la ecuación y es igual al numero raíces o ceros que son solución de la igualdad; estas soluciones pertenecen al dominio de los complejos y pueden ser reales o imaginarias; en el caso de soluciones imaginarias se dan en parejas de complejos conjugados.

### c. Ecuaciones trascendentes
Si la ecuación no puede expresarse o reducirse a un polinomio como las ecuaciones algebraicas se denominan ecuaciones trascendentes y su resolución va más allá del álgebra (trascienden el álgebra); las ecuaciones trascendentes más simples son las trigonométricas, logarítmicas y exponenciales sencillas.

> **NOTA:** La solución para x de estas ecuaciones puede no ser trivial y suelen ser objeto de estudio de los métodos numéricos.

```python
x = np.linspace(-20, 20, 1024, endpoint=True)
y1 = (-1/1e7)*(x**7 - 350*x**5 + 30_625*x**3 - 562_500*x)
y2 = np.sin(x)/x

# Gráficas
#-------------------
fig, ax = plt.subplots(1, 2)
fig.set_size_inches(16, 4)
fig.suptitle('Raíces de funciones algebraicas y trascendentes'.upper(), fontsize=14, fontweight="bold")

# Gráficas 1
ax[0].axhline(y = 0, linewidth=1, color="#ccc")
ax[0].plot(x, y1, label="$y_1(x)$")

ax[0].set_xlim(-20, 20)
ax[0].set_ylim(-1.5, 1.5)

ax[0].set_title("Función polinómica", fontsize=8)
ax[0].set_xlabel("Variable independiente")
ax[0].set_ylabel("variable dependiente")
ax[0].legend()

# Gráficas 2
ax[1].axhline(y = 0, linewidth=1, color="#ccc")
ax[1].plot(x, y2, label="$y_2(x)$")

ax[1].set_xlim(-20, 20)
ax[1].set_ylim(-1.5, 1.5)

ax[1].set_title("Función trascendentes", fontsize=8)
ax[1].set_xlabel("Variable independiente")
ax[1].set_ylabel("variable dependiente")
ax[1].legend()

plt.show()
```

## 2. Linealidad
---
### a. Ecuaciones lineales
Un caso particular de las ecuaciones algebraicas sucede cuando solo los dos primeros coeficientes son distintos de cero y la solución para $\color{#a78a4d}{x}$ es única y trivial.

\begin{align}
\tag{7} a_0 + a_1 x & = 0, \quad a_1 \neq 0 \\
\tag{8} x & = \frac{-a_0}{a_1} \\
\end{align}

### b. Ecuaciones cuadráticas
Un caso particular de las ecuaciones algebraicas sucede cuando solo los tres primeros coeficientes son distintos de cero y las 2 soluciones para $\color{#a78a4d}{x}$ es un par de complejos conjugados.

\begin{align}
\tag{9} a_0 + a_1 x + a_2 x^2 & = 0, \quad a_1 \neq 0 \\
\tag{10} x_1, x_2 & = \frac{-a_1 \pm \sqrt{a_1^2 - 4 a_2 a_0 }}{2 a_2} \\
\end{align}

### b. Ecuaciones de orden superior
Sucede que el número de soluciones es proporcional al orden de la ecuación; cuando más de tres coeficientes son distintos de cero en las ecuaciones algebraicas la solución puede ser más compleja; aunque la solución algebraica es posible debido a que existen métodos matemáticos para acotar la solución.

```python
y3 = (x - 10)
y4 = (x - 10) * (x + 10)
y5 = (x + 10) * (x - 10) * x

# Gráficas
#-------------------
fig, ax = plt.subplots(1, 3)
fig.set_size_inches(16, 4)
fig.suptitle('Raíces de funciones algebraicas'.upper(), fontsize=14, fontweight="bold")

# Gráficas 1
ax[0].axhline(y = 0, linewidth=1.0, color="#ccc")
ax[0].plot(x, y3, label="$y_1(x)$")
ax[0].set_title("Función líneal", fontsize=8)
ax[0].set_xlabel("Variable independiente")
ax[0].set_ylabel("variable dependiente")
ax[0].legend()

# Gráficas 2
ax[1].axhline(y = 0, linewidth=1, color="#ccc")
ax[1].plot(x, y4, label="$y_2(x)$")
ax[1].set_title("Función cuadrática", fontsize=8)
ax[1].set_xlabel("Variable independiente")
ax[1].legend()

# Gráficas 3
ax[2].axhline(y = 0, linewidth=1, color="#ccc")
ax[2].plot(x, y5, label="$y_3(x)$")
ax[2].set_title("Función cubica", fontsize=8)
ax[2].set_xlabel("Variable independiente")
ax[2].legend()

plt.show()
```

## 3. Puntos periódicos
---
En el estudio de sistemas dinámicos, un punto periódico de una función iterada es el punto al cual el sistema retorna luego de un cierto número de iteraciones, al cabo de un cierto tiempo.

En matemáticas, los puntos periódicos vuelven al mismo valor después de un número finito de iteraciones de la función y un caso particular de ellos son los puntos fijos, donde el periodo $\color{#a78a4d}{T=1}$.

\begin{align}
\tag{11} (f:x \to x) \wedge (p, \text{ es periódico}) \Rightarrow \exists \ n : f_n(p)=p\\

\end{align}

### a. Punto fijo
En matemáticas, un punto fijo de una función real es un punto cuya imagen producida por la función es él mismo; esto que significa que si $\color{#a78a4d}{p}$ es un número real, $\color{#a78a4d}{p}$ es un punto fijo de la función $\color{#a78a4d}{f(x)}$ si y sólo si $\color{#a78a4d}{p = f(p)}$

> **NOTA:** En la gráfica (p, f(p)) pertenece a la recta $y=x$

### Ejemplo: Puntos fijos de una función
---
Sea la función $f(x) = x^2$ los puntos fijos son números reales p tal que $p = p^2$; entonces 0 y 1 son los puntos fijos de $f(x)$, porque $f(0) = 0$ y $f(1) = 1$.

```python
x = np.linspace(-2, 2, 256, endpoint=True)
plt.figure(figsize=(5, 5))
plt.plot(x, x**2, linewidth=2, color="#00F" )
plt.plot(x, x, linestyle="--", color="gray")
plt.plot(0, 0, marker="o", markersize=7, markeredgecolor="black", color="#fc0")
plt.plot(1, 1, marker="o", markersize=7, markeredgecolor="black", color="#fc0")
plt.xlim(x.min(), x.max())
plt.ylim(-1, 3)
plt.text(-0.7, 2, f"Puntos fijos\n   $f(x)=x^2$", fontsize=17)
plt.grid()
plt.show()
```

## 4. Solución de ecuaciones no lineales
---
Solucionar una ecuación es encontrar las raíces o ceros de una ecuación y aunque en algunos casos los métodos analíticos o exactos son posibles y sencillos; en otros casos es necesario recurrir a métodos numéricos.

### a. Métodos Numéricos
Los métodos de la bisección y del punto fijo son los primeros métodos numéricos que se utilizaron para resolver ecuaciones algebraicas y trascendentes; estos representan el punto de partida para otros métodos más eficientes; del primer método o de la bisección derivan los métodos cerrados que se aproximan a la solución reduciendo intervalos y del segundo método o del punto fijo se derivan los métodos abiertos que se aproximan a la solución mediante una función auxiliar.

Los métodos cerrados requieren dos valores iniciales que estén a ambos lados de la raíz, para acotarla. Este “acotamiento” se mantiene en tanto se aproxima a la solución; así, dichas técnicas son siempre convergentes; los métodos abiertos solo requieren de un valor, para aproximarse a la solución a través de la función auxiliar y son generalmente más rápidos; sin embargo, una mayor rapidez de convergencia se da cambio de una estabilidad menor.

### b. Selección de un método numérico
Para seleccionar un método numérico que nos permita encontrar las raíces de una ecuación por aproximaciones sucesivas se requiere conocer información preliminar como:

* Tipo de ecuación.
* Número y tipo de las posibles raíces de la ecuación.
* Información del dominio, rango, continuidad, derivadas, intervalos de crecimiento.
* Ventajas y limitaciones que aporta cada algoritmo a la solución del problema.
* Semilla o valoración inicial de la solución.

### c. Consideraciones generales
Del [teorema del valor intermedio](https://es.wikipedia.org/wiki/Teorema_del_valor_intermedio) se tiene que si $\color{#a78a4d}{f(x)}$ es continua en un intervalo $\color{#a78a4d}{(a, b)}$ y cambia de signo en ese intervalo existe al menos una raíz real de la ecuación asociada en ese intervalo; sin embargo, existen consideraciones particulares para cada uno de los tipos de ecuaciones estudiadas.

### d. Ecuaciones algebraicas
* El número de raíces es igual al grado del polinomio asociado; por lo tanto, en una ecuación de grado $\color{#a78a4d}{n}$, hay $\color{#a78a4d}{n}$ raíces reales o complejas.
* Las raíces pueden ser reales o complejas conjugadas; por lo tanto, si el grado del polinomio es impar hay al menos una raíz real.
* La posible existencia de raíces múltiples complica el problema; pero si $\color{#a78a4d}{f(x)}$ es derivable en $\color{#a78a4d}{(a, b)}$ y $\color{#a78a4d}{f'(x)}$ no cambia de signo en el ese intervalo, la raíz en ese intervalo es única.
* Existen métodos como la [regla de Descartes](https://es.wikipedia.org/wiki/Regla_de_los_signos_de_Descartes), el [teorema de Budan]() y [teorema de Sturm](https://es.wikipedia.org/wiki/Teorema_de_Sturm) que nos permiten acotar convenientemente las raíces del polinomio.

### e. Ecuaciones trascendentes
* Estas ecuaciones pueden tener cualquier número de posibles soluciones.
* Pueden tener ninguna, una o más raíces en un intervalo.
* Garantizar la existencia y unicidad de soluciones en estas ecuaciones es difícil de determinar y existe una mayor variedad de casos y comportamientos.
* Pueden tener múltiples raíces, donde tanto la función $\color{#a78a4d}{f(x)}$ como su derivada $\color{#a78a4d}{f'(x)}$ son iguales a cero.
* Algunas ecuaciones no pueden resolverse por métodos numéricos aún con un número muy grande de iteraciones.

```python
x = np.linspace(-3, 6, 256, endpoint=True)
y6 = np.exp(3*x - 12) + x * np.cos(3*x) - x**2 + 7.15

# Gráficas
#-------------------
fig, ax = plt.subplots(1, 1)
fig.set_size_inches(16, 4)
fig.suptitle('Búsqueda de raíces en funciones no lineales'.upper(), fontsize=14, fontweight="bold")

# Gráficas 1
ax.axhline(y = 0, linewidth=1, color="#ccc")
ax.plot(x, y6, label="$y_1(x)$")

ax.set_xlim(-3, 6)
ax.set_ylim(-11, 8)

ax.set_title("Función trascendente", fontsize=8)
ax.set_xlabel("Variable independiente")
ax.set_ylabel("variable dependiente")
ax.legend()

plt.show()
```

## Analizando la figura anterior se observa:
* La función $\color{#a78a4d}{f}$ asociada a la ecuación $\color{#a78a4d}{f(x)=0}$ es continua
* En el intervalo $\color{#a78a4d}{(-3, -2)}$ visualmente no es posible determinar si la curva corta el eje $\color{#a78a4d}{x}$ una vez, dos veces o ninguna; con la posibilidad que un método iterativo pueda no detectar un intervalo válido, porque el signo $\color{#a78a4d}{f(-3)}$ es igual al signo de $\color{#a78a4d}{f(-2)}$.
* En cada uno de los intervalos $\color{#a78a4d}{(2, 3)}$ y $\color{#a78a4d}{(5, 6)}$ existe una raíz única y real.

---
## Mas Recursos

- [Ecuación](https://es.wikipedia.org/wiki/Ecuaci%C3%B3n) (Wikipedia)
- [Ecuación algebraica](https://es.wikipedia.org/wiki/Ecuaci%C3%B3n_algebraica) (Wikipedia)
- [Polinomio](https://es.wikipedia.org/wiki/Polinomio) (Wikipedia)
- [Solución de Ecuaciones](https://es.wikipedia.org/wiki/Resoluci%C3%B3n_num%C3%A9rica_de_ecuaciones_no_lineales) (Wikipedia)