```
import igraph as ig
import numpy as np
from sympy.solvers import nsolve
from sympy import *
```

```
import scipy as sp
from scipy import stats
```

```
from scipy.stats import rankdata
```

```
from math import radians, cos, sin, asin, sqrt

def haversine(lon1, lat1, lon2, lat2):
    """
    Calculate the great circle distance between two points
    on the earth (specified in decimal degrees)
    """
    # convert decimal degrees to radians
    lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])

    # haversine formula
    dlon = lon2 - lon1
    dlat = lat2 - lat1
    a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
    c = 2 * asin(sqrt(a))
    r = 6371 # Radius of earth in kilometers. Use 3956 for miles
    return c * r
```

```
from shapely.geometry import Point,MultiPoint,LineString,Polygon
```

```
import powerlaw as pl
%matplotlib inline
import matplotlib.pyplot as plt
```

```
from scipy.spatial import distance
def dist(v1,v2):
    dist = distance.euclidean((v1[0],v1[1]),(v2[0],v2[1]))
    return dist
```

```
from scipy import special
from scipy import stats
import pandas as pd
```

```
aggragate = pd.read_csv('final_table_ca.csv')
```

```
x_coord = np.array((aggragate['Lon'].tolist(),aggragate['Lat'].tolist())).T
y_coord = np.array((aggragate['Lon'].tolist(),aggragate['Lat'].tolist())).T
```

```
population = ((aggragate['pop'].values).reshape(111,1)).tolist()
```

```
coords = np.concatenate([x_coord,y_coord],axis=1)
```

```
n = 111
alldist = []
A = np.zeros((n,n))
A_binom = np.zeros((n,n))
count=0
for i in range(0,n):
    for j in range(i+1,n):
        m = (5*10**-7)*(population[i][0]*population[j][0])/haversine(x_coord[i][0], x_coord[i][1], y_coord[j][0], y_coord[j][1])**2#(dist(coords[i],coords[j]))**2
        A[i][j] = m
        A[j][i] = m
        h = (np.random.binomial(111, 0.01503695919007753, 1))/110
        A_binom[i][j] = h
        A_binom[j][i] = h
```

```
Dt = np.sum(A,axis=1).tolist()
DD = np.diag(Dt)
L = A - DD

Dt_binom = np.sum(A_binom,axis=1).tolist()
DD_binom = np.diag(Dt_binom)
L_binom = A_binom - DD_binom
```

```
def bins_counts(array,a,b,c):
    count, bins = np.histogram(array, bins=np.logspace(a,b,c),density=1)
    t =[]
    for i in range(len(count)):
        t.append((bins[i + 1] + bins[i])/float(2))
    return t,count
```

```
d_frac = aggragate['d_frac'].tolist()
```

```
#np.random.shuffle(d_frac)
x0_cf = []
y0_cf = []
c=[]#### status parameter
a_12 = []
a_21 = []

for i in range(111):
    if d_frac[i] > 0.5:
        x0_cf.append(1.2)
        y0_cf.append(0.4)
        c.append(0.5)
        a_12.append(21)
        a_21.append(0.0)
    if d_frac[i] <= 0.5:
        x0_cf.append(0.4)
        y0_cf.append(1.2)
        c.append(-0.5)
        a_12.append(0.0)
        a_21.append(21)
aggragate['c'] = c
aggragate['x0'] = x0_cf
aggragate['y0'] = y0_cf
aggragate['a12'] = a_12
aggragate['a21'] = a_21
```

```
cc = (aggragate['c'].values).reshape(111,1)
a_12 = (aggragate['a12'].values).reshape(111,1)
a_21 = (aggragate['a21'].values).reshape(111,1)
```

```
deltat=10.**(-3.)
def fun(e,f):
    l1 = np.matmul(L,e)
    l5 = np.matmul(L,f)
    l3 = np.matmul(L,(e*f))
    return (e + deltat*(e*(1-e) + cc*e*f +l1*d1 + l3*a_12), f + deltat*(f*(1-f) - cc*e*f + l5*d2 + l3*a_21))
```

```
d1 = 0.01
d2 = 0.01
```

```
t= 140000#Pasos temporales totales
tiempo=np.linspace(0,t,t+1)
```

```
evolution_2x = []
evolution_2y = []
for i in range(5):
    up = []
    vp = []
    uavg=[]
    x_0   = 1.2#aggragate['x0'].tolist()
    y_0  = 0.4#aggragate['y0'].tolist()
    for n in range((111)):
        up.append(x_0 + 0.1*x_0*(2.*np.random.rand()-1))
        vp.append(y_0 + 0.1*y_0*(2.*np.random.rand()-1))
    upp = np.array([up]).T
    vpp = np.array([vp]).T
    ui = upp
    vi = vpp
    #uavg= []
    for j in tiempo:
        u =(fun(ui,vi))[0]
        v=(fun(ui,vi))[1]
        ui = u
        vi = v
        uavg.append(np.average(ui))
    evolution_2x.append(ui)
    evolution_2y.append(vi)
    print(i)
```

```
aggragate['xi_sim'] = np.mean(evolution_2x,axis=0)
aggragate['yi_dim'] = np.mean(evolution_2y,axis=0)
```

```

```

```
new=pd.DataFrame()
new['xi_sim'] = (aggragate.groupby('districts').apply(lambda x: np.average(x['xi_sim']/(x['xi_sim']+x['yi_sim']), weights=x['pop']))).tolist()

new['yi_sim'] = (aggragate.groupby('districts').apply(lambda x: np.average(x['yi_sim']/(x['xi_sim']+x['yi_sim']), weights=x['pop']))).tolist()

```

```
sp.stats.pearsonr(new['yi_fix_emp'].tolist(),new['s_frac'].tolist())
```

```
fig, axes = plt.subplots(1,2, figsize=(2*6.4, 4.8))
font0 = FontProperties()
font1 = font0.copy()
font1.set_weight('bold')
font1.set_size('large')
font1.set_family('sans-serif')

axes[0].text(0.5, 1.45,
                'German',
        verticalalignment='center', horizontalalignment='center',

        color='black',fontproperties=font1,fontsize=20,transform=axes[0].transAxes, **hfont)
ka_agg_1.plot(ax=axes[0], column='xi_e_s_1', cmap='Oranges', edgecolor='black',vmin=0,vmax=1)

axes[0].set_yticklabels([])
axes[0].set_xticklabels([])
axes[0].set_yticks([])
axes[0].set_xticks([])

axes[0].spines['right'].set_visible(False)
axes[0].spines['top'].set_visible(False)
axes[0].spines['bottom'].set_visible(False)
axes[0].spines['left'].set_visible(False)

axes[0].text(0.1,0.035,r'$\rho^G_p = 0.89$',fontsize=20,transform=axes[0].transAxes, **hfont)

axes[0].text(0.05, 0.95, '(a)',
        verticalalignment='center', horizontalalignment='center',

       color='black',fontproperties=font1,fontsize=20,transform=axes[0].transAxes, **hfont)

#########
axes[1].text(0.5, 1.45,
                'Slovenian',
        verticalalignment='center', horizontalalignment='center',

        color='black',fontproperties=font1,fontsize=20,transform=axes[1].transAxes, **hfont)
ka_agg_1.plot(ax=axes[1], column='yi_e_s_1', cmap='Purples', edgecolor='black',vmin=0,vmax=1)

axes[1].set_yticklabels([])
axes[1].set_xticklabels([])
axes[1].set_yticks([])
axes[1].set_xticks([])

axes[1].spines['right'].set_visible(False)
axes[1].spines['top'].set_visible(False)
axes[1].spines['bottom'].set_visible(False)
axes[1].spines['left'].set_visible(False)

axes[1].text(0.1,0.035,r'$\rho^S_p = 0.89$',fontsize=20,transform=axes[1].transAxes, **hfont)

axes[1].text(0.05, 0.95, '(b)',
        verticalalignment='center', horizontalalignment='center',

       color='black',fontproperties=font1,fontsize=20,transform=axes[1].transAxes, **hfont)

vmin = 0
vmax = 1
fig.tight_layout()
sm = plt.cm.ScalarMappable(cmap='Oranges', norm=plt.Normalize(vmin=vmin, vmax=vmax))
sm._A = []
sm_1 = plt.cm.ScalarMappable(cmap='Purples', norm=plt.Normalize(vmin=vmin, vmax=vmax))
sm_1._A = []
cbaxes = fig.add_axes([0.4735, 0.04, 0.012, 0.9])
cbar = fig.colorbar(sm,cax=cbaxes,orientation='vertical',ticks=[0, 0.5, 1])
cbar.ax.tick_params(labelsize=20)
cbaxes = fig.add_axes([.97, 0.04, 0.012, 0.9])
cbar = fig.colorbar(sm_1,cax=cbaxes,orientation='vertical',ticks=[0, 0.5, 1])
cbar.ax.tick_params(labelsize=20)

plt.savefig('carinthia_extra_map_1.pdf',bbox_inches='tight')
```


# PyFolding SI Notebook 5
---

[Author] ERGM

---

## This is to fit Equilibrium Curves to Homopolymer (also called Homozipper) Ising Model ##

In this notebook we will show how equilbrium folding data can be imported into a notebook and fitted to Homopolymer Ising Model.

If you are less script/computer orientated, you can simply change the data paths and other parameters for your proteins and re-run the jupyter notebook ( "Kernal/Restart & Run all" from the menu above).

---

## Data Format

Please see PyFolding SI Notebooks 1 and 2 for the format your data has to be in to enable this type of analysis.
Remember for Ising Model Analysis here each protein dataset (equilibrium denaturation curve) must have its own .csv

---

#### First off lets load pyfolding & pyplot into this ipython notebook (pyplot allows us to plot more complex figures of our results):

---

```python
# use this command to tell Jupyter to plot figures inline with the text
%matplotlib inline

# import pyfolding, the pyfolding models and ising models
import pyfolding
from pyfolding import *

# import the package for plotting, call it plt
import matplotlib.pyplot as plt

# import numpy as well
import numpy as np
```

    <IPython.core.display.Javascript object>

    PyFolding: Jupyter autoscrolling has been disabled

---

#### Now, we need to load some data to analyse.

I will import the equilibrium denaturation of a series of CTPRa proteins from:

`Phillips, J. J., Javadi, Y., Millership, C. & Main, E. R. Modulation of the multistate folding of designed TPR proteins through intrinsic and extrinsic factors.
Protein Sci 21, 327–338 (2012).`

I will load 7 denaturation curves that correspond to the following proteins:

Protein     |Filename     |No. of Helices (repeating unit)
:--------:  |:-------:    |:-------:
CTPRa2      | H2_25C.csv  |  5
CTPRa3      | H3_25C.csv  |  7
CTPRa4      | H4_25C.csv  |  9
CTPRa5      | H5_25C.csv  |  11
CTPRa6      | H6_25C.csv  |  13
CTPRa8      | H8_25C.csv  |  17
CTPRa10     | H10_25C.csv |  21

---

### We will load all of the data together, as follows:

---

```python
# arguments are "path", "filename"
pth = "../examples/CTPRan"

#this is a set of commands to automate loading the data for each proteins
fn = ["H2_25C.csv","H3_25C.csv","H4_25C.csv",
      "H5_25C.csv","H6_25C.csv","H8_25C.csv","H10_25C.csv"]

#Here we are loading all the curves in a list called "proteins" and assigning them names
proteins = [pyfolding.read_equilibrium_data(pth,f) for f in fn]

# also store the number of helices for each protein
n_helices = [5, 7, 9, 11, 13, 17, 21]
```

---

### Now set the Temperature

---

```python
# Set temperature to 25.00°C
# (NOTE: Careful, this sets the temperature for all subsequent calculations)
pyfolding.set_temperature(25.)
```

    Set temperature to 25.00°C
    (NOTE: Careful, this sets the temperature for all subsequent calculations)

---

### Now lets plot the data

---

```python
# the following commands plot all the data curves on one plot

plt.figure(figsize=(10,6))
for c in proteins:
    plt.plot(c.x, c.y, '.')

# the following commands plot all the data curves on one plot, where "loc" command determines where the legend goes.
plt.legend([c.ID for c in proteins], loc='best')
plt.title("CTPRa Equilm Curves")
plt.xlim([0, 6.5])                                              # x axis from 0 to 6.5
plt.show()

```

---

### Lets Print out the information on the HomoZipper Ising Model

---

```python
# printing the equation for viewing

models.HomozipperIsingEquilibrium().info()
```

