# Problema de selección de portafolio con preferencias media-varianza

En la clase pasada hablamos acerca de:
- preferencias,
- funciones de utilidad,
- la actitud de los inversionistas de cara al riesgo,
- la aversión al riesgo, entre otros.

Todas ellas son piezas que necesitamos para responder la pregunta de ¿cómo un inversionista toma la decisión óptima de selección de portafolio?

En esta clase al fin estamos listos para ensamblar estos conceptos y escribir el problema de selección de portafolios.

En el camino aprenderemos acerca del concepto de **utilidad esperada**, que nos permite trabajar con incertidumbre en el modelado económico (una de las ideas más importantes en economía). Esta idea tiene más de 60 años, y básicamente dice que los individuos, cuando están de cara a incertidumbre, maximizan el valor esperado de su utilidad (solo cierto si somos homo economicus).

Además del concepto de utilidad esperada, aprenderemos acerca de **preferencias media-varianza**. Es decir, supondremos que los inversionistas toman decisiones basados en un tipo particular de preferencias.

Con lo anterior, estableceremos el problema de selección de portafolios.

**Objetivos:**

- ¿Qué es utilidad esperada?
- ¿Qué son las preferencias media-varianza?
- Funciones de utilidad media-varianza.
- Enunciado y solución del problema básico de selección de portafolio.

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

## 1. Utilidad esperada
- Básicamente combina las probabilidades de los resultados con cómo los inversionistas se sienten con dichos resultados.
- En otras palabras, la utilidad esperada multiplica la probabilidad de suceso de un evento con la utilidad que genera dicho evento.

Recordemos que las *funciones de utilidad* permiten a los inversionistas expresar cómo se sienten con los resultados, especialmente en los malos ratos.

Entonces la *utilidad esperada* es una herramienta que nos permite cuantificar cómo nos sentimos en nuestros malos momentos económicos, capturando el riesgo con la probabilidad de ocurrencia de dichos malos momentos.

Dado este marco de trabajo, cualquier decisión se puede escribir como la maximización de la utilidad esperada:
\begin{align}
\max_{\theta} & \quad E[U(W)], \\
\end{align}
mediante la escogencia de cierta variable $\theta$ (gastos, planes de ahorro, compra de activos, planes de producción, etc.).

Para nuestros propósitos, la variable de decisión serán los pesos o ponderaciones del portafolio.

Adicionalmente, en el contexto de la decisión de distribución de la riqueza entre activos, el problema de maximización tendrá comúnmente las siguientes restricciones:
- universo de inversión,
- posición en los activos dados.

**Ejemplo.**

Supongamos que un inversionista debe determinar la composición óptima de su portafolio, que contiene activos y bonos. Supongamos que son los únicos instrumentos disponibles.

Sean:
- $w_s$: peso o ponderación de activos en el portafolio,
- $w_b$: peso o ponderación de bonos en el portafolio,
- $r_s$: rendimiento de los activos, y
- $r_b$: rendimiento de los bonos.

De manera que podemos escribir el problema de selección de portafolios como la maximización de la utilidad esperade de nuestra riqueza futura, la cual dependerá de nuestros rendimientos:

\begin{align}
\max_{w_s,w_b} &\quad E[U(W)]\\
\text{s. a.}   &\quad W=W_0(1+w_sr_s+w_br_b)\\
               &\quad w_s+w_b=1
\end{align}

Preguntas:
- ¿Qué significan las restricciones?
- Ya que tenemos planteado este problema básico, ¿qué haría falta para empezar a resolverlo?
___

## 2. Preferencias media-varianza

### 2.1. Utilidad media-varianza

Entonces, ¿qué funciones de utilidad deberíamos de usar en este problema de selección de portafolios?

- La respuesta es: **preferencias media-varianza**.
- Éstas serán representadas en términos de funciones de utilidad como: **utilidad media-varianza**.

Usamos la *utilidad media-varianza* en el problema de selección de portafolios dado que ésta decribe el "trade-off" entre riesgo y rendimiento que enfrentan los inversionistas. La *utilidad media-varianza* está dada por la siguiente expresión:

$$U=E[r_p]-\frac{1}{2}\gamma\sigma_p^2,$$

donde
- $E[r_p]$ es el rendimiento esperado del portafolio,
- $\sigma_p^2$ es la varianza del portafolio, y
- $\gamma$ es el coeficiente de aversión al riesgo.

