# Matemáticas

### Valor absoluto
Valor positivo independientemente de su signo.

```python
x = -5
print(abs(x))

def valor_absoluto(x):
  if x<0:
    x = -x
  return x

print(valor_absoluto(x))
```

    5
    5

### Potencia de un número

```python
x = 2
y = 5
print(x**y)
print(pow(x,y))

def potencia(x,y):    # y tiene que ser un número entero
  resultado = 1
  for i in range(y):
    resultado *= x
  return resultado

potencia(x,y)
```

    32
    32

    32

### Raiz cuadrada

#### Método 1
* Método muy poco eficiente.
* Admite un error grande, o si queremos que el error admitido sea menor, el tiempo de cálculo será muy grande.

```python
import math
a = 81
print(a**.5)           # elevando a (1/2)
print(math.sqrt(a))    # con la librería math usando la función sqrt

def raiz1(n):
  x = 0
  while x * x <= n:    # buscamos un número (x) que multiplicado por si mismo de el número buscado (n)
    x += 0.001
  return x

print(raiz1(a))
```

    1414.213562373095
    1414.213562373095
    1414.2139999734702

#### Método 2
Método babilónico

```python
import math
x = 2_000_000       # tiene que ser no negativo
print(x**.5)
print(math.sqrt(x))

def raiz2(x):
  b = 1             # base del rectángulo
  h = x / b         # altura del rectángulo
  if x<1: b,h=h,b   # para que conveja el caso en que x<1
  while True:       # deseamos que b y h coincidan para que el rectángulo sea un cuadrado
    med = (b+h)/2   # media
    if abs(med*med - x) < 1e-9:
      return med
    elif med*med -x < 0:
      b = med
    else:
      h = med

print(raiz2(x))
```

    1414.213562373095
    1414.213562373095
    1414.213562373095

#### Método 3
* Es una variante del método anterior.
* En lugar de usar una cierta tolerancia o error admitido para salir del bucle y terminar el cálculo, lo que hacemos es aprovecharnos de que los cálculos con coma flotante llega un momento en que ya no admiten más precisión y que la media se estanca y no mejora en precisión.
* Cuando vemos que la media se ha estancado es cuando finaliza el bucle
* Sabemos que la media se ha estancado porque tomamos su valor de la iteración anterior con la variable old y la comparamos con la nueva media med.
* Cuando ambas son iguales se acaba el bucle.
* Cuando ```med == old```

```python
import math
x = 2_000_000
print(x**.5)
print(math.sqrt(x))

def raiz3(x):
  b = 1
  h = x / b
  if x < 1:
    b, h = h, b
  med = None
  while True:
    old = med      # old es el valor de la media de la anterior iteración
    med = (b+h)/2  # en esta nueva iteración se recalcula el valor de la media
    if med == old: # se termina cuando la media se estanca
      return med
    elif med*med -x < 0:
      b = med
    else:
      h = med

print(raiz3(x))
```

    1414.213562373095
    1414.213562373095
    1414.213562373095

#### Método 4
* Deseamos calcular la raiz cuadrada de x
* Buscamos un infimo (inf) suficientemente bajo, con el que estemos seguros de que su cuadrado es menor que x
* Buscamos un supremo (sup) suficientemente alto, con el que estemos seguros de que su cuadrado es mayor que x
* Si el infimo (inf) no es suficientemente bajo nos metemos en un bucle para hacer mitades hasta que se cumpla que su cuadrado es menor o igual que x
* Si el supremo (sup) no es suficientemente alto nos metemos en un bucle para hacer dobles hasta que se cumpla que su cuadrado es mayor o igual que x

```python
import math
x = 2_000_000
print(x**.5)
print(math.sqrt(x))

def raiz4(x):
  tolerancia = 1e-9
  inf = 1   # infimo
  sup = 100 # supremo
  while inf*inf > x:  # nos aseguramos que el inf es suficientemente bajo
    inf /= 2
  while sup*sup < x:  # nos aseguramos que el sup es suficientemente alto
    sup *= 2
  while True:
    med = (inf+sup)/2   # media
    dif_med = med*med-x # diferencia entre x y la media al cuadrado
    if abs(dif_med) < tolerancia:
      break
    if dif_med > 0:
      sup = med
    else:
      inf = med
  return med

print(raiz4(x))
```

    1414.213562373095
    1414.213562373095
    1414.2135623730953

### Factorial
* El factorial de 5 es:
 - 5! = 5 * 4 * 3 * 2 * 1 = 120
* El factorial de n es:
 - n! = n * (n-1) * (n-2) * ···· * 1

#### Método 1
Usando una librería.

```python
import math
print(math.factorial(5))
print(math.factorial(0))  # por definición, el factorial de cero es uno
```

    120
    1

#### Método 2
Programando nuestra propia función factorial.

```python
def fac(n):
  f = 1
  for i in range(1,n+1):
    f *= i
  return f

print(fac(5))
print(fac(0))
```

    120
    1

### Combinaciones sin repetición
* Calcular el número de combinaciones sin repetición de m elementos tomados de n en n.
* Requiere calcular el factorial.

#### Método 1
$$C^n_m=\frac{m!}{n!*(m-n)!}$$

```python
from math import factorial
def combinaciones_sin_repeticion(m,n):
  if m<n:
    return 0
  else:
    comb = factorial(m) / (factorial(n) * factorial(m-n))
    return comb

print(combinaciones_sin_repeticion(8,3))
```

    56.0

#### Método 2
* Podemos simplificar la expresión para no realizar tantos cálculos.

$$C^8_3 = \frac{8!}{3!*(8-3)!} = \frac{8!}{3!*5!} = \frac{8·7·6·5!}{3!*5!} = \frac{8·7·6}{3·2·1} = 56$$

```python
from math import factorial

def combina_sin_rep(m,n):
  if m<n:
    return 0
  else:
    numerador = denominador = 1
    for i in range(m,m-n,-1):
      numerador *= i
      denominador *= i-(m-n)
    return numerador / denominador

print(combina_sin_rep(8,3))
print(factorial(8) / (factorial(3) * factorial(8-3)))
```

    56.0
    56.0

### Números primos
#### Ver si un número es primo

# Método 1
Medimos tiempos en este algoritmo sin optimizar.

```python
from sympy import isprime
from time import time

def es_primo(n):
  if n<2: return False  # para el caso en que sea n=1
  for i in range(2,n):
    if n%i==0:
      return False
  return True

n = 1234567891

t0 = time()
print(es_primo(n))
t1 = time()
print(isprime(n))
print(t1-t0, "segundos")
```

    True
    True
    170.44832563400269 segundos

# Método 2
Algoritmo optimizado:
 -  buscamos divisores hasta la raiz cuadrada del número analizado
 - solo los impares

```python
from sympy import isprime
from math import sqrt
from time import time

def es_primo(n):  # n tiene que ser un entero mayor que 1
  if n == 2: return True # si n es 2 es primo
  elif n%2==0: return False # si n!=2 es par, entonces no es primo
  for i in range(3,int(sqrt(n)+1),2):
    if n%i == 0:
      return False
  return True

n = 1234567891

t0 = time()
print(es_primo(n))
t1 = time()
print(isprime(n))
print(t1-t0, "segundos")
```

    True
    True
    0.0044820308685302734 segundos

#### Encontrar todos los primos en un rango
* Crear una función que proporcione todos los números primos que existen en un rango.
* El usuario nos proporciona el número menos y el mayor del rango y se pasan como parámetros a la función

#### Método 1

```python
def encontrar_primos(a, b):
  lista = []
  for n in range(a, b+1):
    contador = 0
    for i in range(1, n+1):
      if n%i == 0:
        contador += 1
    if contador == 2: # Un primo solo es divisible entre 1 y él mismo
      lista.append(n)
  return lista

a = int(input("Número inicial: ") or 1)
b = int(input("Número final: ") or 101)
print(encontrar_primos(a,b))
```

    Número inicial:
    Número final:
    [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101]

#### Método 2
* Eliminamos tener que dividir entre 1 y entre n
* Ya no hace falta que el contador sea igual a 2
* Eliminamos el contador y usamos una bandera

```python
def encontrar_primos(a, b):
  lista = []
  if a == 1: # para que no se considere el 1 como primo
    a = 2    # el número inicial fuera 1, asi se comiena en 2
  for n in range(a, b+1):
    primo = True    # bandera, inicialmente consideramos el número como Primo
    for i in range(2, n):
      if n%i == 0:
        primo = False
        break
    if primo:
      lista.append(n)
  return lista

a = int(input("Número inicial: ") or 1)
b = int(input("Número final: ") or 101)
print(encontrar_primos(a,b))
```

    Número inicial:
    Número final:
    [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101]

#### Método 3

```python
from math import sqrt

def encontrar_primos(a, b):
  lista = []
  for n in range(a,b+1):
    primo = True
    if n==2:
      lista.append(2)
      continue
    if n%2==0 or n==1:
      continue
    for i in range(3,int(sqrt(n))+1,2):
      if n%i == 0:
        primo = False
        break
    if primo:
      lista.append(n)
  return lista

a = int(input("Número inicial: ") or 1)
b = int(input("Número final: ") or 101)
print(encontrar_primos(a,b))
```

    Número inicial:
    Número final:
    [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101]