$$\text{the partition function } (q) \text{ and thus fraction of folded protein } (f) \text{ of n arrayed repeats are given by:}\\ \
                \begin{equation} \\ \
                \begin{aligned} \
                & q = 1 + \frac{\kappa([\kappa \tau]^{n+1} - [n+1]\kappa \tau - n)} {(\kappa \tau + 1)^2}  \\ \
                \\ \
                & f = \frac{1} {n} \sum^{n}_{i=0}i\frac{(n-i+1)\kappa^i\tau^{i-1}} {q} \\ \
                \\ \
                \text{where:} \\ \
                & \kappa (x) = \exp\frac{-G_i} {RT} = \exp\frac{-G_{i,H_20} + m_i x} {RT}  \\ \
                \\ \
                & \tau (x) = \exp\frac{-G_{i,i+1}} {RT} \
                \end{aligned}\
                \end{equation}$$

     Homopolymer Zipper Ising model

        Params:
            q = partition function
            f = fraction of folded protein
            Kappa = equilibrium constant of folding for a given repeating unit
            Tau = equilibrium constant of association between 2 repeating units
            n = number of repeating units
            x = denaturant concentration (M)
            Gi = intrinsic stability (folding energy) of a repeating unit i
            mi = denaturant sensitivity of the intrinsic stability of a repeating unit i
            Gi,i+1 = interface interaction energy between 2 repeating units
            R = Universal Gas Constant (kcal.mol-1.K-1)
            T = Temperature (Kelvin)

        Reference:
            Aksel and Barrick. Analysis of repeat-protein folding using
            nearest-neighbor statistical mechanical models.
            Methods in enzymology (2009) vol. 455 pp. 95-125

---

### Fitting to a Homopolymer Ising model
We need to define a topology for each protein, this is essentially the domains of the protein in order (N->C)

#### Protein topology can be defined via the following Ising motifs

 Module              | Abbreviation | Defined in Ising as
-----------------    | :----------: | --------------------
Module, type 1       | r            | RepeatDomain
Module, type 2       | Mr           | MutantRepeatDomain
Module, type 3       | h            | HelixDomain
Module, type 4       | l            | LoopDomain
Module, type 5       | Ml           | MutantLoopDomain
Cap Module           | c            | CapDomain
Mut. Cap Module      | Mc           | MutantCapDomain
Decoupled Cap Domain | DC           | DecoupleCapDomain

For Homopolymer fitting Helix, Repeat & loop motif functions are identical.
We will need the differing types when using the Heteropolymer in the next Notebook. The nomenclature stems from our CTPR proteins analysis, as their repeated motifs can be defined in terms of either repeated TPRs, repeated helices and/or loops.

##### Each motif is defined as having:
1. an intrinisic stability term (Gi)
2. a denaturation dependence term (mi) - which is associated with Gi
3. an interface energy term (Gij) - defined as the interaction between folded units i and i-1, i.e. the interface of the i unit with the N-terminal unit i-1 to it.
4. The Ising model automatically takes into account the first N-terminal repeat does not have an interface term {ij} associated with it.

---

### Lets Set up Our Topology

---

```python
# set up the protein topologies automatically
h = ising.HelixDomain

# We have already defined the number of helices for each protein
# in cell (3) above as "n_helices", so:
topology = [[h]*n for n in n_helices]

# plot them either in an expanded form
ising.plot_domains(topology, collapse=False)

# or in a collapsed form
ising.plot_domains(topology, collapse=True)
```

---

### Automatic global fitting to the homozipper model

---

```python
# try with a one-liner which can also save out the curve fits via:"save='/Users/ergm/Desktop/test.csv'
r_homo = ising.fit_homopolymer(proteins, n_helices) #, save='/Users/ergm/Desktop/test.csv')
```

```python
ising.plot_domains(topology, collapse=False)
```

```python
models.HomozipperIsingEquilibrium().info()
```

$$\text{the partition function } (q) \text{ and thus fraction of folded protein } (f) \text{ of n arrayed repeats are given by:}\\ \
                \begin{equation} \\ \
                \begin{aligned} \
                & q = 1 + \frac{\kappa([\kappa \tau]^{n+1} - [n+1]\kappa \tau - n)} {(\kappa \tau + 1)^2}  \\ \
                \\ \
                & f = \frac{1} {n} \sum^{n}_{i=0}i\frac{(n-i+1)\kappa^i\tau^{i-1}} {q} \\ \
                \\ \
                \text{where:} \\ \
                & \kappa (x) = \exp\frac{-G_i} {RT} = \exp\frac{-G_{i,H_20} + m_i x} {RT}  \\ \
                \\ \
                & \tau (x) = \exp\frac{-G_{i,i+1}} {RT} \
                \end{aligned}\
                \end{equation}$$

     Homopolymer Zipper Ising model

        Params:
            q = partition function
            f = fraction of folded protein
            Kappa = equilibrium constant of folding for a given repeating unit
            Tau = equilibrium constant of association between 2 repeating units
            n = number of repeating units
            x = denaturant concentration (M)
            Gi = intrinsic stability (folding energy) of a repeating unit i
            mi = denaturant sensitivity of the intrinsic stability of a repeating unit i
            Gi,i+1 = interface interaction energy between 2 repeating units
            R = Universal Gas Constant (kcal.mol-1.K-1)
            T = Temperature (Kelvin)

        Reference:
            Aksel and Barrick. Analysis of repeat-protein folding using
            nearest-neighbor statistical mechanical models.
            Methods in enzymology (2009) vol. 455 pp. 95-125

---

#### End of this Notebook.

---


---
## 02. Ecuación de Advección Multidimensional

Eduard Larrañaga (ealarranaga@unal.edu.co)

---

### Resumen

Se obtiene la solución de la ecuación de advección multidimensional por el método de volumenes finitos

---

---

## Ecuación Lineal de Advección 2-Dimensional

La ecuación lineal de advección en 2-dimensiones para la función $\psi = \psi(t,x,y)$ es

\begin{equation}
\partial_t \psi + u \partial_x \psi + v \partial_y \psi = 0
\end{equation}

donde $u$ es la velocidad de advección en direccion-x y $v$ es la velocidad en dirección-y . El promedio de la función $\psi(t,x,y)$ en la zona $i,j$ se denotará como $\psi_{i,j}$ y en general, el índice $i$ etiquetará la dirección-x mientras que el índice $j$ etiqueta la dirección-y.

---

### Método de Volumenes Finitos en 2-dimensiones

Ya que $u$ y $v$ se conisderarán como constantes, es posible colocarlas dentro de las derivadas parciales en la ecuación de advección,

\begin{equation}
\partial_t \psi + \partial_x (u \psi) + \partial_y (v \psi) = 0.
\end{equation}

Se define el promedio de la función $\psi$ en una zona al integras sobre el *volumen* 2-dimensional de uno de los intervalos,

\begin{equation}
\psi_{i,j} = \frac{1}{\Delta x \Delta y}
   \int_{x_{i-\frac{1}{2}}}^{x_{i+\frac{1}{2}}} \int_{y_{j-\frac{1}{2}}}^{y_{j+\frac{1}{2}}}
   \psi(x,y,t) \, dx \, dy .
\end{equation}

De esta forma, al integrar la ecuación de advección con respecto a $x$ y a $y$, se obtiene

\begin{align}
\frac{1}{\Delta x \Delta y}
  \int_{x_{i-\frac{1}{2}}}^{x_{i+\frac{1}{2}}}
  \int_{y_{j-\frac{1}{2}}}^{y_{j+\frac{1}{2}}} \partial_t \psi \, dx \, dy =
  &- \frac{1}{\Delta x \Delta y}
       \int_{x_{i-\frac{1}{2}}}^{x_{i+\frac{1}{2}}} \int_{y_{j-\frac{1}{2}}}^{y_{j+\frac{1}{2}}}
      \partial_x (u \psi) \, dx \, dy \nonumber \\
  &- \frac{1}{\Delta x \Delta y}
       \int_{x_{i-\frac{1}{2}}}^{x_{i+\frac{1}{2}}} \int_{y_{j-\frac{1}{2}}}^{y_{j+\frac{1}{2}}}
      \partial_y (v \psi) \, dx \, dy
\end{align}

y al intercambiar la integral con el operador de derivada temporal al lado izquierdo e integrar adecuadamente los terminos del lado derecho se llega a

\begin{align}
 \frac{\partial \psi_{i,j}}{\partial t} =
  &- \frac{1}{\Delta x\Delta y} \int_{y_{j-\frac{1}{2}}}^{y_{j+\frac{1}{2}}}
     \left \{ (u \psi)_{i+\frac{1}{2},j} - (u \psi)_{i-\frac{1}{2},j} \right \} dy \nonumber \\
  &- \frac{1}{\Delta x\Delta y} \int_{x_{i-\frac{1}{2}}}^{x_{i+\frac{1}{2}}}
     \left \{ (v \psi)_{i,j+\frac{1}{2}} - (v \psi)_{i,j-\frac{1}{2}} \right \} dx
\end{align}

Integración de esta ecuación entre los tiempos $t^n$ y $t^{n+1}$ da como resultado

\begin{align}
 \psi_{i,j}^{n+1} - \psi_{i,j}^n =
  &- \frac{1}{\Delta x\Delta y} \int_{t^n}^{t^{n+1}} \int_{y_{j-\frac{1}{2}}}^{y_{j+\frac{1}{2}}}
     \left \{ (u \psi)_{i+\frac{1}{2},j} - (u \psi)_{i-\frac{1}{2},j} \right \} dy dt \nonumber \\
  &- \frac{1}{\Delta x\Delta y} \int_{t^n}^{t^{n+1}} \int_{x_{i-\frac{1}{2}}}^{x_{i+\frac{1}{2}}}
     \left \{ (v \psi)_{i,j+\frac{1}{2}} - (v \psi)_{i,j-\frac{1}{2}} \right \} dx dt .
\end{align}

Ahora bien, el flujo a través de una superficie de interfaz se define como el promedio sobre la superficie de esa cara y en el tiempo. Esto da como resultado las siguientes expresiones:

1. A través de una cara de x constante:
\begin{equation}
\langle (u\psi)_{i+\frac{1}{2},j}\rangle_{(t)} = \frac{1}{\Delta y \Delta t}
    \int_{t^n}^{t^{n+1}} \int_{y_{j-\frac{1}{2}}}^{y_{j+\frac{1}{2}}} (u\psi)_{i+\frac{1}{2},j}\, dy dt
\end{equation}

2. A través de una cara de y constante:
\begin{equation}
\langle (v\psi)_{i,j+\frac{1}{2}}\rangle_{(t)} = \frac{1}{\Delta x \Delta t}
    \int_{t^n}^{t^{n+1}} \int_{x_{i-\frac{1}{2}}}^{x_{i+\frac{1}{2}}} (v\psi)_{i,j+\frac{1}{2}}\, dx dt
\end{equation}

donde $\langle . \rangle_{(t)}$ denotará el promedio temporal sobre esa cara.

Al igual que en el caso de la advección 1-dimensional, el promedio temporal se reemplaza por el flujo en el punto medio en el tiempo y ahora, también el promedio sobre la cara se reemplazará por el valor del flujo en el centro de la cara,

\begin{equation}
\langle (u\psi)_{i+\frac{1}{2},j} \rangle_{(t)} \approx (u\psi)_{i+\frac{1}{2},j}^{n+\frac{1}{2}}
\end{equation}

y por lo tanto,

\begin{equation}
\psi_{i,j}^{n+1} = \psi_{i,j}^n - \Delta t \left [
   \frac{(u\psi)_{i+\frac{1}{2},j}^{n+\frac{1}{2}} - (u\psi)_{i-\frac{1}{2},j}^{n+\frac{1}{2}}}{\Delta x} +
   \frac{(v\psi)_{i,j+\frac{1}{2}}^{n+\frac{1}{2}} - (v\psi)_{i,j-\frac{1}{2}}^{n+\frac{1}{2}}}{\Delta y} \right ]
\end{equation}

En este problea de advección lineal, en el que $u$ y $v$ son constantes, solo es necesario encontrar los valores de $a$ aen las interfaces, esto quiere decir $\psi^{n+1/2}_{i\pm 1/2 , j}$ en las interfaces-x y $\psi^{n+1/2}_{i, j \pm 1/2}$ en las interfaces-y. Existen dos métodos para calcular estos estados: **con división dimensional** y **sin división**.

---
### Método de División Dimensional

Los métodos con división dimensional son los más sencillos de implementar y tienen como base que cada una de las dimensiones se trabaja de forma independiente a las demás. Esto implica que se utilizará el método de solución 1-dimensional descrito en clases anteriores en cada una de las direcciones.

El método de Strang es un algoritmo de segundo orden de precisión en el tiempo en el cual se alterna el orden de actualización dimensional en cada paso temporal. De esta forma, cada actualización temporal en un $\Delta t$ consiste en una actualización en $x$ seguida de una actualización en $y$,

