# The Efficient Frontier of Optimal Portfolio Transactions

### Introduction

[Almgren and Chriss](https://cims.nyu.edu/~almgren/papers/optliq.pdf) showed that for each value of risk aversion there is a unique optimal execution strategy. The optimal strategy is obtained by minimizing the **Utility Function** $U(x)$:

\begin{equation}
U(x) = E(x) + \lambda V(x)
\end{equation}

where $E(x)$ is the **Expected Shortfall**, $V(x)$ is the **Variance of the Shortfall**, and $\lambda$ corresponds to the trader’s risk aversion. The expected shortfall and variance of the optimal trading strategy are given by:

In this notebook, we will learn how to visualize and interpret these equations.

# The Expected Shortfall

As we saw in the previous notebook, even if we use the same trading list, we are not guaranteed to always get the same implementation shortfall due to the random fluctuations in the stock price. This is why we had to reframe the problem of finding the optimal strategy in terms of the average implementation shortfall and the variance of the implementation shortfall. We call the average implementation shortfall, the expected shortfall $E(x)$, and the variance of the implementation shortfall $V(x)$. So, whenever we talk about the expected shortfall we are really talking about the average implementation shortfall. Therefore, we can think of the expected shortfall as follows. Given a single trading list, the expected shortfall will be the value of the average implementation shortfall if we were to implement this trade list in the stock market many times.

To see this, in the code below we implement the same trade list on 50,000 trading simulations. We call each trading simulation an episode. Each episode will consist of different random fluctuations in stock price. For each episode we will compute the corresponding implemented shortfall. After all the 50,000 trading simulations have been carried out we calculate the average implementation shortfall and the variance of the implemented shortfalls. We can then compare these values with the values given by the equations for $E(x)$ and $V(x)$ from the Almgren and Chriss model.

```python
%matplotlib inline

import matplotlib.pyplot as plt

import utils

# Set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Set the liquidation time
l_time = 60

# Set the number of trades
n_trades = 60

# Set trader's risk aversion
t_risk = 1e-6

# Set the number of episodes to run the simulation
episodes = 10

utils.get_av_std(lq_time = l_time, nm_trades = n_trades, tr_risk = t_risk, trs = episodes)

# Get the AC Optimal strategy for the given parameters
ac_strategy = utils.get_optimal_vals(lq_time = l_time, nm_trades = n_trades, tr_risk = t_risk)
ac_strategy
```

# Extreme Trading Strategies

Because some investors may be willing to take more risk than others, when looking for the optimal strategy we have to consider a wide range of risk values, ranging from those traders that want to take zero risk to those who want to take as much risk as possible. Let's take a look at these two extreme cases. We will define the **Minimum Variance** strategy as that one followed by a trader that wants to take zero risk and the **Minimum Impact** strategy at that one followed by a trader that wants to take as much risk as possible. Let's take a look at the values of $E(x)$ and $V(x)$ for these extreme trading strategies. The `utils.get_min_param()` uses the above equations for $E(x)$ and $V(x)$, along with the parameters from the trading environment to calculate the expected shortfall and standard deviation (the square root of the variance) for these strategies. We'll start by looking at the Minimum Impact strategy.

```python
import utils

# Get the minimum impact and minimum variance strategies
minimum_impact, minimum_variance = utils.get_min_param()
```

### Minimum Impact Strategy

This trading strategy will be taken by trader that has no regard for risk. In the Almgren and Chriss model this will correspond to having the trader's risk aversion set to $\lambda = 0$. In this case the trader will sell the shares at a constant rate over a long period of time. By doing so, he will minimize market impact, but will be at risk of losing a lot of money due to the large variance. Hence, this strategy will yield the lowest possible expected shortfall and the highest possible variance, for a given set of parameters. We can see that for the given parameters, this strategy yields an expected shortfall of \$197,000 dollars but has a very big standard deviation of over 3 million dollars.

```python
minimum_impact
```

### Minimum Variance Strategy

This trading strategy will be taken by trader that wants to take zero risk, regardless of transaction costs. In the Almgren and Chriss model this will correspond to having a variance of $V(x) = 0$. In this case, the trader would prefer to sell the all his shares immediately, causing a known price impact, rather than risk trading in small increments at successively adverse prices. This strategy will yield the smallest possible variance, $V(x) = 0$, and the highest possible expected shortfall, for a given set of parameters. We can see that for the given parameters, this strategy yields an expected shortfall of over 2.5 million dollars but has a standard deviation equal of zero.

```python
minimum_variance
```

# The Efficient Frontier

The goal of Almgren and Chriss was to find the optimal strategies that lie between these two extremes. In their paper, they showed how to compute the trade list that minimizes the expected shortfall for a wide range of risk values. In their model, Almgren and Chriss used the parameter $\lambda$ to measure a trader's risk-aversion. The value of $\lambda$ tells us how much a trader is willing to penalize the variance of the shortfall, $V(X)$, relative to expected shortfall, $E(X)$. They showed that for each value of $\lambda$ there is a uniquely determined optimal execution strategy. We define the **Efficient Frontier** to be the set of all these optimal trading strategies. That is, the efficient frontier is the set that contains the optimal trading strategy for each value of $\lambda$.

The efficient frontier is often visualized by plotting $(x,y)$ pairs for a wide range of $\lambda$ values, where the $x$-coordinate is given by the equation of the expected shortfall, $E(X)$, and the $y$-coordinate is given by the equation of the variance of the shortfall, $V(X)$. Therefore, for a given a set of parameters, the curve defined by the efficient frontier represents the set of optimal trading strategies that give the lowest expected shortfall for a defined level of risk.

In the code below, we plot the efficient frontier for $\lambda$ values in the range $(10^{-7}, 10^{-4})$, using the default parameters in our trading environment. Each point of the frontier represents a distinct strategy for optimally liquidating the same number of stocks. A risk-averse trader, who wishes to sell quickly to reduce exposure to stock price volatility, despite the trading costs incurred in doing so, will likely choose a value of $\lambda = 10^{-4}$. On the other hand, a trader
who likes risk, who wishes to postpones selling, will likely choose a value of $\lambda = 10^{-7}$. In the code, you can choose a particular value of $\lambda$ to see the expected shortfall and level of variance corresponding to that particular value of trader's risk aversion.

```python
%matplotlib inline

import matplotlib.pyplot as plt

import utils

# Set the default figure size
plt.rcParams['figure.figsize'] = [17.0, 7.0]

# Plot the efficient frontier for the default values. The plot points out the expected shortfall and variance of the
# optimal strategy for the given the trader's risk aversion. Valid range for the trader's risk aversion (1e-7, 1e-4).
utils.plot_efficient_frontier(tr_risk = 1e-6)
```

```python

```


# Álgebra Lineal Numérica

El álgebra lineal es el área de las matemáticas que estudia los espacios vectoriales y las transformaciones lineales entre dichos espacios. Independientemente del espacio vectorial que trabajemos, mientras sea de dimensión finita, todos elementos del espacio se pueden representar como un **vector** (una $n$-ada de elementos del campo del espacio) y toda transofmación lineal se representa como una **matriz** (una colección de $n \times n$ elementos del campo del espacio)

## El problema $\mathbf{Ax}=\mathbf{b}$ y sus equivalencias

En matemáticas y física, muchas veces surgen problemas en los que tenemos que resolver un sistema de ecuaciones lineales de la siguiente forma:

$$
\begin{align}
a_{11} x_1 + a_{12} x_2 + \ldots + a_{1n} x_n &= b_1 \\
a_{21} x_1 + a_{22} x_2 + \ldots + a_{2n} x_n &= b_2 \\
\vdots \quad \quad \quad \vdots \quad \quad \quad \vdots \\
a_{n1} x_1 + a_{n2} x_2 + \ldots + a_{nn} x_n &= b_n \\
\end{align}
$$

Que se pueden representar, de forma matricial, como

$$
 \begin{pmatrix}
  a_{1,1} & a_{1,2} & \cdots & a_{1,n} \\
  a_{2,1} & a_{2,2} & \cdots & a_{2,n} \\
  \vdots  & \vdots  & \ddots & \vdots  \\
  a_{n,1} & a_{n,2} & \cdots & a_{n,n}
 \end{pmatrix}
 \begin{pmatrix}
 x_1 \\
 x_2 \\
 \vdots \\
 x_n
 \end{pmatrix}
 =
 \begin{pmatrix}
 b_1 \\
 b_2 \\
 \vdots \\
 b_n
 \end{pmatrix}
$$

Si definimos

$$
\mathbf{A} =
\begin{pmatrix}
  a_{1,1} & a_{1,2} & \cdots & a_{1,n} \\
  a_{2,1} & a_{2,2} & \cdots & a_{2,n} \\
  \vdots  & \vdots  & \ddots & \vdots  \\
  a_{n,1} & a_{n,2} & \cdots & a_{n,n}
 \end{pmatrix} \quad , \quad \mathbf{x} = \begin{pmatrix}
 x_1 \\
 x_2 \\
 \vdots \\
 x_n
 \end{pmatrix} \quad \text{y} \quad \mathbf{b}= \begin{pmatrix}
 b_1 \\
 b_2 \\
 \vdots \\
 b_n
 \end{pmatrix} $$

 Podemos escribir el sistema de manera compacta:

 $$\mathbf{Ax} = \mathbf{b}$$

### Equivalencias del problema

Muchos problemas de álgebra lineal se pueden plantear como problemas de resolver un sistema de ecuaciones lineales. Por ejemplo:

* Encontrar la inversa de una matriz, es decir, encontrar $\mathbf{B}$ tal que $\mathbf{A}\mathbf{B} = \mathbf{Id}$

* Encontrar intersecciones de rectas o planos en el espacio

* Encontrar matrices de cambio de base.

* Encontrar los coeficientes una transformación lineal aplicada en $n$ vectores linealmente independientes

* Resolver ecuaciones diferenciales (parciales y ordinarias) discretizadas o numéricas.

### Sistemas de ecuaciones lineales problemáticos

En principio, no necesariamente se da el caso donde la matriz $A$ es de $n\times n$, puede suceder que tengamos menos o más ecuaciones que incógnitas. Tratar con esos casos es **complicado**. Así, siempre vamos a realizar la suposición de que *la matriz es de $n\times n$*.

Aunque nuestra matriz $A$ sea de $n\times n$, puede darse el caso de que nuestro sistema de ecuaciones lineales **no acepte una solución única**. Nuevamente, por simplicidad, siempre asumiremos que nuestro sistema admite una solución **única**. Condiciones equivalentes para eso son:

* $\mathbf{A}$ es invertible

* $\text{det}(\mathbf{A}) \neq 0$

* $\text{rango}(\mathbf{A}) = n$

## ¿Cómo resolvemos un sistema lineal?

Existen varias maneras analíticas y exactas de resolver un sistema de ecuaciones lineales (Regla de Kraemer, Eliminación Gaussiana, Sustitución, etc). Sin embargo, la gran mayoría de ellas son **tediosas**, **mecánicas** y largas de realizar manualmente. En particular, cuando tenemos más de $4$ ecuaciones, se vuelve extremadamente tedioso resolverlas.

Debido a su tediosidad y a su importancia en todas las ramas, resulta ideal utilizar la computadora para resolver dichos sistemas.

## Preludio computacional: vectores y matrices en Julia

En Julia podemos trabajar de manera nativa con matrices y vectores. Es claro que un **arreglo de flotantes** puede interpretarse como un **vector** por ser un conjunto ordenado de elementos del mismo tipo.

Dentro de Julia, es posible operar a los arreglos entre sí de la misma manera que operamos a los vectores: con suma y producto por un escalar

```julia
vec1 = [1.0,1.0,0.0]
vec2 = [1.0,0.0,1.0]
print("suma: ")
println(vec1+vec2)
alpha = 5.0
print("producto por un escalar: ")
println(alpha*vec1)
```

    suma: [2.0, 1.0, 1.0]
    producto por un escalar: [5.0, 5.0, 0.0]

Para representar a las matrices, una de las ventajas de Julia es que podemos construir **arreglos $n$-dimensionales**. Por ahora, nos basta con construir arreglos $2$ dimensionales. La sintaxis para construir un arreglo 2 dimensional de manera explícita es la siguiente:

```julia
# ponemos los renglones como arreglos 1D separados por ESPACIOS, no por comas
# cada renglon se separa entre sí por un punto y coma
A = [[1.0 2.0 3.0] ; [4.0 5.0 6.0] ]
```

    2×3 Array{Float64,2}:
     1.0  2.0  3.0
     4.0  5.0  6.0

Podemos concatenar también vectores de manera horizontal (i.e. como si fueran columnas) para construir una matriz, utilizando la siguiente sintaxis

```julia
A2 = [vec1 vec2]
```

    3×2 Array{Float64,2}:
     1.0  1.0
     1.0  0.0
     0.0  1.0

Es posible transponer una matriz utilizando la función `transpose`

```julia
transpose(A2)
```

    2×3 LinearAlgebra.Transpose{Float64,Array{Float64,2}}:
     1.0  1.0  0.0
     1.0  0.0  1.0

Si ahora nos preguntamos cuál es el tipo de una matriz, podemos usar la función `typeof`

```julia
typeof(A)
```

    Array{Float64,2}

Notemos que ahora la función `typeof(A)` nos muestra un `2` después de `Float64`, lo que nos indica que nuestro objeto `A` es un arreglo de dimensión 2. Para los vectores normales, utilizamos arreglos de dimensión $1$. Las dimensiones del arreglo se pueden consultar con la función `size`

```julia
size(A2)
```

    (3, 2)

Podemos multiplicar una matrix por un vector si coinciden las dimensiones

```julia
# matriz de 3x3
A4 = [[1 2 3] ; [2 4 5] ; [7 8 9]]
# vector de 3 entradas
vec = [1,0,0]
# su multiplicación
println(A4*vec)
```

    [1, 2, 7]

```julia
# matriz de 4x3
A5 = [[0 1 4] ; [1 2 3] ; [2 4 5] ; [7 8 9]]
# vector de 3 entradas
vec = [1,0,0]
# su multiplicación
println(A5*vec)
```

    [0, 1, 2, 7]

Si no coinciden las columnas de la matriz con la longitud del vector, intentar la multiplicación nos arrojará un error:

```julia
# matriz de 3x2
A6 = [[1 2] ; [2 4] ; [7 8]]
# vector de 3 entradas
vec = [1,0,0]
# no está definida la multiplicación
println(A6*vec)
```

Si le pedimos a Julia que nos imprima una matriz, el formato es bastante feo

```julia
# impresión fea
println(A6)
```

    [1 2; 2 4; 7 8]

Podemos utilizar la función `display(A)` para imprimirlo como una matriz. La sintaxis es la siguiente:

```julia
# impresión bonita
display(A6)
```

    3×2 Array{Int64,2}:
     1  2
     2  4
     7  8

Para acceder a los elementos de una matriz, utilizamos la sintaxis `A[i,j]`

```julia
display(A)
println(A[1,2])
println(A[2,3])
```

    2×3 Array{Float64,2}:
     1.0  2.0  3.0
     4.0  5.0  6.0

    2.0
    6.0

Podemos acceder a renglones o columnas completos utilizando `:` en lugar de `i` o `j`, respectivamente

```julia
# primera renglón
println(A[1,:])
# tercer columna
println(A[:,3])
```

    [1.0, 2.0, 3.0]
    [3.0, 6.0]

Notemos que en julia podemos utilizar una rango o arreglo de enteros como índice de otro arreglo para obtener el subarreglo con los elementos de dichos índices. Por ejemplo:

```julia
arr1 = [2,4,8,16,32,128]
println(arr1[[2,5]])
println(arr1[1:3])
```

    [4, 32]
    [2, 4, 8]

Lo mismo sucede con las matrices: podemos obtener una submatriz de esa forma

```julia
mat1 = [[1 2 3 4] ; [5 6 7 8]]
display(mat1)
display(mat1[1:2,3:4])
display(mat1[1:2,2:4])
```

    2×4 Array{Int64,2}:
     1  2  3  4
     5  6  7  8

    2×2 Array{Int64,2}:
     3  4
     7  8

    2×3 Array{Int64,2}:
     2  3  4
     6  7  8

## Caso sencillo: $A$ es triangular superior

Regresando a querer resolver el problema $\mathbf{Ax} = \mathbf{b}$

Antes que atacar el problema de manera general, consideremos el caso sencillo en el que la matriz $\mathbf{A}$
 es una matriz triangular superior, es decir, de la forma.

 $$
 \mathbf{A} =
\begin{pmatrix}
  a_{1,1} & a_{1,2} & \cdots & a_{1,n} \\
  0 & a_{2,2} & \cdots & a_{2,n} \\
  \vdots  & \vdots  & \ddots & \vdots  \\
   0 & \ldots  & a_{n-1,n-1} & a_{n-1,n} \\
 0 & \ldots  & 0 & a_{n,n} \\
 \end{pmatrix}
 $$

 Matemáticamente, eso quiere decir que para $i>j$, $A_{ij} = 0$

Cuando la matriz es así, es muy sencillo resolver el sistema de ecuaciones.

$$
\begin{pmatrix}
  a_{1,1} & a_{1,2} & \cdots & a_{1,n} \\
  0 & a_{2,2} & \cdots & a_{2,n} \\
  \vdots  & \vdots  & \ddots & \vdots  \\
   0 & \ldots  & a_{n-1,n-1} & a_{n-1,n} \\
 0 & \ldots  & 0 & a_{n,n} \\
 \end{pmatrix}
 \begin{pmatrix}
 x_1 \\
 x_2 \\
 \vdots \\
 x_{n-1} \\
 x_n
 \end{pmatrix}
 =
 \begin{pmatrix}
 b_1 \\
 b_2 \\
 \vdots \\
 b_{n-1} \\
 b_n
 \end{pmatrix}
$$

### Ejercicio 1

(i) Suponiendo que $\mathbf{A}$ es triangular superior y que no tiene ceros en la diagonal, encuentra la expresión (analítica) para $x_n$ en términos de las entradas de $A$ y las entradas del vector solución $\mathbf{b}$.

(ii) Ya que tengas la expresión para $x_n$, ahora encuentra una expresión para $x_{n-1}$ (que también debe de depender del valor de $x_n$) y para $x_{n-2}$ (que depende también del valor de $x_n$ y $x_{n-1}$ )

(iii) Generaliza las expresiones anteriores para obtener una expresión general para cualquier $x_i$ en términos de las entradas de $A$, las entradas del vector solución $\mathbf{b}$ y de otros valores $x_k$ con $k>i$

### Ejercicio 2

Implementa una función `solTriSuperior(A,b)` que tome como argumento un arreglo 2D `A`, que representa a una matriz **triangular superior, sin ceros en la diagonal**, de $n\times n$ , un vector solución `b` de longitud $n$ y que regresa un arreglo `xs` con la solución del sistema $\mathbf{Ax} = \mathbf{b}$ calculada usando la fórmula del inciso (iii) del ejercicio anterior.

## Caso general: volver a $\mathbf{A}$ una matriz triangular superior

Generalmente, $\mathbf{A}$ no va a ser una matriz triangular superior. Sin embargo, mediante el procedimiento de [**eliminación gaussiana**](https://en.wikipedia.org/wiki/Gaussian_elimination) es posible llevarla a una de ellas.

### Eliminación Gaussiana

La eliminación Gaussiana consiste en trabajar con la **matriz aumentada**, denotada $\mathbf{C}$, cuya expresión es

$$
\mathbf{C} =
\begin{pmatrix}
  a_{1,1} & a_{1,2} & \cdots & a_{1,n} & b_1 \\
  a_{2,1} & a_{2,2} & \cdots & a_{2,n} & b_2 \\
  \vdots  &   & \ddots &   & \vdots \\
  a_{n,1} & a_{n,2} & \cdots & a_{n,n} & b_n
 \end{pmatrix}
 $$

Mediante operaciones elementales de renglones (intercambiar renglones, multiplicarlos por un escalar o sumarlos), buscamos reducir la matriz $\mathbf{C}$ a una forma **escalonada**, denotada $\mathbf{C}^*$, en la que todos los elementos por debajo de la diagonal sean $0$:

$$
\mathbf{C}^* =
\begin{pmatrix}
  a^*_{1,1} & a^*_{1,2} & \cdots & a^*_{1,n} & b^*_1 \\
  0 & a^*_{2,2} & \cdots & a^*_{2,n} & b^*_2 \\
  \vdots  &   & \ddots &   & \vdots \\
  0 & 0 & \cdots & a^*_{n,n} & b^*_n
 \end{pmatrix}
 $$

 Esa matriz aumenta representa al sistema de ecuaciones:

 $$
\begin{pmatrix}
  a^*_{1,1} & a^*_{1,2} & \cdots & a^*_{1,n} \\
  0 & a^*_{2,2} & \cdots & a^*_{2,n} \\
  \vdots  &   & \ddots &   \vdots \\
  0 & 0 & \cdots & a^*_{n,n}
 \end{pmatrix}
 \begin{pmatrix}
x_1 \\
x_2 \\
\vdots \\
x_n
 \end{pmatrix}=
 \begin{pmatrix}
b^*_1 \\
b^*_2 \\
\vdots \\
b^*_n
 \end{pmatrix}
 $$

El sistema de ecuaciones representado por $\mathbf{C}^*$ es equivalente al representado por $\mathbf{C}$, por lo que sus soluciones son las mismas. Sin embargo, el sistema asociado a la matriz escalonada es triangular superior, por lo que es mucho más fácil de resolver, como vimos en los ejercicios anteriores.

### Ejercicio 3:

(i) Supón que $a_{1,1} \neq 0$. ¿Qué operación elemental podrías aplicarle al renglón $n$ para hacer que el elemento $a_{n,1}$ se vuelva 0?

**Sugerencia** La operación es convertir al renglón $n$ en una combinación lineal del renglón $1$ y el renglón $n$

(ii) ¿Puedes aplicar el procedimiento un procedimiento similar al del inciso anterior pero ahora para hacer $a_{n-1,1}=0$? ¿y para cualquier otro $a_{k,1}=0$ con $k \neq 1$?

(iii) Vamos ahora a la segunda columna. Supón que ya realizaste todas las operaciones elementales necesarias para que $a_{k,1} =0$ para $k \neq 1$. Nuevamente suponiendo que $a_{2,2}\neq 0$, ¿cómo puedes hacer algo parecido a las operaciones elementales de los incisos anteriores para volver $a_{k,2} =0$ para $k \neq 2$?

(iv) Generaliza todo lo visto anteriormente para encontrar las operaciones elementales necesarias (y el orden de ellas) para convertir a la matriz aumentada $\mathbf{C}$ en la matriz escalonada $\mathbf{C}^*$.

### Ejercicio 4

Implementa una función `elimGaussBasica(A,b)` que toma como argumento un arreglo 2D `A` de $n\times n$ **suponiendo que no tiene ceros en la diagonal** y un arreglo 1D `b` de longitud $n$. La función debe de construir la matriz aumentada $\mathbf{C}$ obtenida a partir de `A` y `b` y luego realizar las operaciones elementales obtenidas en el ejercicio anterior para generar la matriz escalonada $\mathbf{C}^*$. La función debe de regresar la matriz escalonada obtenida de $n\times (n+1)$.

**Sugerencia:** la función `hcat` te permite concatenar vectores y matrices horizontalmente de la siguiente forma:

```julia
A = [[1 2 3] ; [4 5 6]]
B = [8,9]
display(A)
display(B)
display(hcat(A,B))
```

    2×3 Array{Int64,2}:
     1  2  3
     4  5  6

    2-element Array{Int64,1}:
     8
     9

    2×4 Array{Int64,2}:
     1  2  3  8
     4  5  6  9

## ¿Qué sucede si hay elementos $a_{ii}=0$?

Es fundamental que los elementos de la diagonal no sean $0$ para que nuestro método funcione. Sin embargo, es posible que se de el caso en el que sean $0$. Si $a_{k,k}=0$, podemos buscar un renglón $l$ en el que $a_{l,k}\neq 0 $ y luego **intercambiar** el renglón $k$ por el $l$.

Debido a que estamos suponiendo que la solución al sistema es única, no puede nunca darse el caso de $a_{l,k}=0$ para toda $l$ pues eso implicaría que no existe solución única para el sistema

### Ejercicio 5

Implementa una función `checarDiagonal(A,b)` que toma como argumento un arreglo 2D `A` de $n\times n$ **suponiendo que no tiene ceros en la diagonal** y un arreglo 1D `b` de longitud $n$. La función debe de construir la matriz aumentada $\mathbf{C}$ obtenida a partir de `A` y `b` y luego revisar sus elementos diagonales para ver que no sean $0$. Si encuentra alguno que sea $0$, debe de intercambiar el renglón por otro en el que sea distinto de cero. La función debe de regresar una matriz aumentada  en la que no haya ceros en las diagonales.

### Ejercicio 6

Implementa una función `eliminaciónGaussiana(A,b)` que toma como argumento un arreglo 2D `A` de $n\times n$ **suponiendo que no tiene ceros en la diagonal** y un arreglo 1D `b` de longitud $n$. La función primero debe de usar `checarDiagonal(A,b)` para obtener una matriz aumentada sin ceros en las diagonales y luego debe de aplicar las operaciones elementales del ejercicio 3 para obtener una matriz escalonada $\mathbf{C}^*$. La función debe de regresar dicha matriz escalonada de $n\times (n+1)$.

### Ejercicio 7

Implementa una función `ecLineales(A,b)` que toma como argumento un arreglo 2D `A` de $n\times n$  y un arreglo 1D `b` de longitud $n$. Tu función debe de utilizar las funciones `eliminaciónGaussiana(A,b)` y `solTriSuperior(A,b)` para resolver el sistema de ecuaciones lineales y regresar un arreglo 1D de longitud $n$ correspondiente a las soluciones $x_i$.

### Ejercicio 8

Prueba tu función `ecLineales(A,b)` con el siguiente sistema

$$
\begin{align}
    x+y+z &= 1 \\
    3x-2y+w &= -4 \\
    y - w &= 2 \\
    x-2y+4z-5w &= -6
\end{align}
$$

Cuya solución está dada por $x= -0.0555$, $y=1.8333$, $z=-0.7777$ y $-w=0.1666$


## Alternating Direction Method of Multipliers (ADMM)

ADMM is applied to problems whose structure can be represented as a bipartite graph.

A bipartite graph is a graph whose vertices (nodes) are divided into two disjoint sets. Edges only connect nodes from the opposite sets.

#### Notation

The bipartite graph representing the problem structure is referred to as computation graph. Each node in the graph is associated with a computing agent.

In the above graph
1. Circles - nodes (agents) of type I
2. Rectangles - nodes (agents) of type II

ADMM solves the problem

$
    \begin{aligned}
    \underset{x \in \Omega, z \in \Theta}{\text{minimize}}\quad & \sum\limits_{i} f_i(x_i) + \sum\limits_{i} g_i(z_i)\\
    \text{subject to}\quad  & x_i = z_j
    \end{aligned}
$

where\
$x_i$ and $f_i(x_i)$ are variables and objective of *i*-th agent I\
$z_i$ and $g_i(z_i)$ are variables and objective of *i*-th agent II

Shared variables represented by edges must meet the consensus constraint $x_i = z_j$

The augmented Lagrangian function associated with the above problem

$L(x,z,\lambda) = \sum\limits_{i} f_i(x_i) + \sum\limits_{i} g_i(z_i) + \frac{\rho}{2} \sum\limits_{i} \|x_i-z_i+u_i\|_2^2$

The Lagrangian function is solved applying the following steps

__Step 1__ Type I agents solve their local subproblems

$x_{i}^{(k+1)}  = \text{argmin} \; f_i(x_i) + \frac{\rho}{2} \|x_i - (z_i^k - u_i^k)\|_2^2$

__Step 2__ Type II agents solve their local subproblems

$ z_{i}^{(k+1)} = \text{argmin} \; g_i(z_i) + \frac{\rho}{2} \|z_i - (x_i^{(k+1)} + u_i^k)\|_2^2 $

__Step 3__ Type II agents update dual variables

$u_{i}^{(k+1)}  = u_i^k + (x_i^{(k+1)} - z_i^{(k+1)})$

The above steps are repeated until convergence criteria are met. The convergence criteria are defined locally for primal and dual residuals.

$
\begin{equation}
\begin{aligned}
r^{\text{primal}} &<  \epsilon^{\text{primal}} \\
r^{\text{dual}} &<  \epsilon^{\text{dual}} \\
\end{aligned}
\end{equation}
$

where $\epsilon^{\text{primal}},\epsilon^{\text{dual}}$ are small positive numbers representing primal and dual tolerances, respectively. The primal and dual residuals are computed as

$
\begin{equation}
\begin{aligned}
r^{\text{primal}} &= \| x_i^k - z_i^k \|_2 \\
r^{\text{dual}} &= \| \rho (z_i^k - z_i^{k-1}) \| _2 \\
\end{aligned}
\end{equation}
$

## ADMM4J: Alternating Direction Method of Multipliers for Java

ADMM4J is an implementation of ADMM algorithm in Java. It is devised to be able to run in parallel and distributed settings and being applicable to a wide range of problems.

Note: Currently, only parallel implementation is available. Distributed implementation could be available in future.

To use ADMM4J the following steps need to be performed:

1. Devise a computation graph representing the problem as a bipartite graph
2. Implement nodes as Java classes extending org.admm4j.core.Node
3. Create the JSON input defining the graph
4. Execute admm4j (in Java environment or through command line)

At the heart there is a notion of node.

The org.admm4j.core.Node.java is an abstract class representing a node in a bipartite graph.\
This class implements ADMM specific functionality through its local variables and methods.

An application specific node can be implemented by extending org.admm4j.core.Node.java.\
A minimal implementation requires overriding the _solve_ method specifing how a local subproblem is solved (Step 1 or 2 depending on whether the node is of type I or II).\
A specific input to the node can provided overriding _build_ method. This method is called before the start of optimization.\
Overriding the methods _evalObjective_ and _evalConstraintViolation_ allows to output the values of objective function and constraint violation in the results. Though they are optional.\
Additionaly, any specific information about this node can be returned overriding the _getJsonInternal_ method.

## Interface

ADMM4J accepts input and returns output in the form of JSON objects.

JSON format is used because:
1. It has a well defined structured that is easy to understand.
2. It provides a convenient way to address a broad range of problems with the same structure.
3. It is human readable.
4. It can be created manually as well as programmatically almost in any programming languages.
5. It is convenient for running in a distributed setting.

### Input

The following JSON schema describes input

The nodesI and nodesII list JSON models of nodes in the computation graph.

In the input, JSON schema of node is

The fields are self-explonatory:
- name - the name of the node. All nodes in the graph should have different names.
- class - the full name of the Java class implementing the node.
- neighbors - list of nodes connected through edges with this node
- input - JSON object with additional input for this node, it can be null if the node has no input

### Output

The following JSON schema describes output

The nodesI and nodesII list outcomes of optimization for nodes in the computation graph.

In the output, JSON schema of node is

The main information about results of optimization is given in _variables_ and _multipliers_ JSON objects.\
These contain corresponding values for each neighbor of the node.\
Examples of output files provide more insights.

# Example

Suppose the problem at hand is represented by the following graph

The vectors $x$ and $z$ represent variables of the corresponding nodes.\
Edges represent shared variables and these should satisfy $x=z$.\
A node can have different number of shared variables with its neighbors.

The JSON model and input for admm4j would have the following form

#### Specifying neighbors

Each element in the array of neighbors follows the format __"name:nvar:rho"__
- name - name of the neighboring node (neighbor is the node with which there is an edge)
- nvar - number of shared variables with this neighbor
- rho - scaling parameter in ADMM algorithm

In principle, the value of scaling parameter ($\rho$) can be different in the different parts of the computation graph. Though, this example uses rho=1 for all nodes.

Consider the node with the name "B".\
Its neighbors are specified by the array ["D:20:1", "E:10:1"]\
This means the node B has the nodes D and E as its neighbors, sharing 20 and 10 variables with each of them correspondingly. The scaling parameter rho is equal to 1 for all nodes.

See examples of input and output files for more details.

For modeling complex energy networks:

R. Denysiuk, F. Lilliu, M. Vinyals and D. Reforgiato Recupero (2020). Multiagent system for community energy management.\
*In Proceedings of the 12th International Conference on Agents and Artificial Intelligence - Volume 1: ICAART*, pages 28-39.

```python

```


# SMC samplers
SMC samplers are SMC algorithms that sample from a sequence of target distributions. In this tutorial, these target distributions will be Bayesian posterior distributions of static models. SMC samplers are covered in Chapter 17 of the book.

## Defining a static model

A static model is a Python object that represents a Bayesian model with static parameter $\theta$. One may define a static model by subclassing base class `StaticModel`, and defining method `logpyt`, which evaluates the log-likelihood of datapoint $Y_t$ (given $\theta$ and past datapoints $Y_{0:t-1}$). Here is a simple example:

```python
%matplotlib inline
import warnings; warnings.simplefilter('ignore')  # hide warnings

from matplotlib import pyplot as plt
import seaborn as sb
from scipy import stats

import particles
from particles import smc_samplers as ssp
from particles import distributions as dists

class ToyModel(ssp.StaticModel):
    def logpyt(self, theta, t):  # density of Y_t given theta and Y_{0:t-1}
        return stats.norm.logpdf(self.data[t], loc=theta['mu'],
                                 scale = theta['sigma'])
```

In words, we are considering a model where the observations are $Y_t\sim N(\mu, \sigma^2)$. The parameter is $\theta=(\mu, \sigma)$.

Class `ToyModel` contains information about the likelihood of the considered model, but not about its prior, or the considered data. First, let's define those:

```python
T = 1000
my_data = stats.norm.rvs(loc=3.14, size=T)  # simulated data
my_prior = dists.StructDist({'mu': dists.Normal(scale=10.),
                             'sigma': dists.Gamma()})
```

For more details about to define prior distributions, see the documentation of module `distributions`, or the previous [tutorial on Bayesian estimation of state-space models](Bayes_estimation_ssm.ipynb). Now that we have everything, let's specify our static model:

```python
my_static_model = ToyModel(data=my_data, prior=my_prior)
```

This time, object `my_static_model` has enough information to define the posterior distribution(s) of the model (given all data, or part of the data). In fact, it inherits from `StaticModel` method `logpost`, which evaluates (for a collection of $\theta$ values) the posterior log-density at any time $t$ (meaning given data $y_{0:t}$).

```python
thetas = my_prior.rvs(size=5)
my_static_model.logpost(thetas, t=2)  # if t is omitted, gives the full posterior
```

    array([ -103.81939366,    -8.74342665, -1574.47064329,   -33.24706434,
             -89.17306505])

The input of `logpost` (and output of `myprior.rvs()`) is a [structured array](https://docs.scipy.org/doc/numpy/user/basics.rec.html), with the same keys as the prior distribution:

```python
thetas['mu'][0]
```

    4.5636339239154635

Typically, you won't need to call `logpost` yourself, this will be done by the SMC sampler for you.

## IBIS
The IBIS (iterated batch importance sampling) algorithm is a SMC sampler that samples iteratively from a sequence of posterior distributions,
$p(\theta|y_{0:t})$, for $t=0,1,\ldots$.

Module `smc_samplers` defines `IBIS` as a subclass of `FeynmanKac`.

```python
my_ibis = ssp.IBIS(my_static_model)
my_alg = particles.SMC(fk=my_ibis, N=1000, store_history=True)
my_alg.run()
```

Since we set `store_history` to `True`, the particles and their weights have been saved at every time (in attribute `hist`, see previous tutorials on smoothing). Let's plot the posterior distributions of $\mu$ and $\sigma$ at various times.

```python
plt.style.use('ggplot')
for i, p in enumerate(['mu', 'sigma']):
    plt.subplot(1, 2, i + 1)
    for t in [100, 300, 900]:
        plt.hist(my_alg.hist.X[t].theta[p], weights=my_alg.hist.wgt[t].W, label="t=%i" % t, alpha=0.5, density=True)
    plt.xlabel(p)
plt.legend();
```

As expected, the posterior distribution concentrates progressively around the true values.

As before, once the algorithm is run, `my_smc.X` contains the N final particles. However, object `my_smc.X` is no longer a simple (N,) or (N,d) numpy array. It is a `ThetaParticles` object, with attributes:

* theta: a structured array: as mentioned above, this is an array with fields; i.e. `my_smc.X.theta['mu']` is a (N,) array that contains the the $\mu-$component of the $N$ particles;
* `lpost`: a (N,) numpy array that contains the target (posterior) log-density of each of the N particles;
* `acc_rates`: a list of the acceptance rates of the resample-move steps.

```python
print(["%2.2f%%" % (100 * np.mean(r)) for r in my_alg.X.acc_rates])
plt.hist(my_alg.X.lpost, 30);
```

You do not need to know much more about class `ThetaParticles` for most practical purposes (see however the documention of module `smc_samplers` if you do want to know more, e.g. in order to implement other classes of SMC samplers).

## Regarding the Metropolis steps

As the text output of `my_alg.run()` suggests, the algorithm "resample-moves" whenever the ESS is below a certain threshold ($N/2$ by default). When this occurs, particles are resampled, and then moved through a certain number of Metropolis-Hastings steps. By default, the proposal is a Gaussian random walk, and both the number of steps and the covariance matrix of the random walk are chosen automatically as follows:

* the covariance matrix of the random walk is set to `scale` times   the empirical (weighted) covariance matrix of the particles. The default value for `scale` is $2.38 / \sqrt{d}$, where $d$ is the dimension of $\theta$.
* the algorithm performs Metropolis steps until the relative increase of the  average distance between the starting point and the end point is below a certain threshold $\delta$.

Class `IBIS` takes as an optional argument `mh_options`, a dictionary which may contain the following (key, values) pairs:

* `'type_prop'`: either `'random walk'` or `'independent`'; in the latter case, an independent Gaussian proposal is used. The mean of the Gaussian is set to the weighted mean of the particles. The variance is set to `scale` times the weighted variance of the particles.
* `'scale`': the scale of the proposal (as explained above).
* `'nsteps'`: number of steps. If set to `0`, the adaptive strategy described above is used.

Let's illustrate all this by calling IBIS again:

```python
alt_ibis = ssp.IBIS(my_static_model, mh_options={'type_prop': 'independent',
                                                 'nsteps': 10})
alt_alg = particles.SMC(fk=alt_ibis, N=1000, ESSrmin=0.2)
alt_alg.run()
```

Well, apparently the algorithm did what we asked. We have also changed the threshold of
 Let's see how the ESS evolved:

```python
plt.plot(alt_alg.summaries.ESSs)
plt.xlabel('t')
plt.ylabel('ESS')
```

As expected, the algorithm waits until the ESS is below 200 to trigger a resample-move step.

## SMC tempering

SMC tempering is a SMC sampler that samples iteratively from the following sequence of distributions:

\begin{equation}
\pi_t(\theta) \propto \pi(\theta) L(\theta)^\gamma_t
\end{equation}

with $0=\gamma_0 < \ldots < \gamma_T = 1$. In words, this sequence is a **geometric bridge**, which interpolates between the prior and the posterior.

SMC tempering implemented in the same was as IBIS: as a sub-class of `FeynmanKac`, whose `__init__` function takes as argument a `StaticModel` object.

```python
fk_tempering = ssp.AdaptiveTempering(my_static_model)
my_temp_alg = particles.SMC(fk=fk_tempering, N=1000, ESSrmin=1., verbose=True)
my_temp_alg.run()
```

    t=0, ESS=500.00, tempering exponent=2.94e-05
    t=1, Metropolis acc. rate (over 6 steps): 0.275, ESS=500.00, tempering exponent=0.000325
    t=2, Metropolis acc. rate (over 6 steps): 0.261, ESS=500.00, tempering exponent=0.0018
    t=3, Metropolis acc. rate (over 6 steps): 0.253, ESS=500.00, tempering exponent=0.0061
    t=4, Metropolis acc. rate (over 6 steps): 0.287, ESS=500.00, tempering exponent=0.0193
    t=5, Metropolis acc. rate (over 6 steps): 0.338, ESS=500.00, tempering exponent=0.0636
    t=6, Metropolis acc. rate (over 6 steps): 0.347, ESS=500.00, tempering exponent=0.218
    t=7, Metropolis acc. rate (over 5 steps): 0.358, ESS=500.00, tempering exponent=0.765
    t=8, Metropolis acc. rate (over 5 steps): 0.366, ESS=941.43, tempering exponent=1

**Note**: Recall that `SMC` resamples every time the ESS drops below value N times option `ESSrmin`; here we set it to to 1, since we want to resample at every time. This makes sense: Adaptive SMC chooses adaptively the successive values of $\gamma_t$ so that the ESS drops to $N/2$ (by default).

**Note**: we use option `verbose=True` in `SMC` in order to print some information on the intermediate distributions.

We have not saved the intermediate results this time (option `store_history` was not set) since they are not particularly interesting. Let's look at the final results:

```python
for i, p in enumerate(['mu', 'sigma']):
    plt.subplot(1, 2, i + 1)
    sb.distplot(my_temp_alg.X.theta[p])
    plt.xlabel(p)
```

This looks reasonable!
You can see from the output that the algorithm automatically chooses the tempering exponents $\gamma_1, \gamma_2,\ldots$. In fact, at iteration $t$, the next value for $\gamma$ is set that the ESS drops at most to $N/2$. You can change this particular threshold by passing argument ESSrmin to TemperingSMC. (Warning: do not mistake this with the `ESSrmin` argument of class `SMC`):

```python
lazy_tempering = ssp.AdaptiveTempering(my_static_model, ESSrmin = 0.1)
lazy_alg = particles.SMC(fk=lazy_tempering, N=1000, verbose=True)
lazy_alg.run()
```

    t=0, ESS=100.00, tempering exponent=0.00097
    t=1, Metropolis acc. rate (over 5 steps): 0.233, ESS=100.00, tempering exponent=0.0217
    t=2, Metropolis acc. rate (over 6 steps): 0.323, ESS=100.00, tempering exponent=0.315
    t=3, Metropolis acc. rate (over 5 steps): 0.338, ESS=520.51, tempering exponent=1

The algorithm progresses faster this time, but the ESS drops more between each step.
Another optional argument for Class `TemperingSMC` is `options_mh`, which works exactly as for `IBIS`, see above. That is, by default, the particles are moved according to a certain (adaptative) number of random walk steps, with a variance calibrated to the particle variance.


# Using Sparse Solvers in Python
## ChEN 6355 - Computational Fluid Dynamics
### Tony Saad
 Assistant Professor of Chemical Engineering
 University of Utah

We will solve the Poisson equation on the unit square with Dirichlet boundary conditions
\begin{equation}
\nabla^2 u = -S;\quad u(0) = 0,\quad u(1) = 0
\end{equation}
We will use a random number generator for the RHS.

```python
import numpy as np
import scipy.linalg
import scipy.sparse
import scipy.sparse.linalg
import matplotlib.pyplot as plt
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
import pyamg # make sure you install pyamg
```

```python
# 1d example
l = 1.0
ntotal = 34
dx = dy = 1/(ntotal -1)

n = 32 # interior points excludes BC points where the BC is specified
S = -np.random.rand(n)

Ae = 1/dx/dx*np.ones(n)
Aw = 1/dx/dx*np.ones(n)
Ap = -(Ae + Aw) # create Ap

# set BCs on east and west coefs
Ae[-1] = 0.0 # right wall
Aw[0] = 0.0 # left wall

# create the diagonals
d0 = Ap
de = Ae[:-1]
dw = Aw[1:]

A = scipy.sparse.diags([d0, de, dw], [0, 1, -1], format='csr')
print(A.toarray())
```

    [[-2178.  1089.     0. ...     0.     0.     0.]
     [ 1089. -2178.  1089. ...     0.     0.     0.]
     [    0.  1089. -2178. ...     0.     0.     0.]
     ...
     [    0.     0.     0. ... -2178.  1089.     0.]
     [    0.     0.     0. ...  1089. -2178.  1089.]
     [    0.     0.     0. ...     0.  1089. -2178.]]

```python
# direct solver
Ainv = scipy.sparse.linalg.inv(A.tocsc())
u = Ainv@S
plt.plot(u)
# to add the values at the boundary points you can augment u with two extra values
```

    [<matplotlib.lines.Line2D at 0xd1f80b588>]

```python
# scipy sparse solver
u = scipy.sparse.linalg.spsolve(A,S)
plt.plot(u)
```

    [<matplotlib.lines.Line2D at 0xd1f8e29e8>]

```python
# cg solver
u,err = scipy.sparse.linalg.cg(A,S)
plt.plot(u)
```

    [<matplotlib.lines.Line2D at 0xd1f9c7128>]

```python
# amg solver
ml = pyamg.ruge_stuben_solver(A)
u = ml.solve(S, tol=1e-9,cycle='V')
plt.plot(u)
```

    [<matplotlib.lines.Line2D at 0xd1fa9e860>]

```python
import urllib
import requests
from IPython.core.display import HTML
def css_styling():
    styles = requests.get("https://raw.githubusercontent.com/saadtony/NumericalMethods/master/styles/custom.css")
    return HTML(styles.text)
css_styling()

```

CSS style adapted from https://github.com/barbagroup/CFDPython. Copyright (c) Barba group
<link href='http://fonts.googleapis.com/css?family=Merriweather' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Bitter' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Oxygen' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Lora' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Fenix' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' type='text/css'>

```python

```


# Introduction

On Twitter today Jonathan McDowell made the following comment (https://twitter.com/planet4589/status/879927853332213760):

>Russian cubesat 'Mayak' to launch Jul 14 will deploy a reflector that may be as bright as mag minus 10. Astronomers likely to be annoyed.

This resulted in comprehensive nerd sniping (https://xkcd.com/356/) of both myself (https://twitter.com/vacant3rdman/status/879937146508812289) and Matthew Kenworthy (https://twitter.com/mattkenworthy/status/879954847809888256), and possibly others. As a result I was compelled to attempt to work out the maximum apparent brightness of mirrored surfaces in Earth orbit. This notebook is the result.

# Brightness of an orbital mirror

The apparent brightness of a specular reflection of sunlight off a flat surface is simply given by the apparent brightness of the Sun scaled by the ratio of the solid angles subtended by the reflector and the Sun from the observer's position and by the specular reflectance of the surface (which will, in general, be a function of wavlength and angle of incidence). Expressed in astronomical magnitudes (yuck), this becomes

\begin{equation}
m_\mathrm{sat} = m_{\odot} - 2.5 \times \log_{10}\left(\frac{\Omega_\mathrm{sat}}{\Omega_\odot}.R_\mathrm{sat}\right),
\end{equation}

where $m_\mathrm{sat}$ and $m_\odot$ are the apparent magnitudes of the satellite and Sun respectively, $\Omega_\mathrm{sat}$ and $\Omega_\odot$ are their solid angles and $R_\mathrm{sat}$ is the reflectance of the flat surface on the satellite.

For a flat surface of area $A$ at an angle $\theta$ to the line of sight and seen from a distance $d$ the solid angle (using small angle approximations) is given by

\begin{equation}
\Omega_\mathrm{sat} = \frac{A \cos{\theta}}{d^2}.
\end{equation}

## Conversative upper limit for the Mayak CubeSat

We can put an upper limit on the apparent brightness of the satellite by using the minimum distance between observer and satellite (the altitude of the satellite's orbit, $a$) and normal incidence ($\theta=0$) so that we obtain

\begin{equation}
m_\textrm{sat} > m_{\odot} - 2.5 \times \log_{10}\left(\frac{A.R_\textrm{sat}}{a^2 \Omega_\odot}\right).
\end{equation}

Substituting in values for Mayak (http://space.skyrocket.de/doc_sdat/mayak.htm) of $A = 4\textrm{m}^2$, $a = 500\textrm{km}$ and $R_\textrm{sat} = 0.95$ we obtain the following limit

```python
%matplotlib inline
```

```python
import numpy as np
import astropy.units as u
import astropy.constants as c
import matplotlib.pyplot as plt
from scipy.optimize import minimize
```

```python
omega_sun = (np.pi * (c.R_sun / c.au)**2).to(u.sr, equivalencies=u.equivalencies.dimensionless_angles())
omega_sun
```

$6.7905243 \times 10^{-5} \; \mathrm{sr}$

```python
m_sun = -26.74 * u.dimensionless_unscaled
m_sun
```

$-26.74 \; \mathrm{}$

```python
area = 4 * u.m**2
altitude = 500 * u.km
reflectance = 0.95 * u.dimensionless_unscaled
```

```python
omega_sat_max = (area / altitude**2).to(u.sr, equivalencies=u.equivalencies.dimensionless_angles())
omega_sat_max
```

$1.6 \times 10^{-11} \; \mathrm{sr}$

```python
m_sat_min = m_sun - 2.5 * np.log10(omega_sat_max * reflectance / omega_sun)
m_sat_min
```

$-10.114851 \; \mathrm{}$

**So we find that the apparent brightness of Mayak should be no brighter than magnitude -10.1, which is consistent with the -10 figure quoted on Gunter's Space page.**  It is also in agreement with the result that Matthew Kenworthy arrived at independently, https://twitter.com/mattkenworthy/status/879954847809888256

This is a conservative upper limit, however. For the satellite to be at a distance equal to the altitude of its orbit it must be directly overhead of the observer, and $\theta=0$ implies that the reflector surface is directly facing the observer. For a specular reflection of sunlight to reach the observer the angle of incidence must equal the angle of reflection, i.e. in this case the Sun must be in line with the satellite and observer, in other words on the opposite side of the Earth. The transmission of the entire planet at visible wavelengths is *extremely* low so in practice very little light would reach the satellite in this geometry.

## Slightly more rigourous limit

In reality the maximum brighness will occur when the observer, satellite and Sun lie in the same plane with the Sun some distance below the horizon from the observer's point of view but above the horizon from the satellite's point of view. The geometry is illustrated in this lovingly hand crafted diagram:

This introduces two new variables, the angular distance of the Sun below the horizon at the observer's position, $\phi$, and the zenith angle of the satellite from the observer's position. We must solve for the line of sight distance between the observer and satellite, $d$, and the angle of incidence and reflection at the mirror surface, $\theta$ in terms of these two variables. It was at this point that I realised that I had forgotten almost everything that I ever knew about elementary geometry and trigonometry. Fortunately Wikipedia came to the rescue.

### Angle of incidence/reflection

From the sum of the interior angles of a triangle,
\begin{equation}\theta = \frac{\pi/2 - z - \phi}{2}.
\end{equation}

### Line of sight distance

This an instance of the two side and non-included angle triangle problem (https://en.wikipedia.org/wiki/Solution_of_triangles#Two_sides_and_non-included_angle_given_.28SSA.29). The solution for the length of the third side gives

\begin{equation}
d = R_\oplus \cos(\pi - z) \pm  \sqrt{(R_\oplus + a)^2 - R_\oplus \sin^2(\pi - z)}.
\end{equation}

After a bit of arguably useful rearrangement this becomes

\begin{equation}
d = a \left[ \sqrt{1 + \frac{2 R_\oplus}{a} + \frac{R_\oplus^2}{a^2}\cos^2 z} - \frac{R_\oplus}{a} \cos{z} \right],
\end{equation}

which reassuringly gives $d = a$ when zenith angle is zero. Now we create a function to calculate $d$ as a function of $a$ and $z$ and plot the results.

```python
def distance(altitude, zenith_angle):
    s = 1 + 2 * c.R_earth / altitude + (c.R_earth * np.cos(zenith_angle) / altitude)**2
    return (altitude * (s**0.5 - c.R_earth * np.cos(zenith_angle) / altitude)).to(u.km)
```

```python
angles = np.arange(-90, 91, 1) * u.degree
plt.plot(angles, distance(altitude, angles))
plt.xlim(0, 90)
plt.ylim(0, 2600)
plt.xlabel('Zenith angle / degree')
plt.ylabel('Distance / km')
plt.title('Line of sight distance as a function of zenith angle for an altitude of 500 km ')
plt.gcf().set_size_inches(12, 8)
```

### Eclipse condition

In order for sunlight to be reflected from the satellite to the observer it is necessary that the Earth is not in the way, i.e. the spacecraft is not in eclipse. Calculating this condition in terms of satellite altitude, zenith angle and observer's Sun elevation required doing some more geometry. The solution is

\begin{equation}
(R_\oplus + a) .\cos(\psi - \phi) > R_\oplus,
\end{equation}

where

\begin{equation}
\psi = \arcsin\left(\frac{d \sin z}{R_\oplus + a}\right).
\end{equation}

We will write a function to determine whether the satellite is eclipsed:

```python
def is_eclipsed(altitude, zenith_angle, sun_elevation):
    los_distance = distance(altitude, zenith_angle)
    psi = np.arcsin(los_distance * np.sin(zenith_angle) / (c.R_earth + altitude))
    return (c.R_earth + altitude) * np.cos(psi - sun_elevation) < c.R_earth
```

```python
plt.plot(angles, is_eclipsed(altitude, angles, +6* u.degree), label='+6$^\circ$')
plt.plot(angles, is_eclipsed(altitude, angles, 0 * u.degree), label='0$^\circ$')
plt.plot(angles, is_eclipsed(altitude, angles, -6 * u.degree), label='-6$^\circ$')
plt.plot(angles, is_eclipsed(altitude, angles, -12 * u.degree), label='-12$^\circ$')
plt.plot(angles, is_eclipsed(altitude, angles, -18 * u.degree), label='-18$^\circ$')
plt.plot(angles, is_eclipsed(altitude, angles, -24 * u.degree), label='-24$^\circ$')
plt.plot(angles, is_eclipsed(altitude, angles, -30 * u.degree), label='-30$^\circ$')
plt.xlim(-90, 90)
plt.ylim(1.1,-0.1)
plt.xlabel('Zenith angle / degree')
plt.yticks((0, 1), ('Not eclipsed', 'Eclipsed'))
plt.title('Eclipsed condition for a satellite at 500 km altitude')
plt.legend(title='Sun elevation')
plt.gcf().set_size_inches(12, 8)
```

The perhaps surprising 'eclipsed' results for positive Sun elevation angles are indicating the situation when the satellite is in daylight but the Sun is on the opposite side of the mirror surface to the observer.

### Maximum brightness as a function of zenith angle and Sun elevation

Now we can write a function to calculate the maximum apparent brightness as a function of zenith angle and Sun elevation.

```python
omega_sun = (np.pi * (c.R_sun / c.au)**2).to(u.sr, equivalencies=u.equivalencies.dimensionless_angles())
m_sun = -26.74 * u.dimensionless_unscaled

def m_sat(area, reflectance, altitude, zenith_angle, sun_elevation):
    angle_of_incidence = (np.pi * u.radian / 2 - zenith_angle + sun_elevation) / 2 # Elevation negative in twilight
    los_distance = distance(altitude, zenith_angle)
    omega_sat = (area * np.cos(angle_of_incidence) / \
                 los_distance**2).to(u.sr, equivalencies=u.equivalencies.dimensionless_angles())

    m_sat = np.where(is_eclipsed(altitude, zenith_angle, sun_elevation),
                     999 * u.dimensionless_unscaled,
                     m_sun - 2.5 * np.log10(omega_sat * reflectance / omega_sun)) * u.dimensionless_unscaled

    return m_sat
```

```python
plt.plot(angles, m_sat(area, reflectance, altitude, angles, 6* u.degree), label='+6$^\circ$')
plt.plot(angles, m_sat(area, reflectance, altitude, angles, 0 * u.degree), label='0$^\circ$')
plt.plot(angles, m_sat(area, reflectance, altitude, angles, -6 * u.degree), label='-6$^\circ$')
plt.plot(angles, m_sat(area, reflectance, altitude, angles, -12 * u.degree), label='-12$^\circ$')
plt.plot(angles, m_sat(area, reflectance, altitude, angles, -18 * u.degree), label='-18$^\circ$')
plt.plot(angles, m_sat(area, reflectance, altitude, angles, -24 * u.degree), label='-24$^\circ$')
plt.plot(angles, m_sat(area, reflectance, altitude, angles, -30 * u.degree), label='-30$^\circ$')
plt.xlim(-90, 90)
plt.ylim(-6,-10.5)
plt.xlabel('Zenith angle / degree')
plt.ylabel('Maximum brightness / magnitudes')
plt.title('Maximum apparent brightness for a 4m$^2$ 95% mirror at 500 km altitude')
plt.legend(title='Sun elevation')
plt.gcf().set_size_inches(12, 8)
plt.savefig('mayak.png')
```

### Final result

We can maximise brightness (minimise magnitude) over zenith angle and Sun elevation to find the highest possible apparent brightness.

```python
results = minimize(lambda x: m_sat(area, reflectance, altitude, x[0] * u.degree, x[1] * u.degree),
                   (0, -18),
                   method='Powell')
results
```

       direc: array([[ 1.,  0.],
           [ 0.,  1.]])
         fun: <Quantity -9.93558237180596>
     message: 'Optimization terminated successfully.'
        nfev: 56
         nit: 2
      status: 0
     success: True
           x: <Quantity [  8.84357143,-21.33344709]>

**So the maximum apparent brightness is, apparently, magnitude -9.9, which can occur when the Sun elevation is -21.3° and the satellite is at a zenith angle of 8.8° towards the antisolar direction**

```python
m_sat_peak = results['fun']
mag_diff = m_sat_peak - m_sat_min
mag_diff
```

$0.17926833 \; \mathrm{}$

```python
flux_ratio = 10**(-0.4 * mag_diff)
flux_ratio
```

$0.84779855 \; \mathrm{}$

```python
inverse_flux_ratio = 10**(0.4 * mag_diff)
inverse_flux_ratio
```

$1.1795255 \; \mathrm{}$

This value is only 0.2 magnitudes fainter than the conservative (and far simpler) upper limit above, which is equivalent to 85% of the flux. I appear to have wasted a *lot* of time for a 18% improvement in accuracy...

# Flash duration

The entry for Mayak on Gunter's Space Page notes the following

>The reflector will provide a -10 optical magnitude at the beginning of the flight to allow for easy tracking. Mayak will be put into a tumbling motion over all axes, with at least 1 revolution per second

This suggests that Mayak may initially be in a stable orientation, resulting in flares of comparable duration to those from Iridium satellite (i.e. a few seconds) along a narrow ground track, but will spend most if not all of its mission in a rapid tumble. This will result in bright but very brief flashes of light visible over many different ground tracks as beams of reflected light sweep rapidly over the ground below.

## Maximum duration

During a flash the observer is effectively looking at a small portion of the Sun reflected in the mirrored surface of the satellite. The direction of the observer's reflected view will move across the sky at twice the angular rotation rate of the mirror, therefore the maximum flash duration will be time it takes for the satellite to rotate by half the angular diameter of the Sun.

```python
omega_min = 2 * np.pi * u.radian / u.second
omega_min
```

$6.2831853 \; \mathrm{\frac{rad}{s}}$

```python
duration_max = (c.R_sun / c.au).to(u.radian, equivalencies=u.equivalencies.dimensionless_angles()) / omega_min
duration_max = duration_max.to(u.millisecond)
duration_max
```

$0.73994059 \; \mathrm{ms}$

**The maximum flash duration is therefore 0.74 milliseconds**.

## Mean flash duration

The maximum flash duration will occur when the observer's view of the Sun crosses the centre of the solar disc. In general the track across the disc will be off centre, resulting in a distribution of shorter flash durations. The (angular) length of a chord across the solar disc (angular radius $\rho_\odot$) is given by $\delta = 2 \rho_\odot \sqrt{1 - \lambda^2 / \rho_\odot^2}$, where $\lambda$ is the offset. The corresponding flash duration is given by

\begin{equation}
t = \frac{\delta}{2 \omega_\mathrm{sat}} = \frac{\rho_\odot}{\omega_\mathrm{sat}} \sqrt{1 - \frac{\lambda^2}{\rho_\odot^2}} = t_\mathrm{max} \sqrt{1 - \frac{\lambda^2}{\rho_\odot^2}}
\end{equation}

Both the angular offset and orientation of the track (which has no effect on duration) can be assumed to be randomly and uniformly distributed so that the normalised probability of a given offset, $P(\lambda)\,d\lambda = 2 \lambda/ \rho_\odot^2\,d\lambda$. The expected value for flash duration is then given by

\begin{equation}
\left<t\right> = \int_0^{\rho_\odot}{t(\lambda) P(\lambda)\,d\lambda} = t_\mathrm{max} \int_0^{\rho_\odot} \sqrt{1 - \frac{\lambda^2}{\rho_\odot^2}} . 2 \lambda/ \rho_\odot^2\,d\lambda = t_\mathrm{max} \left[ -\frac{2}{3} \left(1 - \frac{\lambda^2}{\rho_\odot^2} \right)^\frac{3}{2} \right]_0^{\rho_\odot} = \frac{2}{3} t_\mathrm{max}.
\end{equation}

```python
duration_mean = 2 / 3 * duration_max
duration_mean
```

$0.49329372 \; \mathrm{ms}$

**The mean flash duration from Mayak will be 0.5 millisecond or less, if the tumbling rate exceeds 1 revolution per second.**

The flashes from Mayak are so short that it is likely to significantly reduce the perceived brightness as seen by the human eye.

## Perceived brightness of short pulses

Bloch's Law states that below a certain threshold perceived brightness scales with total pulse energy rather than flux so that a pulse of half the length needs to be twice as bright in order to be perceived as the same brightness, however the situation is complicated by an apparent peak in sensitivity for pulse durations of ~50-100 milliseconds (Broca-Sulzer effect), see for example http://www.pnas.org/content/109/48/19828. If we assume that the Bloch's Law scaling holds for durations of from 40 milliseconds then the 0.74 millisecond maximum duration of the Mayak flashes could mean that they appear a *lot* fainter than magnitude -9.9.

```python
perception_factor_max = (duration_max / (40 * u.ms)).to(u.dimensionless_unscaled)
perception_factor_max
```

$0.018498515 \; \mathrm{}$

```python
1 / perception_factor_max
```

$54.058394 \; \mathrm{}$

```python
perceived_magnitude_offset_min = - 2.5 * np.log10(perception_factor_max)
perceived_magnitude_offset_min
```

$4.3321579 \; \mathrm{}$

```python
m_sat_peak + perceived_magnitude_offset_min
```

$-5.6034245 \; \mathrm{}$

**So Bloch's Law suggests that even the longest Mayak flashes would be perceived as if they were ~50 times fainter, or 4.3 magnitudes. This would make even the brightest possible Mayak flashes appear to be magnitude -5.6 or fainter, depending on duration.**

```python
perception_factor_mean = (duration_mean / (40 * u.ms)).to(u.dimensionless_unscaled)
perception_factor_mean
```

$0.012332343 \; \mathrm{}$

```python
1 / perception_factor_mean
```

$81.087592 \; \mathrm{}$

```python
perceived_magnitude_offset_mean = - 2.5 * np.log10(perception_factor_mean)
perceived_magnitude_offset_mean
```

$4.772386 \; \mathrm{}$

```python
m_sat_peak + perceived_magnitude_offset_mean
```

$-5.1631964 \; \mathrm{}$

**A mean duration Mayak flash would be perceived as if they were ~80 times fainter, or 4.8 magnitudes. This would make even the brightest possible Mayak flashes appear no brighter than magnitude -5.2.**


```
from __future__ import division
from sympy import *
from IPython.display import display, Math, Latex
from IPython.core.display import display_html
init_session(quiet=True, use_latex='mathjax')
init_printing()
```

    IPython console for SymPy 0.7.6 (Python 2.7.6-32-bit) (ground types: python)

```
x1,x2,tau,delta = symbols('x1,x2,tau,delta')
L11 = symbols('L11', cls=Function)(x1,x2,tau,delta)
L12 = symbols('L12', cls=Function)(x1,x2,tau,delta)
L21 = symbols('L21', cls=Function)(x1,x2,tau,delta)
L22 = symbols('L22', cls=Function)(x1,x2,tau,delta)
```

```
Lstar = Matrix([[L11,L12],[L21,L22]])
L1star = Lstar.det()
deriv1 = L1star.diff(t)
deriv2 = (Lstar.adjugate()*Lstar.diff(t)).trace()

Mstar = Matrix([[L11,L12],[Lstar.det().diff(x1),Lstar.det().diff(x2)]])

deriv1 = Mstar.det().diff(tau)
deriv2 = (Mstar.adjugate()*Mstar.diff(tau)).trace()
simplify(deriv1-deriv2)
```

```
Mstar = Matrix([[0.00112865, 8.76232e-006],[9.57021e-007, 7.42578e-009]])
dMstar_dTau = Matrix([[-0.000245724,-0.00118232],[3.20921e-006, -7.171e-008]])
adjM = Matrix([[7.42578e-009,-9.57021e-007],[-8.76232e-006, 0.00112865]])
(adjM*dMstar_dTau).trace()
print Mstar.adjugate()
print adjM
```

    Matrix([[7.42578000000000e-9, -8.76232000000000e-6], [-9.57021000000000e-7, 0.00112865000000000]])
    Matrix([[7.42578000000000e-9, -9.57021000000000e-7], [-8.76232000000000e-6, 0.00112865000000000]])

```
a,b,c,d = symbols('a,b,c,d')
M = Matrix([[a,b],[c,d]])
display(M)
M.adjugate()
```

```

```

doc/notebooks/Check critical point derivative matrices.ipynb


# MAST30032: Biological Modelling and Simulation

Author: Stephen Zhang

## Discrete-time models

Discrete-time models in general concern _difference equations_. We will focus on _first order_ difference equations, which describe sequences $\{x_n\}$ such that:

$$x_{n+1} \leftarrow f(x_n)$$

The function $f(\cdot)$ is called the _map_.

### Solution of first-order linear difference equations

Consider an equation with the general form

$$x_{n+1} = ax_n + P(x), a \in \mathbb{R}$$

We rewrite this as $x_{n+1} - ax_n = P(n)$, then split into _homogeneous_ and _inhomogeneous_ equations:
\begin{align}
    x_{n+1} - ax_n &= 0 \quad &\text{homogeneous}\\
    x_{n+1} - ax_n &= P(n) \quad &\text{inhomogeneous}\\
\end{align}

A simple solution (verify this) of the homogeneous solution is ${x_n}^h = Ca^n$. We need to **guess** some $Q(n)$, a solution to the inhomogeneous equation.

The **overall** solution is given by superposition:

$$x_n = {x_n}^h + {x_n}^i = Ca^n + Q(n)$$

### Behaviour of systems of the form $x_{n+1} = rx_n$

Consider the case $x_{n+1} = rx_n$ which has solution $x_n = r^tx_0$. There are **seven** classes

1. $r = 0$: solution is zero except possibly $x_0$
2. $r = 1$: solution is constant everywhere
3. $r = -1$: solution oscillates between $+x_0$ and $-x_0$
4. $r \in (0, 1)$: $x_n \to 0$ as $n \to \infty$ monotonically
5. $r \in (-1, 0)$: $x_n \to 0$ as $n \to \infty$ by bounded oscillation
6. $r > 1$: $x_n \to \infty$ as $n \to \infty$ monotonically
7. $r < -1$: $|x_n| \to \infty$ as $n \to \infty$ and $x_n$ oscillates unboundedly

### Analysis by cobwebbing

Cobwebbing is a graphical method for determining the long-term behaviour of a discrete dynamical system. Consider the general case $x_{n+1} = f(x_n)$. Fixed points of our system are solutions of $x = f(x)$.

That is, they will be the intersection between the graph of $y = f(x)$ and $y = x$.

Here, we see the cobweb of $x_{n+1} = \sqrt{x}$ starting from $x_0 = 0.6$. Observe that we converge towards $x = 1$, one of the fixed points of our dynamical system. We call $x = 1$ **stable**, because starting from $x = 0.6$ results in attraction to $x = 1$. On the other hand, $x = 0$ is an **unstable** fixed point, since starting close to $x = 0$ will result in repulsion (see below).

Cobwebbing can show us situations that result in oscillatory behaviour around a fixed point, such as when $f(x) = 1-0.5x^2$ and $x_0 = 0.91$:

The results observed from cobwebbing are formalised as _linear stability analysis_.

### General linear stability analysis

Consider the dynamical system described by:

$$u_{t+1} = f(u_t ; r)$$

Here, $r$ is some parameter in the model (its importance will show later!)

Let us first determine the _stable (fixed) points_ of our system - such points are solutions to the equation $u = f(u; r)$. Let $u^*$ be such a stable point.

Clearly, if $u_0 = u^*$ then $u_t = u^*$ everywhere. This is not interesting - we want to know about the behaviour of our system _close_ to $u^*$. Let us thus consider some perturbation $\delta_n$ away from our fixed point - we would like to know the _fate_ of this perturbation as it propagates with the evolution of time in our system.

Consider using the Taylor series expansion:

\begin{align}
    u^* + \delta_{n+1} &= f(u^* + \delta_n; r)\\
    &= f(u^*) + \delta_nf'(u^*) + \mathcal{O}(\delta_n^2)
\end{align}

Since $\delta_n \ll 1$, we have

$$u^* + \delta_{n+1} = \underbrace{(u^*)}_{ = u^*} + \delta_nf'(u^*)$$

Thus

$$\delta_{n+1} = f'(u^*)\delta_n$$

*** Eigenvalues of steady states***

We call $\lambda = f'(u^*)$ the _eigenvalue_ of the system associated with the steady state $u^*$.

Clearly, if $|\lambda| < 1$, then $\delta_{n} \to 0$ as $n \to \infty$; that is, $u^*$ is said to be stable.
* Specifically, if $-1 < \lambda < 0$, we obtain _oscillatory_ behaviour.

Consider the case $\lambda = \pm 1$. This corresponds to the points in our parameter space where the nature of our steady state *changes*.

Consider the **quadratic map** specified by $f(x) = rx(1-x)$.

Solving for steady states, we obtain:
\begin{align*}
    x^*_1 &= 0\\
    x^*_2 &= 1 - \dfrac{1}{r}\\
\end{align*}

Compute the derivative at these points using $f'(x) = r\left( 1-x\right) -rx$

\begin{align*}
    f'(x^*_1) &= r\\
    f'(x^*_2) &= 2-r
\end{align*}

$x^*_1 = 0$ is stable for $|r| < 1$. $x^*_2 = 1 - 1/r$ is stable for $1<r<3$. We can thus plot the **stable** solutions to our system as a function of $r$.

```python
import matplotlib as mp
import matplotlib.pyplot as plt
plt.figure()
x = [0.01*i for i in range(0, 300)]
y = [0 for i in range(0, 100)] + [1 - 1/(0.01*i) for i in range(100, 300)]
plt.plot(x, y)
plt.title("Plot of stable fixed points for map $f(x) = rx(1-x)$ for $r \in [0, 3]$")
```

```python
from iterate import iterate
r = 0.5
x = iterate(30, lambda x: r*x*(1-x), x0 = 0.5)

plt.figure()
plt.scatter([i[0] for i in x], [i[1] for i in x])
plt.title("Plot of iterative solution with r = 0.5")
plt.ylim(-0.1, 1)

r = 1.5
x = iterate(30, lambda x: r*x*(1-x), x0 = 0.01)
plt.figure()
plt.scatter([i[0] for i in x], [i[1] for i in x])
plt.ylim(0, 1)
plt.title("Plot of iterative solution with r = 1.5")

r = 3.1
x = iterate(30, lambda x: r*x*(1-x), x0 = 0.01)
plt.figure()
plt.scatter([i[0] for i in x], [i[1] for i in x])
plt.ylim(0, 1)
plt.title("Plot of iterative solution with r = 3.1")
```

In the last case where $r = 3.1$, we see that our steady state is a **2-cycle** that alternates between 2 values. We say that $r$ increased past the bifurcation point $r = 3$.

How does this arise?

For $r > 3$, consider some fixed point of our map, $u^* = f(u^*)$. Clearly, $u^* = f(f(u^*))$. That is,

$$u^* = f^2(u^*)$$

We can consider solutions to the second-iterate map.

$$r^2(1-u^*)u^*(1-r(1-u^*)u^*) = u^*$$

We can attempt to solve this equation and recover the solutions:
\begin{align*}
    u^* &= 0 &\text{for } |r| < 1\\
    u^* &= 1 - 1/r &\text{for } 1 < r < 3\\
    u^* &= \alpha, \beta &\text{for } r > 3
\end{align*}

We can now consider stability under the second-iterate map $f^2$, so we consider $\mathrm{d}_x f^2$ at our fixed points to determine where they are stable.

Following on from this, we may consider the next iteration of our map, $f^3$, and so on.

In general, to investigate the stability of specific points on our map, we may consider:

$$u^* = f^m(u^*) \Rightarrow \lambda = \dfrac{\partial f^m}{\partial u}(u^*) = \Pi_{i = 0}^{m-1} f'(u_i)$$

### Systems of difference equations

Definition: a *k*th order discrete system of difference equations is described by an expression of the form:

$$X_{t+k} = f(X_{t+k-1}, X_{t+k-2}, ..., X_t ; t)$$

#### 'Reduction of order' for systems of difference equations

A system of order $k > 1$ can be reduced to a first order system by augmenting the number of variables:

Consider

$$y_{t+2} = g(y_{t+1}, y_t)$$

We set $u_t = y_{t+1}$ and $v_t = y_t$. This gives:

\begin{align*}
    u_{t+1} &= g(u_t, v_t)\\
    v_{t+1} &= u_t
\end{align*}

This is a *first order* system that can be solving methods we will presently show.

#### Analysis of linear systems of difference equations

A linear system of difference equations can be expressed in the form

$$\vec{x}_{n+1} = \mathbf{A}\vec{x}_n + \vec{c}$$

Where $\vec{x}_n = (x_{1,n}, x_{2,n}, ..., x_{m, n})$ and $\mathbf{A}$ is a matrix of constants, $\vec{c}$ is a vector of constants.

Consider a certain solution where $\vec{x}$ takes the form $\vec{x}_n = (A\lambda^n, B\lambda^n) = \lambda^n \vec{x}_0$. Thus we achieve the following:

\begin{align*}
\lambda^{n+1}\vec{x}_0 &= \mathbf{A} \lambda^n \vec{x}_0\\
(\mathbf{A} - \lambda \mathbf{I}) \vec{x}_0 &= \mathbf{0}
\end{align*}

Clearly in this situation, $\vec{x}_0 \ne \vec{0}$ in general so we require $\mathrm{det}(\mathbf{A} - \lambda\mathbf{I}) = 0$, i.e. $\lambda$ must be an **eigenvalue** of $\mathbf{A}$.

Consequently for our system to undergo the behaviour $\vec{x}_{n+1} = \lambda \vec{x}_n$, $\vec{x}_0$ must be *an eigenvector* corresponding to the eigenvalue $\lambda$.

Our solution will converge $\vec{x}_n \to \vec{0}$ if $|\lambda| < 1$, and diverge if $|\lambda| > 1$.

***What happens in the general case where $\vec{x}_0$ is a general vector?***

Assuming $\mathbf{A}$ is nonsingular ($\Rightarrow$ $\mathbf{A}$ is of full rank), the rank-nullity theorem implies it will have a 'non-degenerate eigenspace'.

Thus, for any $\vec{x}_n$ we may write an expansion in basis vectors:

$$\vec{x}_n = \sum_i k_i \vec{e}_i$$

Thus

$$\vec{x}_{n+1} = \mathbf{A}\vec{x}_n = \mathbf{A}\sum_i k_i\vec{e}_i = \sum_i \lambda_ik_i\vec{e}_i$$

From this it is clear that:

* To ensure that $\vec{x}_n \to \vec{0}$, $|\lambda_i| < 1$ for **all** our eigenvalues.
* If **one** of our eigenvalues is $>1$, one of our eigenvector expansion terms will explode.

#### Nonlinear systems

We can consider the most general case of our problem:

\begin{equation}
\begin{bmatrix}x_{n+1}\\y_{n+1}\end{bmatrix} = \begin{bmatrix}f(x_n, y_n)\\g(x_n, y_n)\end{bmatrix} \Rightarrow \vec{x}_{n+1} = \mathbf{F}(\vec{x}_n)
\end{equation}

Consider some fixed point $\vec{x}^*$ of our system, and take some small perturbation $\vec{delta}_n$ with $|\vec{\delta}_n| \ll 1$

As with univariate linear stability analysis, we can consider the behaviour of our system around the stable state.

\begin{align*}
    \vec{x}^* + \vec{\delta}_{n+1} &= \mathbf{F}(\vec{x}^* + \vec{\delta}_n)\\
    \vec{x}^* + \vec{\delta}_{n+1} &= \vec{x}^* + \mathbf{DF}(\vec{x}^*) + \vec{e}
\end{align*}

Here, $\mathbf{DF}$ is the derivative matrix

\begin{equation}
\mathbf{DF} = \begin{bmatrix}f_x&f_y\\g_x&g_y\end{bmatrix}
\end{equation}

```python

```


```python
import powersddp as psddp

TestSystem = psddp.PowerSystem(path='system.yml')
```

```python
operation = TestSystem.dispatch(solver='ulp', scenario=1, plot=True)
```

## Decision Variables

### Hydro Units
- Final volume $v_f$: The final volume of the reservoir after the operational period
- Turbined flow  $v_t$: The ammount of water that was turbined during the period
- Shed volume $v_v$: The ammount of water that was shed in the period
- Initial volume $v_i$
- Influx $afl$

### Thermal Units
- total generation $g_t$: The total amount of generation provided by the unit during the period

### System
- Outage $def$: The total ammount of power that will not be delivered by the system

## The Objective Function

Assuming a problem with 3 generation units (2 TGUs and 1 HGU) let's write down the Objetive Function of our problem:

$$
\begin{equation}
    \begin{aligned}
        \min \quad                    & C_1\cdot g_{t_1} + C_2\cdot g_{t_2} + C_{def}\cdot def + 0.01\cdot v_v\\
        \textrm{s.t.} \quad           & \\
        \textrm{hydro balance} \quad  & v_f(i) = v_i(i) + afl(i) - v_t(i) \\
        \textrm{load supplying} \quad & \rho\cdot v_t + g_{t_1} + g_{t_2} + def = \textrm{load}\\
        \textrm{constraints} \quad    & \\
                                      & v_{f_{min}}\leq v_f \leq v_{f_{max}}\\
                                      & v_{t_{min}}\leq v_t \leq v_{t_{max}}\\
                                      & v_{v_{min}}\leq v_v \leq v_{v_{max}}\\
                                      & g_{t_{min}}\leq g_t^\ast \leq g_{t_{max}}\\
        ^\ast \textrm{for each TGU}&
    \end{aligned}
\end{equation}
$$

> Later we shall also add the Future Cost Function $\alpha$ in the minimization function

```python
import cvxopt.modeling as model
from cvxopt import solvers

import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots

solvers.options['glpk'] = dict(msg_lev='GLP_MSG_OFF')

def dispatch(system, v_i, inflow, cuts, stage, verbose:bool=False):
    n_tgu = len(system.data['thermal-units'])
    n_hgu = len(system.data['hydro-units'])
    solvers.options['show_progress'] = verbose

    ## Initializing Model Variables
    v_f = model.variable(n_hgu, "Final Volume of the Hydro Unit")
    v_t = model.variable(n_hgu, "Turbined Flow of the Hydro Unit")
    v_v = model.variable(n_hgu, "Shed flow of the Hydro Unit")
    g_t = model.variable(n_tgu, "Power generated by the Thermal Unit")
    deficit = model.variable(1, "Power deficit")
    alpha = model.variable(1, "Future Cost")

    ## Objective Function
    fob = 0
    for i, tgu in enumerate(system.data['thermal-units']):
        fob += tgu["cost"]*g_t[i]
    fob+=TestSystem.data['outage_cost']*deficit[0]
    for i, _ in enumerate(system.data['hydro-units']):
        fob += 0.01*v_v[i]
    fob += 1.0 * alpha[0]

    ## Constraints

    ### Hydro Balance
    constraints = []
    for i, hgu in enumerate(system.data['hydro-units']):
        constraints.append( v_f[i] == float(v_i[i]) + float(inflow[i]) - v_t[i] - v_v[i] )

    supplying = 0
    ### Load Supply
    for i, hgu in enumerate(system.data['hydro-units']):
        supplying += hgu["prod"] * v_t[i]

    for i, tgu in enumerate(system.data['thermal-units']):
        supplying += g_t[i]

    supplying += deficit[0]

    constraints.append(supplying == system.data['load'][stage-2])

    ### Bounds
    for i, hgu in enumerate(system.data['hydro-units']):
        constraints.append(v_f[i] >= hgu["v_min"])
        constraints.append(v_f[i] <= hgu["v_max"])
        constraints.append(v_t[i] >= 0)
        constraints.append(v_t[i] <= hgu["flow_max"])
        constraints.append(v_v[i] >= 0)

    for i, tgu in enumerate(system.data['thermal-units']):
        constraints.append(g_t[i] >= 0)
        constraints.append(g_t[i] <= tgu["capacity"])

    constraints.append(deficit[0] >= 0)
    constraints.append(alpha[0] >= 0)

    ### Cut constraint (Future cost function of forward stage)
    for cut in cuts:
        if cut['stage'] == stage:
            equation = 0
            for hgu in range(n_hgu):
                equation += float(cut['coefs'][hgu])*v_f[hgu]
            equation += float(cut['coef_b'])
            constraints.append(alpha[0] >= equation)

    ## Solving
    opt_problem = model.op(objective=fob, constraints=constraints)
    opt_problem.solve(format='dense',solver='glpk')

    ## Print
    if verbose:
        print("Total Cost: {}".format(fob.value()))

        for i, hgu in enumerate(system.data['hydro-units']):
            print("{} {} is {} hm3".format(v_f.name,i,v_f[i].value()))
            print("{} {} is {} hm3".format(v_t.name,i,v_t[i].value()))
            print("{} {} is {} hm3".format(v_v.name,i,v_v[i].value()))

        for i, tgu in enumerate(system.data['thermal-units']):
            print("{} {} is {} MWmed".format(g_t.name,i,g_t[i].value()))

        print("{} is {} MWmed".format(deficit.name,deficit[0].value()))

        for i, hgu in enumerate(system.data['hydro-units']):
            print("The cost of water at Hydro Unit {} is {} hm3".format(i,constraints[i].multiplier.value))

        print("The Marginal Cost is: {}".format(constraints[n_hgu].multiplier.value))

    return {
        "deficit": deficit[0].value()[0],
        "operational_marginal_cost": constraints[n_hgu].multiplier.value[0],
        "total_cost": fob.value()[0],
        "future_cost": alpha[0].value()[0],
        "hydro_units": [{
            "v_f": v_f[i].value()[0],
            "v_t": v_t[i].value()[0],
            "v_v": v_v[i].value()[0],
            "water_marginal_cost": constraints[i].multiplier.value[0]} for i in range(n_hgu)],
        "thermal_units": [{"g_t": g_t[i].value()[0]} for i in range(n_tgu)]
    }

def plot_future_cost_function(operation: pd.DataFrame):

    n_stages = len(operation['stage'].unique())

    fig = make_subplots(rows=n_stages, cols=1)

    i = 1
    for stage in operation['stage'].unique():
        stage_df = operation.loc[operation['stage'] == stage]
        fig.add_trace(go.Scatter(x=stage_df["v_i"],
                                 y=stage_df['average_cost'],
                                 mode='lines',
                                 name="Stage {}".format(i)), row=stage, col=1)
        i+=1

    fig.update_xaxes(title_text="Final Volume [hm3]")
    fig.update_yaxes(title_text="$/MW")

    fig.update_layout(height=300*TestSystem.data['stages'], title_text="Future Cost Function")
    fig.show()
```

```python
from itertools import product
import numpy as np

n_hgu = len(TestSystem.data['hydro-units'])
n_tgu = len(TestSystem.data['thermal-units'])

step = 100/(TestSystem.data['discretizations']-1)
discretizations = list(product(np.arange(0,100+step,step), repeat=n_hgu))

cuts = []
operation = []
for stage in range(TestSystem.data['stages'],0,-1):
    for discretization in discretizations:

        v_i = []
        # For Every Hydro Unit
        for i, hgu in enumerate(TestSystem.data['hydro-units']):
            v_i.append(hgu['v_min'] + (hgu['v_max']-hgu['v_min'])*discretization[i]/100)

        # For Every Scenario
        average = 0.
        avg_water_marginal_cost = [0 for _ in TestSystem.data["hydro-units"]]
        for scenario in range(TestSystem.data['scenarios']):
            inflow = []
            for i, hgu in enumerate(TestSystem.data['hydro-units']):
                inflow.append(hgu['inflow_scenarios'][stage-1][scenario])

            result = dispatch(TestSystem, v_i, inflow, cuts, stage+1)
            average += result["total_cost"]
            for i, hgu in enumerate(result["hydro_units"]):
                avg_water_marginal_cost[i] += hgu["water_marginal_cost"]

        # Calculating the average of the scenarios
        average = average/TestSystem.data['scenarios']
        coef_b = average
        for i, hgu in enumerate(result["hydro_units"]):
            # ! Invert the coeficient because of the minimization problem inverts the signal
            avg_water_marginal_cost[i] = - avg_water_marginal_cost[i]/TestSystem.data['scenarios']
            coef_b -= v_i[i]*avg_water_marginal_cost[i]

        cuts.append({"stage": stage, "coef_b": coef_b, "coefs": avg_water_marginal_cost})
        operation.append({'stage': stage, 'discretization': discretization[i], 'v_i': v_i[0], 'average_cost': round(average,2)})
operation_df = pd.DataFrame(operation)

if n_hgu == 1:
    plot_future_cost_function(operation=operation_df)
```

```python
operation_df
```

## Considering the Future Cost Function

### Modelling the cost of water

Now, let's consider the Future Cost Function to back propagate the solutions. By back propagating we assume that the future cost function of the "stage ahead" is used as input for the previous stage solution.

Assuming that any Future Cost Function is aproximated by a series of straigh line discretizations. Any given point can be identified by a straight line, which is mathematically represented by:

$$
\begin{equation}
    \begin{aligned}
        \alpha = a \cdot v_f + b
    \end{aligned}
\end{equation}
$$

Where $\alpha$ is cost at a given point of final volume. Where shall find the coeficients $a$ and $b$

- $a$: Is the marginal cost of the water, which comes from the solution of the minimization problem.

If we assume $\alpha = 75$ and $v_f = 60$ which means a cost of $\$60.00$ at Final Volume $60 hm^3$, that gives us:

$$
\begin{equation}
    \begin{aligned}
        b = \alpha - a \cdot v_f
    \end{aligned}
\end{equation}
$$

> Naturaly, this process is repeated for every discretization used in the problem.

> $a$ is given by the average value of every scenario considered when calculating the marginal cost of the water.

If we evaluate for multiple Hydro Units, naturaly:

$$
\begin{equation}
    \begin{aligned}
        \alpha =b + \sum_{i=1}^{n} a_i \cdot v_{i}
    \end{aligned}
\end{equation}
$$

Where $n$ = number of Hydro units

### Considering the cost function in the back propagation

In the previos stage (back propagating from the end to the beggining) we have the objetive function:

$$
\begin{equation}
    \begin{aligned}
        \min \quad                                   & C_1\cdot g_{t_1} + C_2\cdot g_{t_2} + C_{def}\cdot def + 0.01\cdot v_v + \alpha\\
        \textrm{s.t.} \quad                          & \\
        \textrm{hydro balance} \quad                 & v_f(i) = v_i(i) + afl(i) - v_t(i) - v_v(i) \\
        \textrm{load supplying} \quad                & \rho\cdot v_t(i) + g_{t_1} + g_{t_2} + def = \textrm{load}\\
        \textrm{considering the forward state}\quad  & \\
        \textrm{for every scenario} `s` \quad          & \alpha \geq a^{s} \cdot v_f(i) + b^{s}\\
                \textrm{constraints} \quad           & \\
                                                     & v_{f_{min}}\leq v_f(i) \leq v_{f_{max}}\\
                                                     & v_{t_{min}}\leq v_t(i)  \leq v_{t_{max}}\\
                                                     & v_{v_{min}}\leq v_v(i)  \leq v_{v_{max}}\\
                                                     & g_{t_{min}}\leq g_t^\ast \leq g_{t_{max}}\\
        ^\ast \textrm{for each TGU}&
    \end{aligned}
\end{equation}
$$


<!--

author:  Michelle P. Kuchera
created: 18 Aug 2016
license: This code is released under the GNU GPL. Please feel free to use, modify, re-release at your will. You can not construct closed-source or proprietary software with this code. Please contact me if this is your desire.

-->

# Introduction to Jupyter Notebooks and Python

### Welcome to your (maybe) first **Jupyter notebook**!

In this class, we are learning to use the python language to solve problems in physics. So far, we have used an Integrated Development Enviroment (IDE) to write and execute python code. Today, we will use a Jupyter notebook to go through a brief overview of the python language. We will use both the IDE and the Jupyter notebooks to write python code this semester.

## Let's get started.

From the Jupyter [documentation](http://jupyter.readthedocs.io/en/latest/index.html):

> First and foremost, the Jupyter Notebook is an interactive environment for writing and running code. The notebook is capable of running code in a wide range of languages. However, each notebook is associated with a single kernel.

In this class, we will use the Jupyter notebook to run Python code. You should therefore have this notebook opened in a Python kernel. Look at the top right of the browser window. If you see "Python 2" or "Python 3", this is a python notebook.

### Code cells allow you to enter and run code
You run code by holding `Shift` + `Enter`.

#### Try this with the python code below.

```python
a = 6
b = 2
c = a+b
print(c)
```

    8

#### Now, try to change a value of `a` or `b` above and re-execute the above cell.

### A *comment* is a non-code note within code. In python, a comment is preceded by a `#` for single-line comments.

Comments are useful as notes to yourself (the programmer), and/or notes to a user of your code.

Below, the code from the first cell is reproduced with helpful comments.

####  Run this code again and note that the addition of the comments did not change the execution of the code

```python
# The following is simple python code, with comments, for adding two variables together
a = 6 # assigning the value of 6 to variable a
b = 2 # assigning the value 2 to the variable b
c = a+b # adding the variables a and b and storing the result in c
print(c) # printing the contents of c to the screen, right below the cell
```

    8

### A *function* is a block of code that can be called at any point after it has been evaluated

```python
def add(a,b): # a function is indicated by def. We named this function add. It takes two values as arguments
    c = a+b   # we add the two values that were passed into the function
    return c  # and return the result
```

```python
answer = add(6,2)    # We store the result of add into a new variable answer
print(answer)
answer = add(5,100)  # We overwrote the variable answer with the new value
print(answer)
test = add('h','i')  # The + sign concatenates other data types, such as characters, rather that adding them
print(test)
```

    8
    105
    hi

**The python language itself does not contain all of the mathematics and visualization that we would be interested in for exploring physics.**

**To take advantage of the math and visualization that is useful to us in this course, we can to use python packages. These contain *modules*, which are files that define *objects* and *functions* that we will use this semester.**

Let's start with `SymPy`, a package for symbolic (rather than numeric) calculations.
#### We must `import` the package of interest:

```python
from sympy.interactive import printing # for pretty printing of variables, etc
printing.init_printing(use_latex='mathjax') # specifying what type of pretty printing (this will be LaTeX)
# now import the package and define a namespace, sym, that will precede the function calls from the sympy function
import sympy as sym
```

#### Now, let's use the package in a simple example

```python
x = sym.symbols("x") # defining x as a variable
sym.Integral(1/x,x) # Calling the Integral function within sympy, this defines our integral
```

$\displaystyle \int \frac{1}{x}\, dx$

```python
sym.integrate(1/x,x) # Calling the itegrate function within sympy, which symbolically integrates the function
```

$\displaystyle \log{\left(x \right)}$

**The three most commonly used python packages for scientific computing are `math` (for math functions), `numpy` (for arrays), and `matplotlib` (for plotting).
In fact, you will sometimes see numpy and matplotlib imported together as one package called `scipy`.**

### Below, I have written an example of receiving user input as a starting point.

```python
h = input("height in m: ")
print("the height is", h, "meters")
```

    height in m: 9
    the height is 9 meters

## Now, some final notes about the Jupyter notebooks:
#### Jupyter notebook files are designated by the *.ipynb* extension.
#### We can download notebooks in other formats, but they will not be executable.
#### The notebooks run *locally* (on your computer) in a web browser.
#### When the notebook application is launched, a local notebook server runs.
#### For each notebook you open, an iPython *kernel* starts running.
#### The kernel does *not* stop running when the browser tab is closed.
#### Make sure to shutdown each kernel that you begin, either from the notebook itself or the dashboard.

## I hope you had fun working through this Jupyter notebook!

#### Please run everything as you would like me to see it, then go to File->Rename.
#### Save this file as lastname1_lastname2_jupyter.ipynb (you do not have to enter the extension if you don't see it)


# Objective: Segmentation of tangrams from an image and a surface

```python
from torch_pdegraph.operators import GradMinusNorm
import torch
import numpy as np
import matplotlib.image as mpimg
from torch_pdegraph.utilities import *
from tqdm import tqdm
import matplotlib.pyplot as plt
import faiss
```

```python
# load the tangram image
img = mpimg.imread("../data/tangram.png")
dispImg(img)
```

```python
# InitialFronts/initial_distances are seeds on the image (on each tangram  and backgroun)
# One can use any image annotation tool to create hese  seeds I have used Pandore by Greyc
f = plt.figure(figsize=(12,12))
Front = []
for i in range(1,10,1):
    seed = mpimg.imread(f"../data/dist_{i}.png")
    f.add_subplot(3,3,i)
    plt.imshow(seed, cmap="gray")
    initial_dist = getDist(seed)
    Front.append(initial_dist)
```

# Graph construction

- In order to simply show the effectiveness of PDEs on graph,  I am only creating a simple K-NN based graphs. This may or maynot be the best graph for a given problem at hand.

- One can create graph using whatsoever apt approach or one can even use third-party network datasets and run a PDE on that graph.  PDEs are extensible to any given graph/network at hand as long as that graph has edges and weights( edge_index and edge_attr).

Although torch_cluster comes with a knn-graph method. I found it to be limited and  slow when the node-features have high dimensions. We shall be using facebook's faiss library which is blazingly fast for a KNN-graph construction.

```python
p = toPmat(img.shape).astype(np.float32) # position coords vector
texture = toFmat(img).flatten() # gray-scale values

# Create a KNN graph using the (x,y) pixel-coord of the image and assign weights using differnce
# between the texture at the pixel-coords
res = faiss.StandardGpuResources()
index = faiss.IndexFlatL2(p.shape[1])
gpu_index_flat = faiss.index_cpu_to_gpu(res,0,index)
gpu_index_flat.add(p)
k = 10
_, I = gpu_index_flat.search(p,k+1)

#Graph
edge_index = np.vstack((I[:,1:].flatten(), np.repeat(I[:,0].flatten(),10)))
edge_attr = np.exp(-((texture[I]-texture[I][:,0,None])**2)/1e-04)[:,1:].flatten()
edge_index = torch.tensor(edge_index, dtype=torch.long).to('cuda:0')
edge_attr = torch.tensor(edge_attr, dtype=torch.float32).to('cuda:0')
edge_attr = edge_attr.view(-1,1)
graph = Graph(edge_index, edge_attr)
```

# Run a manually defined PDE:

\begin{equation}
\mathbf{x}^{n+1}_{i}  = \mathbf{x}^{n}_{i} + \Delta t \ (\mathbf{1} - \|\nabla^{-}_{w}\ \mathbf{x}^{n}_{i}\|_{p})\\
\end{equation}

- $\mathbf{x}_{i}$ is the node feature/signal at the $i^{th}$ node
- $\nabla^{-}_{w}$ is the negative directional gradient on weighted graphs

**Example:**
```python
from torch_pdegraph.operators import GradMinusNorm
ope = GradMinusNorm.OPE(graph, **hp) # create an operator instance

for i in range(itr): # run the manual pde inside a loop
     fr = fr + dt * (1.0 - ope(fr))
```

Though not exactly, this equation is approximates time depenendent **eikonal equation on weighted graphs which can be used to calculate generalized distances on the graphs**. Check the [Thong](https://hal.archives-ouvertes.fr/file/index/docid/404070/filename/VinhThongTA_09_ssvm.pdf)

```python
# Params
dt = 0.05
itr = 5000
hp = dict(p_val = 1)

# Evolved Front
new_front = []

# Instantiate the operator
ope = GradMinusNorm.OPE(graph, **hp)

# Run explict PDE
for fr in tqdm(Front):
    fr = toFmat(fr)
    fr = torch.tensor(fr, dtype=torch.float32).to('cuda:0')
    for i in range(itr):
        fr = fr + dt * (1.0 - ope(fr))
    newfr = fr.to('cpu')
    new_front.append(newfr.numpy())
```

    100%|██████████| 9/9 [00:11<00:00,  1.25s/it]

```python
# visualize the segmented image
dist_img = []
for fr in new_front:
    tmp_img = toImg(fr, img.shape[0:2])
    dist_img.append(tmp_img)
m = np.argmin(dist_img, axis=0)

import matplotlib.pyplot as plt
f = plt.figure(figsize=(8,8))
f.add_subplot(1,2, 1)
plt.imshow(m) # Segmented image
f.add_subplot(1,2, 2)
plt.imshow(img, cmap="gray") # Orignal image
plt.show(block=True)
```

# Reproduce the result for a pointcloud

```python
# Same thing reproduction for a point clould surface
p = np.load("../data/tangram/position_tangram.npz")
t = np.load("../data/tangram/smooth_texture_tangram.npz")
p = p["position"].astype(np.float32)
signal = t["texture"]

# Create a knn graph
res = faiss.StandardGpuResources()
index = faiss.IndexFlatL2(p.shape[1])
gpu_index_flat = faiss.index_cpu_to_gpu(res,0,index)
gpu_index_flat.add(p)
k = 25
_, I = gpu_index_flat.search(p,k+1)
edge_index = np.vstack((I[:,1:].flatten(), np.repeat(I[:,0].flatten(),k)))
edge_attr = np.exp(-np.sum(((signal[I]-signal[I][:,0,None])**2),axis=2)/(1.3e-02)**2)[:,1:].flatten()
edge_index = torch.tensor(edge_index, dtype=torch.long).to('cuda:0')
edge_attr = torch.tensor(edge_attr, dtype=torch.float32).to('cuda:0')
edge_attr = edge_attr.view(-1,1)
graph = Graph(edge_index, edge_attr)

# load the intial_distance/seed pcd
Front = []  # tiff should be preffered than png
for i in range(1,10,1):
    seed = np.load(f"../data/tangram/{i}_texture.npz")
    seed = seed["texture"]
    initial_dist = getDistPcd(seed)
    initial_dist = np.reshape(initial_dist, (len(initial_dist),1))
    Front.append(initial_dist)

# solve the pde
dt = 0.05
itr = 5000
new_front = []
hp = dict(p_val = 1)
ope = GradMinusNorm.OPE(graph, **hp)
for fr in tqdm(Front):
    fr = torch.tensor(fr, dtype=torch.float32).to('cuda:0')
    for i in range(itr):
        fr = fr + dt * (1.0 - ope(fr))
    newfr = fr.to('cpu')
    new_front.append(newfr.numpy())

```

    100%|██████████| 9/9 [00:20<00:00,  2.33s/it]

```python
#visualzie the segmented pcd using matplotlib colormap
m = np.argmin(new_front, axis=0)
m_t = np.uint8(255*(m/np.max(m)))

import matplotlib.cm as cmx
import matplotlib.colors as colors
jet = plt.get_cmap('jet')
cNorm = colors.Normalize(vmin=0, vmax=np.max(m_t))
scalarMap = cmx.ScalarMappable(norm=cNorm, cmap=jet)
scalarMap.get_clim()
colorVal = scalarMap.to_rgba(m_t)
val_color = np.squeeze(colorVal, axis=1)
val_color = val_color[:,0:3]
```

```python
displayJSur(**dict(position=p, texture=val_color))#Or use displaySur for no jupyter visualization
```

    JVisualizer with 1 geometries

```python
displayJSur(**dict(position=p,texture=signal))#Or use displaySur for no jupyter visualization
```

    JVisualizer with 1 geometries

```python
# Unlike an image, a pcd display cannot be saved in ipython notebook, hence the notebook must be running to
# to view the pcd. Expect to see the tangrams segmented from the surface like shown in the following image.
from matplotlib import image as mpimg
from matplotlib import pyplot as plt
#res = mpimg.imread("../gallery/tangram_surface.png")
res = mpimg.imread("../gallery/tangram_surf3.png")
f = plt.figure(figsize=(12,12))
plt.imshow(res)
```

```python

```


```python
import sympy as sp
import numpy as np
import matplotlib.pyplot as plt

from ratter import *
```

```python
wavelength = 600 # length units: nm

Si = Material('Si', refractive_index_value=3.9400+0.019934j)  # Green 2008
air = Material('air', refractive_index_value=1.00027698) # Ciddor 1996
SiO2 = Material('SiO2', refractive_index_value=1.4580) # Malitson 1965: Fused silica
AlOx = Material('Al2O3', refractive_index_value=1.7675) # Malitson and Dodge 1972: α-Al2O3 (Sapphire)
```

```python
environment = Layer('env', air)
coating1 = Layer('coat1', SiO2)
coating2 = Layer('coat2', AlOx)
bulk = Layer('bulk', Si)
```

```python
stack = Layerstack([environment, coating1, coating2, bulk])
```

```python
r = stack.reflectance_amplitude()
```

```python
R = sp.conjugate(r)*r
```

```python
R_ = R.subs(LAMBDA_VAC, wavelength)
R_of_coating_thickness = as_function_of(R_, [coating1.thickness_symbol, coating2.thickness_symbol])
```

```python
d1 = np.arange(0,500)

for d2 in [50,200,500]:
    plt.plot(d1, np.real(R_of_coating_thickness(d1, d2)), label='{}'.format(d2))
plt.legend(title='$Al_2O_3$ thickness (nm)')
plt.ylabel('reflectivity')
plt.xlabel('$SiO_2$ thickness (nm)')
```