#### Método 4  For ... break ... else
* Ver el uso de ```break``` y de ```else``` en un ```for```
* [w3schools](https://www.w3schools.com/python/python_for_loops.asp)
* [documentación oficial](https://docs.python.org/3/tutorial/controlflow.html)
* Las instrucciones que van con el ```else``` se ejecutan al final del bucle ```for```
* El bloque ```else``` NO se ejecutará si el bucle se detiene con una instrucción ```break```.
* Si el ciclo se rompe (```break```), el bloque ```else``` no se ejecuta.

```python
def encontrar_primos(a, b):
  lista = []
  for n in range(a, b+1):
    for i in range(2, n):
      if n % i == 0:
        break
    else:
      lista.append(n)
  return lista

a = int(input("Número inicial: ") or 2) # no empezar en 1
b = int(input("Número final: ") or 101)
print(encontrar_primos(a,b))
```

    Número inicial:
    Número final:
    [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101]

# Método 5
* Buscando un método eficiente para números grandes

```python
def encontrar_primos(a, b):
  lista = []
  for n in range(a, b+1):
    if n==2:
      lista.append(2)
      continue
    if n%2==0 or n==1:
      continue
    for i in range(3, int(sqrt(n))+1, 2): # solo impares y hasta la raiz cuadrada
      if n%i == 0:
        break
    else:
      lista.append(n)
  return lista

a = int(input("Número inicial: ") or 1_000_000_000)
b = int(input("Número final: ") or 1_000_000_100)
print(encontrar_primos(a,b))
```

    Número inicial:
    Número final:
    [1000000007, 1000000009, 1000000021, 1000000033, 1000000087, 1000000093, 1000000097]

### Descomposición en factores primos
* Dado un número realizar la descomposición en factores primos.
* Ejemplo:
 - 33.880   =   2 × 2 × 2 × 5 × 7 × 11 × 11   =   2^3 × 5 × 7 × 11^2
 - 19800  =  2^3 × 3^2 × 5^2 × 11
 - 12108  =  2^2 × 3 × 1009
 - 19800  =  2^3 × 3^2 × 5^2 × 11
  - 202 = 2 × 101
 - 9900000246 = 2 × 3 × 61 × 27049181

#### Método 1
Generamos una lista con todos los factores primos de un número.

```python
def descomponer(n):
  primos = []
  for i in range(2, n+1):
    while n % i == 0:
      primos.append(i)
      n /= i            # también puede ser: n //= i
  return primos

#print(descomponer(33880))
#print(descomponer(19800))
#print(descomponer(12108))
#print(descomponer(19800))
print(descomponer(202))
print(descomponer(9900000246))
```

    [2, 101]
    [2, 3, 61, 27049181]

#### Método 3
* Intentaremos reducir tiempos de proceso considerando solo los impares mayores de 2

```python
#!/bin/python3
# Descomposición en factores primos

def factoring(n): #descomposición en factores primos
  text= str(n) + ' = '
  i = 1
  for i in range(1, n+1, 2): # impares, hasta el mismo n ya que podría ser primo
    if i==1: i=2             # salvo el 1 que será 2
    counter = 0
    while n % i == 0:
      n /= i
      counter += 1
    if counter == 1:
      text += str(i)+ ' × '
    elif counter > 1:
      text += str(i) + '^' + str (counter) + ' × '
  text += '1'
  return text

if __name__ == "__main__":
  while True:
    try:
      n = int(input('Introduzca el número a factorizar: ') or 202)
      if 1 < n <= 1e10:
        break
      else:
        print('Por favor, introduzca un número en el rango [2,10_000_000_000]')
    except ValueError:
      print('Por favor itroduzca un número entero positivo.')

  print(factoring(n))
```

    Introduzca el número a factorizar:
    202 = 2 × 101 × 1

#### Método 3
* Similar al método anterior, pero ahora llegamos en el for solo hasta la mitad (aproximadamente) de los valores anteriores.
* Esto es debido a que si n es divisible entre 2 (que es el primer primo), el siguiente n será n/2.
* Con ello nos hemos quitado la mitad de los valores de n para evaluar.

```python
#!/bin/python3
# Descomposición en factores primos

def factoring(n): #descomposición en factores primos
  text= str(n) + ' = '
  i = 1
  for i in range(1, int(n/2)+1, 2):      # impares
    if i==1: i=2                         # salvo el 1 que será 2
    counter = 0
    while n % i == 0:
      n /= i
      counter += 1
    if counter == 1:
      text += str(i)+ ' × '
    elif counter > 1:
      text += str(i) + '^' + str (counter) + ' × '
  if text[-2] == "=":       # si no hay divisores
    text += str(n) + ' × '  # en ese caso el propio n será primo
  text += '1'
  return text

print(factoring(33880))
print(factoring(19800))
print(factoring(12108))
print(factoring(19800))
print(factoring(202))
print(factoring(9900000246))
```

### Múltiplos de un número
* Los múltiplos de un número son infinitos
* Ejemplo
 - Los múltiplos de 5 son:
  * 5 × 1 = 5
  * 5 × 2 = 10
  * 5 × 3 = 15
  * 5 × 4 = 20
  * 5 × 5 = 25
  * ... / ...

#### Método 1
Múltiplos de un número hasta cierto límite

```python
def multiplos_hasta_limite(numero, limite):
  multiplos = []
  i = 1
  while numero*i <= limite:
    multiplos.append(numero*i)
    i += 1
  return multiplos

multiplos_hasta_limite(5,50)
```

    [5, 10, 15, 20, 25, 30, 35, 40, 45, 50]

#### Método 2
* Múltiplos de un número, indicando el número y la cantidad de múltiplos que queremos obtener

```python
def multiplos_hasta_cantidad(numero, cantidad):
  multiplos = []
  for i in range(1,cantidad+1):
    multiplos.append(numero*i)
  return multiplos

multiplos_hasta_cantidad(5, 10)
```

    [5, 10, 15, 20, 25, 30, 35, 40, 45, 50]

### Divisores de un número
* Son todos los números que son capaces de dividir a un número con división exacta
* La división es exacta cuando el resto es cero
* Ejemplo
 - El 6 es divisor de 30 porque al dividir 30 entre 6 el resultado es 5.0
 - 30 % 6 = 0
 - Los divisores de 30 son: [1, 2, 3, 5, 6, 10, 15, 30]

```python
def divisores(n):
  lista = []
  for i in range(1,n+1):
    if n % i == 0:
      lista.append(i)
  return lista

divisores(30)
```

    [1, 2, 3, 5, 6, 10, 15, 30]

### Máximo común divisor
* Dados variso números, el máximo común divisor de todos ellos se obtiene calculando el máximo de todos los divisores comunes.
* Ejemplo
 - Divisores de 20: 1,2,4,5,10,20
 - Divisores de 12: 1,2,3,4,6,12
 - Divisores comunes: 1,2,4
 - El Máximo Común Divisor es: 4

# Método 1

```python
def divisores(n):
  lista = []
  for i in range(1,n+1):
    if n % i == 0:
      lista.append(i)
  return lista

def mcd(n1,n2):
  d1 = divisores(n1)
  d2 = divisores(n2)
  print(d1)
  print(d2)
  comunes = []
  for i in d1:
    if i in d2:
      comunes.append(i)
  print(comunes)
  return comunes[-1] # retorna el último de los divisores comunes que es el mayor

mcd(60,48)
```

    [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60]
    [1, 2, 3, 4, 6, 8, 12, 16, 24, 48]
    [1, 2, 3, 4, 6, 12]

    12

#### Método 2
* Por fuerza bruta.
* Recorre todos los número posibles hasta encontrar el máximo común divisor
* Recorremos todos los posibles valores para encontrar el divisor común mayor.
* Este método es más eficiente que el anterior (tarda menos).

```python
def mcd(n1, n2):
  for i in range(min(n1,n2), 1, -1): # el bucle es descendente hasta el mínimo de n1 y n2
    if n1%i == 0 and n2%i == 0:      # i es divisor exacto de n1 y n2
      return i                       # se retorna ese i al ser el primer divisor común

print(mcd(60, 48))
print(mcd(1199092733403101, 50420370))
```

    12
    73

#### Método 3 Algoritmo de Euclides
* Si tenemos dos números enteros positivos, a y b, tales que a > b >= 0:
 - a = b*q + r
* entonces:
 - mcd(a,b) = mcd(b,r)
* Ejemplo 1:
 - buscamos el mcd entre 12 y 8
 - 12 = 8*1 + 4
 - mcd(12,8) = mcd(8,4)
* Ejemplo 2: usando segmentos
 - buscamos el mcd entre 16 y 10
 - 16 % 10 = 6
 - 10 % 6 = 4
 - 6 % 4 = 2
 - 4 % 2 = 0 (fin)
 - mcd(16,10)=mcd(10,6)=mcd(6,4)=mcd(4,2)=mcd(2,0)=2
* Ejemplo 3:
 - buscamos el mcd entre 16 y 9
 - 16 % 9 = 7
 - 9 % 7 = 2
 - 7 % 2 = 1
 - 2 % 1 = 0 (fin)
 - mcd(16,9)=mcd(9,7)=mcd(7,2)=mcd(2,1)=mcd(1,0)=1
 - 16 y 9 son "primos entre si" lo que supone que su único divisor común es 1

```python
def mcd_euclides(n1, n2):
  while True:          # repetimos el proceso
    resto = n1 % n2
    if resto == 0:     # hasta que el resto se cero
      return n2        # al llegar a ese punto se retorna el mcd
    else:              # si no se ha llegado a resto cero
      n1 = n2          # n1 pasa a ser n2
      n2 =resto        # n2 toma el valor del resto

print(mcd_euclides(60,48))
```

    12

#### Método 4
Una variante del código anterior, mejorando un poco el estilo.

```python
def mcd_euclides(n1, n2):
  while n1 % n2 != 0:          # repetimos el proceso mientras no se llege a resto cero
    resto = n1 % n2
    n1 = n2
    n2 = resto
  return n2

print(mcd_euclides(60,48))
```

    12

#### Método 5
Una pequeña variación sobre el código anterior.

```python
def mcd_euclides(n1, n2):
  while n1 % n2 != 0:
    n1, n2 = n2, n1%n2
  return n2

print(mcd_euclides(60,48))
```

    12

##### Tiempos
Veamos cuantos segundos tarda el algoritmo de Euclides (método 5) y el de fuerza bruta (método 2) con números grandes.

```python
import time
a =1199092733403101
b = 50420370
t0 = time.time()
fb = mcd(a,b)
t1 = time.time()
t2 = time.time()
eu = mcd_euclides(a,b)
t3 = time.time()
print(fb, t1-t0)
print(eu, t3-t2)
```

    73 6.726821422576904
    73 4.6253204345703125e-05

### Conjetura de Collatz
* Una conjetura es una proposición matemática que se ha comprobado que se cumple para una gran cantidad de casos pero aún no se ha logrado probar que se cumple para todos los casos posibles.
* Si una conjetura no se cumpliera para un solo caso quedaría refutada.
* Si se llegara a probar que esa conjetura es válida para todos los casos posibles pasaría a ser considerada como un teoréma matemático.
* La [conjetura de Collatz](https://es.wikipedia.org/wiki/Conjetura_de_Collatz) nos dice que si partimos de un número n, entero positivo y hacemos las siguientes operaciones recurrentes, siempre se llega a 1.
 - Si n es par se divide entre 2
 - Si n es impar se multiplica por 3 y luego se suma 1
* Ejemplo:
  - n = 13
  - por ser 13 impar: 3 * 13 + 1 = 40
  - por ser 40 par: 40 / 2 = 20
  - por ser 20 par: 20 / 2 = 10
  - por ser 10 par: 10 / 2 =5
  - por ser 5 impar: 5 * 3 + 1 = 16
  - por ser 16 par: 16 / 2 = 8
  - por ser 8 par: 8 / 2 = 4
  - por ser 4 par: 4 / 2 = 2
  - por ser 2 par: 2 / 2 = 1 (ya hemos llegado a 1)
  - La secuencia es: 13, 40, 20, 10, 5, 16, 8, 4, 2, **1**

#### Método 1
Mostramos la secuencia hasta llegar a 1.

```python
def collatz(n):
  sec = [n]
  while n > 1:
    if n % 2:
      n = 3 * n + 1
    else:
      n //= 2    # poniendo división entera no devuelve números float
    sec.append(n)
  return sec

print(collatz(11))
```

    [11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1]

#### Método 2
Comprobamos que se cumple la conjetura para los números hasta un millón.

```python
def collatz(n):
  while n > 1:
    if n % 2:
      n = 3 * n + 1
    else:
      n //= 2
  return n

for i in range(1, 1_000_001):
  if i % 100_000 == 0:
    print("Comprobado hasta el", i)
  if collatz(i) != 1:
    print("No se cumple para el", i)
    break
else:
  print("Se cumple para todos los números del rango indicado.")
```

    Comprobado hasta el 100000
    Comprobado hasta el 200000
    Comprobado hasta el 300000
    Comprobado hasta el 400000
    Comprobado hasta el 500000
    Comprobado hasta el 600000
    Comprobado hasta el 700000
    Comprobado hasta el 800000
    Comprobado hasta el 900000
    Comprobado hasta el 1000000
    Se cumple para todos los números del rango indicado.

### Conjetura de Goldbach
La [Conjetura de Goldbach](https://es.wikipedia.org/wiki/Conjetura_de_Goldbach) dice:
* Todo número par mayor que 2 puede escribirse como suma de dos números primos.
* Hacer un programa que muestre todas las parejas de primos en las que se puede expresar todo número par mayor que 2.
* Ejemplo para el número 14
 - 3+11=14
 - 7+7=14
 - 1+13=14 no vale porque el 1 no se considera primo

#### Método 1

```python
def es_primo(n):
  if n<2:
    return False
  for i in range(2,n):
    if n % i == 0:
      return False
  return True

def lista_primos(n):
  primos = []
  for i in range(2,n):
    if es_primo(i):
      primos.append(i)
  return primos

def parejas(n):
  primos = lista_primos(n)
  listado = []
  for i in primos:
    for j in primos:
      if i+j==n:
        candidato = [i,j]
        candidato.sort()
        if candidato not in listado:
          listado.append([i,j])
  return listado

n = 50
print(lista_primos(n))
print(parejas(n))
```

    [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
    [[3, 47], [7, 43], [13, 37], [19, 31]]

#### Método 2
* Dado el número par n (por ejemplo n = 14)
* Recorremos todos los números primos desde 2 hasta n-1 (hasta 13)
 - Llegamos al primer primo (a) que es 2
 - Si al número n le restamos el primer primo (a), nos quedará otro número (llamado b)
 - Si b es primo, ya tenemos nuestra primera pareja de primos
 - Continuamos con el siguiente primo después del 2, que es 3 y repetimos el proceso
* Al final la parejas se repiten ya que:
 - 3 + 11 = 14
 - 11 + 3 = 14
* Para evitar repetidos añadimos otro condicional para que solo trabaje con números donde a<=b

```python
def es_primo(n):
  if n<2:
    return False
  for i in range(2, n):  # desde 2 hasta n-1
    if n % i == 0:
      return False
  return True

n = 50
for a in range(2,n):
  if es_primo(a):
    b = n - a
    if es_primo(b):
      if a <= b:
        print(f"Pareja de Primos {a} + {b} = {n}")
```

    Pareja de Primos 3 + 47 = 50
    Pareja de Primos 7 + 43 = 50
    Pareja de Primos 13 + 37 = 50
    Pareja de Primos 19 + 31 = 50

#### Método 3
Optimizando el código para evitar que nos den un n impar o un n que no sea mayor que 2.

```python
def es_primo(n):
  if n<2:
    return False
  for i in range(2,n):  # desde 2 hasta n-1
    if n % i == 0:
      return False
  return True

n = 100

if n % 2 == 0 and n > 2:
  print(f"{n} ", end="")
  encontrado = False
  for a in range(2, n):
    if es_primo(a):
      b = n - a
      if es_primo(b):
        encontrado = True
        if a <= b:
          print(f"= {a}+{b} ", end="")
  if not encontrado:
    print("No se ha encontrado ninguna pareja")
else:
  print("No es un número válido")
```

    100 = 3+97 = 11+89 = 17+83 = 29+71 = 41+59 = 47+53


# Significant Digits
The significant digits of a number are those that we know with confidence. Numbers on a computer are not different.

Floating point numbers are represented in the the form
\begin{equation}
m \times b^e
\end{equation}
where $m$ is called the mantissa and represents the digits of the number, $b$ is the base (10 for decial, 2 for binary, 8 for octal...), and $e$ is the exponent.

These numbers are stored in a computer as a series of digits to represent the sign of the number, the mantissa, and the exponent, such that

This representation divides the memory into bits or registers and each digit occupies one of those bits. The digits in a number will occupy the manitssa up to however many places are allowed by the computer. For example, if a computer has a 7 bit representation where 1 bit is reserved for the sign of the number, 2 bits are reserved for the signed exponent, and 4 bits are reserved for the mantissa, then, the number 0.5468113287 is represented as:
++00547

Note how the computer had to round (or chop) the last digit. This chopping or rounding leads to round of errors (discussed later), but keep that in mind for the moment. The implications of this are very important.

## Example 1

When two floating-point numbers are added, the mantissa of the number with the smaller exponent is modified so that the exponents are the same. This will align the decimal points to make addition straightforward. Suppose we wanted to add

$0.1557\times 10^1 + 0.4381 \times 10^{-1}$

on a computer that with a four digit mantissa and a one digit exponent. First the computer will align the number with the smallest exponent to the number with the larger one, such that:

$0.4381 \times 10^{-1} \to 0.004381 \times 10^1$

But this result can't fit on the mantissa, so the number either gets chopped ($0.004381 \to 0.0043$ or rounded $0.004381 \to 0.0044$). Now the addition returns $0.1600\times 10^1$.

In practice, numbers in a computer are represented using a binary system following the same idea for the exponent and mantissa discussed above.

Modern computers can deal with a huge mantissas and can represent very large and very small numbers. For a 32bit representation (default float in Python and Matlab), the mantissa is a whopping 24 bits! But still, round off errors are present!

## Example 2
Let's add a large number to a small number

```python
x = 1e18
x - 1
```

    1e+18

now try to evaluate $ x - x + 1$ and compare that to $x - (x-1)$

```python
x - x + 1
```

    1.0

```python
x - (x-1)
```

    0.0

The impact of the binary representation also has other effects. Fractions for example cannot be fully represented in binary digits and have to be roudned on chopped!
For example, the true value of

$1/10 = 0.1 = 0.000 11 00 11 00 11 00 11 00 11 00 \ldots $

but in practice, on a 32-bit representation, this number gets chopped or rounded. Here's an example of how this could induce error.

## Example 3
In numerical methods, we often have to iterate and repeat calculations hundreds of thousands of times. This leads to accumulation of round off errors. Consider for example evaluating the sum

$\sum_1^{10^5} 10^{-5} = 10^{-5} + 10^{-5} + \ldots + 10^{-5} = 100,000 \times 10^{-5} = 1$

Perform this summation using half (np.float16), single (np.float32), double (np.float64), and triple precision (np.float128)

```python
import numpy as np # we need numpy to define the different floats
x = 0.1
s16 = s32 = s64 = s128 = 0.0
ntotal = 100000
exactval = x*ntotal
for i in range(0,ntotal):
    s16 = s16 + np.float16(x)
    s32 = s32 + np.float32(x)
    s64 = s64 + np.float64(x)
    s128 = s128 + np.float128(x)
print(s16)
print(s32)
print(s64)
print(s128)
```

    9997.55859375
    10000.000149011612
    10000.000000018848
    9999.999999999998721

```python
print((s16 - exactval))
print((s32 - exactval))
print((s64 - exactval))
print((s128 - exactval))
```

    -2.44140625
    0.00014901161193847656
    1.8848368199542165e-08
    -1.2789769243681803346e-12

```python
print(np.float16(1.0/3.0))
```

    0.3333

```python

```


## Problem 1

The transition matrix is given by:
$$
\begin{bmatrix}
1-\alpha & \alpha\\
\beta & 1-\beta
\end{bmatrix}
$$

### Part (a)

$\eta = min(n>0, X_n=1)$ given $X_0=0$

*To Prove* $\eta \sim Geom(\alpha)$

$\eta = P(X_0=0,X_1=0, \dots X_{n-1}=1, X_n=1)$

Using the Markov property this can be written as:

$$
\eta = P(X_0=0)P(X_1=0|X_0=0)P(X_2=0|X_1=0)P(X_3=0|X_2=0) \dots P(X_{n-1}=0|X_{n-2}=0)P(X_{n}=1|X_{n-1}=0)
$$

And being time-homogenous, this simplifies to:

$$
\eta = P(X_0=0)\big(P(X_1=0|X_0)\big)^{n-1}\times P(X_1=1|X_0=0)
$$

$\implies$

$$
\eta = P(X_0=0)\big(1-\alpha)^{n-1}\alpha = \big(1-\alpha)^{n-1}\alpha
$$

And hence $\eta \sim Geom(\alpha)$

### Part (b)

Spectral decomposition of $P$ and value for $P(X_n=1|X_0=0)$

Spectral decomposition of $P$:

$$
det\begin{bmatrix}
\alpha-\lambda & 1-\alpha\\
1-\beta & \beta-\lambda
\end{bmatrix} = 0
$$

$$
\lambda^2 +(\alpha + \beta-2) \lambda + (1-\alpha -\beta) = 0
$$

Thus, $\lambda_1 = 1$ and $\lambda_2 = 1-\alpha-\beta$

Eigenvectors are given by:

$v_1^T = \big( x_1\ x_1 \big)\ \forall\ x_1 \in R$

and for $\lambda_2$ , $v_2 = \big( x_1\ \frac{-\beta x_1}{\alpha} \big)$

Now using Markov property: $P(X_n=1|X_0=0) = (P^n)_{01}$

Now,

$P^n = VD^nV^{-1}$

where:
$$
V = \begin{bmatrix}
1 & 1\\
1 & \frac{-\beta}{\alpha}
\end{bmatrix}
$$

and

$$
D = \begin{bmatrix}
1 & 0 \\
0 & (1-\alpha-\beta)
\end{bmatrix}
$$

$$
V^{-1} = \frac{-1}{\frac{\beta}{\alpha}+1}\begin{bmatrix}
-\frac{\beta}{\alpha} & -1 \\
-1 & 1
\end{bmatrix}
$$

Thus,

$$
P^n = \begin{bmatrix}
1 & 1\\
1 & \frac{-\beta}{\alpha}
\end{bmatrix} \times \begin{bmatrix}
1 & 0 \\
0 & (1-\alpha-\beta)^n
\end{bmatrix} \times \frac{-1}{\frac{\beta}{\alpha}+1}\begin{bmatrix}
-\frac{\beta}{\alpha} & -1 \\
-1 & 1
\end{bmatrix}
$$

$$
P^n = \frac{1}{\alpha+\beta} \begin{bmatrix}
\beta + \alpha(1-\alpha-\beta)^n & \alpha-\alpha(1-\alpha-\beta)^n\\
\beta - \beta(1-\alpha-\beta)^n & \alpha + \beta(1-\alpha-\beta)^n
\end{bmatrix}
$$

## Part (c)

When $\alpha+\beta=1$, the eigen values are $\lambda_1=1$ and $\lambda_2=0$ and hence

$$
P^n = \begin{bmatrix}
\beta & \alpha \\
\beta & \alpha
\end{bmatrix}
$$

*Check:*

Also consider the following identifiy: $P^{n+1}=PP^n$

then:

$$
\begin{bmatrix}
p_{00}^{n+1} & p_{01}^{n+1}\\
p_{10}^{n+1} & p_{11}^{n+1}\\
\end{bmatrix} = \begin{bmatrix}
p_{00}^n & p_{01}^n\\
p_{10}^n & p_{11}^n
\end{bmatrix} \times \begin{bmatrix}
1-\alpha & \alpha\\
\beta & 1-\beta
\end{bmatrix}
$$

$\implies$

$$
\begin{align}
p_{11}^{n+1} &= p_{10}^n(\alpha) + p_{11}^n(1-\beta)\\
 &= (1-p_{11}^n)(\alpha) +(p_{11}^n)(1-\beta)\\
 &= \alpha + (1-\alpha-\beta)p_{11}^n
\end{align}
$$

Consider the recurrence:

$$
x_{n+1} = \alpha+(1-\alpha-\beta)x_n
$$

Constant solution $x_n=x_{n+1}=x$ is given by: $x=\frac{\alpha}{\alpha+\beta}$

Now let $y_n = x_n-x=x_n-\frac{\alpha}{\alpha+\beta}$ then,

$y_{n+1} = (1-\alpha-\beta)y_n$ and hence $y_n=(1-\alpha-\beta)^n y_0$

Thus,
$$p_{11}^{n} = (1-\alpha-\beta)^np_{11}^0 +\frac{\alpha}{\alpha+\beta}$$

Given $P_{00}=\frac{\beta}{\alpha+\beta}$ and $\alpha+\beta=1$ and hence:

$p_{11}^n = \frac{\alpha}{\alpha+\beta} = \alpha$

and hence, $p_{10}^n = \beta$

Similary,

$p_{00}^n = \beta$ and $p_{01}^n = \alpha$

## Problem 2

$P(X_1=0) \frac{\beta}{\alpha+\beta}$ and hence $P(X_1=1) = \frac{\alpha}{\alpha+\beta}$

$X=X_1X_2\dots X_n$ and $Y=Y_1Y_2\dots Y_n$ representes the reverse string $Y_k=X_{n+k-1}$

### Part (a)

Given string of digits: $a_1,a_2,a_3 \dots a_n $ to find: $P(Y_1=a_1,Y_2=a_2,Y_3=a_3\dots Y_n=a_n)$

$$
\begin{align}
P(Y_1=a_1,Y_2=a_2,Y_3=a_3\dots Y_n=a_n) &= P(X_1=a_n,X_2=a_{n-1}, \dots X_n=a_1) \\
&= P(X_1=a_n)P(X_2=a_{n-1}|X_1=a_n)P(X_3=a_{n-2}|X_2=a_{n-1})\dots P(X_n=a_1|X_{n-1}=a_2) \\
&= P(X_1=a_n)(P_{a_n a_{n-1}})(P_{a_{n-1} a_{n-2}}) \dots (P_{a_2 a_1})
\end{align}
$$

The problem asked about not using spectral decomposition, but I was not sure how spectral decomposition would have come in handy if the states $a_i$ are not specified explicitly.

### Part (b)

$$
z=\begin{cases}
X & if \theta = H\\
Y & otherwise
\end{cases}
$$

Given function f such that, $f :\{0,1\}^n \longrightarrow \{H,T\}$
To show: $P(f(Z)=\theta)=0.5$

$P(\theta=H) = P(\theta=T) = 0.5$

Given Z, guess $\theta$:

$P(\theta=H|Z=X) = \frac{P(\theta=H, Z=X)}{P(Z=X)}$

Z, has only two possible values: $H$ and $T$ and hence assuming the guess function is unbiased:

$P(f(Z)=H) = P(f(Z)=T)=0.5$

## Problem 3

$$
\tau = min\{ n \geq 0: X_n=\dagger\}
$$

$$
E[\tau] = E_a[E_a[\tau|X_n=a]\ where\ a \in \{\phi, \alpha, \beta, \alpha+\beta, pol, \dagger\}
$$

Let $S=\{\phi, \alpha, \beta, \alpha+\beta, pol, \dagger\}$

Consider for $a\neq \dagger$:
$$
h(a) = E[\tau|X_0=a] = \sum_{s \in S}P_{as} \times (1) + P_{as}\times E[\tau|X_0=s)
$$

$\implies$

$$
h(a) = ((I-P_{-})^{-1})_a
$$

where $P_{-}$ represents the matrix with the row and column representng $X_i=\dagger$ removed.

```python
%matplotlib inline
from __future__ import division
import numpy as np
from numpy import linalg as LA
k_a=0.2
k_b=0.2
k_p = 0.5
P = np.matrix([[1-k_a-k_b, k_a ,k_b, 0, 0, 0],
               [k_a, 1-k_a-k_b, 0, k_b, 0, 0],
               [k_b, 0, 1-k_a-k_b, k_a, 0, 0],
               [0, k_b, k_a, 1-k_a-k_b-k_p, k_p, 0],
               [0, 0, 0, 0, 0, 1],
               [0, 0, 0, 1, 0, 0]])
q = [[k_a-k_b,k_a,k_b,0,0],
     [k_a,k_a+k_b,0,k_b,0],
     [k_b,0,k_a+k_b,0,0],
     [0,k_b,k_a,k_a+k_b+k_p,k_p],
     [0,0,0,0,0]]
qq = np.array(q)

print(P)
states = ['phi', 'alpha', 'beta', 'ab', 'pol', 'd']

```

    [[ 0.6  0.2  0.2  0.   0.   0. ]
     [ 0.2  0.6  0.   0.2  0.   0. ]
     [ 0.2  0.   0.6  0.2  0.   0. ]
     [ 0.   0.2  0.2  0.1  0.5  0. ]
     [ 0.   0.   0.   0.   0.   1. ]
     [ 0.   0.   0.   1.   0.   0. ]]

```python
import networkx as nx

G=nx.from_numpy_matrix(P,create_using=nx.MultiDiGraph())
G.edges(data=True)
#nx.draw_graphviz(G)# labels=states)
nx.write_dot(G,'G.dot')
```

```python
!neato -T png G.dot > multi.png
```

The markov chain seems to be irreducible
One way to obtain the stationary state is to look at the eigen vectors correspendoing to the eigen value of 1. However, the eigen vectors come out to be imaginary. This seemed to be an issue wwith the solver so I relied on solving the system of equation: $\pi = P\pi$

```python
w, v = LA.eig(P)
for i in range(0,6):
    print 'Eigen value: {}\n Eigen vector: {}\n'.format(w[i],v[:,i])
```

    Eigen value: (-0.382094716832+0.673687371073j)
     Eigen vector: [[ 0.01610056-0.01846064j]
     [-0.00843895+0.07244209j]
     [-0.00843895+0.07244209j]
     [-0.21867795-0.36569039j]
     [ 0.71032012+0.j        ]
     [-0.27140957+0.4785337j ]]

    Eigen value: (-0.382094716832-0.673687371073j)
     Eigen vector: [[ 0.01610056+0.01846064j]
     [-0.00843895-0.07244209j]
     [-0.00843895-0.07244209j]
     [-0.21867795+0.36569039j]
     [ 0.71032012-0.j        ]
     [-0.27140957-0.4785337j ]]

    Eigen value: (1+0j)
     Eigen vector: [[ 0.40824829+0.j]
     [ 0.40824829+0.j]
     [ 0.40824829+0.j]
     [ 0.40824829+0.j]
     [ 0.40824829+0.j]
     [ 0.40824829+0.j]]

    Eigen value: (0.755104836853+0j)
     Eigen vector: [[-0.49079708+0.j]
     [-0.19031250+0.j]
     [-0.19031250+0.j]
     [ 0.34320513+0.j]
     [ 0.60192068+0.j]
     [ 0.45451322+0.j]]

    Eigen value: (0.309084596812+0j)
     Eigen vector: [[-0.63710764+0.j]
     [ 0.46336106+0.j]
     [ 0.46336106+0.j]
     [-0.03688671+0.j]
     [-0.38611374+0.j]
     [-0.11934181+0.j]]

    Eigen value: (0.6+0j)
     Eigen vector: [[  3.12250226e-17+0.j]
     [  7.07106781e-01+0.j]
     [ -7.07106781e-01+0.j]
     [ -3.12250226e-17+0.j]
     [ -8.67361738e-17+0.j]
     [ -5.20417043e-17+0.j]]

```python
## Solve for (I-Q)^{-1}
iq = np.linalg.inv(np.eye(5)-qq)
iq_phi = iq[0,0]
iq_alpha = iq[1,1]
iq_beta = iq[2,2]
iq_alphabeta = iq[3,3]
iq_pol = iq[4,4]

```

EDIT: I made correction to solve for corrected $\pi$, by acounting for $P^T$ and not $P$

```python
A = np.eye(6)-P.T
A[-1,:] = [1,1,1,1,1,1]

B = [0,0,0,0,0,1]
X=np.linalg.solve(A,B)
print(X)

```

    [ 0.2  0.2  0.2  0.2  0.1  0.1]

Stationary state is given by $\pi = (0.1667, 0.1667, 0.1667, 0.1667, 0.1667, 0.1667)$ The mean number of visits per unit time to $\dagger$ are $\frac{1}{\pi_6} = 6$ However strangely this does not satisfy $\pi=P\pi$. I was not able to figure out where I went wrong.

EDIT: I made correction to solve for corrected $\pi$, by acounting for $P^T$ and not $P$, so this no longer holds

```python
#EDIT: I made correction to solve for corrected $\pi$, by acounting for $P^T$ and not $P$
print('\pi*P={}\n'.format(X*P))
print('But \pi={}'.format(X))
```

    \pi*P=[[ 0.2  0.2  0.2  0.2  0.1  0.1]]

    But \pi=[ 0.2  0.2  0.2  0.2  0.1  0.1]

Simulating the chain:

General strategy: Generate a random number $\longrightarrow$ Select a state $\longrightarrow$ Jump to state $\longrightarrow$ Repeat

```python
## phi
np.random.seed(1)
```

```python
PP = {}
PP['phi']= [1-k_a-k_b, k_a ,k_b, 0, 0, 0]
PP['alpha'] = [k_a, 1-k_a-k_b, 0, k_b, 0, 0]
PP['beta'] = [k_b, 0, 1-k_a-k_b, k_a, 0, 0]
PP['ab']= [0, k_b, k_a, 1-k_a-k_b-k_p, k_p, 0]
PP['pol']=  [0, 0, 0, 0, 0, 1]
PP['d']= [0, 0, 0, 1, 0, 0]
```

```python
##For $h(\phi)$
x0='phi'
x='phi'
def h(x):
    s=0
    new_state=x
    for i in range(1,1000):
        old_state=new_state
        probs = PP[old_state]
        z=np.random.choice(6, 1, p=probs)
        new_state = states[z[0]]
        #print('{} --> {}'.format(old_state, new_state))
        s+=z[0]
    return s/1000

```

## Part (a,b,c)

```python
print(r'$h(\phi)$: From simulation: {}; From calculation: {}'.format(h('phi'),iq_phi))

```

    $h(\phi)$: From simulation: 2.199; From calculation: 1.66666666667

```python
print(r'$h(\alpha)$: From simulation: {}; From calculation: {}'.format(h('alpha'),iq_alpha))

```

    $h(\alpha)$: From simulation: 2.364; From calculation: 7.77777777778

```python
print(r'$h(\beta)$: From simulation: {}; From calculation: {}'.format(h('beta'),iq_beta))

```

    $h(\beta)$: From simulation: 1.988; From calculation: 2.22222222222

```python
print(r'$h(\alpha+\beta)$: From simulation: {}; From calculation: {}'.format(h('ab'),iq_alphabeta))

```

    $h(\alpha+\beta)$: From simulation: 2.189; From calculation: 43.3333333333

```python
print(r'$h(\pol)$: From simulation: {}; From calculation: {}'.format(h('pol'),iq_pol))

```

    $h(\pol)$: From simulation: 2.031; From calculation: 1.0

```python
old_state = [0.1,0.2,0.3,0.4,0,0]

def perturb(old_state):
    new_state = old_state*P
    return new_state
new_state = [0,0,0,0,0,1]

while not np.allclose(old_state, new_state):
    old_state, new_state = new_state, perturb(old_state)

print old_state

```

    [[ 0.19999998  0.20000004  0.20000004  0.20000002  0.1000002   0.09999973]]

```python
# EDIT: I made correction to solve for corrected $\pi$, by acounting for $P^T$ and not $P$
print('From calculation(which is NO LONGER wrong!), stationary distribution:{}'.format(X))

print('From simulation, stationary distribution: {}'.format(old_state))
```

    From calculation(which is NO LONGER wrong!), stationary distribution:[ 0.2  0.2  0.2  0.2  0.1  0.1]
    From simulation, stationary distribution: [[ 0.19999998  0.20000004  0.20000004  0.20000002  0.1000002   0.09999973]]

```python

```


# Local search

In unconstrained optimization, we wish to solve problems of the form
\begin{align}
\text{minimize} & & E(w)
\end{align}

* The local search algorithms have the form :
\begin{align}
w_0 & = \text{some initial value} \\
\text{for}\;\; & \tau = 1, 2,\dots \\
& w_\tau = w_{\tau-1} + g_\tau
\end{align}

Here, $g_\tau$ is a search direction. The loop is executed until a convergence condition is satisfied or
the maximum number of iterations is reached. The algorithm iteratively search for solutions that achieve a lower objective value by moving in the search direction.

# Gradient Descent
* Gradient descent is a popular local search method with the search direction chosen as the negative gradient direction:
\begin{align}
g_\tau & =  - \eta \nabla E(w_{\tau-1})
\end{align}

* When the gradient vanishes, i.e., $\nabla E(w) = 0$, the algorithm does not make any progress. Such points are also called fixed points.

* The iterates, under certain conditions, converge to the minimum $w^* = \arg\min_{w} E(w)$. A natural question here finding the conditions for guaranteed convergence to a fixed point and the rate -- how fast convergence happens as a function of iterations

* The parameter $\eta$ is called the *learning rate*, to be chosen depending on the problem. If the learning rate is not properly chosen, the algorithm can (and will) diverge.

* There is a well developed theory on how to choose $\eta$ adaptively to speed up covergence.

* Even for minimizing quadratic objective functions, or equivalently for solving linear systems, gradient descent can have a quite poor converge properties: it takes a lot of iterations to find the minimum. However, it is applicable as a practical method in many problems as it requires only the calculation of the gradient.

* For maximization problems
\begin{align}
\text{maximize} & & E(w)
\end{align}
we just move in the direction of the gradient so the search direction is $g_\tau = \eta \nabla E(w_{\tau-1})$

```python
%matplotlib inline
import numpy as np
import matplotlib as mpl
import matplotlib.pylab as plt

from ipywidgets import interact, interactive, fixed
import ipywidgets as widgets
from IPython.display import clear_output, display, HTML
from matplotlib import rc

mpl.rc('font',**{'size': 20, 'family':'sans-serif','sans-serif':['Helvetica']})
mpl.rc('text', usetex=True)

import time
import numpy as np

y = np.array([7.04, 7.95, 7.58, 7.81, 8.33, 7.96, 8.24, 8.26, 7.84, 6.82, 5.68])
x = np.array(np.linspace(-1,1,11))
N = len(x)

# Design matrix
#A = np.vstack((np.ones(N), x, x**2, x**3)).T
degree = 9
A = np.hstack([np.power(x.reshape(N,1),i) for i in range(degree+1)])

# Learning rate
eta = 0.001

# initial parameters
w = np.array(np.random.randn(degree+1))
W = []
Err = []
for epoch in range(50000):
    # Error
    err = y-A.dot(w)

    # Total error
    E = np.sum(err**2)/N

    # Gradient
    dE = -2.*A.T.dot(err)/N

    if epoch%100 == 0:
        #print(epoch,':',E)
        # print(w)
        W.append(w)
        Err.append(E)

    # Perfom one descent step
    w = w - eta*dE
```

The following cell demonstrates interactively the progress of plain gradient descent
and how its solution differs from the optimum found by solving the corresponding least squares problem.

```python

fig = plt.figure(figsize=(5,5))

left = -1.5
right = 1.5
xx = np.linspace(left,right,50)
AA = np.hstack((np.power(xx.reshape(len(xx),1),i) for i in range(degree+1)))

# Find best
A_orth, R = np.linalg.qr(A)
w_orth, res, rank, s = np.linalg.lstsq(A_orth, y)
w_star = np.linalg.solve(R, w_orth)
yy = AA.dot(w_star)

#ax.set_xlim((2,15))

#dots = plt.Line2D(x,y, linestyle='', markerfacecolor='b',marker='o', alpha=0.5, markersize=5)
#ax.add_line(dots)
plt.plot(x,y, linestyle='', markerfacecolor='b',marker='o', alpha=0.5, markersize=5)
plt.plot(xx, yy, linestyle=':', color='k', alpha=0.3)
ln = plt.Line2D(xdata=[], ydata=[], linestyle='-',linewidth=2)

ax = fig.gca()
ax.add_line(ln)
plt.close(fig)

ax.set_xlim((left,right))
ax.set_ylim((5,9))

def plot_gd(iteration=0):
    w = W[iteration]
    f = AA.dot(w)
    #print(w)
    ln.set_ydata(f)
    ln.set_xdata(xx)

    ax.set_title('$E = '+str(Err[iteration])+'$')

    display(fig)

res = interact(plot_gd, iteration=(0,len(W)-1))

```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

Plotting the Error Surface

```python
%matplotlib inline

import scipy as sc
import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pylab as plt

df_arac = pd.read_csv(u'data/arac.csv',sep=';')
#df_arac[['Year','Car']]

BaseYear = 1995
x = np.matrix(df_arac.Year[0:]).T-BaseYear
y = np.matrix(df_arac.Car[0:]).T/1000000.

plt.plot(x+BaseYear, y, 'o-')
plt.xlabel('Yil')
plt.ylabel('Araba (Millions)')

plt.show()
```

```python
from itertools import product

def Error_Surface(y, A, left=0, right=1, bottom=0, top=1, step=0.1):
    W0 = np.arange(left,right, step)
    W1 = np.arange(bottom,top, step)

    ErrSurf = np.zeros((len(W1),len(W0)))

    for i,j in product(range(len(W1)), range(len(W0))):
        e = y - A*np.matrix([W0[j], W1[i]]).T
        ErrSurf[i,j] = e.T*e/2

    return ErrSurf
```

```python
BaseYear = 1995
x = np.matrix(df_arac.Year[0:]).T-BaseYear
y = np.matrix(df_arac.Car[0:]).T/1000000.

# Setup the vandermonde matrix
N = len(x)
A = np.hstack((np.ones((N,1)), x))

left = -5
right = 15
bottom = -4
top = 6
step = 0.05
ErrSurf = Error_Surface(y, A, left=left, right=right, top=top, bottom=bottom)

plt.figure(figsize=(10,10))
#plt.imshow(ErrSurf, interpolation='nearest',
#           vmin=0, vmax=10000,origin='lower',
#           extent=(left,right,bottom,top), cmap='jet')

plt.contour(ErrSurf,
            vmin=0, vmax=10000,origin='lower', levels=np.linspace(100,5000,10),
            extent=(left,right,bottom,top), cmap='jet')

plt.xlabel('$w_0$')
plt.ylabel('$w_1$')
plt.title('Error Surface')
#plt.colorbar(orientation='horizontal')
plt.show()
```

### Animation of Gradient descent

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

import time
from IPython import display
import numpy as np

# Setup the Design matrix
N = len(x)
A = np.hstack((np.ones((N,1)), x))

# Starting point
w = np.matrix('[15; -6]')

# Number of iterations
EPOCH = 200

# Learning rate: The following is the largest possible fixed rate for this problem
#eta = 0.0001696
eta = 0.0001696

fig = plt.figure()
ax = fig.gca()

plt.plot(x+BaseYear, y, 'o-')

plt.xlabel('x')
plt.ylabel('y')

f = A.dot(w)
ln = plt.Line2D(xdata=x+BaseYear, ydata=f, linestyle='-',linewidth=2,color='red')
ax.add_line(ln)

for epoch in range(EPOCH):
    f = A.dot(w)
    err = y-f

    ln.set_xdata(x)
    ln.set_ydata(f)

    E = np.sum(err.T*err)/2
    dE = -A.T.dot(err)

#    if epoch%1 == 0:
#        print(epoch,':',E)
        # print(w)

    w = w - eta*dE

    ax.set_title(E)
    display.clear_output(wait=True)
    display.display(plt.gcf())
    time.sleep(0.1)
```

```python
# An implementation of Gradient Descent for solving linear a system

# Setup the Design matrix
N = len(x)
A = np.hstack((np.ones((N,1)), x))

# Starting point
w = np.matrix('[15; -6]')

# Number of iterations
EPOCH = 5000

# Learning rate: The following is the largest possible fixed rate for this problem
#eta = 0.00016
eta = 0.000161

Error = np.zeros((EPOCH))
W = np.zeros((2,EPOCH))

for tau in range(EPOCH):
    # Calculate the error
    e = y - A*w

    # Store the intermediate results
    W[0,tau] = w[0]
    W[1,tau] = w[1]
    Error[tau] = (e.T*e)/2

    # Compute the gradient descent step
    g = -A.T*e
    w = w - eta*g
    #print(w.T)

w_star = w
plt.figure(figsize=(8,8))
plt.imshow(ErrSurf, interpolation='nearest',
           vmin=0, vmax=1000,origin='lower',
           extent=(left,right,bottom,top))
plt.xlabel('w0')
plt.ylabel('w1')

ln = plt.Line2D(W[0,:300:1], W[1,:300:1], marker='o',markerfacecolor='w')
plt.gca().add_line(ln)

ln = plt.Line2D(w_star[0], w_star[1], marker='x',markerfacecolor='w')
plt.gca().add_line(ln)
plt.show()

plt.figure(figsize=(8,3))
plt.semilogy(Error)
plt.xlabel('Iteration tau')
plt.ylabel('Error')
plt.show()

```

* The illustration shows the convergence of GD with learning rate near the limit where the convergence is oscillatory.

* $\eta$, Learning rate is a parameter of the algorithm

* $w$, the variable are the parameters of the Model

* $y$: Targets

* $x$: Inputs,

# Accelerating Gradient descent

## Momentum methods, a.k.a., heavy ball

\begin{align}
p(\tau) & =  \nabla E(w(\tau-1)) + \beta p(\tau-1) \\
w(\tau) & =  w(\tau-1) - \alpha p(\tau)
\end{align}

When $\beta=0$, we recover gradient descent.

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

from notes_utilities import pnorm_ball_line

import time
from IPython import display
import numpy as np

#y = np.array([7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42, 5.73])
y = np.array([8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68])
#y = np.array([9.14, 8.14, 8.74, 8.77, 9.26, 8.10, 6.13, 3.10, 9.13, 7.26, 4.74])
x = np.array([10., 8., 13., 9., 11., 14., 6., 4., 12., 7., 5.])
N = len(x)

# Design matrix
A = np.vstack((np.ones(N), x)).T

w_best, E, rank, s = np.linalg.lstsq(A, y)
err = y-A.dot(w_best)
E_min = np.sum(err**2)/N

def inspect_momentum(alpha = 0.005, beta = 0.97):
    ln = pnorm_ball_line(mu=w_best, A=np.linalg.cholesky(np.linalg.inv(A.T.dot(A))),linewidth=1)
    ln2 = pnorm_ball_line(mu=w_best, A=4*np.linalg.cholesky(np.linalg.inv(A.T.dot(A))),linewidth=1)

    # initial parameters
    w0 = np.array([2., 1.])
    w = w0.copy()
    p = np.zeros(2)

    EPOCHS = 100
    W = np.zeros((2,EPOCHS))
    for epoch in range(EPOCHS):
        # Error
        err = y-A.dot(w)
        W[:,epoch] = w
        # Mean square error
        E = np.sum(err**2)/N

        # Gradient
        dE = -2.*A.T.dot(err)/N
        p = dE + beta*p

#        if epoch%10 == 1:
#            print(epoch,':',E)
            # print(w)

        # Perfom one descent step
        w = w - alpha*p

#    print(E_min)

    plt.plot(W[0,:],W[1,:],'.-b')
    plt.plot(w_best[0],w_best[1],'ro')
    plt.plot(w0[0],w0[1],'ko')
    plt.xlim((1.8,4.3))
    plt.ylim((0,1.2))
    plt.title('$\\alpha = $'+str(alpha)+' $\\beta = $'+str(beta))
    plt.gca().add_line(ln)
    plt.gca().add_line(ln2)
    plt.show()

inspect_momentum(alpha=0.0014088, beta=0.95)

```

```python
%matplotlib inline
from __future__ import print_function
from ipywidgets import interact, interactive, fixed
import ipywidgets as widgets
import matplotlib.pylab as plt
from IPython.display import clear_output, display, HTML

interact(inspect_momentum, alpha=(0, 0.02, 0.001), beta=(0, 0.99, 0.001))
```

Failed to display Jupyter Widget of type interactive.

  If you're reading this message in Jupyter Notebook or JupyterLab, it may mean
  that the widgets JavaScript is still loading. If this message persists, it
  likely means that the widgets JavaScript library is either not installed or
  not enabled. See the Jupyter
  Widgets Documentation for setup instructions.

  If you're reading this message in another notebook frontend (for example, a static
  rendering on GitHub or NBViewer),
  it may mean that your frontend doesn't currently support widgets.

    <function __main__.inspect_momentum>

# Advanced Material

Why does momentum work? by Goh. Interactive articles
https://distill.pub/2017/momentum/

Beyond vanilla Gradient Descent, Sebastian Ruder blog
http://ruder.io/optimizing-gradient-descent/

Blog post by M. Hardt
http://blog.mrtz.org/2013/09/07/the-zen-of-gradient-descent.html

A great talk by Ben Recht:
https://simons.berkeley.edu/talks/ben-recht-2013-09-04

Backpropagation, by blog post by S. Arora
http://www.offconvex.org/2016/12/20/backprop/

## Analysis of convergence of Gradient descent for a quadratic function

Recall that the error function we minimize is
$$
E(w) = \frac{1}{2} (y-Aw)^T(y-Aw) = \frac{1}{2}(y^\top y - 2 y^\top A w + w^\top A^\top A w)
$$

The gradient at the point $w$ will be denoted as $\nabla E(w) = g(w)$ where
$$g(w) = -A^\top (y - Aw) = A^\top A w - A^\top y$$

Moreover, the gradient at the minimum will vanish:
$$
g(w_\star) = 0
$$
Indeed, we can solve
$$0 = A^\top (Aw_\star - y)$$
as
$$w_\star =  (A^\top A)^{-1}A^\top y  $$
but this is not our point.

For a constant learning rate $\eta$, gradient descent executes the following iteration
$$
w_t = w_{t-1} - \eta g(w_{t-1}) = w_{t-1} - \eta A^\top (Aw_{t-1} - y)
$$

$$
w_t  =  (I - \eta A^\top A) w_{t-1} + \eta A^\top y
$$

This is a fixed point equation of form
$$
w_t  =  T(w_{t-1})
$$
where $T$ is an affine transformation.

We will assume that $T$ is a contraction, i.e. for any two different
parameters $w$ and $w'$ in the domain we have
$$
\| T(w) - T(w') \| \leq L_\eta \|w-w' \|
$$

where $L_\eta < 1$, then the distance shrinks. Hence the mapping converges to a fixed point (this is a consequence of a deeper result in analysis called the Brouwer fixed-point theorem (https://en.0wikipedia.org/wiki/Brouwer_fixed-point_theorem))

We will consider in particular the distance between the optimum and the current point $w(t)$

$$
\| T(w_t) - T(w_\star) \| \leq L_\eta \|w_t - w_\star \|
$$
But we have
$T(w_\star) = w_\star$ and $w_t = T(w_{t-1})$ so
$\|w_t - w_\star \| = \|T(w_{t-1}) - T(w_\star) \|$.

\begin{align}
\| T(w_t) - T(w_\star) \| & \leq L_\eta \|T(w_{t-1}) - T(w_\star) \| \\
& \leq L^2_\eta \|T(w_{t-2}) - T(w_\star) \| \\
\vdots \\
& \leq L^{t+1}_\eta \| w_{0} - w_\star \|
\end{align}

$$
T(w)   =  (I - \eta A^\top A) w + \eta A^\top y
$$

$$
T(w_\star)   =  (I - \eta A^\top A) w_\star + \eta A^\top y
$$

$$
\| T(w) - T(w') \| = \| (I - \eta A^\top A) (w-w') \| \leq \| I - \eta A^\top A \| \| w-w' \|
$$

When the norm of the matrix $\| I - \eta A^\top A \| < 1$ we have convergence. Here we take the operator norm, i.e., the magnitude of the largest eigenvalue.

Below, we plot the absolute value of the maximum eigenvalues of $I - \eta A^\top A$ as a function of $\eta$.

```python

left = 0.0000
right = 0.015

N = 1000
ETA = np.linspace(left,right,N)

def compute_largest_eig(ETA, A):

    LAM = np.zeros(N)
    D = A.shape[1]
    n = A.shape[0]

    for i,eta in enumerate(ETA):
        #print(eta)
        lam,v = np.linalg.eig(np.eye(D) - 2*eta*A.T.dot(A)/n)
        LAM[i] = np.max(np.abs(lam))

    return LAM

# This number is L_\eta
LAM = compute_largest_eig(ETA, A)

plt.plot(ETA, LAM)
#plt.plot(ETA, np.ones((N,1)))
#plt.gca().set_ylim([0.98, 1.02])
plt.ylim([0.997,1.01])
plt.xlabel('eta')
plt.ylabel('absolute value of the largest eigenvalue')
plt.show()

```

```python
plt.semilogy(ETA,LAM)
plt.ylim([0.997,1])
plt.show()
```

If $E$ is twice differentiable, contractivity means that $E$ is convex.

For $t>0$
\begin{align}
\|T(x + t \Delta x) - T(x) \| & \leq \rho \|t \Delta x\| \\
\frac{1}{t} \|T(x + t \Delta x) - T(x) \| &\leq \rho \|\Delta x\|
\end{align}

If we can show that $\rho< 1$, then $T$ is a contraction.

By definitions
$$
T(x)  = x - \alpha \nabla E(x)
$$

$$
T(x + t \Delta x)  = x + t \Delta x - \alpha \nabla E(x + t \Delta x)
$$

\begin{align}
\frac{1}{t} \|T(x + t \Delta x) - T(x) \| & = \frac{1}{t} \|x + t \Delta x - \alpha \nabla E(x + t \Delta x) - x + \alpha \nabla E(x) \| \\
& = \| \Delta x - \frac{\alpha}{t}  (\nabla E(x + t \Delta x) - \nabla E(x) ) \|  \\
\end{align}

As this relation holds for all $t$, we take the limit when $t\rightarrow 0^+$

\begin{align}
\| \Delta x - \alpha \nabla^2 E(x) \Delta x  \| & =  \| (I - \alpha \nabla^2 E(x)) \Delta x  \| \\
& \leq \| I - \alpha \nabla^2 E(x) \| \| \Delta x  \|
\end{align}

If we can choose $\alpha$ for all $\xi$ in the domain such that
$$
\| I - \alpha \nabla^2 E(\xi) \| \leq \rho < 1
$$
is satisfied, we have a sufficient condition for a contraction.

Lemma:

Assume that for $0 \leq \rho < 1$, $\alpha> 0$ and $U(\xi)$ is a symmetric matrix valued function for all $\xi \in \mathcal{D}$ and we have
$$
\| I - \alpha U(\xi) \| \leq \rho
$$
then $U = U(\xi)$ is positive semidefinite with $$\frac{1 - \rho}{\alpha} I \preceq U $$ for every $\xi$.

Proof:

$$
\|I - \alpha U \| = \sup_{x\neq 0} \frac{x^\top(I - \alpha U )x }{x^\top x} \leq \rho
$$

$$
x^\top(I - \alpha U )x \leq \rho x^\top x
$$

$$
(1- \rho) x^\top x  \leq \alpha x^\top U x
$$

This implies that for all $x$ we have
$$
0 \leq x^\top (U - \frac{1 - \rho}{\alpha} I) x
$$
In other words, the matrix $U - \frac{1 - \rho}{\alpha} I$ is positive semidefinite, or:

$$
\frac{1 - \rho}{\alpha} I \preceq U
$$
We now see that $\rho<1$ we have the guarantee that $U$ is positive semidefinite.

$$
T(x) = M x + b
$$

$$
\|T(x) - T(x_\star) \| = \|Mx + b - M x_\star + b \| = \| M(x-x_\star) \|
$$

By Schwarz inequality

$$
\|T(x) - T(x_\star) \| \leq \|M\| \|x-x_\star\|
$$
If $\|M\| < 1$, we have a contraction. Assume the existence of a fixed point $x_\star$ such that $x_\star = T(x_\star)$. (Does a fixed point always exist for a contraction?)

```python
# Try to fit with GD to the original data
BaseYear2 = 0
x2 = np.matrix(df_arac.Year[31:]).T-BaseYear2

# Setup the vandermonde matrix
N = len(x2)
A = np.hstack((np.ones((N,1)), x2))

left = -8
right = -7.55
N = 100
ETA = np.logspace(left,right,N)

LAM = compute_largest_eig(ETA, A)

plt.plot(ETA, LAM)
plt.plot(ETA, np.ones((N,1)))
plt.gca().set_ylim([0.98, 1.02])
plt.xlabel('eta')
plt.ylabel('absolute value of the largest eigenvalue')
plt.show()

```

Analysis of Momentum

\begin{align}
p(\tau) & =  \nabla E(w(\tau-1)) + \beta p(\tau-1) \\
w(\tau) & =  w(\tau-1) - \alpha p(\tau) \\
w(\tau-1) & =  w(\tau-2) - \alpha p(\tau-1) \\
\end{align}

\begin{align}
\left(\begin{array}{c}
w(\tau) \\
w(\tau-1)
\end{array}
\right)
& = &\left(\begin{array}{cc}
\cdot & \cdot \\
\cdot & \cdot
\end{array}
\right)
\left(\begin{array}{c}
w(\tau-1) \\
w(\tau-2)
\end{array}
\right)
\end{align}

\begin{align}
\frac{1}{\alpha}(w(\tau-1) - w(\tau))  & =  p(\tau) =  \nabla E(w(\tau-1)) + \beta \frac{1}{\alpha}(w(\tau-2) - w(\tau-1)) = \\
\frac{1}{\alpha}(w(\tau-2) - w(\tau-1))  & =  p(\tau-1) \\
\end{align}

\begin{align}
\frac{1}{\alpha}(w(\tau-1) - w(\tau))  & = \nabla E(w(\tau-1)) + \beta \frac{1}{\alpha}(w(\tau-2) - w(\tau-1)) \\
  w(\tau)  & = -\alpha \nabla E(w(\tau-1)) - \beta w(\tau-2) + (\beta+1)  w(\tau-1)
\end{align}

* Note that GD is sensetive to scaling of data
* For example, if we would not have shifted the $x$ axis our original data, GD might not have worked. The maximum eigenvalue is very close to $1$ for all $\eta$ upto numerical precision

```python
w = 7
alpha = 0.7
EPOCH = 100
W = []
for tau in range(EPOCH):
    W.append(w)
    w = w - alpha*(2*w - 4)

plt.plot(W)
plt.show()
```

```python
w = 7
alpha = 0.1
beta = 0.95
p = 0
EPOCH = 1000
W = []
for tau in range(EPOCH):
    W.append(w)
    p = (2*w - 4) + beta*p
    w = w - alpha*p

plt.plot(W)
plt.show()
```


```python
import sympy as sp

print(f"Sympy Version: {sp.__version__}")

# 数式をキレイに表示する
sp.init_printing()
```

    Sympy Version: 1.8

### 極限を計算する

- 極限は `sympy.limit()`を使用することで計算できる。
- 無限は、`sympy.oo`で扱うことが出来る

```python
x, y, z = sp.symbols('x y z')
x, y, z
```

```python
exp = sp.sin(x) / x
exp
```

```python
sp.limit(exp, x, 0)
```

```python
sp.limit(exp, x, sp.oo)
```

### 微分を計算する

- 微分は, `sympy.diff()`で計算できる。
- また、`sympy.Derivative`で微分を定義することもできる。
  + 微分を実行する際は、`sympy.Derivative.doit()`を使う

```python
exp = x ** 2 * sp.sin(x)
exp
```

```python
sp.diff(exp, x)
```

```python
# 2階微分
sp.diff(exp, x, 2)
```

```python
exp = sp.sin(x) * sp.cos(y)
exp
```

```python
# xで偏微分
sp.diff(exp, x)
```

```python
# yで偏微分
sp.diff(exp, y)
```

```python
# xで偏微分 → yで偏微分
sp.diff(exp, x, y)
```

```python
deriv = sp.Derivative(sp.sin(x) * sp.cos(y), x, y)
deriv
```

```python
deriv.doit()
```

```python
# 微分後の式に値を代入
deriv.doit().subs({x: 0, y: sp.pi / 2.0})
```

### テーラー展開を実行する

- `sympy.series()`でテーラー展開することができる。
- 数式のオブジェクトに対して、'数式.series()'を呼び出すことでも、テーラー展開できる。

```python
sp.series(sp.log(x + 1), x)
```

```python
# 数式オブジェクトに対してもテーラー展開できる
sp.log(x + 1).series(x)
```

```python
# テーラー展開の次数も指定可能
sp.series(sp.log(x + 1), x, 0, 3)
```

### 積分を実行する

- 積分は、`sympy.integrate()`で実行できる。
  + 積分範囲積分範囲を指定しない場合は、不定積分として扱われる。
  + 積分範囲ありの場合は、定積分となる。
- `sympy.Integral`を使用することで、積分を定義することができる。
  + 積分の実行は、`sympy.Integral.doit()`で行うことができる。

```python
exp = x ** 2 + 2 * x - 3
exp
```

```python
# 不定積分
sp.integrate(exp, x)
```

```python
# 定積分
sp.integrate(exp, (x, 0, 1))
```

```python
exp = sp.exp(-x ** 2)

# 積分の定義 (この段階では積分は実行されない)
integ = sp.Integral(exp, (x, -sp.oo, sp.oo))
integ
```

```python
integ.doit()
```

### 微分方程式を解く

- 微分方程式のために、`sympy.Function`で関数を定義する必要がある。
- `sympy.dsolve()`で微分方程式を解くことが出来る。

```python
x = sp.Symbol('x')
f = sp.Function('f')
f(x)
```

```python
# 微分の定義
fx = sp.diff(f(x), x)
fx
```

```python
# 微分方程式の作成
equation = sp.Eq(fx, 1 / (1 + x))
equation
```

```python
sp.dsolve(equation)
```

```python
# 2階の微分方程式
t = sp.Symbol('t')
x = sp.Function('x')
lhs = sp.diff(x(t), t, 2)
rhs = -1 * x(t)

equation = sp.Eq(lhs, rhs)
equation
```

```python
ans = sp.dsolve(equation)
ans
```

```python
# 積分定数に値を代入する
C1, C2 = sp.symbols('C1 C2')
ans.subs({C1: 2.0})
```

```python

```


```python
# Importing the necessary packages
from sympy import *
import matplotlib.pyplot as plt
```

```python
r_1,r_2,n = symbols('r_1,r_2,n')       # Defining new symbols. You can use LaTeX here

M = Matrix([[1-r_1,r_2],[r_1,1-r_2]])  # Defining the transition matrix

display(M)                             # Display the answer (it's like printing, for symbolic objects)
```

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

```python
# Numerical values of parameters ############

rONE=0.3
rTWO=0.2

#############################################

M = M.subs(r_1,rONE).subs(r_2,rTWO)         # Substitute the values of r_1 and r_2 in the matrix

psi_0 = Matrix([9,1])                       # Initial state

psi_n = M**n*psi_0                          # Arbitrary state psi_n

# Determining psi_\inf ######################

psi_n_inf = []                              # Start off with an empty vector, psi_n_inf

for el in psi_n:                            # For each element in psi_n
    psi_n_inf.append(Limit(el,n,oo).doit()) # take the limit n -> \infty, and add it to psi_n_inf

psi_n_inf = Matrix(psi_n_inf)               # Make psi_n_inf into a matrix object

# Calculating psi_n at different n ##########

temp = psi_0                                # Starting state

S_list = []                                 # Lists for S, I,and t
I_list = []
t_list = []

S_list.append(9)                            # Initialising them
I_list.append(1)
t_list.append(0)

for i in range(0,100,3):                    # For every time-step
    temp = M*temp                           # Find M^i temp
    S_list.append(temp[0])                  # Append the values of the lists
    I_list.append(temp[1])
    t_list.append(i)

# Graph the results #########################
plt.xlabel('Time(n)')
plt.ylabel('Number')
plt.plot(t_list,S_list,'o-')
plt.plot(t_list,I_list,'o-')
plt.show()
#############################################
```

```python
def SIS(r_1,r_2,init_S,init_I):

    M = Matrix([[1-r_1,r_2],[r_1,1-r_2]])       # Defining the transition matrix

    psi_0 = Matrix([init_S,init_I])             # Initial state

    psi_n = M**n*psi_0                          # Arbitrary state psi_n

    # Determining psi_\inf ######################

    psi_n_inf = []                              # Start off with an empty vector, psi_n_inf

    for el in psi_n:                            # For each element in psi_n
        psi_n_inf.append(Limit(el,n,oo).doit()) # take the limit n -> \infty, and add it to psi_n_inf

    psi_n_inf = Matrix(psi_n_inf)               # Make psi_n_inf into a matrix object

    # Calculating psi_n at different n ##########

    temp = psi_0                                # Starting state

    S_list = []                                 # Lists for S, I,and t
    I_list = []
    t_list = []

    S_list.append(init_S)                       # Initialising them
    I_list.append(init_I)
    t_list.append(0)

    for i in range(1,20,1):                     # For every time-step
        temp = M*temp                           # Find M^i temp
        S_list.append(temp[0])                  # Append the values of the list
        I_list.append(temp[1])
        t_list.append(i)

    # Graph the results #########################
    plt.xlabel('Time(n)')
    plt.ylabel('Number')
    plt.plot(t_list,S_list,'o-')
    plt.plot(t_list,I_list,'o-')
    plt.show()
    #############################################
```

```python
SIS(0.3,0.2,55,45)
```

```python

```


# Lecture 19: Joint, Conditional and Marginal Distributions; 2D LOTUS; Expected Distance between Uniforms; Chicken-egg Problem

## Stat 110, Prof. Joe Blitzstein, Harvard University

----

### Joint, Conditional and Marginal Distributions

#### Joint CDF
A joint CDF is simply where we are dealing with multiple random variables. As an example, a case where we have two random variables $X, Y$, the joint CDF of two random variables $X, Y$ can be expressed as:

\begin{align}
  F(x,y) &= P(X {\le} x, Y {\le} y)
\end{align}

Note that the random variables may be discrete, continuous, or a mixture of both.

#### Joint PDF
The joint PDF, in the case of _continuous_ random variables, is what you would _integrate_ to get the joint CDF. Continuing with our example of two (continous) random variables, we have:

\begin{align}
  f(x,y) &= \frac{\partial^2}{\partial{x}\partial{y}} F(x,y)
\end{align}

Conversely, if we want to know the probability of $X,Y$ in some set $A$, we _integrate_ the density to get that probability.

\begin{align}
  P\left((X,Y) \in A\right) &= \iint\limits_{A} f(x,y) \, dxdy
\end{align}

Integrate by holding one variable constant, and then do the other. The key thing is to be sure to get the _limits of integration_ correct.

#### Marginal PDF
The _marginal PDF of $X$_ is obtained by _integrating out the $Y$_. Recall the $X,Y$ contigency table and the definition of marginal probability.

\begin{align}
  \int_{-\infty}^{\infty} f(x,y) \, dy
\end{align}

Notice that by keeping $X$ constant and _integrating over all $Y$_, the marginal PDF of $X$ no longer depends on $Y$.

And we can do vice-versa for the marginal PDF of $Y$, but keeping $Y$ constant and _integrating over all $X$_.

Do not forget that taking the marginal PDF of $X$ and then integrating over all $X$, we should get 1.0.

\begin{align}
  \int\limits_{-\infty}^{\infty} \int\limits_{-\infty}^{\infty} f(x,y) \, dx dy &= 1.0
\end{align}

#### Conditional PDF

Given that we know $X$, what is the appropriate PDF for $Y$?

Well, we can apply what we know about _conditional probability_ to get a conditional PDF.

\begin{align}
  f_{Y|X} (y|x) &= \frac{f_{XY}(x,y)}{f_{X}(x)} \\
    &= \frac{f_{X|Y}(x|y) \, f_{Y}(y)}{f_{X}(x)}
\end{align}

This is completely analogous to conditional probability.

#### Independence

$X,Y$ are independent if

\begin{align}
  f_{X,Y}(x,y) &= f_{X}(x) \, f_{Y}(y) &\quad \text{for all }x, y \text{ from PDF p.o.v.} \\
  \\
  F(x,y) &= F(x) \, F(y) &\quad \text{for all }x, y \text{ from CDF p.o.v.}
\end{align}

These statements are equivalent, but in most cases it might be easier to work the PDFs.

----

### A Uniform example

Let's revisit that distribution that is uniform on the unit disc $x^2 + y^2 \le 1$.

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

plt.xkcd()

fig = plt.figure(figsize = (5.0,5.0))
ax = fig.add_subplot(1, 1, 1)
ax.set_xlim([-1.5,1.5])
ax.set_ylim([-1.5,1.5])
circ = plt.Circle((0, 0), radius=1.0, color="b", alpha=0.2, lw=5)
ax.add_patch(circ)
plt.axhline(0, color="black", alpha=0.4)
plt.axvline(0, color="black", alpha=0.4)
plt.show()
```

#### Joint PDF

A valid PDF is required to integrate to 1.0, and since we are looking at a Uniform distribution over the unit circle with area equal to $\pi$,  we have

\begin{align}
  f_{XY}(x,y) &=
    \begin{cases}
      \frac{1}{\pi}  & \quad \text{if } x^2 + y^2 \le 1 \\
      0  & \quad \text{otherwise}\\
    \end{cases}
\end{align}

Understand that while this is uniformly distributed, $x$ and $y$ are closely related with $x^2 + y^2 \le 1$, and so $X,Y$ cannot be independent. More on that coming up.

#### Marginal PDF of $X$

\begin{align}
  f_{X}(x) &= \int_{-\sqrt{1-x^2}}^{\sqrt{1-x^2}} \frac{1}{\pi} dy \\
  \\
  &= \frac{2}{\pi} \sqrt{1-x^2} &\quad \text{where } -1 \le x \le 1
\end{align}

Notice that while the joint PDF is Uniform, the marginal PDF is not Uniform: it grows as $x$ approaches 0.

By _symmetry_ we can just replace $x$ with $y$ to get the marginal PDF of $Y$ $f_{Y}(y)$

#### Conditional PDF of $Y|X$

\begin{align}
  f_{Y|X}(y|x) &= \frac{f_{XY}(x,y)}{f_{X}(x)} \\
  &= \frac{\frac{1}{\pi}}{\frac{2}{\pi} \sqrt{1-x^2}} \\
  &= \frac{1}{2 \sqrt{1-x^2}} &\quad \text{if } -\sqrt{1-x^2} \le y \le \sqrt{1-x^2}
\end{align}

But since we are treating $x$ as a constant, and the above equation does not _depend on $y$_, the conditional PDF $f_{Y|X}(y|x)$ is actually Uniform.

\begin{align}
  Y|X &\sim \operatorname{Unif}(-\sqrt{1-x^2}, \sqrt{1-x^2}) &\quad \text{or also } \\
  \\
  Y|X=x \, &\sim \operatorname{Unif}(-\sqrt{1-x^2}, \sqrt{1-x^2})
\end{align}

#### Non-independence

Since in our example above

\begin{align}
  f_{XY}(x,y) \neq f_{X}(x) \, f_{Y}(y)
\end{align}

we can see that $X,Y$ are not independent.

Or, because the _unconditional_ distribution of $Y$ is not the same as the _conditional_ distribution of $Y|X$, it necessarily follows that $X,Y$ are not independent as learning $X$ gives us information about $Y$ (and vice-versa).

----

### 2D LOTUS

Let $X,Y$ have joint PDF $f(x,y)$, and let $g(x,y)$ be a real-valued function of $x,y$.

Then, without trying to find the PDF of $g(x,y)$, LOTUS lets us use joint PDF:

\begin{align}
  \mathbb{E} g(x,y) &= \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} g(x,y) \, f(x,y) \, dx dy
\end{align}

We will illustrate this by proving the following theorem.

#### Theorem
If $X,Y$ are independent, then $\mathbb{E}(XY) = \mathbb{E}(X)\mathbb{E}(Y)$.

Or, in other words, the _independence_ of r.v. $X$ and $Y$ implies that they are _uncorrelated_.

#### Proof

Consider the continuous case

\begin{align}
  \mathbb{E}(XY) &= \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} xy \, f(x,y) \, dx dy &\quad \text{2D LOTUS} \\
  &= \int_{-\infty}^{\infty} \int_{-\infty}^{\infty} xy \, f_{X}(x) \, f_{Y}(y) \, dx dy &\quad \text{since } X,Y \text{ are independent} \\
  &= \int_{-\infty}^{\infty} \left( \int_{-\infty}^{\infty} xy \, f_{X}(x) \, f_{Y}(y) \, dx \right) dy &\quad \text{this is a double integral, so we can group thusly} \\
  &= \int_{-\infty}^{\infty} y \, f_{Y}(y) \underbrace{ \left( \int_{-\infty}^{\infty} x \, f_{X}(x) \, dx \right)}_{\text{actually }\mathbb{E}(X) \text{, which is constant}} dy &\quad \text{since we are holding } y \text{ constant} \\
  &= \mathbb{E}(X)  \int_{-\infty}^{\infty} y \, f_{Y}(y) \, dy \\
  &= \mathbb{E}(X) \, \mathbb{E}(Y) &\quad \blacksquare
\end{align}

----

### Example: expected distance between 2 Uniformly distributed random points

Given _i.i.d._ $X,Y \sim Unif(0,1)$, find the expected distance between $X$ and $Y$, $\mathbb{E}|X-Y|$.

```python
plt.xkcd()

fig,ax = plt.subplots(1, figsize=(6.0,1.0))

plt.plot([1/3, 2/3],[0,0], 'ro')
ax.axhline(y=0, color='k', alpha=0.2)

ax.set_xticks([0.0, (1.0/3.0), (2.0/3.0), 1.0])
ax.set_xticklabels(['0', '1/3', '2/3', '1'])

ax.get_xaxis().set_ticks_position('bottom')
ax.get_yaxis().set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.spines['bottom'].set_visible(False)

plt.show()
```

We don't need to directly find the distribution of $|X-Y|$, because we are only interested in the _average distance between points $x,y$_. Using LOTUS, we have:

\begin{align}
  \mathbb{E}|X-Y| &= \int_{0}^{1} \int_{0}^{1} |x-y| \, dx dy &\quad \text{since }X,Y \text{ are i.i.d., the joint PDF }=1 \\
  &= \iint_\limits{x \gt y} (x-y) \, dx dy + \iint_\limits{x \le y} (y-x) \, dx dy &\quad \text{split into 2 integrals to deal with abs. value} \\
  &= 2 \int_{0}^{1} \int_{y}^{1} (x-y) \, dx dy  &\quad \text{by symmetry; and since } x \gt y \text{, inner integral starts from }y \\
  &= 2 \int_{0}^{1} \left( \frac{x^2}{2} - yx \right) \bigg|_{y}^{1} \, dy \\
  &= 2 \int_{0}^{1} \frac{1}{2} - y + \frac{y^2}{2} \, dy \\
  &= 2 \left( \frac{1}{6} \right) \\
  &= \boxed{ \frac{1}{3} }
\end{align}

----

But looking at that line-segment illustration above suggests another way of looking at this problem.

Let $M = max(X,Y)$

Let $L = min(X,Y)$

And so

\begin{align}
  |X-Y| &= M - L \\
  \\
  \mathbb{E}(M-L) &= \frac{1}{3} &\quad \text{from our previous proof} \\
  \mathbb{E}(M) - \mathbb{E}(L) &= \frac{1}{3} &\quad \text{since } X,Y \text{ are i.i.d.} \\
  \\
  \mathbb{E}(M+L)&= \mathbb{E}(M) + \mathbb{E}(L) &\quad \text{by linearity} \\
  &= \frac{1}{2} + \frac{1}{2} &\quad \text{since} X \sim \operatorname{Unif}(0,1) \text{, } Y \sim \operatorname{Unif}(0,1) \\
  &= 1 \\
  \\
  \mathbb{E}(M) &= \frac{2}{3} \\
  \mathbb{E}(L) &= \frac{1}{3}
\end{align}

----

### Chicken-egg Problem

Say we have $N$ eggs; let $N \sim \operatorname{Pois}(\lambda)$

Some of the eggs hatch, while some don't. Each egg hatches with probability $p$. The event of an egg hatching is independent of the others.

Let $X$ be the number of eggs out of $N$ that do hatch, so $X|N \sim \operatorname{Bin}(N,p)$.

Let $Y$ be the number of eggs that don't hatch, so $X+Y = N$.

Find the joint PMF of $X,Y$. Are they independent?

\begin{align}
  P(X{=}i, Y{=}j) &= \sum_{n=0}^{\infty} P(X{=}i, Y{=}j |N{=}n) \, P(N{=}n) \\
  \\
  &=  P(X{=}i, Y{=}j |N{=i+j}) \, P(N{=i+j})　&\quad \text{since we know that } i + j = n \\
  \\
  &=  P(X{=}i | N{=i+j}) \, P(N{=i+j})　&\quad Y{=}j \text{ is redundant} \\
  \\
  &= \binom{i+j}{i} p^i \, q^j \cdot \frac{e^{-\lambda} \lambda^{i+j}}{(i+j)!} &\quad \text{from definition of binomial and Poisson} \\
  \\
  &=  \frac{(i+j)!}{i! \, j!} p^i \, q^j \cdot \frac{e^{-\lambda} \lambda^{i+j}}{(i+j)!} \\
  \\
  &= \frac{(\lambda p)^i}{i!} \, \frac{(\lambda q)^j}{j!} \, e^{-\lambda} \\
  \\
  &= \left(e^{-\lambda p} \frac{(\lambda p)^i}{i!} \right) \left(e^{-\lambda q}  \frac{(\lambda q)^j}{j!} \right) &\quad \text{since } p + q = 1 \\
  \\
  &\Rightarrow X,Y \text{ are independent, } X \sim \operatorname{Pois}(\lambda p), Y \sim \operatorname{Pois}(\lambda q)
\end{align}

----

View [Lecture 19: Joint, Conditional, and Marginal Distributions | Statistics 110](http://bit.ly/2oRj1aU) on YouTube.d


# Bouncing ball with the Euler integrator
As an introduction to solving ODEs we are solving the problem of the bouncing ball with the simplest of all integrators: the forward Euler scheme.

We have to solve the *second order ODE* (Newton's equations of motion with constant acceleration)
$$
\frac{d^2 y}{dt^2} = -g.
$$

## Euler integrator
The Euler scheme for any *first order ODE*

$$
\frac{dy}{dt} = f(y, t)
$$

is
$$
y(t + h) = y(t) + h f(y(t), t).
$$

## Reduce one 2nd order ODE to two 1st order ODEs
In order to solve the original 2nd order equation of motion we make use of the fact that one $n$-th order ODE can be written as $n$ coupled first order ODEs, namely

\begin{align}
\frac{dy}{dt} &= v\\
\frac{dv}{dt} &= -g.
\end{align}

Solve each of the first order ODEs with Euler:

\begin{align}
y(t + h) &= y(t) + h v(t)\\
v(t + h) &= v(t) - h g.
\end{align}

```python
import numpy as np
```

## Free fall

Start with free fall

```python
g = -9.81
y = 0.0
v = 0.0

t = 0
dt = 0.01

data = []

while t < 10:
    y = y + v*dt
    v = v + g*dt
    data.append([t, y, v])
    t += dt

data = np.array(data)
```

```python
data.shape
```

    (1001, 3)

```python
data = data.transpose()
data.shape  # t, y, v
```

    (3, 1001)

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

```python
plt.plot(data[0], data[1])
plt.xlabel("time (s)")
plt.ylabel("position (m)");
```

## Bouncing
Add a floor at $y = 0$ and release the ball at $y_0 = 5$.

What happens at the floor? – The velocity changes (elastic collision).

```python
g = -9.81
y = 5.0
v = 0.0

t = 0
dt = 0.01

y_floor = 0

data = []

while t < 10:
    y += v*dt
    if y > y_floor:
        v += g*dt
    else:
        v = -v   # bounce off floor
    data.append([t, y, v])
    t += dt

data = np.array(data).transpose()
```

```python
plt.plot(data[0], data[1])
plt.xlabel("time (s)")
plt.ylabel("position (m)");
```

```python

```


##Practica 1 Ejercicio 6
El sistema:

 $$\left\{ \begin{array}{lcc}
            \dot{x}_{1}=-x_{1}-\frac{x_{2}}{ln(\sqrt{x_{1}^{2}+x_{2}^{2}})} \\
             \\ \dot{x}_{2}=-x_{2}+\frac{x_{1}}{ln(\sqrt{x_{1}^{2}+x_{2}^{2}})}
             \end{array}
\right.$$

tiene un punto de equilibrio en el origen.

 * a) Linealizar el sistema alrededor del origen y determinar el tipo de equilibrio

 * b) Obtener el retrato de fase del sistema no lineal cerca del origen transformando las ecuaciones a coordenadas polares y mostrar que es semejante a un foco estable

 * c) Explicar la discrepancia entre los resultados obtenidos en a) y b)

```python
import sympy as sym
```

```python
#Con esto las salidas van a ser en LaTeX
sym.init_printing(use_latex=True)
```

```python
x_1, x_2 = sym.symbols('x_1 x_2')
```

```python
X = sym.Matrix([x_1, x_2])
X
```

```python
f_1 = -x_1 - (x_2) / (sym.ln(sym.sqrt(x_1 ** 2 + x_2 ** 2)))
f_1
```

```python
f_2 = -x_2 + (x_1) / (sym.ln(sym.sqrt(x_1 ** 2 + x_2 ** 2)))
f_2
```

```python
F = sym.Matrix([f_1,f_2])
F
```

```python
A = F.jacobian(X)
#A.simplify()
A
```

```python
A_1 = A.subs({x_1:0,x_2:0})
A_1
```

```python
A_1.eigenvals()
```

```python

```


# Solution {-}

Evaluation of integrals analytically can be quite laborious (some also impossible), so one should not overlook the possibility of using numerical integration. To demonstrate the effectiveness of the numerical approach, consider the following system driven by white noise whose PSD = 10 units.

\begin{equation*}
  G(s)=\frac{1}{s^2+\omega_0^2}, \quad \omega_0=20\pi
\end{equation*}

Lets say we want to find $E[x^2(t)]$ for $0 \leq t \leq 1$ with a sample spacing of 0.001 seconds.

```python
from numpy import arange, pi, sin, cos
from scipy.integrate import quad
from sympy import inverse_laplace_transform, integrate, symbols
import matplotlib.pyplot as plt

s, t, w0, A = symbols('s t w0 A', positive = True)

# Filter in s-domain
G = 1/(s**2 + w0**2)
display(G)
```

$\displaystyle \frac{1}{s^{2} + w_{0}^{2}}$

```python
# Filter in t-domain
g = inverse_laplace_transform(G, s, t)
display(g)
```

$\displaystyle \frac{\sin{\left(t w_{0} \right)}}{w_{0}}$

```python
EX2 = integrate(A*g**2, t)
display(EX2)
```

$\displaystyle \frac{A \left(\frac{t w_{0}}{2} - \frac{\sin{\left(t w_{0} \right)} \cos{\left(t w_{0} \right)}}{2}\right)}{w_{0}^{3}}$

```python
# Numerical values
dt = .001
A = 10
w0 = 20*pi

# Initialize plot vectors
exact = []
approx = []
time=arange(0, 0.1, dt)

# Exact integral
for i in range(100):
    EX2 = A/w0**3*(1/2*w0*i*dt - 1/2*sin(w0*i*dt)*cos(w0*i*dt))
    exact.append(EX2)

# Numerical integration
f = lambda dt: A*(sin(w0*dt)/w0)**2
for i in range(100):
    EX2 = quad(f, 0, i*dt)
    approx.append(EX2[0])

plt.title('Square Impulsive Respons')
plt.xlabel('Time (second)')
plt.ylabel('E(X2)')
plt.plot(time, exact, 'b', label='Exact')
plt.plot(time, approx, 'r', label='Approx')
plt.legend(loc='lower right')
plt.grid(True, which='both')
plt.show()
```


# The Solow Model
This project will solve The Solow Model which explains long-run economic growth. First the basic Solow model for a closed economy is analyzed. The Solow Diagram and the convergence of capital and GDP is plotted. Then the model is extended with technological growth.

## The Basic Solow Model
The equations of the Solow model without any extensions are as follows:

1. $ Y_t = BK^\alpha_tL_t^{1-\alpha}, \quad 0<\alpha<1 $

2. $ K_{t+1} = (1-\delta)K_t+S_t, \quad 0<\delta<1 \; and \; K_0 \; \text{is given} $

3. $ S_t = sY_t, \quad 0<s<1 $

4. $ L_{t+1} = (1+n)L_t, \quad L_0 \text{ given} $

Equation 1. is a Cobb-Douglas production function where $K_t$ is capital, $L_t$ is labor and $B$ is the total factor productivity.
Equation 2. describes how the capital is accumulated from period $t$ to $t+1$.
Equation 3. describes the savings in the economy. Each household saves a fraction $s$ out of the income.
Equation 4. describes the labor force growth from period $t$ to $t+1$ where $n$ is the population growht.

**The Transition Equation**
The transition equation for $k_t$ is

$ k_{t+1} = \dfrac{1}{1+n} (sBk_t^\alpha + (1-\delta)k_t) $

by subtracting $k_t$ on both sides we get the Solow equation

$ k_{t+1} - k_t = \dfrac{1}{1+n} (sBk_t^\alpha - (n+g)k_t) $

**Steady State**
Plugging in $k^*=k_t=k_{t+1}$ and isolating $k^*$ we get the equation for the capital steady state

$ k^* = B^{\frac{1}{1-\alpha}} \left( \dfrac{s}{n+\delta} \right)^{\frac{1}{1-\alpha}} $

Plugging $k^*$ into the BNP per capita equation, $y_t =Bk_t^\alpha$, we get the steady state for BNP/capita

$ y^* = B^{\frac{1}{1-\alpha}} \left( \dfrac{s}{n+\delta} \right)^{\frac{\alpha}{1-\alpha}} $

**Importing needed packages**

```python
import numpy as np
import pandas as pd
import sympy as sm
import matplotlib.pyplot as plt
import ipywidgets as widgets
from ipywidgets import interactive, interact
from scipy import linalg
from scipy import optimize
sm.init_printing(use_unicode=True) #for pretty printing
```

**Defining Symbols**

```python
kt1 = sm.symbols('k_{t+1}')
n = sm.symbols('n')
s = sm.symbols('s')
B = sm.symbols('B')
kt = sm.symbols('k_t')
alpha = sm.symbols('alpha')
delta = sm.symbols('delta')
L0 = sm.symbols('L_0')
L = sm.symbols('L')
K0 = sm.symbols('K_0')
K = sm.symbols('K')
yt = sm.symbols('y_t')
```

**Defining Equations and Finding Steady State Expressions**

```python
# Transition Equation
treq = sm.Eq((1/(1+n)*(s*B*kt**alpha+(1-delta)*kt)),kt1)
sole = sm.Eq((1/(1+n)*(s*B*kt**alpha-(n+delta)*kt)))

# Solving to get the steady state equations for k and y
print('The steady states of k and y are')
k = sm.solve(sole,kt)[0]
display(k)

cdp = sm.Eq(B*k**alpha,yt)
y = sm.solve(cdp,yt)
cdp
display(y)

```

**Defining parameters and solution**

```python
# Making the SS-functions k and y into lambda functions that can be used to calculate numerical values
ssk_sol = sm.lambdify((alpha,delta,s,B,n),k)
ssy_sol = sm.lambdify((alpha,delta,s,B,n),y)

# Defining parameter values
alpha = 1/3
delta = 0.05
s = 0.101
B = 1
n = 0.025

# Getting solution for defined parameters
ssk_val = ssk_sol(alpha,delta,s,B,n)
ssk_val
ssy_val = ssy_sol(alpha,delta,s,B,n)

# Printing result
print(f'For the chosen parameters, the steady state capital is {ssk_val:.3f} and the steady state value of GDP per capita is')
print(ssy_val)
```

    For the chosen parameters, the steady state capital is 1.563 and the steady state value of GDP per capita is
    [1.1604596790352806]

Thus, capital is about 56 percent higher and GDP per capita is 16 percent higher in steady state compared to the initial state.

## The Solow Diagram

```python
# Create model domain and empty array for values of K
K_size = 50
kt = np.arange(K_size)

# Parameters
alpha = 1/3
delta = 0.05
s = 0.3
B = 1
n = 0.025

# Defining CD-function
def GDP(kt):
    y = B * (kt)**(alpha)
    return y

# Defining equations for diagram
y = GDP(kt)
dep = (delta+n)*kt
S = s*B*kt**alpha

# Defining steady state expressions
ssk = (B**(1/(1-alpha))*(s/(n+delta))**(1/(1-alpha)))
ssy = (B**(1/(1-alpha))*(s/(n+delta))**(alpha/(1-alpha)))

# Plotting the Solow Diagram
y_max = np.max(y)
v = [0, 20, 0, 1.75]

fig, ax = plt.subplots(figsize=(10, 8))
ax.plot(kt, dep, ls = '-', label="Depreciation")
ax.plot(kt, S, ls = '-', label="Savings")
ax.set(title="Solow Model", xlabel="Capital Stock")
plt.text(18.4, 1.55,  r'$ (n+\delta)k_t$')
plt.text(19,.85, r'$sBk_t^\alpha$')
plt.axvline(x = ssk, ls = "--", color = 'k', label='SS')
plt.legend(loc=2)
plt.axis(v)
plt.show()
```

## Plotting the convergence to Steady State
In the following the convergence of the values found in the previous section are plotted.

```python
# Defining parameters and arrays
alpha = 1/3
delta = 0.05
s = 0.101
B = 1
n = 0.025
K0 = 1
L0 = 1
K_size = 100
kt = np.arange(K_size)   #creates array of size K_size - here 100
Y_size = 100
yt = np.arange(Y_size)

# Defining starting point of function
T = 100
time = np.arange(T)
k1 = np.zeros(T)
y1 = np.zeros(T)

ssk = (B**(1/(1-alpha))*(s/(n+delta))**(1/(1-alpha)))
k1[0] = K0

ssy = (B**(1/(1-alpha))*(s/(n+delta))**(alpha/(1-alpha)))
y1[0] = L0

# Plotting the functions in per capita terms
# CD production function
def GDP(kt):
    y = B * (kt)**(alpha)
    return y

# The solow function
def solow(kt):
    Dk = (1/(1+delta))*(s *B*(kt)**(alpha) - (n + delta)*kt)
    return Dk

Dk = solow(kt)
kdelta1 = solow(k1[0])

### Plot convergence of capital and GDP
for j in range(1, T):
    k1[j] = k1[j-1] + s*GDP(k1[j-1]) - (delta + n)*k1[j-1]
    y1[j] = B * k1[j-1]**alpha

v = [0, T, 1, ssk*1.05]
fig, ax = plt.subplots(figsize=(10, 8))
ax.plot(time, k1, label="$k_t$ convergence"  , color='b', alpha = 1)
ax.plot(time, y1, label="$y_t$ convergence"  , color='r', alpha = 1)
ax.set(title="Convergence of $k_t$ and $y_t$", xlabel=r'$k$')
plt.legend(loc=4)
plt.axhline(y = ssk, ls = ":", color = 'b', alpha = 0.7)
plt.axhline(y = ssy, ls = ":", color = 'r', alpha = 0.7)
plt.axis(v)
plt.xlabel('Time')
plt.show()

```

## Solow Model with Technical Growth
In the basic Solow we have no positive growth in the long run because of the diminishing marginal product of capital. This is not realistic compared to reality and thus the model is now extended with technical growth. The only difference is that the $B$ in the basic model is not constant anymore, it is replaced by $ A_t^{1-\alpha}$ that grows with an exogenous growth rate, $g$. Thus, we have
    $A_{t+1}=(1+g)A_t$
where $A_0$ is given. In this model we consider Harrod-neutral growth which means that the technological growth is labor augmenting - $Y_t = F(K_t,A_tL_t)$. The model is now
1. $Y_t = K_t^\alpha (A_tL_t)^{1-\alpha}$

2. $ K_{t+1} = (1-\delta)K_t+S_t, \quad 0<\delta<1 \; and \; K_0 \; \text{is given} $

3. $ S_t = sY_t, \quad 0<s<1 $

4. $ L_{t+1} = (1+n)L_t, \quad L_0 \text{ given} $

5. $ A_{t+1}=(1+g)A_t, \quad A_0 \text{ given} $

In the basic Solow we analyzed the model in $y_t$ and $k_t$ where we found that $y^*$ and $k^*$ were constant in steady state. If we did this here we would have a continuosly growing savings curve in terms of per capita due to the constantly growing $A_t$. Thus, the model with technological growth is analyzed in tilde-variables, which are defined as:
    $ \tilde{k}_t = \dfrac{K_t}{A_tL_t} = \dfrac{k_t}{A_t} $
    $ \tilde{y}_t = \dfrac{Y_t}{A_tL_t} = \dfrac{y_t}{A_t} $

This gives us the physical capital and the GDP in per effective capita terms. Thus, capital and GDP are adjusted for technology and population. These terms turn out to be constant in steady state and we use them for solving the model. In reality they do not make much sense.

**The Transition Equation**
The transition equation in this model is

$ \tilde{k}_{t+1} = \dfrac{1}{(1+n)(1+g)} (s\tilde{k}_t^\alpha + (1-\delta)\tilde{k}_t ) $

**Solow Equation**
As before it is found by subtracting $\tilde{k}_t$ on both sides of the transition equation

$ \tilde{k}_{t+1} - \tilde{k}_{t}  = \dfrac{1}{(1+n)(1+g)} [s\tilde{k}_t^\alpha - (n+g+\delta+ng)\tilde{k}_t ] $

We can also get the modified Solow equation by dividing with $\tilde{k}_{t}$ on both sides

$ \dfrac{\tilde{k}_{t+1} - \tilde{k}_{t}}{\tilde{k}_{t}}  = \dfrac{1}{(1+n)(1+g)} [s\tilde{k}_t^{\alpha-1} - (n+g+\delta+ng)] $

**Steady State**
The steady states in tilde-variables can be found by setting $\tilde{k}_{t}=\tilde{k}_{t+1}=\tilde{k}^{*}$

$ \tilde{k}^{*} = \left(\dfrac{s}{n+g+\delta+ng}\right)^{\frac{1}{1-\alpha}} $

inserting $\tilde{k}^{*}$ in $\tilde{y}_{t}=\tilde{k}_{t}^\alpha$ we also get the steady state for GDP per effective capita

$ \tilde{y}^{*} = \left(\dfrac{s}{n+g+\delta+ng}\right)^{\frac{\alpha}{1-\alpha}} $

```python

```

```python

```

```python

```

```python

```


## Matrix to pauli string

Here we summarize how a $d$-level matrix can be written as a sum of pauli strings.

A matrix A with elements $a_{ij}$ can be written as,
\begin{equation}
    A=\sum_{i,j} a_{ij} \mid i \rangle \langle j \mid
\end{equation}
where $i,j$ are integers labelling the basis elements.  To map this to qubits, we first translate the integers from base 10 to binary, using some encoding $\mathcal{R}$.  which transforms basis elements as
\begin{equation}
    \mid i \rangle \rightarrow\mid\mathcal{R}(i) \rangle
        =\mid i_{n-1} \rangle \cdots \mid i_0 \rangle,
\end{equation}
where the $x_i\in\{0,1\}$. Which maps our matrix $A$ to a tensor product over local qubit terms
\begin{equation}
    A=\sum_{i,j}a_{ij}\otimes_n \mid i_{n}\rangle\langle j_n \mid.
\end{equation}
Each qubit local term can be written as a pauli string using the formulas
\begin{align}
    \mid 0 \rangle \langle 1 \mid &= \frac{1}{2}\left( X + i Y \right) \\
    \mid 1 \rangle \langle 0 \mid &= \frac{1}{2}\left( X - i Y \right) \\
    \mid 0 \rangle \langle 0 \mid &= \frac{1}{2}\left( 1 + Z \right)   \\
    \mid 1 \rangle \langle 1 \mid &= \frac{1}{2}\left( 1 - Z \right).
\end{align}

Diagonal matrices with size equal to a power of 2 have efficient implementations.  As a concrete example consider the number operator with a cutoff of 4 possible states.

\begin{equation}
    n = \begin{pmatrix} 0 & 0 & 0 & 0 \\
                        0 & 1 & 0 & 0 \\
                        0 & 0 & 2 & 0 \\
                        0 & 0 & 0 & 3 \\
        \end{pmatrix}
\end{equation}
This can be written succinctly as
\begin{equation}
    n = 1 \mid 1 \rangle\langle 1 \mid + 2 \mid 2 \rangle\langle 2 \mid + 3 \mid 3 \rangle\langle 3 \mid
\end{equation}
By using the standard binary encoding this becomes
\begin{equation}
    n = 1 \mid 01 \rangle\langle 01 \mid + 2 \mid 10 \rangle\langle 10 \mid + 3 \mid 11 \rangle\langle 11 \mid
\end{equation}
With the above definitions for replacing qubit local terms with pauli operators this becomes
\begin{align}
    n &= \frac{1}{4}\left[(1^1 + Z^1)(1^0 - Z^0)\right] + \frac{2}{4}\left[(1^1-Z^1)(1^0+Z^0)\right] + \frac{3}{4}\left[(1^1-Z^1)(1^0-Z^0)\right] \\
      &= \frac{1}{4}\left[1^11^0 + Z^11^0 - 1^1Z^0 - Z^1Z^0\right] \\
      &+ \frac{2}{4}\left[1^11^0 - Z^11^0 + 1^1Z^0 - Z^1Z^0\right] \\
      &+ \frac{3}{4}\left[1^11^0 - Z^11^0 - 1^1Z^0 + Z^1Z^0\right] \\
      &= \frac{3}{2}1^11^0 - Z^11^0 - \frac{1}{2}1^1Z^0
\end{align}

```python
# the code version requires you to input a matrix and an encoding
import sys
sys.path.append('..')
from src.MatrixToPauliString import *
from src.BinaryEncodings import *

import numpy as np
import sympy as sp
```

```python
matrix = np.array([[0,0,0,0],[0,1,0,0],[0,0,2,0],[0,0,0,3]])
pauli_strings = matrix_to_pauli_strings(matrix, standard_encode)
```

```python
sp.expand(pauli_strings)
```

$\displaystyle 1.5 I^{0} I^{1} - 1.0 I^{0} Z^{1} - 0.5 I^{1} Z^{0}$