\begin{eqnarray}
\bar{\psi}_{i,j} &=& \psi_{i,j}^n
  - \Delta t \frac{ u \psi_{i+\frac{1}{2},j}^{n+\frac{1}{2}} - u \psi_{i-\frac{1}{2},j}^{n+\frac{1}{2}} }{\Delta x}\\
\psi_{i,j}^{n+1} &=&  \bar{\psi}_{i,j}
  - \Delta t \frac{ v \bar{\psi}_{i,j+\frac{1}{2}}^{n+\frac{1}{2}} - v \bar{\psi}_{i,j-\frac{1}{2}}^{n+\frac{1}{2}} }{\Delta y}.
\end{eqnarray}

Para construir los estados en las interfaces se sigue el mismo porceso descrito para la ecuación de advección 1-dimensional, i.e. considerando las expansiones desde la izquierda o desde la derecha y resolviendo el problema de Riemann correspondiente.

```python

```

11. PDE II. Volumenes Finitos/02. Ecuacion de Adveccion Multidimensional.ipynb


# Examen Módulo 1 - Optimización. Tipo de examen 1.
Lea cuidadosamente las siguientes **indicaciones** antes de comenzar el examen:
- Para resolver el examen edite este mismo archivo y renómbrelo de la siguiente manera: *Examen1_ApellidoNombre*, donde *ApellidoNombre* corresponde a su apellido paterno con la inicial en mayúscula, seguido de su primer nombre con la inicial en mayúscula **sin acentos**. Por ejemplo, en mi caso el nombre del archivo sería *Examen1_ZapataCristian*.
- Resuelva los puntos en el espacio provisto. Si requiere agregar más celdas para código o escritura, hágalo.
- Recuerde que también se está evaluando su capacidad de interpretar los resultados. Escriba sus interpretaciones/conclusiones en celdas utilizando *Markdown*.
- El formato de presentación de todo el examen en general debe ser adecuado. Use tamaños de letra, colores, etiquetas, etcétera.
- No se resuelven dudas de ningún tipo por los profesores de la asignatura. Por favor, absténgase de preguntar.
- Revise bien el tipo de examen. Sólo se le calificará si realiza el tipo de examen asignado.

## Primer punto. Optimización de funciones escalares usando `sympy` (30 puntos).

**Enunciado.** Un granjero tiene $2400$ m de cerca y desea cercar un campo rectangular de longitud $x$ y altura $y$ que limita con un río recto. No necesita cercar a lo largo del río. ¿Cuáles son las dimensiones
del campo que tiene el área más grande?

Ver la siguiente figura como ayuda

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

```python
xr = [-1100, -1100, 1100, 1100]
yr = [0, 100, 100, 0]

xx = [-1100, 1100]
yx = [0, 0]

xy = [1100, 1100]
yy = [0, 100]

plt.figure(figsize = (12, 8))
plt.plot(xr, yr, 'r')
plt.plot(xx, yx, 'b')
#plt.plot(xy, yy, 'g')
plt.text(-50, 5, 'Río', fontsize = 16)
plt.text(1140, 45, '$y$', fontsize = 16, color = 'k')
plt.text(0, 102, '$x$', fontsize = 16, color = 'k')
plt.show()
```

**Punto 1 (10 puntos).** Encuentre el área del rectángulo en función de la variable $x$, $A_R(x)$. También provea el dominio de la función para que tenga sentido (Recuerde que las áreas no pueden ser negativas).

Realice este punto usando fórmulas en LaTeX, en celdas Markdown.

**Solución punto 1.**

$$A_R = xy$$

$$P_R = 2y+x = 2400$$

Por lo tanto despejando $y$

$$y = (2400-x)/2$$

Reemplazando

$$A_R(x) = \frac{x(2400-x)}{2}$$

El dominio es

$$\text{Dom}_{A_R(x)}= [0,\,2400]$$

**Punto 2 (15 puntos).** Usando `sympy` maximice la función $A_R(x)$ en su dominio.

**Solución punto 2.**

```python
import sympy as sym
```

```python
sym.var('x')
```

    x

```python
def AR(x):
    return x*(2400-x)/2
```

```python
dA = sym.diff(AR(x),x)
dA
```

    -x + 1200

```python
xc=sym.solve(dA,x)
xc
```

    [1200]

```python
AR(0), AR(1200), AR(2400)
```

    (0.0, 720000, 0.0)

El máximo de la función es alcanzado en $x = 1200$ y tiene un valor de $72000$

**Punto 3 (5 puntos).** ¿Cuáles son las dimensiones $x$ y $y$ del rectángulo resultante?

**Solución punto 3.**

Para la dimensión de x se tiene que

```python
x = xc[0]
x
```

    1200

Y para $y$ se tiene que

```python
y = (2400-xc[0])/2
y
```

    600

## Segundo punto. Programación lineal (35 puntos).

**Enunciado.** Una compañía aérea dispone de dos tipos de aviones $A_1$, y $A_2$., para cubrir un determinado
trayecto. El avión $A_1$, debe hacer el trayecto más veces que el avión $A_2$ pero no puede sobrepasar $120$
viajes. Entre los dos aviones deben hacer más de $60$ vuelos, pero menos de $200$. En cada viaje del avión $A_1$ la empresa gana $\$30000$ y $\$20000$ por cada viaje del avión $A_2$. ¿Cuántos viajes debe hacer cada avión para obtener el máximo de ganancias?

*Definición de variables*: Sea $x_1$ la cantidad de viajes del avión $A_1$ y $x_2$ la cantidad de e viajes del avión $A_2$.

**Punto 1 (20 puntos).** Escriba la función a minimizar junto con las restricciones, explicando detalladamente cada paso (usando fórmulas en LaTeX, en celdas Markdown). Además escriba el problema en la forma
\begin{equation}
\begin{array}{ll}
\min_{\boldsymbol{x}} & \boldsymbol{c}^T\boldsymbol{x} \\
\text{s. a. }        & \boldsymbol{A}_{eq}\boldsymbol{x}=\boldsymbol{b}_{eq} \\
                     & \boldsymbol{A}\boldsymbol{x}\leq\boldsymbol{b},
\end{array}
\end{equation}

proporcionando $\boldsymbol{c}$, $\boldsymbol{A}$ y $\boldsymbol{b}$ ($\boldsymbol{A}_{eq}$ y $\boldsymbol{b}_{eq}$, de ser necesario) como arreglos de `NumPy` (no es necesario escribir en LaTeX el problema en la forma indicada, solo proporcionar las matrices como arreglos de `NumPy`). Adicionalmente, si se requiere, recuerde escribir las cotas (límites superiores e inferiores) para cada una de las variables.

**Solución punto 1.**

La función a maximizar viene dada por

$$\max_{x_1,x_2} 30000x_1 + 20000x_2$$

y las restricciones son las siguientes:

$$ 60\leq x_1 + x_2 \leq 200 $$
$$ x_1 \leq 120 $$
$$ x_2\leq x_1$$

```python
import scipy.optimize as opt
import numpy as np
```

```python
# Escriba acá las matrices como arreglos de Numpy
c = -np.array([30000,20000])
A = np.array([[-1,1],
              [-1,-1],
              [1,1],
              [1,0]])
b = np.array([0,-60,200,120])
```

**Punto 2 (15 puntos).** Resuelva el problema utilizando la función `linprog`. ¿Cuántos será la ganancia total?, ¿Cuántos viajes debe hacer cada avión?

```python
sol = opt.linprog(c,A,b,method='revised simplex')
```

```python
sol
```

         fun: -5200000.0
     message: 'Optimization terminated successfully.'
         nit: 3
       slack: array([ 40., 140.,   0.,   0.])
      status: 0
     success: True
           x: array([120.,  80.])

La ganancia total en los intereses será de $5200000$

## Tercer punto. Ajuste de curvas (35 puntos).

**Enunciado**. El archivo `renewable_energy.csv` contiene información histórica anual de México relativa al consumo de energías de fuentes renovables en porcentaje desde el año 1990 hasta el año 2015. La primer columna corresponde al consecutivo, la segunda a los años, y la tercera al consumo.

Tomado de: https://data.worldbank.org/indicator/EG.FEC.RNEW.ZS?view=chart

**Punto 1 (10 puntos).** Importar los datos en un DataFrame de pandas.

**Solución punto 1.**

```python
import pandas as pd
```

```python
archivo = 'renewable_energy.csv'
data = pd.read_csv(archivo)
data
```

      Year
      % of total final energy consumption

      0
      1990
      14.413302

      1
      1991
      13.195512

      2
      1992
      13.427092

      3
      1993
      13.303969

      4
      1994
      11.863103

      5
      1995
      13.261833

      6
      1996
      13.064724

      7
      1997
      12.699777

      8
      1998
      12.238453

      9
      1999
      12.748461

      10
      2000
      12.167360

      11
      2001
      11.343885

      12
      2002
      10.682781

      13
      2003
      10.169039

      14
      2004
      10.213293

      15
      2005
      10.305756

      16
      2006
      9.824107

      17
      2007
      9.456580

      18
      2008
      9.769618

      19
      2009
      9.168405

      20
      2010
      9.359978

      21
      2011
      9.066472

      22
      2012
      8.964824

      23
      2013
      9.231363

      24
      2014
      9.758462

      25
      2015
      9.215213

**Punto 2 (15 puntos).** Usando el año como variable independiente $x$ y el consumo de energías renovables como variable dependiente $y$, ajustar polinomios de grado 1 hasta grado 3.

**En caso de ser necesario, aplique el escalado de características usando el método de normalización**

Mostrar en un solo gráfico los datos de consumo contra los años, y los polinomios ajustados.

Graficar el error cuadrático acumulado contra el número de términos. ¿Cuál es el polinomio que mejor se ajusta?

**Solución punto 2.**

```python
import numpy as np
import scipy.optimize as opt
```

```python
x = data.iloc[:,0]
y = data.iloc[:,1]
x_norm = (x-min(x))/(max(x)-min(x))
y_norm = (y-min(y))/(max(y)-min(y))
```

```python
def fun_obj(beta,x,y,n):
    yhat = beta.dot([x**i for i in range(n+1)])
    error = y-yhat
    return np.sum(error**2)
```

```python
#Ajusta polinomio de grado 1
b01 = np.zeros(2)    #beta_inicial
ajuste1 = opt.minimize(fun_obj, b01, args=(x_norm,y_norm,1))

#Ajusta polinomio de grado 2
b02 = np.zeros(3)
ajuste2 = opt.minimize(fun_obj, b02, args=(x_norm,y_norm,2))

#Ajusta polinomio de grado 3
b03 = np.zeros(4)
ajuste3 = opt.minimize(fun_obj, b03, args=(x_norm,y_norm,3))

#Ajusta polinomio de grado 4
b04 = np.zeros(5)
ajuste4 = opt.minimize(fun_obj, b04, args=(x_norm,y_norm,4))
```

```python
ajuste1
```

          fun: 0.27378107881388425
     hess_inv: array([[ 0.07264957, -0.10683761],
           [-0.10683761,  0.21367521]])
          jac: array([-3.7252903e-09, -3.7252903e-09])
      message: 'Optimization terminated successfully.'
         nfev: 20
          nit: 3
         njev: 5
       status: 0
      success: True
            x: array([ 0.88606664, -0.98393945])

```python
ajuste2
```

          fun: 0.22818684541875256
     hess_inv: array([[ 0.14440563, -0.5609048 ,  0.45651969],
           [-0.5609048 ,  3.07968152, -2.87832469],
           [ 0.45651969, -2.87832469,  2.88932303]])
          jac: array([ 8.27014446e-07, -2.58907676e-06, -9.29459929e-07])
      message: 'Optimization terminated successfully.'
         nfev: 55
          nit: 9
         njev: 11
       status: 0
      success: True
            x: array([ 0.96948741, -1.50531779,  0.5213777 ])

```python
ajuste3
```

          fun: 0.1590737589202797
     hess_inv: array([[  0.22934968,  -1.7004934 ,   3.37214998,  -1.94792408],
           [ -1.7004934 ,  18.12713567, -41.21721553,  25.57734215],
           [  3.37214998, -41.21721553, 100.34378265, -64.91704869],
           [ -1.94792408,  25.57734215, -64.91704869,  43.19195436]])
          jac: array([-1.32806599e-06,  2.74367630e-06,  3.03238630e-06,  2.81631947e-06])
      message: 'Optimization terminated successfully.'
         nfev: 120
          nit: 18
         njev: 20
       status: 0
      success: True
            x: array([ 0.86126234, -0.06350736, -3.15469812,  2.45071381])