#### Intuición acerca de la función de utilidad media-varianza:
- Sólo se preocupa por medias :) y varianzas :(.
- Incrementa con: rendimiento esperado del portafolio.
- Decrece con: varianza del portafolio.
- Malos tiempos: rendimientos son bajos y las volatilidades son altas.
- Conecta bastante bien con la teoría moderna de portafolios, la cual caracteriza los rendimientos con medias y varianzas únicamente.
- Criticada por su limitación: supone que los inversionistas sólo se preocupan por medias y varianzas.

### 2.2. Curvas de indiferencia

*¿Recuerdan las curvas de nivel que se ven en cálculo de varias variables?*
- Bien, acá nos servirán para representar la utilidad media-varianza gráficamente.
- En el contexto de utilidad media-varianza, las curvas de nivel se llaman **curvas de indiferencia**.

Dados ciertos niveles de utilidad $U_1>U_2>U_3$, las curvas de indiferencia relativas a estos niveles de utilidad, son los lugares geométricos en el espacio de rendimiento esperado vs. volatilidad representados por las siguientes expresiones

$$U_1=E[r_p]-\frac{1}{2}\gamma\sigma_p^2\Rightarrow E[r_p]=\frac{1}{2}\gamma\sigma_p^2+U_1,$$

$$U_2=E[r_p]-\frac{1}{2}\gamma\sigma_p^2\Rightarrow E[r_p]=\frac{1}{2}\gamma\sigma_p^2+U_2,$$

$$U_3=E[r_p]-\frac{1}{2}\gamma\sigma_p^2\Rightarrow E[r_p]=\frac{1}{2}\gamma\sigma_p^2+U_3.$$

**Gráficamente**

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

```python
# Coeficiente de aversión al riesgo (entre 1 y 10 comúnmente)
g = 7
# Niveles de utilidad
U1, U2, U3 = 3, 2, 1
# Vector de volatilidades (sugerido 1%-60%)
sp = np.linspace(0.01, 0.60, 100)
# Curvas de indiferencia
Erp1, Erp2, Erp3 = 0.5*g*sp**2+U1, 0.5*g*sp**2+U2, 0.5*g*sp**2+U3
```

```python
# Gráfica
plt.figure(figsize=(10,8))
plt.plot(sp, Erp1, lw=5, label='U1')
plt.plot(sp, Erp2, lw=5, label='U2')
plt.plot(sp, Erp3, lw=5, label='U3')
plt.grid()
plt.xlabel('Volatilidad $\sigma_p$')
plt.ylabel('Rendimiento esperado $E[r_p]$')
plt.legend(loc='best')
```

Bueno, ¿y porqué se llaman curvas de indiferencia?, ¿qué representa una cuva de indiferencia?

- Porque sobre una misma curva el nivel de utilidad es el mismo (es indiferente).
- Son todas las combinaciones de riesgo y rendimiento que producen un mismo nivel de utilidad.

Volviendo al problema de selección de portafolios, queremos la utilidad más alta.
- ¿Cuál de las anteriores curvas de indiferencia corresponde a la utilidad más alta?
- Intuitivamente, ¿porqué?
- Curvas de indiferencia para niveles de utilidad más altos, estarán...

Notamos además que las anteriores curvas de indiferencia son *paralelas* una con otra. Claro, las dibujamos con el mismo coeficiente de aversión al riesgo.

¿Cómo cambian estas curvas para coeficientes de aversión al riesgo más altos?

```python
# Coeficientes de aversión al riesgo (entre 1 y 10 comúnmente)
g1, g2, g3 = 3, 5, 7
# Nivel de utilidad
U = 1
# Vector de volatilidades (sugerido 1%-60%)
sp = np.linspace(0.01, 0.60, 100)
# Curvas de indiferencia
Erp1, Erp2, Erp3 = 0.5*g1*sp**2+U, 0.5*g2*sp**2+U, 0.5*g3*sp**2+U
```

```python
# Gráfica
plt.figure(figsize=(10,8))
plt.plot(sp, Erp1, lw=5, label='$\gamma_1$')
plt.plot(sp, Erp2, lw=5, label='$\gamma_2$')
plt.plot(sp, Erp3, lw=5, label='$\gamma_3$')
plt.grid()
plt.xlabel('Volatilidad $\sigma_p$')
plt.ylabel('Rendimiento esperado $E[r_p]$')
plt.legend(loc='best')
```

¿Cómo interpretamos las anteriores gráficas?, ¿qué pasa con las personas más aversas al riesgo?
- Se puede ver de dos maneras: para un mismo nivel de rendimiento esperado, una persona más aversa al riesgo soporta un nivel menor de riesgo; equivalentemente, para un mismo nivel de riesgo, una persona más aversa al riesgo requerirá un nivel de rendimiento esperado más alto.

Con todo lo anterior, el problema de selección de portafolios se puede plantear como *"encontrar la curva de indeferencia más alta dado el conjunto de oportunidades de inversión y restricciones"*.

## 3. Problema de selección de portafolios: una ilustración

Ahora ilustraremos el problema de selección de portafolios con algunos datos.
- Por ahora solo queremos ilustrar gráficamente cómo se resuelve este problema. Trabajar en la intuición.
- En las siguientes dos clases nos enfocaremos en cómo resolverlo analíticamente.

Acá tenemos el rendimiento medio anual y la volatilidad para dos instrumentos usando datos de EU: instrumentos de deuda (bonos) y acciones. Supondremos que el inversionista solo puede invertir en estas dos clases de instrumentos.

```python
# Importamos pandas
import pandas as pd
```

```python
# Datos
data=pd.DataFrame(index=['Stocks','Bonds', 'CorrSB'], columns=['Mean', 'Std'])
data['Mean'] = np.array([0.119,0.0591,0.113])
data['Std'] = np.array([0.1915,0.0833,None])
data.round(4)
```

      Mean
      Std

      Stocks
      0.1190
      0.1915

      Bonds
      0.0591
      0.0833

      CorrSB
      0.1130
      None

Entonces, ¿cuál es la distribución de riqueza óptima?, o más bien, ¿cuál es la composición óptima del portafolio para un inversionista dado su nivel de aversión al riesgo?

**Primero.** Recordamos que, para dos activos, podemos trazar la frontera de mínima varianza tomando todas las posibles combinaciones de los dos activos.

De nuevo, sean:
- $w_s=w$: peso o ponderación de activos en el portafolio,
- $w_b=1-w$: peso o ponderación de bonos en el portafolio,
- $r_s$: rendimiento de los activos, y
- $r_b$: rendimiento de los bonos.

Entonces

$$E[r_p]=wE[r_{s}]+(1-w)E[r_b]$$

$$\sigma_p^2=w^2\sigma_{s}^2+(1-w)^2\sigma_b^2+2w(1-w)\rho_{s,b}\sigma_s\sigma_b$$

```python
# Vector de w variando entre 0 y 1 con n pasos
n = 100
w = np.linspace(0, 1, n)
# Rendimientos esperados individuales
Ers = data.loc['Stocks', 'Mean']
Erb = data.loc['Bonds', 'Mean']
# Volatilidades individuales
ss = data.loc['Stocks', 'Std']
sb = data.loc['Bonds', 'Std']
# Correlacion
rsb = data.loc['CorrSB', 'Mean']
```

```python
# Crear un DataFrame cuyas columnas sean rendimiento
# y volatilidad del portafolio para cada una de las w
# generadas
portafolios = pd.DataFrame(index = w, columns=['Erp', 'sp'])
portafolios.loc[:,'Erp'] = w*Ers+(1-w)*Erb
portafolios.loc[:,'sp'] = ((w*ss)**2+((1-w)*sb)**2+2*w*(1-w)*ss*sb*rsb)**0.5
portafolios
```

      Erp
      sp

      0.000000
      0.0591
      0.0833

      0.010101
      0.0597051
      0.0826995

      0.020202
      0.0603101
      0.0821443

      0.030303
      0.0609152
      0.0816354

      0.040404
      0.0615202
      0.0811735

      0.050505
      0.0621253
      0.0807596

      0.060606
      0.0627303
      0.0803944

      0.070707
      0.0633354
      0.0800784

      0.080808
      0.0639404
      0.0798124

      0.090909
      0.0645455
      0.0795967

      0.101010
      0.0651505
      0.0794319

      0.111111
      0.0657556
      0.0793183

      0.121212
      0.0663606
      0.0792559

      0.131313
      0.0669657
      0.0792451

      0.141414
      0.0675707
      0.0792858

      0.151515
      0.0681758
      0.0793778

      0.161616
      0.0687808
      0.0795212

      0.171717
      0.0693859
      0.0797154

      0.181818
      0.0699909
      0.0799603

      0.191919
      0.070596
      0.0802553

      0.202020
      0.071201
      0.0805999

      0.212121
      0.0718061
      0.0809934

      0.222222
      0.0724111
      0.0814352

      0.232323
      0.0730162
      0.0819244

      0.242424
      0.0736212
      0.0824602

      0.252525
      0.0742263
      0.0830417

      0.262626
      0.0748313
      0.083668

      0.272727
      0.0754364
      0.0843381

      0.282828
      0.0760414
      0.0850509

      0.292929
      0.0766465
      0.0858053

      ...
      ...
      ...

      0.707071
      0.101454
      0.140272

      0.717172
      0.102059
      0.141944

      0.727273
      0.102664
      0.143625

      0.737374
      0.103269
      0.145314

      0.747475
      0.103874
      0.147012

      0.757576
      0.104479
      0.148717

      0.767677
      0.105084
      0.150431

      0.777778
      0.105689
      0.152152

      0.787879
      0.106294
      0.15388

      0.797980
      0.106899
      0.155616

      0.808081
      0.107504
      0.157358

      0.818182
      0.108109
      0.159107

      0.828283
      0.108714
      0.160862

      0.838384
      0.109319
      0.162623

      0.848485
      0.109924
      0.16439

      0.858586
      0.110529
      0.166163

      0.868687
      0.111134
      0.167942

      0.878788
      0.111739
      0.169726

      0.888889
      0.112344
      0.171515

      0.898990
      0.112949
      0.173309

      0.909091
      0.113555
      0.175108

      0.919192
      0.11416
      0.176912

      0.929293
      0.114765
      0.178721

      0.939394
      0.11537
      0.180534

      0.949495
      0.115975
      0.182352

      0.959596
      0.11658
      0.184173

      0.969697
      0.117185
      0.185999

      0.979798
      0.11779
      0.187829

      0.989899
      0.118395
      0.189663

      1.000000
      0.119
      0.1915

100 rows × 2 columns

```python
# Gráfica
plt.figure(figsize=(8,6))
plt.plot(portafolios.loc[:,'sp'], portafolios.loc[:,'Erp'], label='Portafolios')
plt.grid()
plt.xlabel('Volatilidad $\sigma_p$')
plt.ylabel('Rendimiento esperado $E[r_p]$')
plt.legend(loc='best')
```

**Segundo.** Graficamos en la misma ventana, curvas de indiferencia.

```python
# Niveles de utilidad 0.07, 0.08, 0.09
U1, U2, U3 = 0.06, 0.07, 0.0725
# Coeficiente de aversión al riesgo
g = 3
# Curvas de indiferencia
sp = portafolios.loc[:,'sp']
Erp1, Erp2, Erp3 = 0.5*g*sp**2+U1, 0.5*g*sp**2+U2, 0.5*g*sp**2+U3
```

```python
# Gráfica
plt.figure(figsize=(8,6))
plt.plot(portafolios.loc[:,'sp'], portafolios.loc[:,'Erp'], label='Portafolios')
plt.plot(sp, Erp1, lw=2, label='U1')
plt.plot(sp, Erp2, lw=2, label='U2')
plt.plot(sp, Erp3, lw=2, label='U3')
plt.grid()
plt.xlabel('Volatilidad $\sigma_p$')
plt.ylabel('Rendimiento esperado $E[r_p]$')
plt.legend(loc='best')
```

**Tercero.** La elección óptima está dada por la curva de indiferencia para el nivel de utilidad más alto que es tangente a la frontera media-varianza.
- Claramente, esta selección depende del coeficiente de aversión al riesgo.

```python
# Gráfica con zoom
# Gráfica
plt.figure(figsize=(8,6))
plt.plot(portafolios.loc[:,'sp'], portafolios.loc[:,'Erp'], label='Portafolios')
plt.plot(sp, Erp1, lw=2, label='U1')
plt.plot(sp, Erp2, lw=2, label='U2')
plt.plot(sp, Erp3, lw=2, label='U3')
plt.grid()
plt.axis([0.12, 0.14, 0.09, 0.11])
plt.xlabel('Volatilidad $\sigma_p$')
plt.ylabel('Rendimiento esperado $E[r_p]$')
plt.legend(loc='best')
```

# Anuncios parroquiales

## 1. Quiz la siguiente clase.
## 2. Segunda entrega de la tarea 5 para el miércoles 27 de Marzo.
## 3. Un par de artículos del WSJ y el NYT que discuten herramientas disponibles para la medición de su propia tolerancia al riesgo:
- [Artículo 1](https://www.nytimes.com/2016/02/13/your-money/as-stocks-fall-its-time-to-measure-your-risk-tolerance.html)
- [Artículo 2](https://www.wsj.com/articles/check-your-tolerance-for-investment-risk-now-before-markets-sag-1405619939)

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


```python
from IPython.display import Image
Image('../../Python_probability_statistics_machine_learning_2E.png',width=200)
```

This chapter takes a geometric view of probability theory and relates it to
familiar concepts in linear algebra and geometry.  This approach connects your
natural geometric intuition to the key abstractions in probability that can
help
guide your reasoning.  This is particularly important in probability
because it
is easy to be misled. We need a bit of rigor and some
intuition to guide us.

In
grade school, you were introduced to the natural numbers (i.e., `1,2,3,..`)
and
you learned how to manipulate them by operations like addition,
subtraction, and
multiplication. Later, you were introduced to positive and
negative numbers and
were again taught how to manipulate them. Ultimately, you
were introduced to the
calculus of the real line, and learned how to
differentiate, take limits, and so
on. This progression provided more
abstractions, but also widened the field of
problems you could successfully
tackle. The same is true of probability. One way
to think about probability is
as a  new number concept that allows you to tackle
problems that have a special
kind of *uncertainty* built into them. Thus, the
key idea is that there is some
number, say $x$, with a traveling companion, say,
$f(x)$, and this companion
represents the uncertainties about the value of $x$
as if looking at the number
$x$ through a frosted window. The degree of opacity
of the window is
represented by $f(x)$.  If we want to manipulate $x$, then we
have to figure
out what to do with $f(x)$. For example if we want $y= 2 x $,
then we have to
understand how $f(x)$ generates $f(y)$.

Where is the *random*
part?   To conceptualize this, we need still another
analogy: think about a
beehive with the swarm around it representing $f(x)$,
and the hive itself, which
you can barely see through the swarm, as $x$.    The
random piece is you don't
know *which* bee in particular is going to sting you!
Once this happens the
uncertainty evaporates.
Up until that happens, all we have is a concept of a
swarm (i.e., density of
bees) which represents a *potentiality* of which bee
will ultimately sting.
In summary, one way to think about probability is as a
way of carrying through
mathematical reasoning (e.g., adding, subtracting,
taking
limits) with a notion of potentiality that is so-transformed by these
operations.

## Understanding Probability Density

In order to understand the
heart of modern probability, which is built
on the Lesbesgue theory of
integration, we need to extend the concept
of integration from basic calculus.
To begin, let us consider the
following piecewise function

$$
f(x)  =  \begin{cases}
                1 & \mbox{if }  0 < x \leq 1 \\\
2 & \mbox{if }  1 < x \leq 2 \\\
                0 & \mbox{otherwise }
\end{cases}
$$

 as shown in [Figure](#fig:intro_001). In calculus, you learned
Riemann
integration, which you can apply here  as

Simple piecewise-constant function.

$$
\int_0^2 f(x) dx = 1 + 2 = 3
$$

 which has the usual interpretation as the area of the two rectangles
that make
up $f(x)$.  So far, so good.

With Lesbesgue integration, the idea is very
similar except that we
focus on the y-axis instead of moving along the x-axis.
The question
is given $f(x) = 1$, what is the set of $x$ values for which this
is
true? For our example, this is true whenever $x\in (0,1]$. So now we
have a
correspondence between the values of the function (namely, `1`
and `2`) and the
sets of $x$ values for which this is true, namely,
$\lbrace (0,1] \rbrace$ and
$\lbrace (1,2] \rbrace$, respectively. To
compute the integral, we simply take
the function values (i.e., `1,2`)
and some way of measuring the size of the
corresponding interval
(i.e.,  $\mu$) as in the following:

$$
\int_0^2 f d\mu = 1 \mu(\lbrace (0,1] \rbrace) + 2 \mu(\lbrace (1,2] \rbrace)
$$

We have suppressed some of the notation above to emphasize generality. Note
that
we obtain the same value of the integral as in the Riemann case when
$\mu((0,1])
= \mu((1,2]) = 1$. By introducing the $\mu$ function as  a way of
measuring the
intervals above, we have introduced another degree of freedom in
our
integration. This accommodates many weird functions that are not tractable
using
the usual Riemann theory, but we refer you to a proper introduction to
Lesbesgue
integration for further study [[jones2001lebesgue]](#jones2001lebesgue).
Nonetheless,
the key step in the above discussion is the introduction of the
$\mu$ function,
which we will encounter again as the so-called probability
density function.

## Random Variables

Most introductions to probability jump
straight into *random variables* and
then explain how to compute complicated
integrals. The problem with this
approach is that it skips over some of the
important subtleties that we will now
consider. Unfortunately, the term *random
variable* is not very descriptive. A
better term is *measurable function*.  To
understand why this is a better term,
we have to dive into the formal
constructions of probability by way of a simple
example.

Consider tossing a
fair six-sided die. There are only six outcomes possible,

$$
\Omega=\lbrace 1,2,3,4,5,6 \rbrace
$$

As we know, if the die is fair, then the probability of each outcome is $1/6$.
To say this formally, the measure  of each set (i.e., $\lbrace 1 \rbrace,\lbrace
2 \rbrace,\ldots,\lbrace 6 \rbrace$) is $\mu(\lbrace 1 \rbrace ) =\mu(\lbrace 2
\rbrace ) \ldots = \mu(\lbrace 6 \rbrace ) = 1/6$. In this case, the $\mu$
function we discussed earlier is the usual *probability* mass function, denoted
by
$\mathbb{P}$. The measurable function  maps a set into a
number on the real
line. For example, $ \lbrace 1 \rbrace \mapsto 1 $ is
one such function.

Now,
here's where things get interesting. Suppose you were asked to construct a
fair
coin from the fair die. In other words, we want to throw the die and then
record
the outcomes as if we had just tossed a fair coin. How could we do this?
One way
would be to define a measurable function that says if the die comes up
`3` or
less, then we declare *heads* and otherwise declare *tails*. This has
some
strong intuition behind it, but let's articulate it in terms of formal
theory.
This strategy creates two different non-overlapping sets $\lbrace
1,2,3 \rbrace$
and $\lbrace 4,5,6 \rbrace$. Each set has the same probability
*measure*,

$$
\begin{eqnarray*}
\mathbb{P}(\lbrace 1,2,3 \rbrace)  & = &  1/2  \\\
\mathbb{P}(\lbrace 4,5,6 \rbrace) & = & 1/2
\end{eqnarray*}
$$

 And the problem is solved. Everytime the die comes up
$\lbrace 1,2,3 \rbrace$,
we record heads and record tails otherwise.

Is this the only way to construct a
fair coin experiment from a
fair die? Alternatively, we can define the sets as
$\lbrace 1 \rbrace$,
$\lbrace 2 \rbrace$, $\lbrace 3,4,5,6 \rbrace$. If we
define the corresponding
measure for each set as the following

$$
\begin{eqnarray*}
\mathbb{P}(\lbrace 1 \rbrace)  & = &  1/2  \\\
\mathbb{P}(\lbrace 2 \rbrace) & = & 1/2 \\\
\mathbb{P}(\lbrace 3,4,5,6 \rbrace)
& = & 0
\end{eqnarray*}
$$

  then, we have another solution to the fair coin problem. To
implement this,
all we do is ignore every time the die shows `3,4,5,6` and
throw again. This is
wasteful, but it solves the problem.  Nonetheless,
we hope you can see how the
interlocking pieces of the theory provide a
framework for carrying the notion of
uncertainty/potentiality from one problem
to the next (e.g., from the fair die
to the fair coin).

Let's consider a slightly more interesting problem where we
toss two dice. We
assume that each throw is *independent*, meaning that the
outcome of one does
not influence the other. What are the sets in this case?
They are all pairs
of possible outcomes from two throws as shown below,

$$
\Omega = \lbrace (1,1),(1,2),\ldots,(5,6),(6,6) \rbrace
$$

  What are the measures of each of these sets? By virtue of the
independence
claim, the measure of each is the product of the respective measures
of each
element.  For instance,

$$
\mathbb{P}((1,2)) = \mathbb{P}(\lbrace 1 \rbrace) \mathbb{P}(\lbrace 2
\rbrace) = \frac{1}{6^2}
$$

 With all that established, we can ask the following
question: what is the
probability that the sum of the dice equals
seven? As before, the first thing to
do is characterize the
measurable function for this as $X:(a,b) \mapsto (a+b)$.
Next, we
associate all of the $(a,b)$ pairs with their sum. We can create a
Python dictionary for this as shown,

```python
d={(i,j):i+j for i in range(1,7) for j in range(1,7)}
```

The next step is to collect all of the $(a,b)$ pairs that sum to
each of the
possible values from two to twelve.

```python
from collections import defaultdict
dinv = defaultdict(list)
for i,j in d.items():
    dinv[j].append(i)
```

**Programming Tip.**

The `defaultdict` object from the built-in collections
module creates dictionaries with
default values when it encounters a new key.
Otherwise, we would have had to
create default values manually for a regular
dictionary.

 For example, `dinv[7]` contains the following list of pairs that
sum to seven,

```python
dinv[7]
```

    [(1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1)]

The next step is to compute the probability measured for each of these items.
Using the independence assumption, this means we have to compute the sum of the
products of the individual item probabilities in `dinv`. Because we know that
each outcome is equally likely, the probability of every term in the sum equals
$1/36$. Thus, all
we have to do is count the number of items in the
corresponding list for each
key in `dinv` and divide by `36`. For example,
`dinv[11]` contains `[(5, 6),
(6, 5)]`.  The probability of `5+6=6+5=11` is the
probability of this set which
is composed of the sum of the probabilities of the
individual elements
`{(5,6),(6,5)}`. In this case, we have $\mathbb{P}(11) =
\mathbb{P}(\lbrace
(5,6) \rbrace)+ \mathbb{P}(\lbrace (6,5) \rbrace) = 1/36 +
1/36 = 2/36$.
Repeating this procedure for all the elements, we derive the
probability mass
function as shown below,

```python
X={i:len(j)/36. for i,j in dinv.items()}
print(X)
```

    {2: 0.027777777777777776, 3: 0.05555555555555555, 4: 0.08333333333333333, 5: 0.1111111111111111, 6: 0.1388888888888889, 7: 0.16666666666666666, 8: 0.1388888888888889, 9: 0.1111111111111111, 10: 0.08333333333333333, 11: 0.05555555555555555, 12: 0.027777777777777776}

**Programming Tip.**

In the preceding code note that `36.` is written with
the
trailing decimal mark. This is a good habit to get into
because the default
division operation changed between Python 2.x and
Python 3.x.  In Python 2.x
division is integer division by default,
and it is floating-point division in
Python 3.x.

The above example exposes the elements of probability theory that
are in play for this simple problem while deliberately suppressing some of the
gory technical details. With this framework, we can ask other questions like
what is the probability that half the product of three dice will exceed the
their sum? We can solve this using the same method as in the following. First,
let's create the first mapping,

```python
d={(i,j,k):((i*j*k)/2>i+j+k) for i in range(1,7)
                                for j in range(1,7)
                                   for k in range(1,7)}
```

The keys of this dictionary are the triples and the values are the
logical
values of whether or not half the product of three dice exceeds their sum.
Now,
we do the inverse mapping to collect the corresponding lists,

```python
dinv = defaultdict(list)
for i,j in d.items():
  dinv[j].append(i)
```

Note that `dinv` contains only two keys, `True` and `False`. Again,
because the
dice are independent, the probability of any triple is $1/6^3$.
Finally, we
collect this for each outcome as in the following,

```python
X={i:len(j)/6.0**3 for i,j in dinv.items()}
print(X)
```

    {False: 0.37037037037037035, True: 0.6296296296296297}

Thus, the probability of half the product of three dice exceeding their sum is
`136/(6.0**3) = 0.63`. The set that is induced by the random variable has only
two elements in it, `True` and `False`, with $\mathbb{P}(\mbox{True})=136/216$
and $\mathbb{P}(\mbox{False})=1-136/216$.

As a final example to exercise
another layer of generality, let is consider the
first problem with the two dice
where we want the probability of a
seven, but this time one of the dice is no
longer fair.  The distribution for
the unfair die is the following:

$$
\begin{eqnarray*}
\mathbb{P}(\lbrace 1\rbrace)=\mathbb{P}(\lbrace 2
\rbrace)=\mathbb{P}(\lbrace 3 \rbrace)  = \frac{1}{9} \\\
\mathbb{P}(\lbrace
4\rbrace)=\mathbb{P}(\lbrace 5 \rbrace)=\mathbb{P}(\lbrace 6 \rbrace)  =
\frac{2}{9}
\end{eqnarray*}
$$

From our earlier work, we know the elements corresponding to the sum of seven
are the following:

$$
\lbrace (1,6),(2,5),(3,4),(4,3),(5,2),(6,1) \rbrace
$$

  Because we still have the independence assumption, all we need to
change is
the probability computation of each of elements. For example, given
that the
first die is the unfair one, we have

$$
\mathbb{P}((1,6)) = \mathbb{P}(1)\mathbb{P}(6) = \frac{1}{9} \times
\frac{1}{6}
$$

  and likewise for $(2,5)$ we have the following:

$$
\mathbb{P}((2,5)) = \mathbb{P}(2)\mathbb{P}(5) = \frac{1}{9} \times
\frac{1}{6}
$$

  and so forth. Summing all of these gives the following:

$$
\mathbb{P}_X(7) =  \frac{1}{9} \times \frac{1}{6}
+\frac{1}{9} \times \frac{1}{6}
                      +\frac{1}{9} \times
\frac{1}{6}
                      +\frac{2}{9} \times \frac{1}{6}
+\frac{2}{9} \times \frac{1}{6}
                      +\frac{2}{9} \times
\frac{1}{6}  = \frac{1}{6}
$$

 Let's try computing this using Pandas instead
of Python dictionaries. First, we
construct
a `DataFrame` object with an index of tuples
consisting of all pairs
of possible dice outcomes.

```python
from pandas import DataFrame
d=DataFrame(index=[(i,j) for i in range(1,7) for j in range(1,7)],
            columns=['sm','d1','d2','pd1','pd2','p'])
```

Now, we can populate the columns that we set up above
where the outcome of the
first die is the `d1` column and
the outcome of the second die is `d2`,

```python
d.d1=[i[0] for i in d.index]
d.d2=[i[1] for i in d.index]
```

Next, we compute the sum of the dices in the `sm`
column,

```python
d.sm=list(map(sum,d.index))
```

With that established, the DataFrame now looks like
the following:

```python
d.head(5) # show first five lines
```

      sm
      d1
      d2
      pd1
      pd2
      p

      (1, 1)
      2
      1
      1
      NaN
      NaN
      NaN

      (1, 2)
      3
      1
      2
      NaN
      NaN
      NaN

      (1, 3)
      4
      1
      3
      NaN
      NaN
      NaN

      (1, 4)
      5
      1
      4
      NaN
      NaN
      NaN

      (1, 5)
      6
      1
      5
      NaN
      NaN
      NaN

Next, we fill out the probabilities for each face of the
unfair die (`d1`) and
the fair die (`d2`),

```python
d.loc[d.d1<=3,'pd1']=1/9.
d.loc[d.d1 > 3,'pd1']=2/9.
d.pd2=1/6.
d.head(10)
```

      sm
      d1
      d2
      pd1
      pd2
      p

      (1, 1)
      2
      1
      1
      0.111111
      0.166667
      NaN

      (1, 2)
      3
      1
      2
      0.111111
      0.166667
      NaN

      (1, 3)
      4
      1
      3
      0.111111
      0.166667
      NaN

      (1, 4)
      5
      1
      4
      0.111111
      0.166667
      NaN

      (1, 5)
      6
      1
      5
      0.111111
      0.166667
      NaN

      (1, 6)
      7
      1
      6
      0.111111
      0.166667
      NaN

      (2, 1)
      3
      2
      1
      0.111111
      0.166667
      NaN

      (2, 2)
      4
      2
      2
      0.111111
      0.166667
      NaN

      (2, 3)
      5
      2
      3
      0.111111
      0.166667
      NaN

      (2, 4)
      6
      2
      4
      0.111111
      0.166667
      NaN

Finally, we can compute the joint probabilities
for the sum of the shown faces
as the following:

```python
d.p = d.pd1 * d.pd2
d.head(5)
```

      sm
      d1
      d2
      pd1
      pd2
      p

      (1, 1)
      2
      1
      1
      0.111111
      0.166667
      0.0185185

      (1, 2)
      3
      1
      2
      0.111111
      0.166667
      0.0185185

      (1, 3)
      4
      1
      3
      0.111111
      0.166667
      0.0185185

      (1, 4)
      5
      1
      4
      0.111111
      0.166667
      0.0185185

      (1, 5)
      6
      1
      5
      0.111111
      0.166667
      0.0185185

With all that established, we can compute the
density of all the dice outcomes
by using `groupby` as in the
following,

```python
d.groupby('sm')['p'].sum()
```

    sm
    2     0.018519
    3     0.037037
    4     0.055556
    5     0.092593
    6     0.129630
    7     0.166667
    8     0.148148
    9     0.129630
    10    0.111111
    11    0.074074
    12    0.037037
    Name: p, dtype: float64

These examples have shown how the theory of probability
breaks down sets and
measurements of  those sets and how these can be
combined to develop the
probability mass functions for new random
variables.

## Continuous Random
Variables

The same ideas work with continuous variables but managing the sets
becomes trickier because the real line, unlike discrete sets, has many
limiting
properties already built into it that have to be handled
carefully.
Nonetheless, let's start with an example that should
illustrate the analogous
ideas. Suppose a random variable $X$ is
uniformly distributed on the unit
interval. What is the probability
that the variable takes on values less than
1/2?

In order to build intuition onto the discrete case, let's go back to our
dice-throwing experiment with the fair dice. The sum of the values of the dice
is a measurable function,

$$
Y \colon \lbrace 1,2,\dots,6 \rbrace^2 \mapsto \lbrace 2,3,\ldots, 12 \rbrace
$$

 That is, $Y$ is a mapping of the cartesian product of sets to a
discrete set of
outcomes. In order to compute probabilities of the set of
outcomes, we need to
derive the probability measure for $Y$, $\mathbb{P}_Y$,
from the corresponding
probability measures for each die. Our previous discussion
went through the
mechanics of that. This means that

$$
\mathbb{P}_Y \colon \lbrace 2,3,\ldots,12 \rbrace \mapsto [0,1]
$$

 Note there is a separation between the function definition and where the
target
items of the function are measured in probability. More bluntly,

$$
Y \colon A \mapsto B
$$

 with,

$$
\mathbb{P}_Y \colon B \mapsto [0,1]
$$

 Thus, to compute $\mathbb{P}_Y$, which is derived
from other random variables,
we have to express the equivalence classes
in $B$ in terms of their progenitor
$A$ sets.

The situation for continuous variables follows the same pattern, but
with many more deep technicalities that we are going to skip. For the continuous
case, the random variable is now,

$$
X \colon \mathbb{R} \mapsto \mathbb{R}
$$

 with corresponding probability measure,

$$
\mathbb{P}_X \colon \mathbb{R} \mapsto [0,1]
$$

 But where are the corresponding sets here? Technically, these are the
*Borel*
sets, but we can just think of them as intervals. Returning to our
question,
what is the probability that a uniformly distributed random variable
on the unit
interval takes values less than $1/2$? Rephrasing this question
according to the
framework, we have the following:

$$
X \colon [0,1] \mapsto [0,1]
$$

 with corresponding,

$$
\mathbb{P}_X \colon [0,1] \mapsto [0,1]
$$

 To answer the question,  by the definition of the uniform random
variable on
the unit interval, we compute the following integral,

$$
\mathbb{P}_X([0,1/2]) = \mathbb{P}_X(0 < X < 1/2) =  \int_0^{1/2} dx  = 1/2
$$

  where the above integral's $dx$ sweeps through intervals of the
$B$-type. The
measure of any $dx$ interval (i.e., $A$-type set) is equal to
$dx$, by
definition of the uniform random variable. To get all the moving parts
into one
notationally rich integral, we can also  write this  as,

$$
\mathbb{P}_X(0 < X < 1/2) = \int_0^{ 1/2 } d\mathbb{P}_X(dx) = 1/2
$$

Now, let's consider a slightly more complicated and interesting example. As
before, suppose we have a uniform random variable, $X$ and let us introduce
another random variable defined,

$$
Y = 2 X
$$

  Now, what is the probability that $0 < Y < \frac{1}{2}$?
To express this in
our framework, we write,

$$
Y \colon [0,1] \mapsto [0,2]
$$

 with corresponding,

$$
\mathbb{P}_Y \colon [0,2] \mapsto [0,1]
$$

 To answer the question, we need to measure the set $[0,1/2]$, with
the
probability measure for $Y$, $\mathbb{P}_Y([0,1/2])$. How can we do this?
Because $Y$ is derived from the $X$ random variable, as with the fair-dice
throwing experiment, we have to create a set of equivalences in the target
space
(i.e., $B$-type sets) that reflect back on the input space (i.e.,
$A$-type
sets). That is, what is the interval $[0,1/2]$ equivalent to in terms
of the $X$
random variable? Because, functionally, $Y=2 X$, then the $B$-type
interval
$[0,1/2]$ corresponds to the $A$-type interval $[0,1/4]$. From the
probability
measure of $X$, we compute this with the integral,

$$
\mathbb{P}_Y([0,1/2]) =\mathbb{P}_X([0,1/4])=  \int_0^{1/4} dx  = 1/4
$$

Now, let's up the ante and consider the following random variable,

$$
Y = X^2
$$

 where now $X$ is still uniformly distributed, but now over the
interval
$[-1/2,1/2]$.  We can express this in our framework as,

$$
Y \colon [-1/2,1/2] \mapsto [0,1/4]
$$

 with corresponding,

$$
\mathbb{P}_Y \colon [0,1/4] \mapsto [0,1]
$$

 What is the $\mathbb{P}_Y(Y < 1/8)$?  In other words, what is the
measure of
the set $B_Y= [0,1/8]$? As before, because $X$ is derived from our
uniformly
distributed random variable, we have to reflect the $B_Y$ set onto
sets of the
$A$-type.  The thing to recognize is that because $X^2$
is symmetric about zero,
all $B_Y$ sets reflect back into two sets.
This means that for any set $B_Y$, we
have the correspondence $B_Y = A_X^+ \cup
A_X^{-}$.  So, we have,

$$
B_Y=\Big\lbrace 0<Y<\frac{1}{8}\Big\rbrace=\Big\lbrace 0<X<\frac{1}{\sqrt{8}}
\Big\rbrace \bigcup \Big\lbrace -\frac{1}{\sqrt {8}}<X<0 \Big\rbrace
$$

  From this perspective, we have the following solution,

$$
\mathbb{P}_Y(B_Y)=\mathbb{P}(A_X^+) + \mathbb{P}(A_X^{-})
$$

Also,

$$
\begin{align*}
A_X^+    &= \Big\lbrace 0<  X<\frac{1}{\sqrt{8}}   \Big\rbrace
\\\
A_X^{-}  &= \Big\lbrace -\frac{1}{\sqrt {8}} <  X<0 \Big\rbrace
\end{align*}
$$

  Therefore,

$$
\mathbb{P}_Y(B_Y) = \frac{1}{\sqrt 8} + \frac{1}{\sqrt 8}
$$

  because $\mathbb{P}(A_X^+) =\mathbb{P}(A_X^-) = 1/\sqrt 8$.  Let's
see if this
comes out using the usual transformation of variables method from
calculus.
Using this method, the density $f_Y(y) =
\frac{1}{
\sqrt y} $. Then, we obtain,

$$
\int_0^{\frac{1}{8}} \frac{1}{\sqrt y} dy = \frac{1}{\sqrt 2}
$$

  which is what we got using the sets method. Note that you would
favor the
calculus method in practice, but it is important to
understand the deeper
mechanics, because sometimes
the usual calculus method fails, as the next
problem shows.

## Transformation of Variables Beyond Calculus

Suppose $X$ and
$Y$ are uniformly distributed in the unit interval and we
define $Z$ as

$$
Z = \frac{X}{Y-X}
$$

  What is the $f_Z(z)$? If you try this using the usual calculus
method, you
will fail (try it!). The problem is one
of the technical prerequisites for the
calculus method is not in force.

The key observation is that $Z \notin (-1,0]$.
If this were
possible, the $X$ and $Y$ would have different signs, which cannot
happen,
given that $X$ and $Y$ are uniformly distributed over $(0,1]$. Now,
let's
consider when $Z>0$. In this case, $Y>X$ because $Z$ cannot be positive
otherwise. For the density function, we are interested in the set
$\lbrace  0
< Z < z \rbrace $. We want to compute

$$
\mathbb{P}(Z<z) = \int \int B_1 dX dY
$$

 with,

$$
B_1 = \lbrace 0 < Z < z \rbrace
$$

  Now, we have to translate that interval into an interval
relevant to $X$ and
$Y$. For $0 < Z$, we have $ Y > X$. For $Z < z $,
we have $Y > X(1/z+1)$.
Putting this together gives

$$
A_1 = \lbrace \max (X,X(1/z+1)) < Y < 1 \rbrace
$$

  Integrating this over $Y$ as follows,

$$
\int_0^1\lbrace\max(X,X(1/z+1))<Y<1 \rbrace dY=\frac{z-X-Xz}{z}\mbox{ where }
z > \frac{X}{1-X}
$$

 and integrating this one more time over $X$ gives

$$
\int_0^{\frac{z}{1+z}} \frac{-X+z-Xz}{z} dX = \frac{z}{2(z+1)} \mbox{ where }
z > 0
$$

 Note that this is the computation for the *probability*
itself, not the
probability density function. To get that, all we have
to do is differentiate
the last expression to obtain

$$
f_Z(z) = \frac{1}{(z+1)^2} \mbox{ where }  z > 0
$$

 Now we need to compute this density using the same process
for when $z < -1$.
We want  the interval $ Z < z $ for when $z < -1$.
For a fixed $z$, this is
equivalent to $ X(1+1/z) < Y$. Because $z$
is negative, this also means that $Y
< X$. Under these terms, we
have the following integral,

$$
\int_0^1 \lbrace X(1/z+1) <Y< X\rbrace dY = -\frac{X}{z} \mbox{ where }  z <
-1
$$

 and integrating this one more time over $X$ gives the following

$$
-\frac{1}{2 z} \mbox{ where }  z < -1
$$

 To get the density for $z<-1$, we differentiate this with
respect to $z$ to
obtain the following,

$$
f_Z(z) = \frac{1}{2 z^2} \mbox{ where }  z < -1
$$

 Putting this all together, we obtain,

$$
f_Z(z) =
\begin{cases}
      \frac{1}{(z+1)^2}     & \mbox{if }  z > 0 \\\
\frac{1}{2 z^2}       & \mbox{if }  z < -1 \\\
      0 & \mbox{otherwise }
\end{cases}
$$

  We will leave it as an exercise to show that this
integrates out to one.

## Independent Random Variables

Independence is a standard assumption.
Mathematically, the
necessary and sufficient condition for independence between
two
random variables $X$  and $Y$ is the following:

$$
\mathbb{P}(X,Y) = \mathbb{P}(X)\mathbb{P}(Y)
$$

 Two random variables $X$  and $Y$
are *uncorrelated* if,

$$
\mathbb{E}\left( (X-\overline{X})(Y-\overline{Y}) \right)=0
$$

 where $\overline{X}=\mathbb{E}(X)$ Note that uncorrelated random
variables are
sometimes called *orthogonal* random variables.  Uncorrelatedness
is a weaker
property than independence, however. For example, consider the
discrete random
variables $X$ and $Y$ uniformly distributed over the set
$\lbrace 1,2,3 \rbrace$
where

$$
X =
\begin{cases}
1 & \mbox{if } \omega =1 \\\
0 & \mbox{if } \omega =2
\\\
-1 & \mbox{if } \omega =3
\end{cases}
$$

 and also,

$$
Y =
\begin{cases}
0 & \mbox{if } \omega =1 \\\
1 & \mbox{if } \omega =2
\\\
0 & \mbox{if } \omega =3
\end{cases}
$$

 Thus, $\mathbb{E}(X)=0$ and $\mathbb{E}(X Y)=0$, so
$X$ and $Y$ are
uncorrelated. However, we have

$$
\mathbb{P}(X=1,Y=1)=0\neq \mathbb{P}(X=1)\mathbb{P}(Y=1)=\frac{1}{9}
$$

 So, these two random variables are *not* independent.
Thus, uncorrelatedness
does not imply independence, generally, but
there is the important case of
Gaussian random variables for which
it does. To see this, consider the
probability density function
for two zero-mean, unit-variance Gaussian random
variables $X$ and
$Y$,

$$
f_{X,Y}(x,y) = \frac{e^{\frac{x^2-2 \rho x
   y+y^2}{2
\left(\rho^2-1\right)}}}{2 \pi
   \sqrt{1-\rho^2}}
$$

 where $\rho:=\mathbb{E}(X Y)$  is the correlation coefficient.  In
the
uncorrelated case where $\rho=0$, the probability density function factors
into
the following,

$$
f_{X,Y}(x,y)=\frac{e^{-\frac{1}{2}\left(x^2+y^2\right)}}{2\pi}=\frac{e^{-\frac{x^2}{2}}}{\sqrt{2\pi}}\frac{e^{-\frac{y^2}{2}}}{\sqrt{2\pi}}
=f_X(x)f_Y(y)
$$

 which means that $X$ and $Y$ are independent.

Independence and conditional
independence are closely related, as in the following:

$$
\mathbb{P}(X,Y\vert Z) =\mathbb{P}(X\vert Z) \mathbb{P}(Y\vert Z)
$$

 which says that $X$ and $Y$ and independent conditioned
on $Z$.  Conditioning
independent random variables can break
their independence. For example, consider
two independent
Bernoulli-distributed random variables,  $X_1, X_2\in\lbrace 0,1
\rbrace$.  We define $Z=X_1+X_2$. Note that $Z\in \lbrace
0,1,2 \rbrace$. In the
case where $Z=1$, we have,

$$
\begin{align*}
\mathbb{P}(X_1\vert Z=1) &>0 \\\
\mathbb{P}(X_2\vert Z=1) &>0
\end{align*}
$$

 Even though $X_1,X_2$ are independent,
after conditioning on $Z$, we have the
following,

$$
\mathbb{P}(X_1=1,X_2=1\vert Z=1)=0\neq \mathbb{P}(X_1=1\vert
Z=1)\mathbb{P}(X_2=1\vert Z=1)
$$

 Thus, conditioning on $Z$ breaks the independence of
$X_1,X_2$. This also works
in the opposite direction ---
conditioning can make dependent random variables
independent.
Define $Z_n=\sum_i^n X_i$ with $X_i$ independent, integer-valued
random variables.  The $Z_n$ variables are
dependent because they stack the
same telescoping set of
$X_i$ variables. Consider the following,

$$
\begin{equation}
\mathbb{P}(Z_1=i,Z_3=j\vert Z_2=k) =
\frac{\mathbb{P}(Z_1=i,Z_2=k,Z_3=j)}{\mathbb{P}(Z_2 =k)}
\label{_auto1} \tag{1}
\end{equation}
$$

$$
\begin{equation} \
=\frac{\mathbb{P}(X_1 =i)\mathbb{P}(X_2 =k-i)\mathbb{P}(X_3
=j-k) }{\mathbb{P}(Z_2 =k)}
\end{equation}
\label{eq:condIndep} \tag{2}
$$

 where the factorization comes from the independence of
the $X_i$ variables.
Using the definition of conditional
probability,

$$
\mathbb{P}(Z_1=i\vert Z_2)=\frac{\mathbb{P}(Z_1=i,Z_2=k)}{\mathbb{P}(Z_2=k)}
$$

 We can continue to expand Equation [2](#eq:condIndep),

$$
\begin{align*}
\mathbb{P}(Z_1=i,Z_3=j\vert Z_2=k) &=\mathbb{P}(Z_1 =i\vert
Z_2) \frac{\mathbb{P}( X_3 =j-k)\mathbb{P}( Z_2 =k)}{\mathbb{P}( Z_2 =k)}\\\
&=\mathbb{P}(Z_1 =i\vert Z_2)\mathbb{P}(Z_3 =j\vert Z_2)
\end{align*}
$$

 where $\mathbb{P}(X_3=j-k)\mathbb{P}(Z_2=k)=
\mathbb{P}(Z_3=j,Z_2)$.  Thus, we
see that dependence between
random variables can be broken by conditioning to
create
conditionally independent random variables. As we have just
witnessed,
understanding how conditioning influences independence
is important and is the
main topic of
study in Probabilistic Graphical Models, a field
with many
algorithms and concepts to extract these
notions of conditional independence
from graph-based
representations of random variables.

## Classic Broken Rod
Example

Let's do one last example to exercise fluency in our methods by
considering the following classic problem: given a rod of unit-length,
broken
independently and randomly at two places, what is the
probability that you can
assemble the three remaining pieces into a
triangle? The first task is to find a
representation of a triangle as
an easy-to-apply constraint. What we want is
something like the
following:

$$
\mathbb{P}(\mbox{ triangle exists }) = \int_0^1 \int_0^1 \lbrace \mbox{
triangle exists }  \rbrace dX dY
$$

 where $X$ and $Y$ are independent and uniformly distributed
in the unit-
interval.  Heron's formula for the area of the triangle,

$$
\mbox{ area } = \sqrt{(s-a)(s-b)(s-c)s}
$$

 where $s = (a+b+c)/2$ is what we need. The idea is that this
yields a valid
area only when each of the terms under the square root is
greater than or equal
to zero. Thus, suppose that we have

$$
\begin{eqnarray*}
a  & = &  X  \\\
b  & = & Y-X \\\
c  & = & 1-Y
\end{eqnarray*}
$$

 assuming that $Y>X$.  Thus, the criterion for a valid triangle boils down
to

$$
\lbrace (s > a) \wedge (s > b) \wedge (s > c) \wedge (X<Y) \rbrace
$$

 After a bit of manipulation, this  consolidates into:

$$
\Big\lbrace \frac{1}{2} < Y < 1 \bigwedge \frac{1}{2}(2 Y-1) < X <
\frac{1}{2} \Big\rbrace
$$

  which we integrate out by $dX$ first to obtain

$$
\mathbb{P}(\mbox{ triangle exists }) = \int_{0}^1 \int_{0}^1 \Big\lbrace
\frac{1}{2} < Y < 1 \bigwedge \frac{1}{2}(2 Y-1) < X < \frac{1}{2} \Big\rbrace
dX dY
$$

$$
\mathbb{P}(\mbox{ triangle exists }) = \int_{\frac{1}{2}}^1 (1-Y) dY
$$

 and then by $dY$ to obtain finally,

$$
\mathbb{P}(\mbox{ triangle exists }) = \frac{1}{8}
$$

 when $Y>X$. By symmetry, we get the same result for $X>Y$. Thus, the
final
result is the following:

$$
\mathbb{P}(\mbox{ triangle exists }) = \frac{1}{8}+\frac{1}{8} = \frac{1}{4}
$$

We can quickly check using this result using Python for the case $Y>X$ using
the
following code:

```python
import numpy as np
x,y = np.random.rand(2,1000) # uniform rv
a,b,c = x,(y-x),1-y # 3 sides
s = (a+b+c)/2
np.mean((s>a) & (s>b)  & (s>c) & (y>x)) # approx 1/8=0.125
```

    0.129

**Programming Tip.**

The chained logical `&` symbols above tell Numpy that the
logical operation
should be considered element-wise.


# A short & practical introduction to Tensor Flow!

Part 4

The goal of this notebook is to train a LSTM character prediction model over [Text8](http://mattmahoney.net/dc/textdata) data.

This is a personal wrap-up of all the material provided by [Google's Deep Learning course on Udacity](https://www.udacity.com/course/deep-learning--ud730), so all credit goes to them.

Author: Pablo M. Olmos (olmos@tsc.uc3m.es)

Date: March 2017

```python
# These are all the modules we'll be using later. Make sure you can import them
# before proceeding further.
from __future__ import print_function
import os
import numpy as np
import random
import string
import tensorflow as tf
import zipfile
from six.moves import range
from six.moves.urllib.request import urlretrieve
```

```python
# Lets check what version of tensorflow we have installed. The provided scripts should run with tf 1.0 and above

print(tf.__version__)
```

```python
url = 'http://mattmahoney.net/dc/'

def maybe_download(filename, expected_bytes):
    """Download a file if not present, and make sure it's the right size."""
    if not os.path.exists(filename):
        filename, _ = urlretrieve(url + filename, filename)
    statinfo = os.stat(filename)
    if statinfo.st_size == expected_bytes:
        print('Found and verified %s' % filename)
    else:
        print(statinfo.st_size)
        raise Exception(
            'Failed to verify ' + filename + '. Can you get to it with a browser?')
    return filename

filename = maybe_download('XXX/textWordEmbeddings/text8.zip', 31344016) ## Change according to the folder where you saved the dataset provided
```

```python
def read_data(filename):
    with zipfile.ZipFile(filename) as f:
        name = f.namelist()[0]
        data = tf.compat.as_str(f.read(name))
    return data

text = read_data(filename)
print('Data size %d' % len(text))
```

```python
text[0:20]
```

Create a small validation set

```python
valid_size = 1000
valid_text = text[:valid_size]
train_text = text[valid_size:]
train_size = len(train_text)
print(train_size, train_text[:64])
print(valid_size, valid_text[:64])
```

Utility functions to map characters to vocabulary IDs and back

```python
vocabulary_size = len(string.ascii_lowercase) + 1 # [a-z] + ' '
first_letter = ord(string.ascii_lowercase[0])

def char2id(char):
    if char in string.ascii_lowercase:
        return ord(char) - first_letter + 1
    elif char == ' ':
        return 0
    else:
        print('Unexpected character: %s' % char)
        return 0
```

```python
def id2char(dictid):
    if dictid > 0:
        return chr(dictid + first_letter - 1)
    else:
        return ' '

print(char2id('a'), char2id('z'), char2id(' '), char2id('ï'))
print(id2char(1), id2char(26), id2char(0))
```

Function to generate a training batch for the LSTM model.

```python
batch_size=64      ## Number of batches, but also number of segments in which we divide the text. We read batch_size
                   ## batches in parallel, each read from a different segment. The implementation is not obvious, the
                   ## key seems to be the zip function inside the for loop below

num_unrollings=10  ## Each sequence is num_unrolling character long

### NOW I GET IT!! Every batch is a batch_size times 27 (num letters) matrix. Every row correspond to a letter. Each letter
### comes from a different sequence of (num_unrollings) so that the 64 letters cannot be read together.
## In the next batch, we have the following letter for each of the 64 training sequences!!

class BatchGenerator(object):

    def __init__(self, text, batch_size, num_unrollings):
        self._text = text
        self._text_size = len(text)
        self._batch_size = batch_size
        self._num_unrollings = num_unrollings
        segment = self._text_size // batch_size #We split the text into batch_size pieces
        self._cursor = [ offset * segment for offset in range(batch_size)]    #Cursor pointing every piece
        self._last_batch = self._next_batch()

    #
    def _next_batch(self):
        """Generate a single batch from the current cursor position in the data."""
        batch = np.zeros(shape=(self._batch_size, vocabulary_size), dtype=np.float)
        for b in range(self._batch_size):
            batch[b, char2id(self._text[self._cursor[b]])] = 1.0     #One hot encoding
            #print(self._text[self._cursor[b]])
            self._cursor[b] = (self._cursor[b] + 1) % self._text_size
        return batch

    def next(self):
        """Generate the next array of batches from the data. The array consists of
        the last batch of the previous array, followed by num_unrollings new ones.
        """
        batches = [self._last_batch]
        for step in range(self._num_unrollings):
            batches.append(self._next_batch())
        self._last_batch = batches[-1]
        return batches

def characters(probabilities):
    """Turn a 1-hot encoding or a probability distribution over the possible
    characters back into its (mostl likely) character representation."""
    return [id2char(c) for c in np.argmax(probabilities, 1)]

def batches2string(batches):
    """Convert a sequence of batches back into their (most likely) string
    representation."""
    s = [''] * batches[0].shape[0]
    for b in batches:
        s = [''.join(x) for x in zip(s, characters(b))]            #Clever! The ZIP is the key function here!
    return s

train_batches = BatchGenerator(train_text, batch_size, 10)
valid_batches = BatchGenerator(valid_text, 1, 1)

```

```python
print(batches2string(train_batches.next()))
print(batches2string(train_batches.next()))
```

```python
#OK with this one
def logprob(predictions, labels):
    """Log-probability of the true labels in a predicted batch."""
    predictions[predictions < 1e-10] = 1e-10
    return np.sum(np.multiply(labels, -np.log(predictions))) / labels.shape[0]

#OK with this one
def sample_distribution(distribution):
    """Sample one element from a distribution assumed to be an array of normalized
        probabilities.
    """

    r = random.uniform(0,1)
    s = 0
    for i in range(len(distribution)):
        s += distribution[i]
        if s >= r:
            return i
    return len(distribution) - 1

#OK with this one
def sample(prediction):
    """Turn a (column) prediction into 1-hot encoded samples."""
    p = np.zeros(shape=[1, vocabulary_size], dtype=np.float)
    p[0, sample_distribution(prediction[0])] = 1.0
    return p

def random_distribution():
    """Generate a random column of probabilities."""
    b = np.random.uniform(0.0, 1.0, size=[1, vocabulary_size])
    return b / np.sum(b, 1)[:, None]

```

```python
train_batches.next()[0].shape
```

# Simple LSTM Model

Recall the fundamental model

Also, the un-regularized cost function is

\begin{align}
J(\boldsymbol{\theta})=\frac{1}{N}\sum_{n=1}^N\sum_{t=1}^{T_n}d(\boldsymbol{y}_t^{(n)},\sigma(\boldsymbol{h}_t^{(n)}))
\end{align}
where $d(\cdot,\cdot)$ is the cross-entropy loss function.

About the TF implementation below, see the following excellent [post](http://www.thushv.com/sequential_modelling/long-short-term-memory-lstm-networks-implementing-with-tensorflow-part-2/)

>
Now calculating logits for softmax is a little bit tricky. This a temporal (time-based) network. So after each processing each num_unrolling batches through the LSTM cell, we update h_{t-1}=h_t and c_{t-1}=c_t before calculating logits and the loss. This is done by using tf.control_dependencies. What this does is that, logits will not be calculated until saved_output and saved_states are updated. Finally, as you can see, num_unrolling acts as the amount of history we are remembering.

In other words, in the computation graph everytime something is updated, all the dependent op nodes are updated and this is propagated through the graph. If we want to wait until the very end to compute the loss, we wait using the command tf.control_dependencies.

About the zip() and zip(*) operators, see this [post](https://docs.python.org/2/library/functions.html#zip)

```python
num_nodes = 64

graph = tf.Graph()
with graph.as_default():

    # Parameters:

    #i(t) parameters
    # Input gate: input, previous output, and bias.
    ix = tf.Variable(tf.truncated_normal([vocabulary_size, num_nodes], -0.1, 0.1))   ##W^ix
    im = tf.Variable(tf.truncated_normal([num_nodes, num_nodes], -0.1, 0.1)) ## W^ih
    ib = tf.Variable(tf.zeros([1, num_nodes])) ##b_i

    #f(t) parameters
    # Forget gate: input, previous output, and bias.
    fx = tf.Variable(tf.truncated_normal([vocabulary_size, num_nodes], -0.1, 0.1)) ##W^fx
    fm = tf.Variable(tf.truncated_normal([num_nodes, num_nodes], -0.1, 0.1)) ##W^fh
    fb = tf.Variable(tf.zeros([1, num_nodes])) ##b_f

    #g(t) parameters
    # Memory cell: input, state and bias.
    cx = tf.Variable(tf.truncated_normal([vocabulary_size, num_nodes], -0.1, 0.1)) ##W^gx
    cm = tf.Variable(tf.truncated_normal([num_nodes, num_nodes], -0.1, 0.1)) ##W^gh
    cb = tf.Variable(tf.zeros([1, num_nodes]))  ##b_g

    #o(t) parameters
    # Output gate: input, previous output, and bias.
    ox = tf.Variable(tf.truncated_normal([vocabulary_size, num_nodes], -0.1, 0.1))  ##W^ox
    om = tf.Variable(tf.truncated_normal([num_nodes, num_nodes], -0.1, 0.1))  ##W^oh
    ob = tf.Variable(tf.zeros([1, num_nodes])) ##b_o

    # Variables saving state across unrollings.
    saved_output = tf.Variable(tf.zeros([batch_size, num_nodes]), trainable=False) #h(t)
    saved_state = tf.Variable(tf.zeros([batch_size, num_nodes]), trainable=False) #s(t)

    # Classifier weights and biases (over h(t) to labels)
    w = tf.Variable(tf.truncated_normal([num_nodes, vocabulary_size], -0.1, 0.1))
    b = tf.Variable(tf.zeros([vocabulary_size]))

    # Definition of the cell computation.
    def lstm_cell(i, o, state):
        """Create a LSTM cell. See e.g.: http://arxiv.org/pdf/1402.1128v1.pdf
        Note that in this formulation, we omit the various connections between the
        previous state and the gates."""
        input_gate = tf.sigmoid(tf.matmul(i, ix) + tf.matmul(o, im) + ib)
        forget_gate = tf.sigmoid(tf.matmul(i, fx) + tf.matmul(o, fm) + fb)
        update = tf.matmul(i, cx) + tf.matmul(o, cm) + cb
        state = forget_gate * state + input_gate * tf.tanh(update)    #tf.tanh(update) is g(t)
        output_gate = tf.sigmoid(tf.matmul(i, ox) + tf.matmul(o, om) + ob)
        return output_gate * tf.tanh(state), state      #h(t) is output_gate * tf.tanh(state)

    # Input data. Now it makes sense!!!

    train_data = list()
    for _ in range(num_unrollings + 1):
        train_data.append(tf.placeholder(tf.float32, shape=[batch_size,vocabulary_size]))
    train_inputs = train_data[:num_unrollings]
    train_labels = train_data[1:]  # labels are inputs shifted by one time step.

    # Unrolled LSTM loop.

    outputs = list()
    output = saved_output
    aux = output
    state = saved_state
    for i in train_inputs:
        output, state = lstm_cell(i, output, state)
        outputs.append(output)

    # State saving across unrollings.
    with tf.control_dependencies([saved_output.assign(output),saved_state.assign(state)]):
        #Classifier.
        logits = tf.nn.xw_plus_b(tf.concat(axis=0,values=outputs), w, b)
        loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=tf.concat(axis=0, values=train_labels),logits=logits))

    # Optimizer.

    """Next, we are implementing the optimizer. Remember! we should use “gradient clipping” (tf.clip_by_global_norm)
    to avoid “Exploding gradient” phenomenon. Also, we decay the learning_rate over time."""
    global_step = tf.Variable(0)

    learning_rate = tf.train.exponential_decay(10.0, global_step, 5000, 0.1, staircase=True)
    optimizer = tf.train.GradientDescentOptimizer(learning_rate)

    """ optimizer.compute_gradients(loss) yields (gradient, value) tuples. gradients, v = zip(*optimizer.compute_gradients(loss))
    performs a transposition, creating a list of gradients and a list of values.
    gradients, _ = tf.clip_by_global_norm(gradients, 1.25)
    then clips the gradients, and optimizer = optimizer.apply_gradients(zip(gradients, v), global_step=global_step)
    re-zips the gradient and value lists back into an iterable of (gradient, value)
    tuples which is then passed to the optimizer.apply_gradients method."""

    gradients, v = zip(*optimizer.compute_gradients(loss))
    gradients, _ = tf.clip_by_global_norm(gradients, 1.25)
    optimizer = optimizer.apply_gradients(zip(gradients, v), global_step=global_step)

    # Predictions.
    train_prediction = tf.nn.softmax(logits)

    # Sampling and validation eval: batch 1, no unrolling.
    sample_input = tf.placeholder(tf.float32, shape=[1, vocabulary_size])
    saved_sample_output = tf.Variable(tf.zeros([1, num_nodes]))
    saved_sample_state = tf.Variable(tf.zeros([1, num_nodes]))
    # Create an op that groups multiple operations.
    reset_sample_state = tf.group(saved_sample_output.assign(tf.zeros([1, num_nodes])),
                                  saved_sample_state.assign(tf.zeros([1, num_nodes])))

    sample_output, sample_state = lstm_cell(sample_input, saved_sample_output, saved_sample_state)

    with tf.control_dependencies([saved_sample_output.assign(sample_output),saved_sample_state.assign(sample_state)]):
        sample_prediction = tf.nn.softmax(tf.nn.xw_plus_b(sample_output, w, b))

```

```python
num_steps = 1001
summary_frequency = 100

with tf.Session(graph=graph) as session:
    tf.global_variables_initializer().run()
    print('Initialized')
    mean_loss = 0
    for step in range(num_steps):
        batches = train_batches.next()
        feed_dict = dict()
        for i in range(num_unrollings + 1):
            feed_dict[train_data[i]] = batches[i]
        _, l, predictions, lr = session.run(
            [optimizer, loss, train_prediction, learning_rate], feed_dict=feed_dict)
        mean_loss += l
        if step % summary_frequency == 0:
            if step > 0:
                mean_loss /= summary_frequency
            # The mean loss is an estimate of the loss over the last few batches.
            print(
                'Average loss at step %d: %f learning rate: %f' % (step, mean_loss, lr))
            mean_loss = 0
            labels = np.concatenate(list(batches)[1:])
            print('Minibatch perplexity: %.2f' % float(
                np.exp(logprob(predictions, labels))))
            if step % (summary_frequency * 10) == 0:
                # Generate some samples.
                print('=' * 80)
                for _ in range(5):
                    feed = sample(random_distribution())
                    sentence = characters(feed)[0]
                    reset_sample_state.run()
                    for _ in range(79):
                        prediction = sample_prediction.eval({sample_input: feed})
                        feed = sample(prediction)
                        sentence += characters(feed)[0]
                    print(sentence)
                print('=' * 80)
            # Measure validation set perplexity.
            reset_sample_state.run()
            valid_logprob = 0
            for _ in range(valid_size):
                b = valid_batches.next()
                predictions = sample_prediction.eval({sample_input: b[0]})
                valid_logprob = valid_logprob + logprob(predictions, b[1])
            print('Validation set perplexity: %.2f' % float(np.exp(
                valid_logprob / valid_size)))
```

```python
batches = train_batches.next()
```

```python
batches[0]
```

```python

```


# Tutorial: Unit Conversions
Micromagnetic simulations provide a method of simulating the magnetic structure in real materials. However, in order to do this we need to be able to convert the information we have about the system into the parameters that micromagnetics use.

# CGS to SI
`Ubermag` uses SI units for all of the system parameters. Experimentally, material parameters are can often be measured in CGS units SI units, or a mixture of the two. Firstly, we show a convinent way of converting from CGS to SI units and other useful quantities.

To do this, we make use of `astropy.units`, a package aimed at the astrophysics community for its unit converting functionality. `astropy` is not installed by default. We can install it (e.g. with `pip`) directly from the notebook. Generally, you will not install `astropy` from inside the notebook (using either `pip` or `conda`) but for the sake of demonstration we run the command in here.

```python
# NBVAL_IGNORE_OUTPUT
!pip install astropy
```

    Requirement already satisfied: astropy in /opt/miniconda3/envs/ubermagdev/lib/python3.8/site-packages (4.3.1)
    Requirement already satisfied: pyerfa>=1.7.3 in /opt/miniconda3/envs/ubermagdev/lib/python3.8/site-packages (from astropy) (2.0.0)
    Requirement already satisfied: numpy>=1.17 in /opt/miniconda3/envs/ubermagdev/lib/python3.8/site-packages (from astropy) (1.21.2)

Now we can import `astropy`.

```python
from astropy import units as u
import numpy as np
```

To initalise a value with units, one has to simply multiply the value by the units provided.

```python
field = 100 * u.Gauss
field
```

$100 \; \mathrm{G}$

These units remain with the variable throughout operations.

```python
field2 = field * 10
field2
```

$1000 \; \mathrm{G}$

We can easily convert to SI units by using

```python
field2.si
```

$0.1 \; \mathrm{T}$

Magnetic moment can be expressed using the following CGS units

```python
magnetic_moment = 1 * u.erg/u.Gauss
magnetic_moment
```

$1 \; \mathrm{\frac{erg}{G}}$

and simplified using

```python
magnetic_moment.si
```

$0.001 \; \mathrm{m^{2}\,A}$

If we wish to detach the numervical value for its units, we can do this using `.value`

```python
magnetic_moment.si.value
```

    0.001

## Equivalencies

The `astropy.units` also enables conversions of units with different equivalencies i.e. temperature and energy. To use this we can create a variable with the relevant units of temperature and use the `to` function to convert to the relevant units with the relevant equivalency.

For example, if an exchange interaction has a temperature of 4.15 K we can calculate the equivalent energy in J.

```python
t_k = 4.15 * u.K
t_k.to(u.J, equivalencies=u.temperature_energy())
```

$5.7296934 \times 10^{-23} \; \mathrm{J}$

While `astropy` handles a variety of units and conversions, it does not currently have an equivalency for magnetic induction and magnetic field strength i.e. B to H . As this is a very useful for the magnetism community, we have provided the conversion here.

```python
from astropy import constants as const
induction_field = [(u.T, u.A/u.m,
                    lambda x: x / const.mu0,
                    lambda x: const.mu0 / x)]
```

```python
field = 100 * u.Gauss
field.to(u.A/u.m, equivalencies=induction_field)
```

$7957.7472 \; \mathrm{\frac{A}{m}}$

# Parameters
Here we will describe some select methods for relating atomistic parameters, micromagnetic parameters, and experimental results.

NOTE: Different definitions of the exchange Hamiltonian will lead to different conversion factors. Here, we use the atomistic exchange Hamiltonian
\begin{equation}
{\cal H}_{ex} = -\frac{1}{2}\sum_{i\neq j} J_{ij} {\bf S}_i \cdot {\bf S}_j,
\end{equation}
where $\lvert{\bf S}_i \rvert = \lvert {\bf S}_j \rvert = 1$ are the normaised spin vectors, and $J_{ij}$ is the exchange between the.

## Exchange
### Atomistic
The atomistic exchange $J$ can be obtained from the Curie temperature $T_\text{C}$ of a material by using
\begin{equation}
J = \frac{3k_\text{B}T_\text{C}}{\epsilon z},
\end{equation}
where $k_\text{B}$ is the Boltzmann constant, $z$ is the number of nearest neighbours, and $\epsilon$ is
a structural depended correction factor. The values of this correction factor has been calculated in Table I of [Garanin 1996](https://doi.org/10.1103/PhysRevB.53.11593).

```python
from scipy import constants
def Tc_to_J(Tc, e, z):
    return Tc*3*constants.k/(e*z)
```

For example, a system with a $T_c$ of 100 K and a fcc cubic structure.

```python
Tc = 100
e = 0.808
z = 12
J = Tc_to_J(Tc, e, z)
J
```

    4.271810024752475e-22

### Micromagnetic
The micromagnetic exchange correlation constant $A$ can be related to atomistic exchange using
\begin{equation}
A = \frac{zJl^2}{12V},
\end{equation}
where $J$ is the Heisenberg exchange, $z$ is the number of nearest neighbour atoms, and $l$ is the distance between neighbor atoms and $V$ is the crystal volume per magnetic atom.

```python
def J_to_A(J, z, l, V):
    return J*l*l*z/(12*V)
```

```python
l = 6.84e-10
V = 2.24e-28
A = J_to_A(J, z, l, V)
A
```

    8.922285495270508e-13

The micromagnetic exchange correlation constant can be obtained directly from $T_c$ using
\begin{equation}
A = \frac{k_\text{B}T_\text{C}l^2}{4\epsilon V}.
\end{equation}

```python
def Tc_to_A(Tc, e, l, V):
    return Tc * constants.k * l * l / (4 * e* V)
```

```python
A = Tc_to_A(Tc, e, l, V)
A
```

    8.922285495270509e-13

## DMI
### Atomistic

The Dzyaloshinskii–Moriya interaction is given by
\begin{equation}
{\cal H}_{ex} = -\frac{1}{2}\sum_{i\neq j} {\bf d}_{ij} \cdot \left( {\bf S}_i \times {\bf S}_j \right),
\end{equation}
where ${\bf d}_{ij}$ is the atomistic DMI vector.

### Micromagentic
The micromagnetic DMI constant $D$ can be related to atomistic DMI  using
\begin{equation}
D = \frac{zdl}{12V},
\end{equation}
where $d$ is the atomistic DMI, $z$ is the number of nearest neighbour atoms, and $l$ is the distance between neighbor atoms and $V$ is the crystal volume per magnetic atom.

```python
def d_to_D(d, z, l, V):
    return d*l*z/(12*V)
```

```python
d = 2.01e-23
D = d_to_D(d, z, l, V)
D
```

    6.137678571428571e-05

The DMI constants are not easy to measure experimentally, however they can be be calculated from the helical period P using
\begin{equation}
P = \frac{4\pi A}{|D|},
\end{equation}
where $A$ and $D$ are the micromagnetic exchange and DMI respectively.

```python
def P_to_D(P, A):
    return 4*np.pi*A/P

def D_to_P(D, A):
    return 4*np.pi*A/abs(D)
```

For a system with a micromagentic exchange of $6\times 10^{-14}$ Jm$^{-1}$ and a helical period of 20 nm

```python
A = 6e-14
D = P_to_D(20e-9, A)
D
```

    3.7699111843077517e-05

For atomistic simulations, this can be converted into
\begin{equation}
P = \frac{4\pi J l}{|d|},
\end{equation}
where $J$ is the Heisenberg exchange, $d$ is the atomistic DMI, and $l$ is the distance between neighbor atoms.

## Saturation Magnetisation
### Micromagnetics
The saturation magnetisation is often measured in $\mu_\text{B}/f.u.$ but is needed in A/m in micromagnetics. A simple converstion can be used
\begin{equation}
M_s [ \text{A}/ \text{m}]= \frac{\mu_\text{B} M_s[\mu_\text{B}/f.u.]}{V},
\end{equation}
where $M_s[\mu_\text{B}/f.u.]$ is the saturation magnetisation in $\mu_B$ per formula unit, $\mu_B$ is the Bohr magneton in J/m, and $V$ is the volume of the formula unit in m$^3$.

```python
def Ms_muB_to_Am(Ms, V):
    return constants.value('Bohr magneton') * Ms/V
```

```python
Ms = Ms_muB_to_Am(0.8, 2.24375e-28)
print(Ms)
```

    33066.10835716991

### Atomistic
In atomistic simulations the saturation magnetisation $M_s$ in micromagnetic simulations can be related to the magnetic moment $\mu$ simply by
\begin{equation}
\mu = M_s V,
\end{equation}
where $V$ is the crystal volume per magnetic atom.

```python
def Ms_to_mu(Ms, V):
    return Ms*V
```

```python
Ms = 6e5
mu = Ms_to_mu(Ms, V)
mu
```

    1.3440000000000002e-22

## Anisotropy
### Micromagnetic
Anisotropy can can be measured experimentally in a variety of different ways. The results torque magnetometry, for example, can give correct value for the anisotropy in units of Jm$^{-3}$.

### Atomistic
Similarly to the saturation magnetisation the conversion between micromagnetic $K$ and atomistic anisotropy $k$ is simply volume weighted
\begin{equation}
k = K V,
\end{equation}
where $V$ is the crystal volume per magnetic atom.
This atomistic anisotropy $k$ can also be calculated from the difference in energy of $J$ in different directions. i.e. $J_{\perp} = 6\times 10^{-23}$ J and $J_{\parallel} = 5 \times 10^{-23}$ J gives an atomistic anisotropy $k=1\times 10^{-23}$ J.

```python
def K_to_k(K, V):
    return Ms*V
```

```python
K = 1.2e6
k = K_to_k(K, V)
k
```

    1.3440000000000002e-22

# Worked Example

Here FeGe will be used as example for how to obtain micromagnetic parameters. FeGe has a cubic crystal structure with four Ge and four Fe atoms per unit cell with a lattice constant of $a=
4.6995$ Å and the distance between Fe atoms is 2.881 Å \[[Wilhelm 2007](http://doi.org/10.1016/j.stam.2007.04.004)\]. The saturation magnetisation is $1.07 \mu_\text{B}/f.u.$ \[[Yamada 2003](https://doi.org/10.1016%2FS0921-4526%2802%2902471-7)\] and magnetic ordering temperature is 278 K \[[Lebech 1989](https://iopscience.iop.org/article/10.1088/0953-8984/1/35/010/meta)\]. The helical period of FeGe is $\sim 70$ nm \[[Yu 2011](https://doi.org/10.1038/nmat2916)\].

```python
a = 4.6995e-10
l = 2.881e-10
Ms_orig = 1.07
Tc = 278
P = 70e-9
```

Volume per magnetic atom

```python
V = (a**3)/4
V
```

    2.594746713121875e-29

Saturation magnetisation

```python
Ms = Ms_muB_to_Am(Ms_orig, V)
Ms
```

    382433.88973569183

Exchange

```python
e = 0.644
A = Tc_to_A(Tc, e, l, V)
A
```

    4.76621650209023e-12

DMI

```python
D = P_to_D(P, A)
D
```

    0.000855629185622006


```python
# 그래프, 수학 기능 추가
# Add graph and math features
import pylab as py
import numpy as np
import numpy.linalg as nl
# 기호 연산 기능 추가
# Add symbolic operation capability
import sympy as sy

```

```python
sy.init_printing()

```

# 룽게-쿠타법 (RK4) : 고차 상미방
Runge-Kutta Method (RK4) : Higher Order ODE

## 단진자
Simple Pendulum

다음 미분 방정식은 단진자의 운동을 묘사한다.

Following differential equation describes the motion of a simple pendulum.

Ref : Wikipedia contributors, 'Pendulum (mathematics)', Wikipedia, The Free Encyclopedia, 2 June 2018, 13:28 UTC, <https://en.wikipedia.org/w/index.php?title=Pendulum_(mathematics)&oldid=844080803> [accessed 5 August 2018]

$$
\frac{d^2\theta}{dt^2} + \frac{g}{l}sin\theta = 0
$$

상태변수는 다음과 같다고 가정하자.

Let's assume that the state variables are as follows.

$$
\mathbf{x}
=
\begin{pmatrix}
x_0\\
x_1
\end{pmatrix}
=
\begin{pmatrix}
\theta\\
\frac{d}{dt}\theta
\end{pmatrix}
$$

상태변수의 미분은 다음과 같다.
Differentiation of the state variables are as follows.

$$
\frac{d}{dt}
\begin{pmatrix}
    x_0\\
    x_1
\end{pmatrix}
=
\begin{pmatrix}
    x_1\\
    -\frac{g}{l}sinx_0
\end{pmatrix}
$$

python 함수로 구현해 보면 다음과 같을 것이다.

One possible python implementation would be as follows.

```python
g_mpsps = 9.8
l_m = 0.3

legends = ('$\\theta(deg)$', '$\\frac{d}{dt}\\theta(deg/s)$')
ylabel = ''

# Initial state
x_0 = np.array([np.deg2rad(90), 0])

def pendulum_NL(x, t):
    """
    Parameters
    ==========
    x: array of theta and d(theta)/dt
    t: time value

    Return Value
    ============
    One dimensional array of dx/dt
    """

    return np.array([x[1], (-g_mpsps/l_m)*np.sin(x[0])])

```

룽게-쿠타법을 오일러법, 훈법과 비교해보자.
Let's compare the Runge-Kutta method with Euler method, and Heun's method.

```python
def rk4_step(f, x0, t0, t1):
    """
    One time step of Runge-Kutta method

    f: dx_dt function
    x0 : initial condition
    t0 : this step time
    t1 : next step time
    """
    delta_t = (t1 - t0)
    delta_t_half = delta_t * 0.5
    t_half = t0 + delta_t_half

    # Step 1
    s1 = f(x0, t0)

    # Step 2
    s2 = f(x0 + s1 * delta_t_half, t_half)

    # Step 3
    s3 = f(x0 + s2 * delta_t_half, t_half)

    # Step 4
    s4 = f(x0 + s3 * delta_t, t1)

    # Step 5
    s = (1.0 / 6.0) * (s1 + (s2 + s3) * 2 + s4)

    # Step 6
    x1 = x0 + s * delta_t

    return x1

```

```python
def rk4(f, t_array, x_0):
    time_list = [t_array[0]]
    result_list = [x_0]

    x_i = x_0

    for k, t_i in enumerate(t_array[:-1]):
        # time step
        x_i_plus_1 = rk4_step(f, x_i, t_i, t_array[k+1])

        time_list.append(t_array[k+1])
        result_list.append(x_i_plus_1)

        x_i = x_i_plus_1

    return time_list, result_list

```

```python
def euler(f, t_array, x_0):
    time_list = [t_array[0]]
    result_list = [x_0]

    x_i = x_0

    for k, t_i in enumerate(t_array[:-1]):
        # time step
        delta_t = t_array[k+1] - t_array[k]

        # slope
        s_i = f(x_i, t_i)

        # x[i + 1]
        x_i_plus_1 = x_i + s_i * delta_t

        time_list.append(t_array[k+1])
        result_list.append(x_i_plus_1)

        x_i = x_i_plus_1

    return time_list, result_list

```

```python
def heun(f, t_array, x_0):
    time_list = [t_array[0]]
    result_list = [x_0]

    x_i = x_0

    for k, t_i in enumerate(t_array[:-1]):
        # time step
        delta_t = t_array[k+1] - t_array[k]

        # slope at i
        s_i = f(x_i, t_i)

        # x[i + 1] by Forward Euler
        x_i_plus_1 = x_i + s_i * delta_t

        # slope at i + 1
        s_i_plus_1 = f(x_i_plus_1, t_array[k+1])

        # average of slope
        s_average = (s_i + s_i_plus_1) * 0.5

        # x[i + 1] by Modified Euler
        x_i_plus_1_m = x_i + s_average * delta_t

        time_list.append(t_array[k+1])
        result_list.append(x_i_plus_1_m)

        x_i = x_i_plus_1_m

    return time_list, result_list

```

```python

py.figure(figsize=(12, 12))

# Time array
delta_t = 0.001

t_sec_array = np.arange(0, 6 + delta_t*0.5, delta_t)

# *** Euler ***
t_euler, x_euler = euler(pendulum_NL, t_sec_array, x_0)
py.plot(t_euler, x_euler, 'o', label='Euler')

# *** Heun ***
t_heun, x_heun = heun(pendulum_NL, t_sec_array, x_0)
py.plot(t_heun, x_heun, '.', label='Heun')

# *** RK4 ***
t_rk4, x_rk4 = rk4(pendulum_NL, t_sec_array, x_0)
py.plot(t_rk4, x_rk4, '-', label='RK4')

py.xlabel('t(sec)')
py.ylabel('x(m)')

py.legend(loc=0)
py.grid(True)

```

## 4차 선형 상미방
Fourth Order Linear ODE

$$
         \frac{d^4x}{dt^4}
         + 12 \frac{d^3x}{dt^3}
         + 54 \frac{d^2x}{dt^2}
         + 108 \frac{dx}{dt}
         + 80 x = 0
$$

$$
\mathbf{q} = \begin{pmatrix}q_0 & q_1 & q_2 & q_3 \end{pmatrix}^T = \begin{pmatrix}x & \frac{dx}{dt} & \frac{d^2x}{dt^2} & \frac{d^3x}{dt^3} \end{pmatrix}^T
$$

$$
\frac{d\mathbf{q}}{dt}
=
\begin{bmatrix}
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0 \\
0 & 0 & 0 & 1 \\
- 80 & - 108 & - 54 & -12
\end{bmatrix}
\begin{pmatrix}
q_0 \\ q_1 \\ q_2 \\ q_3
\end{pmatrix}
=
\mathbf{Aq}
$$

```python
matrix_A = np.matrix([
        [0, 1, 0, 0],
        [0, 0, 1, 0],
        [0, 0, 0, 1],
        [-80, -108, -54, -12],]
)

legends = (f'$q_{k}$' for k in range(matrix_A.shape[0]))

ylabel = '$\mathbf{q}$'

def fourth_order(q, t):
    """
    Parameters
    ==========
    q: array of q_0, q_1, q_2, and q_3
    t: time value

    Return Value
    ============
    One dimensional array of dq/dt
    """

    q_column = np.matrix(q).T
    qdot_column = matrix_A * q_column

    qdot_array = np.array(qdot_column.T).flatten()

    return qdot_array

# Initial state
x_0 = np.array([1, 0, 0, 0])

```

```python
# Time array
delta_t = 0.08
t_sec_array = np.arange(0, 6 + delta_t*0.5, delta_t)

# *** Euler ***
t_euler, x_euler_ode4 = euler(fourth_order, t_sec_array, x_0)

# *** Heun ***
t_heun, x_heun_ode4 = heun(fourth_order, t_sec_array, x_0)

# *** RK4 ***
t_rk4, x_rk4_ode4 = rk4(fourth_order, t_sec_array, x_0)

# Convert to NumPy arrays
x_euler_ode4_array, x_heun_ode4_array, x_rk4_ode4_array = (
    np.array(x_euler_ode4),
    np.array(x_heun_ode4),
    np.array(x_rk4_ode4))

py.figure(figsize=(10, 10))

for i_state in range(x_euler_ode4_array.shape[1]):
    py.subplot(x_euler_ode4_array.shape[1], 1, i_state+1)
    py.plot(t_euler, x_euler_ode4_array[:, i_state], '-', label='Euler')
    py.plot(t_heun, x_heun_ode4_array[:, i_state], '-', label='Heun')
    py.plot(t_rk4, x_rk4_ode4_array[:, i_state], '-', label='RK4')

    py.xlabel('t(sec)')
    py.ylabel(f'$q_{i_state}$')

    py.legend(loc=1)
    py.grid(True)

```

## 부록: 단진자의 방향장
Appendix: The direction field of a simple pendulum

각 상태에서 상태변수의 변화의 방향 $\left(\frac{d}{dt}\theta, \frac{d^2}{dt^2}\theta \right)$ 을 표시해 보자.
At each state, let's present the direction of state variable change $\left(\frac{d}{dt}\theta, \frac{d^2}{dt^2}\theta \right)$.

```python
def ode_slopes_2states_cartesian(func, theta_rad_list, theta_dot_rad_list, time_list):
    """
    Plot field of arrows indicating derivatives of the state
    :param func:
    :param theta_rad_list:
    :param theta_dot_rad_list:
    :param time_list:
    :return:
    """

    # cartesian coordinate
    y_rad = np.meshgrid(theta_rad_list, theta_dot_rad_list)

    # derivatives of state at each point
    y_rad_dot = func(y_rad, time_list)

    # color
    color_mesh = np.sqrt(y_rad_dot[0] * y_rad_dot[0] + y_rad_dot[1] * y_rad_dot[1])

    py.figure(figsize=(18, 18))
    py.axis('equal')
    py.quiver(py.rad2deg(y_rad[0]), py.rad2deg(y_rad[1]), py.rad2deg(y_rad_dot[0]), py.rad2deg(y_rad_dot[1]), color_mesh, angles='xy')
    l, r, b, t = py.axis()
    x_span, y2_mesh = r - l, t - b
    py.axis([l - 0.05 * x_span, r + 0.05 * x_span, b - 0.05 * y2_mesh, t + 0.05 * y2_mesh])
    py.grid()

```

```python
time_list = []

# list of theta
theta_deg_array = np.arange(-540, 540+1, 30)
theta_rad_list = np.deg2rad(theta_deg_array)

# list of theta_dot
theta_dot_deg_array = np.arange(-540, 540+1, 45)
theta_dot_rad_list = np.deg2rad(theta_dot_deg_array)

# plot the direction filed
ode_slopes_2states_cartesian(pendulum_NL, theta_rad_list, theta_dot_rad_list, time_list)

# Convert pendulum solution curves to NumPy arrays
x_euler_array, x_heun_array, x_rk4_array = (
    np.array(x_euler), np.array(x_heun), np.array(x_rk4))

# Plot the solution curves
py.plot(py.rad2deg(x_euler_array[:, 0]), py.rad2deg(x_euler_array[:, 1]), '-', label='Euler', alpha=0.7)
py.plot(py.rad2deg(x_heun_array[:, 0]), py.rad2deg(x_heun_array[:, 1]), '.-', label='Heun', alpha=0.7)
py.plot(py.rad2deg(x_rk4_array[:, 0]), py.rad2deg(x_rk4_array[:, 1]), '-', label='RK4')

ax = py.gca()

xlims = py.xlim(left=theta_deg_array[0], right=theta_deg_array[-1])
# http://matplotlib.1069221.n5.nabble.com/How-do-I-set-grid-spacing-td9968.html
ax.set_xticks(np.hstack([np.arange(0, xlims[1]+1, 90), np.arange(-90, xlims[0]-1, -90)]))

ylims = py.ylim(bottom=theta_dot_deg_array[0], top=theta_dot_deg_array[-1])
# http://matplotlib.1069221.n5.nabble.com/How-do-I-set-grid-spacing-td9968.html
ax.set_yticks(np.hstack([np.arange(0, ylims[1]+1, 90), np.arange(-90, ylims[0]-1, -90)]))

py.legend(loc=0)
py.xlabel('$\\theta(deg)$')
py.ylabel('$\\frac{d}{dt}\\theta(deg/sec)$')
py.title('Simple pendulum')

py.savefig('pendulum_direction_field.svg')

```

## 도전 과제
Try This

다음 2계 선형 상미분 방정식의 수치해를 RK4법으로 구하시오:

Find the numerical solutions of the following second order linear ordinary equation using RK4 Method:

$$
\begin{align}
\frac{d^2}{dt^2}x(t) + 2\frac{d}{dt}x(t) + x(t) &= 0 \\
x(0) &= 0 \\
\frac{d}{dt}x(0) &= 1
\end{align}
$$

다음 2계 선형 상미분 방정식의 수치해를 RK4법으로 구하시오:

Find the numerical solutions of the following second order linear ordinary equation using RK4 Method:

$$
\begin{align}
\frac{d^2}{dt^2}x(t) + \frac{d}{dt}x(t) + 4x(t) &= sin(t[rad]) \\
x(0) &= 0 \\
\frac{d}{dt}x(0) &= 0
\end{align}
$$

## Final Bell
마지막 종

```python
# stackoverfow.com/a/24634221
import os
os.system("printf '\a'");

```

```python

```