```python
ajuste4
```

          fun: 0.15348824262328895
     hess_inv: array([[ 3.06939551e-01, -3.56441596e+00,  1.20648031e+01,
            -1.55733088e+01,  6.79709350e+00],
           [-3.56441596e+00,  6.21510046e+01, -2.45658139e+02,
             3.45080996e+02, -1.58962906e+02],
           [ 1.20648031e+01, -2.45658139e+02,  1.04846563e+03,
            -1.54518191e+03,  7.35832516e+02],
           [-1.55733088e+01,  3.45080996e+02, -1.54518191e+03,
             2.35281848e+03, -1.14754948e+03],
           [ 6.79709350e+00, -1.58962906e+02,  7.35832516e+02,
            -1.14754948e+03,  5.69978612e+02]])
          jac: array([-8.92207026e-07,  4.75347042e-06, -3.51853669e-06, -2.65426934e-06,
            4.25428152e-06])
      message: 'Optimization terminated successfully.'
         nfev: 217
          nit: 29
         njev: 31
       status: 0
      success: True
            x: array([ 0.89114914, -0.78970597,  0.26437441, -2.93523081,  2.69309227])

```python
yhat1 = ajuste1.x.dot([x_norm**i for i in range(2)])*(max(y)-min(y)) + min(y)
yhat2 = ajuste2.x.dot([x_norm**i for i in range(3)])*(max(y)-min(y)) + min(y)
yhat3 = ajuste3.x.dot([x_norm**i for i in range(4)])*(max(y)-min(y)) + min(y)
yhat4 = ajuste4.x.dot([x_norm**i for i in range(5)])*(max(y)-min(y)) + min(y)
```

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

```python
plt.figure(figsize=(8,6))

plt.plot(x, y, 'r*', ms=5, label='Datos')
plt.plot(x, yhat1, 'b', lw=2, label='Ajuste 1')
plt.plot(x, yhat2, 'k', lw=2, label='Ajuste 2')
plt.plot(x, yhat3, 'g', lw=2, label='Ajuste 3')
plt.plot(x, yhat4, 'm', lw=2, label='Ajuste 4')

plt.xlabel('Años',fontsize=20)
plt.ylabel('% de consumo de energía',fontsize=20)

plt.grid()
plt.legend(loc='best')
plt.show()
```

```python
error = [ajuste1.fun, ajuste2.fun, ajuste3.fun, ajuste4.fun]
plt.figure(figsize=(8,6))
plt.plot(range(1,5), error, 'ro', ms=5)
plt.xlabel('Orden del polinomio')
plt.ylabel('error')
```

*Conclusión*

Según la gráfica anterior, el polinomio que mejor se ajusta es el de tercer orden.

**Punto 3 (10 puntos).** Con los polinomios ajustados en el punto anterior, estime el consumo en el año 2015 y compare con el dato real $9.215\%$.

¿Cuál polinomio estima mejor este dato? ¿Es el polinomio que mejor se ajusta? Concluya.

**Solución punto 3.**

```python
yhat1
```

    array([13.79253832, 13.57809945, 13.36366057, 13.14922169, 12.93478282,
           12.72034394, 12.50590506, 12.29146619, 12.07702731, 11.86258843,
           11.64814956, 11.43371068, 11.21927181, 11.00483293, 10.79039405,
           10.57595518, 10.3615163 , 10.14707742,  9.93263855,  9.71819967,
            9.50376079,  9.28932192,  9.07488304,  8.86044416,  8.64600529,
            8.43156641])

```python
yhat1[25],yhat2[25],yhat3[25], yhat4[25], y[25]
```

    (8.431566410389427,
     8.886079101041409,
     9.47573156974496,
     9.638686696873242,
     9.215213455)

*Conclusión*. El polinomio que mejor estima este dato es el polinomio de grado tres.


```python
%matplotlib inline
import matplotlib.pylab as plt
import numpy as np
import math
from numba import jit, njit, vectorize
```

## What is Numba?
Numba is a **just-in-time**, **type-specializing**, **function compiler** for accelerating **numerically-focused** Python. That's a long list, so let's break down those terms:

+ **function compiler**: Numba compiles Python functions, not entire applications, and not parts of functions. Numba does not replace your Python interpreter, but is just another Python module that can turn a function into a (usually) faster function.
+ **type-specializing**: Numba speeds up your function by generating a specialized implementation for the specific data types you are using. Python functions are designed to operate on generic data types, which makes them very flexible, but also very slow. In practice, you only will call a function with a small number of argument types, so Numba will generate a fast implementation for each set of types.
+ **just-in-time**: Numba translates functions when they are first called. This ensures the compiler knows what argument types you will be using. This also allows Numba to be used interactively in a Jupyter notebook just as easily as a traditional application.
+ **numerically-focused**: Currently, Numba is focused on numerical data types, like int, float, and complex. There is very limited string processing support, and many string use cases are not going to work well on the GPU. To get best results with Numba, you will likely be using NumPy arrays.

### Problem 1 - A First Numba Function

**1a)** To start our exploration of Numba's features, let's write a python function to add two numbers. We'll creatively name it `add`:

```python
def add(x, y):
  return x + y # add code here
```

Now, test the function, first with two scalar integers:

```python
add(1, 2) # add code here
```

    3

**1b)** With Numpy, we can use our function to add not just scalars, but vectors as well. Using your favorite array creation routine, create two integer arrays with ten elements each, called `a` and `b`, and use your `add` function to add them.

```python
a = np.arange(0,10) # add code here
b = np.arange(1,11) # add code here

add(a, b) # add code here
```

    array([ 1,  3,  5,  7,  9, 11, 13, 15, 17, 19])

Okay, so our function can add things. Now, let's use Numba's `jit` function to create a Numba version of our addition function:

```python
numba_add = jit(add)
```

More commonly, you will use `jit` as a decorator, by adding `@jit` to the line above your function definition, but the above version shows you that at heart, `@jit` is just a python function that takes other functions as its argument!

**1c)**  By default, a Numba function saves the original python version of the function in the variable `py_func`. Check that the original python version gives you the same answer as the Numba version.

```python
print(add(a,b)) #add code here
print(numba_add(a,b)) #add code here
print(numba_add.py_func(a,b)) #add code here
```

    [ 1  3  5  7  9 11 13 15 17 19]
    [ 1  3  5  7  9 11 13 15 17 19]
    [ 1  3  5  7  9 11 13 15 17 19]

#### Profiling
A central feature of parallel programming, Numba, and writing efficient code more generally is **profiling**, or understanding how long various pieces of your program take to run. Profiling tools are becoming ever more sophisticated, but for today we're going to stick with the tried-and-true method of timing things. An easy way to do this in python is using the `%timeit` magic function. Let's try it out on our addition function:

```python
%timeit add(1,2)
```

    The slowest run took 9.13 times longer than the fastest. This could mean that an intermediate result is being cached.
    10000000 loops, best of 3: 163 ns per loop

What's going on here? `%timeit` is running our function many times, and then reporting the average time it takes to run. This is generally a better approach than timing a single function execution, because it accounts for random events that may cause any given run to perform poorly.

**1d)** Compare the time it takes to run your function with scalar vs array arguments, then your function vs python's add function (the standard ''+'' operator).

```python
%timeit add(1,2) # add code here
%timeit add(a, b) # add code here
%timeit a + b # add code here
```

    The slowest run took 15.70 times longer than the fastest. This could mean that an intermediate result is being cached.
    10000000 loops, best of 3: 163 ns per loop
    The slowest run took 52.13 times longer than the fastest. This could mean that an intermediate result is being cached.
    1000000 loops, best of 3: 622 ns per loop
    The slowest run took 49.31 times longer than the fastest. This could mean that an intermediate result is being cached.
    1000000 loops, best of 3: 498 ns per loop

So, scalars are faster than arrays (makes sense), and python's addition function is better than ours (seems reasonable). Now, let's see how fast our pre-compiled Numba addition function is.

```python
%timeit numba_add(a,b) # add code here
```

    The slowest run took 40.65 times longer than the fastest. This could mean that an intermediate result is being cached.
    1000000 loops, best of 3: 759 ns per loop

Hold on - our new pre-compiled function is running even slower than the original python version! What's going on here?

### Problem 2 - A Better Numba Function

(This problem borrowed from [seibert's 2018 gtc numba tutorial](https://github.com/ContinuumIO/gtc2018-numba).)

As we saw in the first example, Numba isn't going to speed up everything. Generally, Numba will help you most in circumstances where python's line-by-line interperability and lack of type casting is slowing it down. We can use a slightly more complicated function to demonstrate this. The following is a function to calculate the hypotenuse of two numbers, that has been carefully designed to compensate for the computer's finite precision representation of numbers (check out https://en.wikipedia.org/wiki/Hypot for more info).

**2a)** Use the `@jit` decorator to generate a Numba version of this function.

```python
@jit # add code here
def hypotenuse(x, y):
  x = abs(x);
  y = abs(y);
  t = min(x, y);
  x = max(x, y);
  t = t / x;
  return x * math.sqrt(1+t*t)
```

**2b)** Use the `%timeit` function to determine whether the Numba version of the hyptonenuse function is better than the original Python implementation.

```python
%timeit hypotenuse(3,4) # add code here
%timeit hypotenuse.py_func(3,4) # add code here
```

    The slowest run took 25.17 times longer than the fastest. This could mean that an intermediate result is being cached.
    1000000 loops, best of 3: 257 ns per loop
    The slowest run took 14.56 times longer than the fastest. This could mean that an intermediate result is being cached.
    1000000 loops, best of 3: 891 ns per loop

**2c)** Numba functions can call other functions, provided they are also Numba functions. Below is a function that loops through two numpy arrays and puts their sum into an output array. Modify the following function to calculate the hypotenuse instead.

```python
@njit # this is an alias for @jit(nopython=True)
def ex_func(x, y, out):
  for i in range(x.shape[0]):
    out[i] = hypotenuse(x[i], y[i]) # change this line
```

```python
in1 = np.arange(10, dtype=np.float64)
in2 = 2 * in1 + 1
out = np.empty_like(in1)

print('in1:', in1)
print('in2:', in2)

ex_func(in1, in2, out)

print('out:', out)
```

    in1: [0. 1. 2. 3. 4. 5. 6. 7. 8. 9.]
    in2: [ 1.  3.  5.  7.  9. 11. 13. 15. 17. 19.]
    out: [ 1.          3.16227766  5.38516481  7.61577311  9.8488578  12.08304597
     14.31782106 16.55294536 18.78829423 21.02379604]

```python
# This test will fail until you fix the ex1 function
np.testing.assert_almost_equal(out, np.hypot(in1, in2))
```

### Problem 3 - Fun with Fractals
Now that we've got the basics of the Numba `jit` decorator down, let's have a little fun. A classic example problem in parallel programming is the calculation of a fractal, because a large fraction of the work can be done in parallel. Below is some code that calculates whether a number is a member of the Julia set, and then computes the set on a discrete domain to calculate a fractal.

**3a)** Modify the code below to use Numba and test how much faster it is than the original python implementation.

```python
@njit # add code here
def julia(x, y, max_iters):
    """
    Given the real and imaginary parts of a complex number,
    determine if it is a candidate for membership in the Julia
    set given a fixed number of iterations.
    """
    i = 0
    c = complex(-0.8, 0.156)
    a = complex(x,y)
    for i in range(max_iters):
        a = a*a + c
        if (a.real*a.real + a.imag*a.imag) > 1000:
            return 0
    return 255
```

```python
@njit # add code here
def create_fractal(min_x, max_x, min_y, max_y, image, iters):
    height = image.shape[0]
    width = image.shape[1]

    pixel_size_x = (max_x - min_x) / width
    pixel_size_y = (max_y - min_y) / height
    for x in range(width):
        real = min_x + x * pixel_size_x
        for y in range(height):
            imag = min_y + y * pixel_size_y
            color = julia(real, imag, iters)
            image[y, x] = color

    return image
```

```python
image = np.zeros((500, 750), dtype=np.uint8)
%timeit create_fractal(-2.0, 2.0, -1.0, 1.0, image, 200)
```

    1 loop, best of 3: 54.4 ms per loop

Want to see what you made? Run the following code to plot the image. Feel free to pick your favorite matplotlib color map :)

```python
plt.imshow(image)
plt.viridis()
plt.show()
```

**3b)** There is more than one type of fractal in the world, however! Below is a function that determines membership in the Mandelbrot set. Modify the function using to take advantage of Numba, then modify the code above to produce a new pretty picture.

```python
@njit #add code here
def mandel(x, y, max_iters):
    """
    Given the real and imaginary parts of a complex number,
    determine if it is a candidate for membership in the Mandelbrot
    set given a fixed number of iterations.
    """
    i = 0
    c = complex(x,y)
    z = 0.0j
    for i in range(max_iters):
        z = z*z + c
        if (z.real*z.real + z.imag*z.imag) >= 4:
            return i
    return 255
```

```python
# add code here
@njit
def create_fractal(min_x, max_x, min_y, max_y, image, iters):
    height = image.shape[0]
    width = image.shape[1]

    pixel_size_x = (max_x - min_x) / width
    pixel_size_y = (max_y - min_y) / height
    for x in range(width):
        real = min_x + x * pixel_size_x
        for y in range(height):
            imag = min_y + y * pixel_size_y
            color = mandel(real, imag, iters)
            image[y, x] = color

    return image
```

```python
image = np.zeros((500, 750), dtype=np.uint8)
create_fractal(-2.0, 2.0, -1.0, 1.0, image, 200)
plt.imshow(image)
plt.viridis()
plt.show()
```

### Problem 4 - Typing

Much of the power of Numba comes from its ability to compile a specific version of a python function based on the **data types** of its arguments. The data type describes what kind of variables the function uses, and in Numpy and Numba, pre-defined type names are based on the kind and size of the number in memory. You can find the type of a variable (or array) using numpy's `dtype` object. For example, let's see what type our `a` array is.

```python
a.dtype
```

    dtype('int64')

This tells us the array contains integers, and each integer has been assigned 64bits in memory (or equivalently, 8 bytes). Most python functions are defined to work on arbitrary types, so that if you use the + operator, for example, you can add integers, floats, complex numbers, or even strings! However, this flexibility comes at a cost, performance-wise. Numba, on the other hand, compiles each function based on the types of its arguments, and infers the type of the result. You can see this if you run the `inspect_types` function on a numba function:

```python
numba_add.inspect_types()
```

    add (array(int64, 1d, C), array(int64, 1d, C))
    --------------------------------------------------------------------------------
    # File: <ipython-input-2-9397aad11c87>
    # --- LINE 1 ---
    # label 0

    def add(x, y):

      # --- LINE 2 ---
      #   x = arg(0, name=x)  :: array(int64, 1d, C)
      #   y = arg(1, name=y)  :: array(int64, 1d, C)
      #   $0.3 = arrayexpr(expr=('+', [Var(x, <ipython-input-2-9397aad11c87> (2)), Var(y, <ipython-input-2-9397aad11c87> (2))]), ty=array(int64, 1d, C))  :: array(int64, 1d, C)
      #   del y
      #   del x
      #   $0.4 = cast(value=$0.3)  :: array(int64, 1d, C)
      #   del $0.3
      #   return $0.4

      return x + y # add code here

    ================================================================================

**4a)** Numba has inferred the types for this function based on how we've used it. Try out your `numba_add` function with two floating point numbers, then re-inspect the types of the Numba function. Are they the same?

```python
#Add code here
numba_add(3.,4.)
numba_add.inspect_types()
```

    add (array(int64, 1d, C), array(int64, 1d, C))
    --------------------------------------------------------------------------------
    # File: <ipython-input-2-9397aad11c87>
    # --- LINE 1 ---
    # label 0

    def add(x, y):

      # --- LINE 2 ---
      #   x = arg(0, name=x)  :: array(int64, 1d, C)
      #   y = arg(1, name=y)  :: array(int64, 1d, C)
      #   $0.3 = arrayexpr(expr=('+', [Var(x, <ipython-input-2-9397aad11c87> (2)), Var(y, <ipython-input-2-9397aad11c87> (2))]), ty=array(int64, 1d, C))  :: array(int64, 1d, C)
      #   del y
      #   del x
      #   $0.4 = cast(value=$0.3)  :: array(int64, 1d, C)
      #   del $0.3
      #   return $0.4

      return x + y # add code here

    ================================================================================
    add (float64, float64)
    --------------------------------------------------------------------------------
    # File: <ipython-input-2-9397aad11c87>
    # --- LINE 1 ---
    # label 0

    def add(x, y):

      # --- LINE 2 ---
      #   x = arg(0, name=x)  :: float64
      #   y = arg(1, name=y)  :: float64
      #   $0.3 = x + y  :: float64
      #   del y
      #   del x
      #   $0.4 = cast(value=$0.3)  :: float64
      #   del $0.3
      #   return $0.4

      return x + y # add code here

    ================================================================================

So far we have been using what Numba refers to as "lazy" (or "call-time") decoration. Basically, we've been letting Numba do the work of figuring out how we're using the function and inferring the types for us. Alternatively, if we know how we are going to use a given function, we can use "eager" (or "compile-time") decoration. To do this, we make use of the `vectorize` decorator. For example, if we want to make an integer-only version of our addition function, we could write:

```python
@vectorize(['int64(int64, int64)'], target='cpu')
def add_ufunc(x, y):
  return x + y
```

#### Numpy Universal Functions
You'll notice a couple of new things here. In the first set of brackets, we have specified both the argument types of the function (those are inside the parentheses), as well as the return type of the function. This is just making explicit what Numba was previously inferring on our behalf. In second set of brackets you'll see that we have specified a 'target' architechture for the function. The default is `cpu`, which means that Numba is optimizing the function to your specific machine. Other options include `parallel`, which allows you to take advantage of multicore processors, and `cuda`, which we'll be discussing more tomorrow. You'll also notice that we called this a 'ufunc', which is short for [Universal Function](https://docs.scipy.org/doc/numpy/reference/ufuncs.html). In brief, universal functions are numpy functions that operate on ndarrays in element by element fashion. So if we pass two vectors to our `add_func`, they will be added together and a return vector of the same shape will be returned.

```python
%timeit add_ufunc(a,b)
```

    The slowest run took 153.01 times longer than the fastest. This could mean that an intermediate result is being cached.
    1000000 loops, best of 3: 512 ns per loop

**4b)** Try your ufunc out with a new target, 'parallel'. How does the speed compare? What if the array size is much larger?

```python
big_array = np.arange(0,1000000) # add code here
%timeit add_ufunc(big_array, big_array) # add code here
```

    The slowest run took 4.77 times longer than the fastest. This could mean that an intermediate result is being cached.
    100 loops, best of 3: 1.7 ms per loop

```python
@vectorize(['int64(int64, int64)'], target='parallel')
def add_ufunc(x, y):
  return x + y
%timeit add_ufunc(big_array,big_array)
```

    1000 loops, best of 3: 946 µs per loop

### Problem 5 - Direct Summation
(This problem borrowed in its entirety from [gforsyth's scipy17 numba tutorial](https://github.com/gforsyth/numba_tutorial_scipy2017).)

Many physical problems require the evaluation of all pairwise interactions of a large number of particles, so-called N-body problems. These problems arise in molecular dynamics, astrodynamics and electromagnetics among others.

Their pairwise interactions can be expressed as:

\begin{equation}
f_i = \sum_{j=1}^n{P \left(\boldsymbol{x}_i, \boldsymbol{x}_j \right)w_j} \ \ \ \text{for } i=1,2,...,n
\end{equation}

*  where subscripts $i$,  $j$ respectively denote *target* and *source*
*  $f_i$ can be a *potential* (or *force*) at target point $i$
*  $w_j$ is the *source weight*
*  $\boldsymbol{x}_i, \boldsymbol{x}_j$ are the *spatial positions* of particles
*  $P \left(\boldsymbol{x}_i, \boldsymbol{x}_j \right)$ is the *interaction kernel*.

In order to evalute the potential $f_i$ at a target point $i$, we have to loop over each source particle $j$. Since there are $n$ target points $i$, this 'brute-force' approach costs $\mathcal{O} \left(n^2 \right)$ operations.

One possible approach in this kind of problem is to define a few classes, say `Point` and `Particle` and then loop over the objects and perform the necessary point-to-point calculations.

```python
class Point():
    """
    Arguments:
        domain: the domain of random generated coordinates x,y,z,
                default=1.0

    Attributes:
        x, y, z: coordinates of the point
    """
    def __init__(self, domain=1.0):
        self.x = domain * np.random.random()
        self.y = domain * np.random.random()
        self.z = domain * np.random.random()

    def distance(self, other):
        return ((self.x - other.x)**2 +
                (self.y - other.y)**2 +
                (self.z - other.z)**2)**.5
```

```python
class Particle(Point):
    """
    Attributes:
        m: mass of the particle
        phi: the potential of the particle
    """

    def __init__(self, domain=1.0, m=1.0):
        Point.__init__(self, domain)
        self.m = m
        self.phi = 0.
```

Next, we define a function to calculate the particle interaction via direct summation:

```python
def direct_sum(particles):
    """
    Calculate the potential at each particle
    using direct summation method.

    Arguments:
        particles: the list of particles

    """
    for i, target in enumerate(particles):
        for source in (particles[:i] + particles[i+1:]):
            r = target.distance(source)
            target.phi += source.m / r
```

All that's left is to create a list of random particles with assigned masses:

```python
n = 1000
particles = [Particle(m = 1 / n) for i in range(n)]
```

**5a)** Run the direct summation code and determine how long it takes with 10, 100, 1000 particles. Is there a relationship?

```python
%timeit direct_sum(particles)
```

Because this is an $\mathcal{O} \left(n^2 \right)$ function, each addition of 10x particles costs 100x more in computational time. Given how expensive it is, let's see if we can improve things a bit with Numba. (This also happens to be a highly parallelizable problem, but we'll get to that tomorrow.)

**How do we use Numba on this problem?** There is a subtle issue here - Numba doesn't support jitting native Python classes.  There is a `jit_class` structure in Numba but it's still in early development. But we'd like to have attributes for readable programming. The solution is to build NumPy custom dtypes.

```python
particle_dtype = np.dtype({'names':['x','y','z','m','phi'],
                             'formats':[np.double,
                                        np.double,
                                        np.double,
                                        np.double,
                                        np.double]})
```

```python
myarray = np.ones(3, dtype=particle_dtype)
```

```python
myarray
```

You can access an individual "attribute" like this:

```python
myarray[0]['x'] = 2.0
```

**5b)** Write a `jit` function `create_n_random_particles` that takes the arguments `n` (number of particles), `m` (mass of every particle) and a domain within which to generate a random number (as in the class above).
It should create an array with `n` elements and `dtype=particle_dtype` and then return that array.

For each particle, the mass should be initialized to the value of `m` and the potential `phi` initialized to zero.

*Hint: You will probably want to loop over the number of particles within the function to assign attributes.*

```python
@njit
def create_n_random_particles(n, m, domain=1):
    '''
    Creates `n` particles with mass `m` with random coordinates
    between 0 and `domain`
    '''
    parts = np.zeros((n), dtype=particle_dtype)
    #attribute access only in @jitted function
    for p in parts:
        p.x = np.random.random() * domain
        p.y = np.random.random() * domain
        p.z = np.random.random() * domain
        p.m = m
        p.phi = 0
    return parts
```

Now we'll create our array of particles using the new function.

```python
particles = create_n_random_particles(1000, .001, 1)
```

```python
particles[:3]
```

We don't have a `distance` method anymore, so we need to write a function to take care of that.

**5c)** Write a `jit` function `distance` to calculate the distance between two particles of dtype `particle_dtype`.

```python
@njit
def distance(part1, part2):
    '''calculate the distance between two particles'''
    return ((part1.x - part2.x)**2 +
            (part1.y - part2.y)**2 +
            (part1.z - part2.z)**2)**.5
```

```python
distance(particles[0], particles[1])
```

```python
%%timeit
distance(particles[0], particles[1])
```

**5d)** Modify the `direct_sum` function above to instead work a NumPy array of particles.  Loop over each element in the array and calculate its total potential. Time the result and compare it to your previous version of this function.

```python
@njit
def direct_sum(particles):
    for i, target in enumerate(particles):
        for j, source in enumerate(particles):
            if i != j:
                r = distance(target, source)
                target.phi += source.m / r

    return particles
```

```python
%timeit direct_sum(particles)
```


## The Basic Idea of Machine-learning

Imagine a monkey drawing on a canvas (say, of `128 * 128` pixels). What's the probability that it draw a human-face? Almost none, isn't it. This implies that

* the manifold of human-face involved in $\mathbb{R}^{128 \times 128}$ has relatively much smaller dimensions.

* Even, the manifold is spares.

To see this, imagine you modify the background of a painting with a human-face in the foreground, the points in $\mathbb{R}^{128 \times 128}$ before and after the modification are generally far from each other.

Thus, the task of machine-learning is to find out the low-dimensional spares manifold, mapping the manifold to a lower dimensional compact space, and mapping the element there back to generate real-world object, like painting.

We call the real-world object "observable", and the low-dimensional spares manifold "latent" space.

This serves both to data-compression and data-abstraction. In fact, these are two aspects of one thing: the probability distribution of data (which we will talk in the next topic).

## Auto-encoder

### Conceptions

This basic idea naturally forces to "auto-encoder", which has two parts:

1. Encoder: mapping the observable to latent.
2. Decoder: mapping the latent to observable.

Let $X$ the space of observable, and $Z$ the latent. Let $f: X \mapsto Z$ denotes the encoder, and $g: Z \mapsto X$ the decoder. Then, for $\forall x \in X$, we would expect

\begin{equation}
  g \circ f(x) \approx x.
\end{equation}

To numerically characterize this approximation, let $d_{\text{obs}}$ some pre-defined distance in the space of observable, we can define loss

\begin{equation}
  \mathcal{L}_{\text{recon}} = \frac{1}{|D|} \sum_{x \in D} d_{\text{obs}} \left(x, g \circ f (x) \right).
\end{equation}

We call this "reconstruction" loss, since $g \circ f (x)$ is a reconstruction of $x$.

For ensuring the compactness of the latent, an additional regularizer is added to the reconstruction loss, by some pre-defined distance in the latant space $d_{\text{lat}}$. Thus, the total loss is

\begin{equation}
  \mathcal{L} = \frac{1}{|D|} \sum_{x \in D} d_{\text{obs}} \left(x, g \circ f (x) \right)
              + d_{\text{lat}} \left( f(x), 0 \right).
\end{equation}

The task is thus to find the functions $f$ and $g$ that minimize the total loss. This utilizes the universality property of neural network.

### Reference:
  1. [Wikipedia](https://en.wikipedia.org/wiki/Autoencoder).

## Implementation

```python
%matplotlib inline

from IPython.display import display
import matplotlib.pyplot as plt
from tqdm import tqdm
from PIL import Image
import numpy as np
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
```

```python
data_path = '../../dat/MNIST/'
mnist = input_data.read_data_sets(
    data_path, one_hot=True,
    source_url='http://yann.lecun.com/exdb/mnist/')
```

    WARNING:tensorflow:From <ipython-input-2-e98df23d2c33>:4: read_data_sets (from tensorflow.contrib.learn.python.learn.datasets.mnist) is deprecated and will be removed in a future version.
    Instructions for updating:
    Please use alternatives such as official/mnist/dataset.py from tensorflow/models.
    WARNING:tensorflow:From /home/apps/anaconda3/lib/python3.6/site-packages/tensorflow/contrib/learn/python/learn/datasets/mnist.py:260: maybe_download (from tensorflow.contrib.learn.python.learn.datasets.base) is deprecated and will be removed in a future version.
    Instructions for updating:
    Please write your own downloading logic.
    WARNING:tensorflow:From /home/apps/anaconda3/lib/python3.6/site-packages/tensorflow/contrib/learn/python/learn/datasets/mnist.py:262: extract_images (from tensorflow.contrib.learn.python.learn.datasets.mnist) is deprecated and will be removed in a future version.
    Instructions for updating:
    Please use tf.data to implement this functionality.
    Extracting ../../dat/MNIST/train-images-idx3-ubyte.gz
    WARNING:tensorflow:From /home/apps/anaconda3/lib/python3.6/site-packages/tensorflow/contrib/learn/python/learn/datasets/mnist.py:267: extract_labels (from tensorflow.contrib.learn.python.learn.datasets.mnist) is deprecated and will be removed in a future version.
    Instructions for updating:
    Please use tf.data to implement this functionality.
    Extracting ../../dat/MNIST/train-labels-idx1-ubyte.gz
    WARNING:tensorflow:From /home/apps/anaconda3/lib/python3.6/site-packages/tensorflow/contrib/learn/python/learn/datasets/mnist.py:110: dense_to_one_hot (from tensorflow.contrib.learn.python.learn.datasets.mnist) is deprecated and will be removed in a future version.
    Instructions for updating:
    Please use tf.one_hot on tensors.
    Extracting ../../dat/MNIST/t10k-images-idx3-ubyte.gz
    Extracting ../../dat/MNIST/t10k-labels-idx1-ubyte.gz
    WARNING:tensorflow:From /home/apps/anaconda3/lib/python3.6/site-packages/tensorflow/contrib/learn/python/learn/datasets/mnist.py:290: DataSet.__init__ (from tensorflow.contrib.learn.python.learn.datasets.mnist) is deprecated and will be removed in a future version.
    Instructions for updating:
    Please use alternatives such as official/mnist/dataset.py from tensorflow/models.

```python
def get_encoder(latent_dim, hidden_layers):

  def encoder(observable, name='encoder', reuse=None):
    with tf.variable_scope(name, reuse=reuse):
      hidden = observable
      for hidden_layer in hidden_layers:
        hidden = tf.layers.dense(hidden, hidden_layer,
                                 activation=tf.nn.relu)
      latent = tf.layers.dense(hidden, latent_dim, activation=None)
      return latent

  return encoder
```

```python
def get_decoder(observable_dim, hidden_layers):

  def decoder(latent, name='decoder', reuse=None):
    with tf.variable_scope(name, reuse=reuse):
      hidden = latent
      for hidden_layer in hidden_layers:
        hidden = tf.layers.dense(hidden, hidden_layer,
                                 activation=tf.nn.relu)
      reconstructed = tf.layers.dense(hidden, observable_dim,
                                      activation=tf.nn.sigmoid)
      return reconstructed

  return decoder
```

```python
def get_loss(observable, encoder, decoder, regularizer=None, reuse=None):
  if regularizer is None:
    regularizer = lambda latent: 0.0

  with tf.name_scope('loss'):
    # shape: [batch_size, latent_dim]
    latent = encoder(observable, reuse=reuse)
    # shape: [batch_size, observable_dim]
    reconstructed = decoder(latent, reuse=reuse)
    # shape: [batch_size]
    squared_errors = tf.reduce_sum(
        (reconstructed - observable) ** 2,
        axis=1)
    mean_square_error = tf.reduce_mean(squared_errors)
    return mean_square_error + regularizer(latent)
```

```python
latent_dim = 64
encoder = get_encoder(latent_dim=latent_dim,
                      hidden_layers=[512, 256, 128])
decoder = get_decoder(observable_dim=28*28,
                      hidden_layers=[128, 256, 512])
observable = tf.placeholder(shape=[None, 28*28],
                            dtype='float32',
                            name='observable')
latent_samples = tf.placeholder(shape=[None, latent_dim],
                                dtype='float32',
                                name='latent_samples')
generated = decoder(latent_samples, reuse=tf.AUTO_REUSE)
```

```python
def regularizer(latent, name='regularizer'):
  with tf.name_scope(name):
    distances = tf.reduce_sum(latent ** 2, axis=1)
    return tf.reduce_mean(distances)
```

```python
loss = get_loss(observable, encoder, decoder,
                regularizer=regularizer,
                reuse=tf.AUTO_REUSE)
```

```python
optimizer = tf.train.AdamOptimizer(epsilon=1e-3)
train_op = optimizer.minimize(loss)
```

```python
sess = tf.Session()
sess.run(tf.global_variables_initializer())
```

```python
loss_vals = []
for i in tqdm(range(100000)):
  X, y = mnist.train.next_batch(batch_size=128)
  _, loss_val = sess.run([train_op, loss], {observable: X})
  if np.isnan(loss_Xy_val):
    raise ValueError('Loss has been NaN.')
  loss_vals.append(loss_val)

print('Final loss:', np.mean(loss_vals[-100:]))

plt.plot(loss_vals)
plt.xlabel('steps')
plt.ylabel('loss')
plt.show()
```

```python
def get_image(array):
  """
  Args:
    array: Numpy array with shape `[28*28]`.

  Returns:
    An image.
  """
  array = 255 * array
  array = array.reshape([28, 28])
  array = array.astype(np.uint8)
  return Image.fromarray(array)
```

```python
latent_sample_vals = np.random.normal(size=[128, latent_dim])
generated_vals = sess.run(generated, {latent_samples: latent_sample_vals})

# Display the results
n_display = 5
for i in range(n_display):
  print('Gnerated:')
  display(get_image(generated_vals[i]))
  print()
```

```python

```


# Unitary Learning without qgrad

 **Disclaimer:** This tutorial does **not** use `qgrad`.
 The intention is to show follow this tutorial with
 another one _using_ `qgrad` to show to how easier
 things are with qgrad.

 In this tutorial, we aim to learn unitary matrices
 using gradient descent. The tutorial numerically
 reproduces part of
 [Lloyd et al.](https://arxiv.org/abs/1901.03431)
 and [Bobak et al.](https://arxiv.org/abs/2001.11897),
 and follows similar formalism as introduced in the two papers.

 For a target unitary matrix, $U$, we intend to find
 optimal parameter vectors for the parameterized unitary
 $U(\vec{t}, \vec{\tau})$, such that $U(\vec{t}, \vec{\tau})$
 approximates $U$ as closely as possible.

\begin{equation}\label{decomp}
U(\vec{t}, \vec{\tau}) =
e^{-iB\tau_{N}}e^{-iAt_{N}} \ldots e^{-iB\tau_{1}}e^{-iAt_{1}}
\end{equation}

where $\vec{t}$ and $\vec{\tau}$ are parameter vectors of size $N$ each
and matrices $A$ and $B$ are special Hermitian matrices chosen
from a Gaussian Unitary Ensemble (GUE).
We shall reproduce the findings of
[Lloyd et al.](https://arxiv.org/abs/1901.03431)
and [Bobak et al.](https://arxiv.org/abs/2001.11897)
that a $d$ dimensional unitary can be approximated
with the number of parameters
$N$ that scale as $O(d^2)$.

For faster numerical computations we intend to learn a simple
$2 \times 2$ unitary matrix $U$. Here the input dataset
consists of $2 \times 1$ random kets, call them
$| \psi_{l} \rangle$ and output dataset is the action of the
target unitary $U$ on these kets, $U |\psi_{l} \rangle$. The
maximum value of $l$ is $80$, meaning that we merely use 80
data points (kets in this case) to efficiently learn the
target unitary $U$ from  $U(\vec{t}, \vec{\tau})$

```python
import matplotlib.pyplot as plt
import numpy as np
import tenpy
from qutip import fidelity, Qobj, rand_ket
from scipy.stats import unitary_group
from scipy.linalg import expm
```

```python
def make_dataset(m, d):
    r"""Prepares a dataset of input and output
        kets to be used for training.

    Args:
    ----
        m (int): Number of data points,
            80% of which would be used for training
        d (int): Dimension of a (square) unitary
            matrix to be approximated

    Returns:
    --------
        tuple: tuple of lists containing (numpy arrays of)
            input and output kets respectively.
    """
    ket_input = []
    ket_output = []
    for i in range(m):
        ket_input.append(rand_ket(d, seed=3000).full())
        #Output data -- action of unitary on a ket states
        ket_output.append(np.matmul(tar_unitr, ket_input[i]))

    return (ket_input, ket_output)

m = 100 # number of training data points
train_len = int(m * 0.8)
d = 2 #dimension of unitary
N = 4 #size of parameter vectors tau and t
# Fixed random d-dimensional target unitary matrix that we want to learn
tar_unitr = unitary_group.rvs(d)
res = make_dataset(m, d)
ket_input, ket_output = res[0], res[1]
```

## Recipe for making $U(\vec{t}, \vec{\tau})$

 We make $U(\vec{t}, \vec{\tau})$ by repeated application of $e^{-iB\tau_{k}}e^{-iAt_{k}}$ at k-th step. We multiply $e^{-iB\tau_{k}}e^{-iAt_{k}}$ in a [QAOA](https://arxiv.org/abs/1411.4028) like fashion $N$ times, where N is the dimension of $\vec{t}$ and $\vec{\tau}$. Higher N $\rightarrow$ better approximation.

[Lloyd et al.](https://arxiv.org/abs/1901.03431) and [Bobak et al.](https://arxiv.org/abs/2001.11897), matrices $A$ and $B$ are chosen from a Gaussian Unitary Ensemble (GUE). We use `tenpy` to sample $A$ and $B$ from GUE.

```python
# tenpy for sampling A and B from GUE
A = tenpy.linalg.random_matrix.GUE((d,d))
B = tenpy.linalg.random_matrix.GUE((d,d))
```

```python
def make_unitary(N, params):
    r"""Returns a paramterized unitary matrix.

    : math:: \begin{equation}\label{decomp}
                U(\vec{t}, \vec{\tau}) =
                e^{-iB\tau_{N}}e^{-iAt_{N}} ... e^{-iB\tau_{1}}e^{-iAt_{1}}
             \end{equation}

    Args:
    ----
        N (int): Size of the parameter vectors,
            :math:`\tau` and :math:`\t`
        params (:obj:`np.ndarray`): parameter vector
                of size :math:`2 * N` where the first half
                parameters are :math:`\vec{t}` params
                and the second half encodes \vec{\tau})
                parameters.

    Returns:
        :obj:`np.ndarray`: numpy array representation of
                paramterized unitary matrix
    """
    unitary = np.eye(d)
    for i in range (N):
        unitary = np.matmul(np.matmul(
            expm(-1j * B * params[i + N][0]),
            expm(-1j * A * params[i][0])), unitary)

    return unitary

```

## Criteria for learnability -- the cost function
We use the same cost function as the authors
[Seth Lloyd and Reevu Maity, 2020](https://arxiv.org/pdf/1901.03431.pdf)
define

\begin{equation} \label{err_ps}
E = 1 - (\frac{1}{M})\sum_{l} \langle \psi_{l}|U^{\dagger} U(\vec{t},\vec{\tau})|\psi_{l}\rangle
\end{equation}

where $|\psi_{l} \rangle$ is the input ket,
$U(\vec{t},\vec{\tau})$ and $U$ are the
parameterized and target unitaries
respectively and $M$ is the total
number of training data points, which in our
example is $80$

```python
def cost(params, inputs, outputs):
    r"""Calculates the cost/error on
    the whole training dataset.

    Args:
    ----
        params: parameters:math:`\t` and
            :math:`\tau` in :math:
            `U^{\dagger} U(\vec{t},\vec{\tau})`
        inputs: input kets :math:`|\psi_{l} \rangle`
            in the dataset
        outputs: output kets :math:`U(\vec{t}, \vec{\tau})
            |ket_{input}\rangle` in the dataset

    Returns:
    -------
        float: cost (evaluated on the entire
            dataset) of parametrizing
            :math:`U(\vec{t},\vec{\tau})`
            with with `params`
    """
    loss = 0.0
    for k in range(train_len):
        #prediction wth parametrized unitary
        pred = np.matmul(make_unitary(N, params), inputs[k])
        loss += np.absolute(np.real(
            np.matmul(outputs[k].conjugate().T, pred)))
    return 1 - (1 / train_len) * loss
```

## Differentation of the cost function

 Gradient descent is a first order method,
 so one definitely needs to take the
 derivative of the cost function.
 Analytically, the gradient of above
 error term, or the cost function, is

\begin{equation}
\frac{\partial}{\partial \tau_{k}} E(\vec{t},\vec{\tau}) = -\frac{1}{M}\sum_{l} \langle \psi_{l}|U^{\dagger}[e^{-iAt_{N}}e^{-iB\tau_{N}} ... (-iB)e^{-iB\tau_{k}}e^{-iAt_{k}} ... e^{-iB\tau_{1}}e^{-iAt_{1}}]|\psi_{l}\rangle
\end{equation}

 We shall, however, write a simpler derivative routine
 using finite differences

```python
def der_cost(params, inputs, outputs):
    r"""Calculates the numerical
    derivative of the cost w.r.t
    to each parameter in the cost
    function.

    Args:
        params (obj:`np.ndarray`): parameters
            :math:`\t` and :math:`\tau` in :math:
            `U^{\dagger} U(\vec{t},\vec{\tau})`
        inputs (obj:`np.ndarray`): input kets
            :math:`|\psi_{l} \rangle` in the dataset
        outputs (obj:`np.ndarray`): output kets
            :math:`U(\vec{t}, \vec{\tau})
            |ket_{input}\rangle` in the dataset

    Returns:
        :obj:`np.ndarray`: Array of cost derivatives
        w.r.t each of the parameters.

    """
    grad = []
    for i in range(params.shape[0]):
        eps = np.zeros((params.shape[0], 1))
        eps[i] = eps[i] + 1e-3
        grad.append((cost(params+eps, inputs, outputs)
                     - cost(params, inputs, outputs)) / 1e-3)
    return np.array(grad).reshape(params.shape[0], 1)
```

## Performance Metric -- Fidelity

While cost is a valid metric to judge the learnability.
We introduce another commonly used metric, the _average_
fidelity between the predicted and the output (label)
states, as another metric to track during training. Average
fidelity over the dataset over a particular set of
parameters is defined as:

\begin{equation}
F_{avg} = \frac{1}{M}\sum_{l}| \langle \psi_{in} | \psi_{pred} \rangle |^2
\end{equation}

where $\psi_{label}$
represents the resulting (or the output)
ket evolved under the target unitary,
$U$ as $U |\psi_{l} \rangle$ and
$\psi_{pred}$ represents the ket $\psi_{l}$
evolved under
$U(\vec{t}, \vec{\tau})$
as $U(\vec{t}, \vec{\tau})|\psi_{l} \rangle$.

```python
def test_score(params, x, y):
    """Calculates the average fidelity
    between the predicted and output
    kets for a given on the whole dataset.

    Args:
        params (obj:`np.ndarray`): parameters
            :math:`\t` and :math:`\tau` in :math:
            `U^{\dagger} U(\vec{t},\vec{\tau})`
        inputs (obj:`np.ndarray`): input kets
            :math:`|\psi_{l} \rangle` in the dataset
        outputs (obj:`np.ndarray`): output kets
            :math:`U(\vec{t}, \vec{\tau})
            |ket_{input}\rangle` in the dataset

       Returns:
           float: fidelity between :math:`U(\vec{t},
               \vec{\tau})|ket_{input} \rangle` and the output
               (label) kets for parameters :math:`\vec{t},
               \vec{\tau}` averaged over the entire training set.
       """
    fidel = 0
    for i in range(train_len):
        pred = np.matmul(make_unitary(N, params), x[i])
        step_fidel = fidelity(Qobj(pred), Qobj(y[i]))
        fidel += step_fidel

    return fidel / train_len
```

## Gradient Descent Implementation

 We implement gradient descent based on

\begin{equation} \label{gd}
t_{k} = t_{k} - \alpha \frac{\partial}{\partial t_{k}}E(\vec{t},\vec{\tau})
\end{equation}

for every parameter $t_{k}$,
where $\alpha$ is the learning rate.

```python
np.random.seed(1000)
epochs = 15
alpha = 1e-1
loss_hist = []
fidel_hist = []
params_hist = []
weights = np.random.rand(2 * N, 1)

for epoch in range(epochs):
    weights = weights - alpha * (der_cost(weights, ket_input, ket_output))
    loss = cost(weights, ket_input, ket_output).item() # convert numpy (1,1) array to native python float
    avg_fidel = test_score(weights, ket_input, ket_output)
    progress = [epoch+1, loss, avg_fidel]
    loss_hist.append(loss)
    fidel_hist.append(avg_fidel)
    params_hist.append(weights)
    if ((epoch) % 5 == 4):
        print("Epoch: {:2f} | Loss: {:3f} | Fidelity: {:3f}".
          format(*np.asarray(progress)))
```

    Epoch: 5.000000 | Loss: 0.092668 | Fidelity: 0.908482
    Epoch: 10.000000 | Loss: 0.011956 | Fidelity: 0.988249
    Epoch: 15.000000 | Loss: 0.001602 | Fidelity: 0.998427

## Efficient reconstruction of the target unitary

The papers claim that if the
dimension, N, of the parameters
is of order $O(d^{2})$, where $d$
is the dimension of the unitary,
then $U(\vec{t},\vec{\tau})$ is
constructed _efficiently_.
We see how this holds true
for $d = 2$ and $N = 4$.

```python
plt.figure(figsize=(9, 6))
plt.plot(range(epochs), np.asarray(fidel_hist).ravel(), marker = 'o',
         label=r"$F_{avg} = \frac{1}{M}\sum_{l}| \langle \psi_{in} | \psi_{pred} \rangle |^2$")
plt.plot( range(epochs), np.asarray(loss_hist).ravel(), marker = 'x',
        label=r"$L = 1 - (\frac{1}{M})\sum_{l}\langle \psi_{l} | U ^{\dagger} U(\vec{t},\vec{\tau}) | \psi_{l} \rangle$")
plt.title("Fidelity and Cost Trends", fontweight = "bold")
plt.legend(["Fidelity","Loss"])
plt.xlabel("epoch")
plt.legend(loc=0, prop = {'size': 15})
```

 We would expect the average fidelity, $F_{avg}$, to
 increase since we defined average fidelity to be between
 $U(\vec{t}, \vec{\tau})|ket_{input} \rangle$ and
 the output (label) kets for parameters
 $\vec{t}, \vec{\tau}$ at a given point in training
. The expected pattern is reflected
 in the training graph above with the loss, $L$,
decreases progressively and the fidelity
, $F_{avg}$, increases.

## Testing on unseen kets

We reserved the last $20$ (which is $20 \%$ of the total dataset)
kets for testing.
Now we shall apply our learned unitary matrix, call it
$U_{opt}(\vec{t}, \vec{\tau})$
to the unseen kets and measure the fidelity of the evolved ket
under $U_{opt}(\vec{t}, \vec{\tau})$
with those that evolved under the target unitary, $U$.

```python
opt_unitary = make_unitary(N, params_hist[-1])
fidel = []
for i in range(train_len, m): # unseen data
    pred = np.matmul(opt_unitary, ket_input[i])
    fidel.append(fidelity(Qobj(pred), Qobj(ket_output[i])))
fidel
```

    [0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953,
     0.9984274341644953]

## Conclusion

The learned unitary matrix, $U(\vec{t}, \vec{\tau})$,
almost perfectly reconstructs the target unitary, $U$,
in the sense that the way $U$ evolves a ket
$|\psi_{l} \rangle$, $U(\vec{t}, \vec{\tau})$ also
evovles $|\psi_{l} \rangle$ in about the same way.

## References

1. Lloyd, Seth, and Reevu Maity. "Efficient implementation of unitary transformations." arXiv preprint arXiv:1901.03431 (2019).

2. Kiani, Bobak Toussi, Seth Lloyd, and Reevu Maity. "Learning unitaries by gradient descent." arXiv preprint arXiv:2001.11897 (2020).


# Selección óptima de portafolios II

Entonces, tenemos que:
- La LAC describe las posibles selecciones de riesgo-rendimiento entre un activo libre de riesgo y un activo riesgoso.
- Su pendiente es igual al radio de Sharpe del activo riesgoso.
- La asignación óptima de capital para cualquier inversionista es el punto tangente de la curva de indiferencia del inversionista con la LAC.

Para todo lo anterior, supusimos que ya teníamos el portafolio óptimo (activo riesgoso).

En la clase pasada aprendimos a hallar este portafolio óptimo si el conjunto de activos riesgosos estaba conformado únicamente por dos activos:

$$w_{1,EMV}=\frac{(E[r_1]-r_f)\sigma_2^2-(E[r_2]-r_f)\sigma_{12}}{(E[r_2]-r_f)\sigma_1^2+(E[r_1]-r_f)\sigma_2^2-((E[r_1]-r_f)+(E[r_2]-r_f))\sigma_{12}}.$$

- Sin embargo, la complejidad del problema crece considerablemente con el número de variables, y la solución analítica deja de ser viable cuando mencionamos que un portafolio bien diversificado consta aproximadamente de 50-60 activos.
- En esos casos, este problema se soluciona con rutinas numéricas que hagan la optimización por nosotros, porque son una solución viable y escalable a más variables.

**Objetivos:**
- ¿Cuál es el portafolio óptimo de activos riesgosos cuando tenemos más de dos activos?
 - ¿Cómo construir la frontera de mínima varianza cuando tenemos más de dos activos?

*Referencia:*
- Notas del curso "Portfolio Selection and Risk Management", Rice University, disponible en Coursera.
___

## 1. Maximizando el radio de Sharpe

### ¿Qué pasa si tenemos más de dos activos riesgosos?

En realidad es algo muy similar a lo que teníamos con dos activos.
- Para dos activos, construir la frontera de mínima varianza es trivial: todas las posibles combinaciones.
- Con más de dos activos, recordar la definición: la frontera de mínima varianza es el lugar geométrico de los portafolios que proveen el mínimo riesgo para un nivel de rendimiento dado.

 Ver en el tablero.

Analíticamente:
- $N$ activos,
- caracterizados por $(\sigma_i,E[r_i])$,
- cada uno con peso $w_i$, con $i=1,2,\dots,N$.

Entonces, buscamos los pesos tales que

\begin{align}
\min_{w_1,\dots,w_n} & \quad \sum_{i=1}^{N}w_i^2\sigma_i^2+\sum_{i=1}^{N}\sum_{j=1,j\neq i}^{N}w_iw_j\sigma_{ij}\\
\text{s.a.}          & \quad \sum_{i=1}^{N}w_i=1, w_i\geq0\\
                     & \quad \sum_{i=1}^{N}w_iE[r_i]=\bar{\mu},
\end{align}

donde $\bar{\mu}$ corresponde a un nivel de rendimiento objetivo.

**Obviamente, tendríamos que resolver este problema para muchos niveles de rendimiento objetivo.**

-  Explicar relación con gráfica.

-  Recordar clase 10.

Lo anterior se puede escribir vectorialmente como:

\begin{align}
\min_{\boldsymbol{w}} & \quad \boldsymbol{w}^T\Sigma\boldsymbol{w}\\
\text{s.a.}          & \quad \boldsymbol{1}^T\boldsymbol{w}=1, \boldsymbol{w}\geq0\\
                     & \quad E[\boldsymbol{r}^T]\boldsymbol{w}=\bar{\mu},
\end{align}

donde:
- $\boldsymbol{w}=\left[w_1,\dots,w_N\right]^T$ es el vector de pesos,
- $\boldsymbol{1}=\left[1,\dots,1\right]^T$ es un vector de unos,
- $E[\boldsymbol{r}]=\left[E[r_1],\dots,E[r_N]\right]^T$ es el vector de rendimientos esperados, y
- $\Sigma=\left[\begin{array}{cccc}\sigma_{1}^2 & \sigma_{12}  & \dots  & \sigma_{1N} \\
                                   \sigma_{21}  & \sigma_{2}^2 & \dots  & \sigma_{2N} \\
                                   \vdots       & \vdots       & \ddots & \vdots      \\
                                   \sigma_{N1}  & \sigma_{N2}  & \dots  & \sigma_{N}^2\end{array}\right]$ es la matriz de varianza-covarianza.

**Esta última forma es la que comúnmente usamos al programar, por ser eficiente y escalable a problemas de N variables.**

### Entonces, ¿para cuántos niveles de rendimiento objetivo tendríamos que resolver el anterior problema con el fin de graficar la frontera de mínima varianza?
- Observar que el problema puede volverse muy pesado a medida que incrementamos el número de activos en nuestro portafolio...
- Una tarea bastante compleja.

### Sucede que, en realidad, sólo necesitamos conocer dos portafolios que estén sobre la *frontera de mínima varianza*.
- Si logramos encontrar dos portafolios sobre la frontera, entonces podemos a la vez encontrar todas las posibles combinaciones de estos dos portafolios para trazar la frontera de mínima varianza.
- Ver el caso de dos activos.

### ¿Qué portafolios usar?
Hasta ahora, hemos estudiando profundamente como hallar dos portafolios muy importantes que de hecho yacen sobre la frontera de mínima varianza:
1. Portafolio de EMV: máximo SR.
2. Portafolio de mínima varianza: básicamente, el mismo problema anterior, sin la restricción de rendimiento objetivo.

Luego, tomar todas las posibles combinaciones de dichos portafolios usando las fórmulas para dos activos de medias y varianzas:
- w: peso para el portafolio EMV,
- 1-w: peso para le portafolio de mínima varianza.

## 2. Ejemplo ilustrativo.

Retomamos el ejemplo de mercados de acciones en los países integrantes del $G5$: EU, RU, Francia, Alemania y Japón.

```python
# Importamos pandas y numpy
import pandas as pd
import numpy as np
```

```python
# Resumen en base anual de rendimientos esperados y volatilidades
annual_ret_summ = pd.DataFrame(columns=['EU', 'RU', 'Francia', 'Alemania', 'Japon'], index=['Media', 'Volatilidad'])
annual_ret_summ.loc['Media'] = np.array([0.1355, 0.1589, 0.1519, 0.1435, 0.1497])
annual_ret_summ.loc['Volatilidad'] = np.array([0.1535, 0.2430, 0.2324, 0.2038, 0.2298])

annual_ret_summ.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      Media
      0.1355
      0.1589
      0.1519
      0.1435
      0.1497

      Volatilidad
      0.1535
      0.243
      0.2324
      0.2038
      0.2298

```python
# Matriz de correlación
corr = pd.DataFrame(data= np.array([[1.0000, 0.5003, 0.4398, 0.3681, 0.2663],
                                    [0.5003, 1.0000, 0.5420, 0.4265, 0.3581],
                                    [0.4398, 0.5420, 1.0000, 0.6032, 0.3923],
                                    [0.3681, 0.4265, 0.6032, 1.0000, 0.3663],
                                    [0.2663, 0.3581, 0.3923, 0.3663, 1.0000]]),
                    columns=annual_ret_summ.columns, index=annual_ret_summ.columns)
corr.round(4)
```

      EU
      RU
      Francia
      Alemania
      Japon

      EU
      1.0000
      0.5003
      0.4398
      0.3681
      0.2663

      RU
      0.5003
      1.0000
      0.5420
      0.4265
      0.3581

      Francia
      0.4398
      0.5420
      1.0000
      0.6032
      0.3923

      Alemania
      0.3681
      0.4265
      0.6032
      1.0000
      0.3663

      Japon
      0.2663
      0.3581
      0.3923
      0.3663
      1.0000

```python
# Tasa libre de riesgo
rf = 0.05
```

Esta vez, supondremos que tenemos disponibles todos los mercados de acciones y el activo libre de riesgo.

#### 1. Construir la frontera de mínima varianza

##### 1.1. Encontrar portafolio de mínima varianza

```python
# Importamos funcion minimize del modulo optimize de scipy
from scipy.optimize import minimize
```

```python
## Construcción de parámetros
# 1. Sigma: matriz de varianza-covarianza
corr
S= np.diag(annual_ret_summ.loc['Volatilidad'])
Sigma= S.dot(corr).dot(S) #no hacer este paso si nos dan cov
# 2. Eind: rendimientos esperados activos individuales
Eind= annual_ret_summ.loc['Media'].values
Eind
```

    array([0.1355, 0.1589, 0.1519, 0.1435, 0.1497], dtype=object)

```python
# Función objetivo
def var(w, Sigma):
    return w.dot(Sigma).dot(w)
```

```python
# Dato inicial
n= len(Eind)
w0=np.ones((n,))/n
# Cotas de las variables
bnds= ((0,1),)*n
# Restricciones
cons= {'type': 'eq','fun':lambda w:w.sum()-1}
```

```python
# Portafolio de mínima varianza
minvar= minimize(fun= var,
                x0=w0,
                args=(Sigma,),
                bounds= bnds,
                constraints=cons)
minvar
```

         fun: 0.01861776391061502
         jac: array([0.03718246, 0.03881475, 0.03859101, 0.03755156, 0.0370423 ])
     message: 'Optimization terminated successfully.'
        nfev: 49
         nit: 7
        njev: 7
      status: 0
     success: True
           x: array([6.17797049e-01, 3.46944695e-18, 0.00000000e+00, 2.09394358e-01,
           1.72808594e-01])

```python
# Pesos, rendimiento y riesgo del portafolio de mínima varianza
w_minvar= minvar.x
E_minvar= Eind.dot(w_minvar)
s_minvar= np.sqrt(var(w_minvar,Sigma))
RS_minvar= (E_minvar-rf)/s_minvar
w_minvar, E_minvar, s_minvar, w_minvar, RS_minvar
```

    (array([6.17797049e-01, 3.46944695e-18, 0.00000000e+00, 2.09394358e-01,
            1.72808594e-01]),
     0.13962903688859557,
     0.13644692708381168,
     array([6.17797049e-01, 3.46944695e-18, 0.00000000e+00, 2.09394358e-01,
            1.72808594e-01]),
     0.6568783834431207)

##### 1.2. Encontrar portafolio EMV

```python
# Función objetivo
def menos_RS(w, Eind, Sigma,rf):

    Ep= Eind.dot(w)
    sp= ((w.dot(Sigma)).dot(w))**0.5
    RS= (Ep-rf)/sp

    return -RS
```

```python
# Dato inicial
n= len(Eind)
w0=np.ones((n,))/n
# Cotas de las variables
bnds= ((0,1),)*n
# Restricciones
cons= {'type': 'eq','fun':lambda w:w.sum()-1}
```

```python
# Portafolio EMV
EMV= minimize(fun=menos_RS,
              x0=w0,
              args=(Eind,Sigma,rf),
              bounds= bnds,
              constraints=cons)
EMV
```

         fun: -0.6644372965632436
         jac: array([-0.3608895 , -0.36076408, -0.36036385, -0.36108153, -0.36062376])
     message: 'Optimization terminated successfully.'
        nfev: 35
         nit: 5
        njev: 5
      status: 0
     success: True
           x: array([0.50714174, 0.07470888, 0.02471533, 0.18943972, 0.20399434])

```python
# Pesos, rendimiento y riesgo del portafolio EMV
w_EMV = EMV.x
E_EMV = Eind.dot(w_EMV)
s_EMV= (w_EMV.dot(Sigma).dot(w_EMV))**0.5
RS_EMV= (E_EMV-rf)/s_EMV
w_EMV, E_EMV, s_EMV, RS_EMV
```

    (array([0.50714174, 0.07470888, 0.02471533, 0.18943972, 0.20399434]),
     0.1420657564717856,
     0.13856199365687238,
     0.6644372965632436)

##### 1.3. Construir frontera de mínima varianza

También debemos encontrar la covarianza (o correlación) entre estos dos portafolios:

```python
# Covarianza entre los portafolios
s_MV_EMV= w_minvar.dot(Sigma).dot(w_EMV)
s_MV_EMV
```

    0.018690275386034134

```python
# Correlación entre los portafolios
r_MV_EMV= s_MV_EMV/(s_minvar*s_EMV)
r_MV_EMV
'Esperamos que sea cercana a uno porque es la relación del mismo activo'
```

    0.9885708894197612

```python
# Vector de w
N= 101
w=np.linspace(0,4,n)
```

```python
# DataFrame de portafolios:
# 1. Índice: i
# 2. Columnas 1-2: w, 1-w
# 3. Columnas 3-4: E[r], sigma
# 4. Columna 5: Sharpe ratio
front_minvar= pd.DataFrame(index= range(1,N+1),
                          data= {'w_EMV':w,
                                'w_minvar': w-1,
                                'Media':w*E_EMV+(1-w)*E_minvar,
                                 'Vol':np.sqrt((w*s_EMV)**2+2*w*(1-w)*s_MV_EMV+((1-w)*s_minvar)**2)}
                                )
front_minvar['RS']=((front_minvar['Media']-rf)/front_minvar['Vol'])
front_minvar.head(10)
```

```python
# Importar librerías de gráficos
import matplotlib.pyplot as plt
%matplotlib inline
```

```python
# Gráfica de dispersión de puntos coloreando
# de acuerdo a SR, los activos individuales
# y los portafolios hallados
plt.fogure(figsize=(6,4))
plt.plot(front_var['Vol'],front_minvar['Media'],
        lw=2,
        color='autumn',
        label='Frontera de minima varianza')
plt.plot(s_EMV, E_EMV,'or', ms=5, label='Port. EMV')
plt.plot(s_minvar, E_minvar,'ob', ms=5, label='Port. minvar')
plt.grid()
plt.show()
```

**A partir de lo anterior, solo restaría construir la LAC y elegir la distribución de capital de acuerdo a las preferencias (aversión al riesgo).**
___

## 3. Comentarios finales

### 3.1. Restricciones adicionales

Los inversionistas pueden tener restricciones adicionales:
1. Restricciones en posiciones cortas.
2. Pueden requerir un rendimiento mínimo.
3. Inversión socialmente responsable: prescinden de inversiones en negocios o paises considerados éticamente o políticamente indeseables.

Todo lo anterior se puede incluir como restricciones en el problema de optimización, y puede ser llevado a cabo a costa de un cociente de Sharpe menor.

### 3.2. Críticas a la optimización media varianza
1. Solo importan medias y varianzas: recordar que la varianza subestima el riesgo en algunos casos.
2. Preferencias media-varianza tratan las ganancias y pérdidas simétricamente: el sentimiento de insatisfacción de una perdida es mayor al sentimiento de satisfacción de una ganancia (aversión a pérdidas).
3. La aversión al riesgo es constante: la actitud frente al riesgo puede cambiar, por ejemplo con el estado de la economía.
4. Horizonte corto (un periodo).
5. Basura entra - basura sale: la optimización media varianza es supremamente sensible a las entradas: estimaciones de rendimientos esperados y varianzas.
___

# Anuncios parroquiales

## 1. Revisar archivo Tarea 6. Definir fecha.
## 2. Fin de módulo. Abrir Clase0 y revisar objetivos del módulo.
## 3. [Nota interesante](http://yetanothermathprogrammingconsultant.blogspot.com/2016/08/portfolio-optimization-maximize-sharpe.html)

Created with Jupyter by Esteban Jiménez Rodríguez.