```python
import sympy as sp
import numpy as np
from IPython.display import display

#display(yourobject)
```

# Dedução das equações dinâmicas por Lagrange

## Introdução

```python
sp.init_printing()
```

```python
t = sp.Symbol("t", positive=True)

# Propriedades da cabeça
m_c, J_c = sp.symbols("m_c J_c", positive=True)
x_c = sp.Function("x_c", real=True)(t)
y_c = sp.Function("y_c", real=True)(t)
theta_c = sp.Function("theta_c", real=True)(t)

# Propriedades do torço
m_t, J_t, L_t = sp.symbols("m_t J_t L_t", positive=True)
x_t = sp.Function("x_t", real=True)(t)
y_t = sp.Function("y_t", real=True)(t)
theta_t = sp.Function("theta_t", real=True)(t)

# Propriedades dos membros inferiores
x_i = sp.Function("x_i", real=True)(t)
m_i = sp.Symbol("m_i", positive=True)

# Interação air bag-cabeça
k_ab, c_ab = sp.symbols("k_ab c_ab", positive=True)

# Propriedades do pescoço
## Deformação do pescoço
x_p = sp.Function("x_p", positive=True)(t)
L_p = sp.Symbol("L_p", positive=True)
## Mola linear
k_p, c_p = sp.symbols("k_p c_p", positive=True)
## Mola de torção
k_rp, c_rp = sp.symbols("k_rp c_rp", positive=True)

# Propriedades do cinto de segurança
k_s, c_s = sp.symbols("k_s c_s", positive=True)

# Propriedades do braço
k_b, c_b = sp.symbols("k_b c_b", positive=True)

# Interação air_bag-torso
k_ab, c_ab = sp.symbols("k_ab c_ab", positive=True)

# Ligamento torço-membros inferiores
k_ri, c_ri = sp.symbols("k_ri c_ri", positive=True)

# Interações de amortecimento dos membros inferiores
k_i, c_i = sp.symbols("k_i c_i", positive=True)

# Desaceleração do carro
a = sp.Function("a", real=True)(t)

g = sp.Symbol("g", positive=True)
```

| Parâmetro  | Unidade    |                      Explicação                      |
| ---------- | ---------- | :--------------------------------------------------: |
| $m_c$      | kg         |                   Massa da cabeça                    |
| $J_c$      | kg*m²      |             Momento de inércia da cabeça             |
| $x_c$      | m          |         Posição do centro de massa da cabeça         |
| $y_c$      | m          |         Posição do centro de massa da cabeça         |
| $\theta_c$ | rad        |                 Inclinação da cabeça                 |
| $m_t$      | kg         |                    Massa do torso                    |
| $J_t$      | kg*m²      |             Momento de inércia do torso              |
| $L_t$      | m          |  Distância do centro de massa do torso até o banco   |
| $x_t$      | m          |         Posição do centro de massa do torso          |
| $y_t$      | m          |         Posição do centro de massa do torso          |
| $\theta_t$ | rad        |                 Inclinação do torso                  |
| $x_i$      | m          |  Posição do centro de massa dos membros inferiores   |
| $m_i$      | kg         |             Massa dos membros inferiores             |
| $x_p$      | m          |                Deformação do pescoço                 |
| $L_p$      | m          |           Comprimento natural do pescoço             |
| $k_p$      | N/m        |        Propriedade elástica linear do pescoço        |
| $c_p$      | N*s/m      |        Propriedade viscosa linear do pescoço         |
| $k_{rp}$   | N*m        |        Constante da mola de torção do pescoço        |
| $c_{rp}$   | N\*s\*m    |    Constante do amortecedor de torção do pescoço     |
| $k_s$      | N/m        |          Propriedades do cinto de segurança          |
| $c_s$      | N*s/m      |          Propriedades do cinto de segurança          |
| $k_b$      | N/m        |                Propriedades do braço                 |
| $c_b$      | N*s/m      |                Propriedades do braço                 |
| $k_{ab}$   | N/m        |                 Interação do air bag                 |
| $c_{ab}$   | N*s/m      |                 Interação do air bag                 |
| $k_{ri}$   | N*m        |     Ligamento dos membros inferiores com o torso     |
| $c_{ri}$   | N\*s\*m    |     Ligamento dos membros inferiores com o torso     |
| $k_i$      | N/m        |  Interações de amortecimento dos membros inferiores  |
| $c_i$      | N*s/m      |  Interações de amortecimento dos membros inferiores  |

Muito bem, senhoras e senhores, iremos agora deduzir as equações dinâmicas para o nosso modelo de *crash test* utilizando o método de Lagrange. Garanto que será uma experiência intensa e engrandecedora. Não sei dizer se _de fato_ engrandecedora, mas certamente será intensa. No mais, confie na matemática e lá vamos nós

## Dedução de T (energia cinética)

$$T = \sum_{i \subset C} \cfrac{m_i*v_{gi}^2}{2} + \cfrac{J_i*\omega_i^2}{2}$$

Onde:

| Parâmetro  | Significado                              |
| ---------- | ---------------------------------------- |
| $C$        | conjunto de corpos de sistema            |
| $v_{gi}$   | velocidade do centro de massa do corpo i |
| $m_i$      | massa do corpo i                         |
| $J_i$      | momento de inércia do corpo i            |
| $\omega_i$ | velocidade angular do corpo i            |

```python
class Corpo():
    def __init__(self, massa, mom_inercia, x, y, theta):
        self.massa        = massa
        self.mom_inercia  = mom_inercia

        self.x = x
        self.y = y
        self.theta = theta

        try:
            self.y_ponto = y.diff()
        except:
            self.y_ponto = 0

        try:
            self.x_ponto = x.diff()
        except:
            self.x_ponto = 0

        try:
            self.theta_ponto = theta.diff()
        except:
            self.theta_ponto = 0

        self.quad_vel_lin = self.x_ponto**2+self.y_ponto**2
        self.quad_vel_ang = self.theta_ponto**2

    def cinetica(self):
        return (self.massa*self.quad_vel_lin)/2 \
                + (self.mom_inercia*self.quad_vel_ang)/2

    def potencial(self, a_x, a_y):
        return self.massa*(a_x*self.x + a_y*self.y)

    def show(self):
        return (self.massa, self.quad_vel_lin,
                self.mom_inercia, self.quad_vel_ang)

Corpos = [Corpo(m_c, J_c, x_c, y_c, theta_c),
          Corpo(m_t, J_t, x_t, y_t, theta_t),
          Corpo(m_i, 0, x_i, 0, 0)]
```

```python
for corpo in Corpos:
    display(corpo.show())
```

```python
display(Corpos[0].cinetica())
```

```python
T = 0
for corpo in Corpos:
    T += corpo.cinetica()

display(T)
```

## Dedução de V (energia potencial)

$$V = \sum_{i \subset M} \cfrac{k_i*q_i^2}{2} + \sum_{i \subset C} m_i(x_i*a + y_i*g)$$

Onde:

| Parâmetro | Significado                               |
| --------- | ----------------------------------------- |
| $M$       | conjunto de molas do sistema              |
| $C$       | conjunto de corpos de sistema             |
| $k_i$     | constante elástica da mola i              |
| $q_i$     | coordenada de distenção da mola i         |
| $m_i$     | massa do corpo i                          |
| $x_i$     | coordenadas do centro de massa do corpo i |
| $y_i$     | coordenadas do centro de massa do corpo i |
| $g$       | aceleração da gravidade                   |
| $a$       | aceleração do carro                       |

```python
class Mola:
    def __init__(self, k, c, q):
        self.k = k
        self.c = c
        self.q = q

    def potencial(self):
        return (self.k*self.q**2)/2

    def dissipador(self):
        return (self.c*self.q.diff()**2)/2

Molas = [Mola(k_p, c_p, x_p),
         Mola(k_rp, c_rp, theta_c-theta_t),
         Mola(k_s + k_ab + k_b, c_s + c_ab + c_b, x_t),
         Mola(k_i, c_i, x_i),
         Mola(k_ri, c_ri, theta_t)]
```

```python
V = 0

for mola in Molas:
    V += mola.potencial()

for corpo in Corpos:
    V += corpo.potencial(a, g)

display(V)
```

## Dedução de D (dissipação de Rayleigh)

$$D = \sum_{i \subset A} \cfrac{c_i*q_i^2}{2} $$

Onde:

| Parâmetro | Significado                              |
| --------- | ---------------------------------------- |
| $A$       | conjunto de amortecedores do sistema     |
| $c_i$     | constante viscosa do amortecedor i       |
| $q_i$     | coordenada de distenção do amortecedor i |

```python
D = 0
for mola in Molas:
    D += mola.dissipador()

display(D)
```

```python
D.diff(x_i.diff())
```

Estamos tratando de um sistem de alta complexidade, portanto é essencial que nos atentemos aos conceitos envolvidos e a progressão lógica envolvida, uma vez que a complexidade das expressões finais tornam impossível sua revisão direta

Muito bem, dito isso, peço que analizem com carinho a lógica de cada etapa do processo, se preocupando em linhas gerais com o resultado final. No mais, essas são as funções equações dinâmicas que descrevem o nosso sistema. Antes de passarmos às simulações numéricas, podemos aplicar algumas restrições geométricas

## Simplificações geométricas

```python
eqs = [(x_t, L_t/2*sp.sin(theta_t) + x_i),
       (y_t, L_t/2*sp.cos(theta_t)),
       (x_p, sp.sqrt((x_c-L_t*sp.sin(theta_t))**2 + (y_c-L_t*sp.cos(theta_t))**2) - L_p),
       (theta_c, sp.atan((x_c-L_t*sp.sin(theta_t))/(y_c-L_t*sp.cos(theta_t))))]
```

```python
for eq in eqs:
    display(sp.Eq(*eq))
```

```python
V_f = V
T_f = T
D_f = D

for eq in eqs:
    V_f = V_f.subs(*eq)
    T_f = T_f.subs(*eq)
    D_f = D_f.subs(*eq)
```

```python
V
```

```python
V_f
```

```python
qs = [x_i, x_c, y_c, theta_t]
```

## Equação de Lagrange

```python
lagrange = []
L_f = T_f - V_f
for q in qs:
    lagrange.append( L_f.diff( q.diff() ).diff(t) - L_f.diff(q) + D_f.diff( q.diff() ) )
```

```python
for eq in lagrange:
    display(eq)
```

```python
qi = sp.Symbol("q_i")
l = sp.Symbol("L")(t, qi)
n = sp.Symbol("N")(t, qi)
```

```python
l.diff(qi).diff(t)
```

```python

```


## Definitions

Σύμβολα:

- $m$: το μέγεθος του training set (τα rows / αριθμός των παρατηρήσεων)
- $n$: ο αριθμός των features του training set (τα columns / αριθμός των μεταβλητών)
- $X$: το σύνολο (set) των input
- $Y$: το σύνολο (set) των output
- $i$: το index ενός συγκεκριμένου row. Παίρνει τιμές από $1 \dots m$
- $x^{(i)}$: ένα συγκεκριμένο row του $X$. Είναι ένα διάνυσμα που περιέχει $n$ τιμές.
- $y^{(i)}$: ένα συγκεκριμένο row του $Y$. Είναι διάνυσμα.
- **TODO: προσέθεσε και τα subscripts**
- $h_{θ}(x)$: Η hypothesis function
- $θ_j$: Οι παράμετροι της hypothesis function.
- $J(θ_j)$: Η cost function

Έννοιες:

- *learning algorithm*: ένα μαθηματικό μοντέλο το οποίο ορίζει τον *τύπο* της **hypothesis function**
- *hypothesis function*: μία συνάρτηση που προβλέπει τιμές output για δεδομένο input
- *cost function*: συνάρτηση την οποία ελαχιστοποιούμε για να διαλέξουμε τις παραμέτρους της
  hypothesis function.

- *regression*: ένα πρόβλημα στο οποίο το $Y$ είναι συνεχές,
- *classification*: ένα πρόβλημα στο οποίο το $Y$ είναι διακριτό,

## High level overview

Ο σκοπός μας είναι να βρούμε μια συνάρτηση η οποία θα μας
επιτρέψει να *προβλέψουμε* το $y$ για δεδομένο $x$.

Αρχικά διαλέγουμε «learning algorithm».
Αυτός είναι ένας fancy τρόπος για να πούμε ότι διαλέγουμε
τον τύπο του μοντέλου που θα χρησιμοποιήσουμε
(γραμμικό, πολυωνυμικό κτλ).

Πχ, για γραμμικό μοντέλο και για μία μεταβλητή (n=1) η hypothesis function είναι:

$$
h_{θ}\left(x^{(i)}\right) = θ_0 + θ_1 x^{(i)}
$$

Ενώ για πολυωνυμικό μοντέλο β' βαθμού και μία μεταβλητή (n=1) θα έχουμε

$$
h_{θ}\left(x^{(i)}\right) = θ_0 + θ_1 x^{(i)} + θ_2 \left(x^{(i)}\right)^2
$$

Αφού έχουμε τον γενικού τύπο του μοντέλου, δηλαδή το $h_{θ}(x)$,
πρέπει να βρούμε τις παραμέτρους του, δηλαδή να υπολογίσουμε τα $θ_j$

O υπολογισμός των $θ_j$ γίνεται μέσω της ελαχιστοποίησης της συνάρτησης κόστους $J(θ)$.

H επιλογή της συνάρτησης κόστους $J(θ)$ γίνεται και αυτή από εμάς.
Για προβλήματα *regression* σύνηθης επιλογή είναι το τετράγωνο της μέσης της διαφοράς
της προβλεπόμενης τιμής από την κανονική  (squared error function *or* mean squared error).

Άρα το πρόβλμα που τελικά καλούμαστε να λύσουμε είναι το

$$
\min{J({θ})}
$$

Aπό τη λύση αυτού του προβλήματος βελτιστοποιήσης (optimization) βρίσκουμε τα $θ_j$ και έτσι έχουμε τον πλήρη ορισμό της συνάρτησης $h_{θ}$

## Επίλυση γραμμικού μοντέλου μιας μεταβλητής (univariate linear regression)

Για προβλήματα μίας μεταβλητής η hypothesis function είναι:

$$
h_{θ}\left(x^{(i)}\right) = θ_0 + θ_1 x^{(i)}
$$

Οι παραμετροι μας λοιπόν είναι:

- $θ_0$
- $θ_1$

Στην γενική περίπτωση, το τετράγωνο της διαφοράς γράφεται ως εξής:

$$
\begin{align}
J(θ) &= \dfrac{1}{2m} \sum_{i=1}^{m}\left( \widehat{y^{(i)}}  - y^{(i)}\right)^2  \\
     &= \dfrac{1}{2m} \sum_{i=1}^{m}\left( h_{θ}\left(x^{(i)}\right) - y^{(i)}\right)^2
\end{align}
$$

> Σημείωση: Τον όρο $\dfrac{1}{2m}$ τον βάζουμε απλά για να κάνουμε πιο εύκολες μερικές από τις πράξεις όταν θα αρχίσουμε τις παραγωγίσεις στη συνέχεια.

Κάνοντας αντικατάσταση της hypothesis function βρίσκουμε ότι:

$$
J(θ_0, θ_1)   = \dfrac{1}{2m} \sum_{i=1}^{m}\left( θ_0 + θ_1 x^{(i)} - y^{(i)}\right)^2
$$

Ο στόχος μας είναι να βρούμε τα $(θ_0, θ_1)$ που ελαχιστοποιούν την συνάρτηση κόστους $J$:

$$
\min{J\left(θ_0, θ_1\right)}
$$

Τη συνάρτηση κόστους στη γενική της μορφή μπορούμε να την γράψουμε σε python ως εξής:

```python
import numpy as np

def J(
    X: np.array,
    Y: np.array,
    theta0: float,
    theta1: float
) -> float:
    return 1 / 2 / len(X) * ((theta0 + theta1 * X - Y)**2).sum()
```

### $θ_0 = 0$

Για να απλοποιήσουμε τα πράγματα ακομα περισσότερο, ας υποθέσουμε για την ώρα ότι $θ_0 = 0$.

Στην περίπτωση αυτή έχουμε:

$$
\begin{align}
h_θ\left(x^{(i)}\right) &= θ_1 x^{(i)} \\
               J(θ_1)   &= \dfrac{1}{2m} \sum_{i=1}^{m}\left( θ_1 x^{(i)} - y^{(i)}\right)^2
\end{align}
$$

Παρόλα αυτά, δεν έχει πολύ νόημα να ξαναορίσουμε την συνάρτηση στην Python,
καθώς μπορούμε απλά να περάσουμε `theta0=0` ως παράμετρο όταν την καλούμε.

Έστω ακόμα ότι τα training set μας είναι το εξής:

```python
X = np.array([1, 2, 3])
Y = np.array([1, 2, 3])
```

Ας πάρουμε μερικές τυχαίες τιμές του $θ_1$ και ας δούμε τι τιμές παίρνει η $J$:

```python
theta0 = 0
theta1 = 0.5
J(X=X, Y=Y, theta0=theta0, theta1=theta1)
```

    0.5833333333333333

Δηλαδή βλέπουμε ότι για `θ1=0.5`, το κόστος είναι περίπου `J=0.58`

Ας υπολογίσουμε το κόστος και για άλλες τιμές:

```python
for theta1 in [-2, -1.5, -0.5, 0, 0.5, 0.75, 1, 1.5, 2, 3]:
    cost = J(X=X, Y=Y, theta0=theta0, theta1=theta1)
    print(f"{theta0=}\t{theta1=} \t{cost=}")
```

    theta0=0	theta1=-2 	cost=21.0
    theta0=0	theta1=-1.5 	cost=14.583333333333332
    theta0=0	theta1=-0.5 	cost=5.25
    theta0=0	theta1=0 	cost=2.333333333333333
    theta0=0	theta1=0.5 	cost=0.5833333333333333
    theta0=0	theta1=0.75 	cost=0.14583333333333331
    theta0=0	theta1=1 	cost=0.0
    theta0=0	theta1=1.5 	cost=0.5833333333333333
    theta0=0	theta1=2 	cost=2.333333333333333
    theta0=0	theta1=3 	cost=9.333333333333332

όπως βλέπουμε η συνάρτηση ελαχιστοποιείται στο 0.

Ας πλοτάρουμε:

```python
import functools
import numpy as np

from bokeh.io import output_notebook
from bokeh.layouts import row
from bokeh.layouts import column
from bokeh.layouts import gridplot
from bokeh.models import Legend
from bokeh.plotting import figure
from bokeh.plotting import show

# Output the visualization directly in the notebook
output_notebook()

# Create a figure with no toolbar and axis ranges of [0,3]
hplot = figure(
    title="theta0=0",
    x_range=(0, 4),
    y_range=(0, 4),
    x_axis_label='x',
    y_axis_label='h(x)',
)

_ = hplot.line(x=[-3.75, 3.75], y=[-5.0, 5.0], legend_label="h(θ1=1.25)", line_color="magenta", line_width=2)
_ = hplot.line(x=[-5, 5], y=[-5.0, 5.0], legend_label="h(θ1=1)", line_color="red", line_width=2)
_ = hplot.line(x=[-5, 5], y=[-2.5, 2.5], legend_label="h(θ1=0.5)", line_color="blue", line_width=2)
_ = hplot.circle(x=X, y=Y, color='green', size=10, alpha=0.5, legend_label="training set")

hplot.legend.location = "top_left"

jplot = figure(
    #x_range=hplot.x_range,
    #y_range=hplot.y_range,
    x_axis_label='θ1',
    y_axis_label='J(θ1)',
)

# plot the curve
theta1s = np.linspace(-0.5, 2.5, 30)
_ = jplot.line(
    x=theta1s,
    y=[J(X=X, Y=Y, theta0=0, theta1=theta1) for theta1 in theta1s],
    line_color="blue",
    legend_label="J(Θ)",
)

# minimum point
theta1 = 1
_ = jplot.triangle(theta1, J(X, Y, theta0=0, theta1=theta1), legend_label=f"J(θ1=1)", size=12, color="magenta", line_color="magenta")

# plot various points
for theta1 in (0, 0.5, 0.85, 1.12, 1.25, 1.5):
    _ = jplot.cross(theta1, J(X, Y, theta0=0, theta1=theta1), legend_label=f"J(θ1={theta1})", size=8, line_color="red")

# Show plot
show(gridplot([[hplot, jplot]]))
```

    Loading BokehJS ...


```python
import sympy as sy
sy.init_printing(use_unicode=False)
```

```python
x,y=sy.symbols('x y')
```

```python
lam=sy.symbols("\lambda")
```

```python
g=x*x+y*y-1.0
f=x+y
phi=f-lam*g
```

```python
[sy.diff(phi,x),
sy.diff(phi,y),
sy.diff(phi,lam)]
```

```python

```

```python

```


Mar 2015, J. Slavič in L. Knez

```python
from sympy import *
init_printing()
```

# Do naslednjih vaj preštudirajte sledeče:

Na sliki je prikazano paličje. Ob delovanju sil $F_1$ in $F_2$ se v palicah razvijejo notranje sile $P_i$. Dimenzije paličja zagotavljajo kota $\alpha$ in $\beta$.

Sile v palicah izračunamo s pomočjo sistema linearnih enačb.

(Opomba: naloga je posplošitev naloge 15 na strani 81 v knjige Numerical methods in Eng with Py 3 z nastavkom za rešitev:

V omenjenem viru je sicer napaka v predzadnji vrstici matrike koeficientov.

Vprašanje 1: V simbolni obliki zapišite ravnotežje sil za točko 1 v $x$ in $y$ smeri

Vprašanje 2: V simbolni obliki zapišite ravnotežje sil za točko 2 v $x$ in $y$ smeri.

Vprašanje 3: V simbolni obliki zapišite ravnotežje sil za točko 3 v $x$ in $y$ smeri.

```python
P1, P2, P3, P4, P5, P6, F1, F2, alpha, beta = symbols('P1, P2, P3, P4, P5, P6, F1, F2, alpha, beta')

# Točka 1
eq1x = Eq(-P1 - P3*cos(alpha) + P2,0)
eq1y = Eq(P3*sin(alpha) + P4 - F1,0)

# Točka 2
eq2x = Eq(-P2 - P5*cos(beta),0)
eq2y = Eq(P5*sin(beta)-F2,0)

# Točka 3
eq3x = Eq(-P6 + P5*sin(pi/2-beta),0)
eq3y = Eq(-P4 - P5*cos(pi/2-beta),0)

# Spravimo skupaj v seznam
eqs = [eq1x, eq1y, eq2x, eq2y, eq3x, eq3y]
eqs
```

Vprašanje 4: Najdite simbolično rešitev za sile $P_i$.

```python
# Simbolično rešimo zgornji sistem
sol = solve(eqs, [P1, P2, P3, P4, P5, P6])
sol
```

Vprašanje 5: Uporabite podatke: $\alpha=\beta=\pi/4$, $F_1=18$ kN in $F_2=12$ kN ter najdite številčno rešitev.

```python
# Vstavim podatke z izpeljevanjem (to naredimo tako, ker imamo seznam in ne pozna direktno ukaza subs)
eqs2 = [_.subs({alpha: pi/4, beta: pi/4, F1: 18000, F2: 12000}).evalf() for _ in eqs]
eqs2
```

```python
# Rešimo sistem
sol = solve(eqs2, [P1, P2, P3, P4, P5, P6])
sol
```

Vprašanje 6: Pripravite si funkcijo, ki bo za poljubne podatke (npr: ``podatki = {a: pi/4, b: pi/4, F1: 18000, F2: 12000}``) vrnila numerično matriko koeficientov $\mathbf{A}$ in vektor konstat $\mathbf{b}$. Če ne uspete tega narediti avtomatizirano, delajte "na roke" (splača se vam potruditi, saj bomo to večkrat rabili).

```python
import numpy as np

# Vrednosti neznank nastavimo na 0, tako se v A in b ne bodo pojavljale
neznanke = [P1, P2, P3, P4, P5, P6]
subst = {P1: 0, P2: 0, P3: 0, P4: 0, P5:0, P6: 0}

def getA_b(podatki = {alpha: pi/4, beta: pi/4, F1: 18000, F2: 12000}):
    A = np.asarray([[eq.args[0].subs(podatki).evalf().coeff(p) for p in neznanke] for eq in eqs])
    b = -np.asarray([eq.args[0].subs(podatki).subs(subst).evalf() for eq in eqs])
    return A, b

```

Nekaj komentarjev na zgornjo funkcijo:

* Najprej iz sistema eqs z izpeljevanjem preberemo posamezno enačbo, ki jo poimenujemo eq.
* Ko izpeljujemo b vstavimo podatke in vse vrednosti za *Pi* kot 0, preostale številke pa množimo z -1, ker smo jih dali na drugo stran enačbe.
* *eq.args[0]* pomeni, da uporabimo le zapis na levi strani enačbe, na desni strani je 0.
* Ukaz *.coeff(p)* poišče vse koeficiente, ki se nahajajo pred trenutno neznanako "p".
* *np.asarray* poskrbi, da se vrednosti shranijo v numerični obliki.

```python
# Uporabimo že kar predpripravljen slovar v funkciji, lahko pa bi napisali poljubnega
A, b = getA_b()
```

```python
A
```

    array([[-1, 1, -0.707106781186548, 0, 0, 0],
           [0, 0, 0.707106781186548, 1, 0, 0],
           [0, -1, 0, 0, -0.707106781186548, 0],
           [0, 0, 0, 0, 0.707106781186548, 0],
           [0, 0, 0, 0, 0.707106781186548, -1],
           [0, 0, 0, -1, -0.707106781186548, 0]], dtype=object)

```python
b
```

    array([0, 18000.0000000000, 0, 12000.0000000000, 0, 0], dtype=object)

```python
# Rešimo sistem
P = np.linalg.solve(A, b)
P
```

    array([-42000.        , -12000.        ,  42426.40687119, -12000.        ,
            16970.56274848,  12000.        ])

pypinm-master/vprasanja za razmislek/Vaja 6 - polovica.ipynb


# Emission of Thermal Bremsstrahlung by a Maxwellian Plasma

[thermal_bremsstrahlung]: ../../api/plasmapy.formulary.radiation.thermal_bremsstrahlung.rst#plasmapy.formulary.radiation.thermal_bremsstrahlung

The [radiation.thermal_bremsstrahlung][thermal_bremsstrahlung] function calculates the bremsstrahlung spectrum emitted by the collision of electrons and ions in a thermal (Maxwellian) plasma. This function calculates this quantity in the Rayleigh-Jeans limit where $\hbar\omega \ll k_B T_e$. In this regime, the power spectrum of the emitted radiation is

\begin{equation}
\frac{dP}{d\omega} = \frac{8 \sqrt{2}}{3\sqrt{\pi}} \bigg ( \frac{e^2}{4 \pi \epsilon_0} \bigg )^3 \bigg ( m_e c^2 \bigg )^{-\frac{3}{2}} \bigg ( 1 - \frac{\omega_{pe}^2}{\omega^2} \bigg )^\frac{1}{2} \frac{Z_i^2 n_i n_e}{\sqrt{k_B T_e}} E_1(y)
\end{equation}

where $w_{pe}$ is the electron plasma frequency and $E_1$ is the exponential integral

\begin{equation}
E_1 (y) = - \int_{-y}^\infty \frac{e^{-t}}{t}dt
\end{equation}

and y is the dimensionless argument

\begin{equation}
y = \frac{1}{2} \frac{\omega^2 m_e}{k_{max}^2 k_B T_e}
\end{equation}

where $k_{max}$ is a maximum wavenumber arising from binary collisions approximated here as

\begin{equation}
k_{max} = \frac{1}{\lambda_B} = \frac{\sqrt{m_e k_B T_e}}{\hbar}
\end{equation}

where $\lambda_B$ is the electron de Broglie wavelength. In some regimes other values for $k_{max}$ may be appropriate, so its value may be set using a keyword. Bremsstrahlung emission is greatly reduced below the electron plasma frequency (where the plasma is opaque to EM radiation), so these expressions are only valid in the regime $w < w_{pe}$.

```python
%matplotlib inline

import astropy.constants as const
import astropy.units as u
import matplotlib.pyplot as plt
import numpy as np

from plasmapy.formulary.radiation import thermal_bremsstrahlung
```

Create an array of frequencies over which to calculate the bremsstrahlung spectrum and convert these frequencies to photon energies for the purpose of plotting the results. Set the plasma density, temperature, and ion species.

```python
frequencies = np.arange(15, 16, 0.01)
frequencies = (10 ** frequencies) / u.s

energies = (frequencies * const.h.si).to(u.eV)

ne = 1e22 * u.cm ** -3
Te = 1e2 * u.eV
ion_species = "C-12 4+"
```

Calculate the spectrum, then plot it.

```python
spectrum = thermal_bremsstrahlung(frequencies, ne, Te, ion_species=ion_species)

print(spectrum.unit)

lbl = "$T_e$ = {:.1e} eV,\n".format(Te.value) + "$n_e$ = {:.1e} 1/cm^3".format(ne.value)
plt.plot(energies, spectrum, label=lbl)
plt.title(f"Thermal Bremsstrahlung Spectrum")
plt.xlabel("Energy (eV)")
plt.ylabel("Power Spectral Density (W s/m^3)")
plt.legend()
plt.show()
```

The power spectrum is the power per angular frequency per volume integrated over $4\pi$ sr of solid angle, and therefore has units of watts / (rad/s) / m$^3$ * $4\pi$ rad = W s/m$^3$.

```python
spectrum = spectrum.to(u.W * u.s / u.m ** 3)
spectrum.unit
```

This means that, for a given volume and time period, the total energy emitted can be determined by integrating the power spectrum

```python
t = 5 * u.ns
vol = 0.5 * u.cm ** 3
dw = 2 * np.pi * np.gradient(frequencies)  # Frequency step size
total_energy = (np.sum(spectrum * dw) * t * vol).to(u.J)
print("Total Energy: {:.2e} J".format(total_energy.value))
```


# Neural Nets
Version 0.5, in `nn`

# Note:
This is the source for `nn.py`, the foundation library of these neural net experiments. It contains a simple library, and a quantity, never sufficient, of test code, guarded by `if __name__ = '__main__'`. It makes various noises as it runs, but should not fail any assertsSee the bottom of the file for the procedure to produce the importable library file `nn.py`.

Should do [Working efficiently with jupyter lab](https://florianwilhelm.info/2018/11/working_efficiently_with_jupyter_lab/)

```python
import numpy as np
```

A network built of components which:
1. accept an ordered set of reals (we'll use `numpy.array`, and  call them vectors) at the input port and produce another at the output port - this is forward propagation. ${\displaystyle f\colon \mathbf {R} ^{n}\to \mathbf {R} ^{m}}$
1. accept an ordered set of reals at the output port, representing the gradient of the loss function at the output, and produce the gradient of the loss function at the input port - this is back propagation, aka backprop. ${\displaystyle b\colon \mathbf {R} ^{m}\to \mathbf {R} ^{n}}$
1. from the gradient of the loss function at the output, calculate the partial of the loss function w.r.t the internal parameters ${\displaystyle \frac{\partial E}{\partial w} }$
1. accept a scalar $\eta$ to control the adjustment of internal parameters. _Or is this effected by scaling the loss gradient before passing? YES_
1. update internal parameters ${\displaystyle w \leftarrow w - \eta \frac{\partial E}{\partial w} }$

```python
class Layer:
    def __init__(self):
        pass

    def __call__(self, x):
        """Compute response to input"""
        raise NotImplementedError

    def backprop(self, output_delE):
        """Use output error gradient to adjust internal parameters, return gradient of error at input"""
        raise NotImplementedError

    def state_vector(self):
        """Provide the layer's learnable state as a vector"""
        raise NotImplementedError

    def set_state_from_vector(self, sv):
        """Set the layer's learnable state from a vector"""
        raise NotImplementedError
```

A network built of a cascade of layers:

```python
class Network:
    version_string = '0.5'

    def __init__(self):
        self.layers = []
        self.eta = 0.1 #FIXME

    def extend(self, net):
        self.layers.append(net)
        return self

    def __call__(self, input):
        v = input
        for net in self.layers:
            v = net(v)
        return v

    def learn(self, facts, eta=None):
        eta = eta or self.eta
        for x, ideal in facts:
            y = self(x)
            e = y - ideal
            egrad = e * eta / e.shape[0]
            for net in reversed(self.layers):
                egrad = net.backprop(egrad)
        #loss = float(e.dot(e.T))/2.0
        loss = np.einsum('...ij,...ij', e, e) / (2.0 * e.shape[0])
        self.eta = eta
        return loss

    def losses(self, facts):
        return [np.einsum('...ij,...ij', e, e) / (2.0 * e.shape[0]) \
                for e in (self(x) - ideal for x, ideal in facts)]

    def state_vector(self):
        """Provide the network's learnable state as a vector"""
        return np.concatenate([layer.state_vector() for layer in self.layers])

    def set_state_from_vector(self, sv):
        """Set the layer's learnable state from a vector"""
        i = 0
        for layer in self.layers:
            lsvlen = len(layer.state_vector())
            layer.set_state_from_vector(sv[i:i+lsvlen])
            i += lsvlen
        return self
```

___

## Useful Layers

### Identify

```python
class IdentityLayer(Layer):
    def __call__(self, x):
        return x

    def backprop(self, output_delE):
        return output_delE

    def state_vector(self):
        return np.array([])

    def set_state_from_vector(self, sv):
        pass
```

### Affine
A layer that does an [affine transformation](https://mathworld.wolfram.com/AffineTransformation.html) aka affinity, which is the classic fully-connected layer with output offsets.

$$ \mathbf{M} \mathbf{x} + \mathbf{b} = \mathbf{y} $$
where
$$
\mathbf{x} = \sum_{j=1}^{n} x_j \mathbf{\hat{x}}_j \\
\mathbf{b} = \sum_{i=1}^{m} b_i \mathbf{\hat{y}}_i \\
\mathbf{y} = \sum_{i=1}^{m} y_i \mathbf{\hat{y}}_i
$$
and $\mathbf{M}$ can be written
$$
\begin{bmatrix}
    m_{1,1} & \dots & m_{1,n} \\
    \vdots & \ddots & \vdots \\
    m_{m,1} & \dots & m_{m,n}
\end{bmatrix} \\
$$

#### Error gradient back-propagation
$$
\begin{align}
 \frac{\partial loss}{\partial\mathbf{x}}
  &= \frac{\partial loss}{\partial\mathbf{y}} \frac{\partial\mathbf{y}}{\partial\mathbf{x}} \\
  &= \mathbf{M}^\mathsf{T}\frac{\partial loss}{\partial\mathbf{y}}
\end{align}
$$

#### Parameter adjustment
$$
 \frac{\partial loss}{\partial\mathbf{M}}
 = \frac{\partial loss}{\partial\mathbf{y}} \frac{\partial\mathbf{y}}{\partial\mathbf{M}}
 = \frac{\partial loss}{\partial\mathbf{y}} \mathbf{x} \\
 \frac{\partial loss}{\partial\mathbf{b}}
 = \frac{\partial loss}{\partial\mathbf{y}} \frac{\partial\mathbf{y}}{\partial\mathbf{b}}
 = \frac{\partial loss}{\partial\mathbf{y}}
$$

#### Adapting to `numpy`

In `numpy` it is more convenient to use row vectors, particularly for calculating the transform on multiple inputs in one operation. We use the identity $ \mathbf{M} \mathbf{x} = (\mathbf{x} \mathbf{M}^\mathsf{T})^\mathsf{T}.$ To avoid cluttering names, we will use `M` in the code below to hold $\mathbf{M}^\mathsf{T}$.

```python
class AffineLayer(Layer):
    """An affine transformation, which is the classic fully-connected layer with offsets.

    The layer has n inputs and m outputs, which numbers must be supplied
    upon creation. The inputs and outputs are marshalled in numpy arrays, 1-D
    in the case of a single calculation, and 2-D when calculating the outputs
    of multiple inputs in one call.
    If called with 1-D array having shape == (n,), e.g numpy.arange(n), it will
    return a 1-D numpy array of shape (m,).
    If called with a 2-D numpy array, input shall have shape (k,n) and will return
    a 2-D numpy array of shape (k,m), suitable as input to a subsequent layer
    that has input width m.
    """
    def __init__(self, n, m):
        self.M = np.empty((n, m))
        self.b = np.empty(m)
        self.randomize()

    def randomize(self):
        self.M[:] = np.random.randn(*self.M.shape)
        self.b[:] = np.random.randn(*self.b.shape)

    def __call__(self, x):
        self.input = x
        self.output = x @ self.M + self.b
        return self.output

    def backprop(self, output_delE):
        input_delE = output_delE @ self.M.T
        o_delE = np.atleast_2d(output_delE)
        self.M -= np.einsum('...ki,...kj->...ji', o_delE, np.atleast_2d(self.input))
        self.b -= np.sum(o_delE, 0)
        return input_delE

    def state_vector(self):
        return np.concatenate((self.M.ravel(), self.b.ravel()))

    def set_state_from_vector(self, sv):
        """Set the layer's learnable state from a vector"""
        l_M = len(self.M.ravel())
        l_b = len(self.b.ravel())
        self.M[:] = sv[:l_M].reshape(self.M.shape)
        self.b[:] = sv[l_M : l_M + l_b].reshape(self.b.shape)
```

### Map
Maps a scalar function on the inputs, for e.g. activation layers.

```python
class MapLayer(Layer):
    """Map a scalar function on the input taken element-wise"""
    def __init__(self, fun, dfundx):
        self.vfun = np.vectorize(fun)
        self.vdfundx = np.vectorize(dfundx)

    def __call__(self, x):
        self.input = x
        return self.vfun(x)

    def backprop(self, output_delE):
        input_delE = self.vdfundx(self.input) * output_delE
        return input_delE

    def state_vector(self):
        return np.array([])

    def set_state_from_vector(self, sv):
        pass
```

---

# Tests
*Incomplete* \
Also `unittest` the `.py` version with a separate test script, see `test-nn_v3.py`.

Make a few test arrays:

```python
if __name__ == '__main__':
    one_wide = np.atleast_2d(np.arange(1*4)).reshape(-1,1)
    print(f"one_wide is:\n{one_wide}")
    two_wide = np.arange(2*4).reshape(-1,2)
    print(f"two_wide is:\n{two_wide}")
    three_wide = np.arange(3*4).reshape(-1,3)
    print(f"three_wide is:\n{three_wide}\n")
```

## Tooling for Testing

```python
if __name__ == '__main__':
    import sympy

    class VC():
        def grad(f, x, eps=1e-6):
            #epsihat = np.eye(x.size) * eps
            epsihat = np.eye(x.shape[-1]) * eps
            yp = np.apply_along_axis(f, 1, x + epsihat)
            ym = np.apply_along_axis(f, 1, x - epsihat)
            return (yp - ym)/(2 * eps)

        def tensor_grad(f, x, eps=1e-6):
            return np.apply_along_axis(lambda v: VC.grad(f, v, eps), 1, x)

    def closenuf(a, b, tol=0.001):
        return np.allclose(a, b, rtol=tol)

    def arangep(n, starting_index=0):
        sympy.sieve.extend_to_no(starting_index + n)
        return np.array(sympy.sieve._list[starting_index:starting_index + n])
```

```python
#VC.grad(lambda x:x**2, three_wide[1])
```

```python
#VC.tensor_grad(lambda x:x**2, three_wide)
```

---

Input to a layer can be a single (row) vector, or a vertical stack of row vectors,
a 2-d array that resembles a matrix. We need to test each layer class with both single and stacked input.

## Identity layer

```python
if __name__ == '__main__':
    iL = IdentityLayer()

    # It's transparent from input to output
    assert np.equal(iL(np.arange(5)), np.arange(5)).all()
    assert (iL(three_wide) == three_wide).all()

    # It back-propagates the loss gradient without alteration
    assert np.equal(iL.backprop(np.arange(7)), np.arange(7)).all()
    assert (iL.backprop(three_wide) == three_wide).all()

    # It works for stacked input
    # (see above)
```

## Map layer

#### Test single vector input behavior

```python
if __name__ == '__main__':
    mL = MapLayer(lambda x:x**2, lambda d:2*d)

    # It applies the forward transformation
    assert np.equal(mL(np.array([-2,1,3])), np.array([4,1,9])).all()

    # It back-propagages the loss gradient
    x = np.array([1,2,2])
    y = mL(x)

    # for loss function, use L2-distance from some ideal
    # (divided by 2, for convenient gradient = error)
    ideal = np.array([2,3,5])
    loss = lambda v: (v - ideal).dot(v - ideal) / 2.0
    loss_at_y = loss(y)
    print(f"x = {x}, y = {y}, loss at y = {loss_at_y}")

    # find numerical gradient of loss function at y, the layer output
    grad_y = VC.grad(loss, y)
    print(f"∇𝑙𝑜𝑠𝑠(𝑦) = {grad_y}")

    # find the numerical gradient of the loss w.r.t. the input of the layer
    grad_x = VC.grad(lambda x:loss(mL(x)), x)
    print(f"∇𝑙𝑜𝑠𝑠(𝑥) = {grad_x}")

    # The backprop method does the same
    _ = mL(x) # Make sure the last x is in the right place
    in_delE = mL.backprop(grad_y)
    print(f"backprop({grad_y}) = {in_delE}")
    assert closenuf(in_delE, grad_x)

    # The backprop operation did not change the behavior
    assert np.equal(mL(x), y).all()
```

#### Test stacked-vectors input:

```python
if __name__ == '__main__':
    mL = MapLayer(lambda x:x**2, lambda d:2*d)

    two_wide_sq = np.array([[ 0,  1],
                            [ 4,  9],
                            [16, 25],
                            [36, 49]])
    # It applies the forward transformation
    assert np.equal(mL(two_wide), two_wide_sq).all()

    # It back-propagages the loss gradient
    x = two_wide
    y = mL(x)

    # for loss function, use L2-distance from some ideal
    # (divided by 2, for convenient gradient = error)
    ideal = two_wide * 2 + 11
    #print(y - ideal)
    #loss = lambda v: (v - ideal).dot(v - ideal) / 2.0
    loss = lambda v: np.einsum('ij,ij', v-ideal, v-ideal) / (2 * v.shape[0])
    loss_at_y = loss(y)
    print(f"x =\n{x}\ny =\n{y}, loss = {loss_at_y}\n")

    # find numerical gradient of loss function at y, the layer output
    grad_y = VC.tensor_grad(loss, y)
    print(f"∇𝑙𝑜𝑠𝑠(𝑦) = {grad_y}\n")

    # find the numerical gradient of the loss w.r.t. the input of the layer
    grad_x = VC.tensor_grad(lambda x:loss(mL(x)), x)
    print(f"∇𝑙𝑜𝑠𝑠(𝑥) = {grad_x}\n")

    # The backprop method does the same
    _ = mL(x) # Make sure the last x is in the right place
    in_delE = mL.backprop(grad_y)
    print(f"backprop({grad_y}) =\n{in_delE}")
    assert closenuf(in_delE, grad_x)

    # The backprop operation did not change the behavior
    assert np.equal(mL(x), y).all()
```

## Affine layer

#### Test single vector input behavior

Test, for single input-vector operations:
* input and output widths
* state vector setting and getting
* forward calculation

```python
if __name__ == '__main__':
    # Affine
    a = AffineLayer(2,3)

    # The input and output widths are correct
    assert a(np.arange(2)).shape == (3,)

    # Its internal state can be set
    a.set_state_from_vector(np.arange(9))
    # and read back
    assert (a.state_vector() == np.arange(9)).all()
    # NOTE: The two assertions below are commented out because they depend
    # on white-box knowledge, and are duplicative of other tests
    #assert np.equal(a.M, np.array([[0, 1, 2],
    #                               [3, 4, 5]])).all()
    #assert np.equal(a.b, np.array([6, 7, 8])).all()

    # Its internal state observed using numerical gradient is correct
    x = np.random.rand(2)
    y = a(x)
    dydx = VC.grad(a, x)
    b = y - x.dot(dydx)
    #print(dydx, b)
    #print(dydx, np.arange(6).reshape(2,-1))
    assert closenuf(dydx, np.arange(6).reshape(2, -1))
    #print(b, np.arange(6, 9))
    assert closenuf(b, np.arange(6, 9))

    # It performs a single-input forward calculation correctly
    x = np.array([2, 1])
    y = a(x)
    #print(f"a.M is:\n{a.M}\na.b is {a.b}\nx is: {x}\ny is: {y}\n")
    assert (y == np.array([9, 13, 17])).all()

    # It performs a different single-input forward calculation correctly
    a.set_state_from_vector(np.array([ 2,  3,  5,  7, 11, 13, 17, 19, 23]))
    x = np.array([[29, 31]])
    y = a(x)
    assert (y == np.array([[292, 447, 571]])).all()
```

Test, for single input-vector operations:
* back-propagation of the loss gradient
* learning (change in forward function) from the back-prop operation

```python
if __name__ == '__main__':
    # Affine
    a = AffineLayer(2,3)
    a.set_state_from_vector(np.arange(9))

    # Doing a single-input-vector calculation
    x = np.array([2, 1])
    y = a(x)
    assert np.equal(y, np.array([9, 13, 17])).all()

    # It back-propagages the loss gradient
    ideal = np.array([11,12,10])
    loss = lambda v: (v - ideal).dot(v - ideal) / 2.0
    loss_at_y = loss(y)
    print(f"x = {x}, y = {y}, loss = {loss_at_y}")
    grad_y = VC.grad(loss, y)
    print(f"∇𝑙𝑜𝑠𝑠(𝑦) = {grad_y}")
    grad_x = VC.grad(lambda x:loss(a(x)), x)
    print(f"∇𝑙𝑜𝑠𝑠(𝑥) = {grad_x}")

    # Back-propagate the loss gradient from layer output to input
    _ = a(x) # Make sure the last x is in the right place
    out_delE = grad_y * 0.1 # Backprop one-tenth of the loss gradient
    in_delE = a.backprop(out_delE)
    print(f"backprop({out_delE}) = {in_delE}")

    # The loss gradient back-propagated to the layer input is correct
    assert closenuf(in_delE / 0.1, grad_x)

    # And how did the learning affect the layer?
    print(f"Now a({x}) = {a(x)}, loss = {loss(a(x))}")
    print(f"state_vector is {a.state_vector()}")
    # FIXME: Check the change is correct
```

#### Test batch operations

Test, for batch operations:
* input and output widths
* forward calculation

```python
if __name__ == '__main__':
    # Affine
    a = AffineLayer(2,3)
    a.set_state_from_vector(np.arange(9))

    # The input and output widths for the forward calculation are correct
    x = two_wide
    y = a(two_wide)
    assert y.shape[0] == x.shape[0]
    assert y.shape[1] == 3

    # The input and output widths for the backprop calculation are correct
    bp = a.backprop(three_wide * 0.001)
    assert bp.shape[0] == three_wide.shape[0]
    assert bp.shape[1] == x.shape[1]

    # The forward calculation is correct (in at least two instances)
    a.set_state_from_vector(np.arange(9))
    x = np.array([[0, 1],
                  [2, 3],
                  [4, 5],
                  [6, 7]])
    assert (a(x) == np.array([[ 9, 11, 13],
                              [15, 21, 27],
                              [21, 31, 41],
                              [27, 41, 55]])).all()
    #print(f"a.M is:\n{a.M}\na.b is {a.b}\nx is: {x}\ny is: {y}")
    a.set_state_from_vector(np.array([ 2,  3,  5,  7, 11, 13, 17, 19, 23]))
    y = a(x)
    #print(f"x is: {x}\ny is: {y}")
    assert (y == np.array([[ 24,  30,  36],
                           [ 42,  58,  72],
                           [ 60,  86, 108],
                           [ 78, 114, 144]])).all()
```

Test, for batch operations:
* back-propagation of the loss gradient
* learning (change in forward function) from the back-prop operation

```python
if __name__ == '__main__':
    # Affine
    a = AffineLayer(2,3)
    a.set_state_from_vector(np.arange(9))
    x = np.array([[0, 1],
                  [2, 3],
                  [4, 5],
                  [6, 7]])
    y = a(x)

    # It back-propagages the loss gradient

    # for loss function, use L2-distance from some ideal
    # (divided by 2, for convenient gradient = error)
    ideal = x @ arangep(2*3).reshape(2,3) + arangep(3,6) # A known, different parameter setting
    print(f"y - ideal =\n{y - ideal}")
    #loss = lambda v: (v - ideal).dot(v - ideal) / 2.0
    loss = lambda v: np.einsum('ij,ij', v-ideal, v-ideal) / (2 * v.shape[0])
    loss_at_y = loss(y)
    print(f"x =\n{x}\nideal =\n{ideal}\ny =\n{y}, loss = {loss_at_y}\n")

    # find numerical gradient of loss function at y, the layer output
    grad_y = VC.tensor_grad(loss, y)
    print(f"∇𝑙𝑜𝑠𝑠(𝑦) =\n{grad_y}")

    # find the numerical gradient of the loss w.r.t. the input of the layer
    grad_x = VC.tensor_grad(lambda x:loss(a(x)), x)
    print(f"∇𝑙𝑜𝑠𝑠(𝑥) =\n{grad_x}")

    # Back-propagate the loss gradient from layer output to input
    _ = a(x) # Make sure the last x is in the right place
    out_delE = grad_y * 0.01 # Backprop one percent of the loss gradient
    in_delE = a.backprop(out_delE)
    print(f"backprop({out_delE}) = {in_delE}")

    # The loss gradient back-propagated to the layer input is correct
    #assert closenuf(in_delE / 0.1, grad_x)

    # And how did the learning affect the layer?
    print(f"Now a({x}) = {a(x)}, loss = {loss(a(x))}")
    print(f"state_vector is {a.state_vector()}")
    # FIXME: Check the change is correct
```

#### Test batch operations when the affine layer has only one input

Test, for batch operations:
* input and output widths
* forward calculation

```python
if __name__ == '__main__':
    # Affine
    a = AffineLayer(1,3)
    a.set_state_from_vector(np.arange(6))

    # The input and output widths for the forward calculation are correct
    x = one_wide
    y = a(one_wide)
    assert y.shape[0] == x.shape[0]
    assert y.shape[1] == 3

    # The input and output widths for the backprop calculation are correct
    bp = a.backprop(three_wide * 0.001)
    assert bp.shape[0] == three_wide.shape[0]
    assert bp.shape[1] == x.shape[1]

    # The forward calculation is correct (in at least two instances)
    a.set_state_from_vector(np.arange(6))
    x = np.array([[0],
                  [1],
                  [2],
                  [3]])
    assert (y == np.array([[ 3.,  4.,  5.],
                           [ 3.,  5.,  7.],
                           [ 3.,  6.,  9.],
                           [ 3.,  7., 11.]])).all()
    #print(f"a.M is:\n{a.M}\na.b is {a.b}\nx is: {x}\ny is: {y}")
    a.set_state_from_vector(np.array([ 2,  3,  5,  7, 11, 13]))
    y = a(x)
    #print(f"x is: {x}\ny is: {y}")
    assert (a(x) == np.array([[ 7, 11, 13],
                              [ 9, 14, 18],
                              [11, 17, 23],
                              [13, 20, 28]])).all()
```

Test, for batch operations:
* back-propagation of the loss gradient
* learning (change in forward function) from the back-prop operation

```python
if __name__ == '__main__':
    # Affine
    a = AffineLayer(1,3)
    a.set_state_from_vector(np.arange(6))
    x = np.array([[0],
                  [1],
                  [2],
                  [3]])
    y = a(x)
    #print(f"x =\n{x}\ny =\n{y}")

    # It back-propagages the loss gradient

    # for loss function, use L2-distance from some ideal
    # (divided by 2, for convenient gradient = error)
    ideal = x @ arangep(1*3).reshape(1,3) + arangep(3,6) # A known, different parameter setting
    print(f"y - ideal =\n{y - ideal}")
    #loss = lambda v: (v - ideal).dot(v - ideal) / 2.0
    loss = lambda v: np.einsum('ij,ij', v-ideal, v-ideal) / (2 * v.shape[0])
    loss_at_y = loss(y)
    print(f"x =\n{x}\nideal =\n{ideal}\ny =\n{y}, loss = {loss_at_y}\n")

    # find numerical gradient of loss function at y, the layer output
    grad_y = VC.tensor_grad(loss, y)
    print(f"∇𝑙𝑜𝑠𝑠(𝑦) =\n{grad_y}")

    # find the numerical gradient of the loss w.r.t. the input of the layer
    grad_x = VC.tensor_grad(lambda x:loss(a(x)), x)
    print(f"∇𝑙𝑜𝑠𝑠(𝑥) =\n{grad_x}")

    # Back-propagate the loss gradient from layer output to input
    _ = a(x) # Make sure the last x is in the right place
    out_delE = grad_y * 0.01 # Backprop one percent of the loss gradient
    in_delE = a.backprop(out_delE)
    print(f"backprop({out_delE}) = {in_delE}")

    # The loss gradient back-propagated to the layer input is correct
    #assert closenuf(in_delE / 0.1, grad_x)

    # And how did the learning affect the layer?
    print(f"Now a({x}) = {a(x)}, loss = {loss(a(x))}")
    print(f"state_vector is {a.state_vector()}")
    # FIXME: Check the change is correct
```

## Network

### Network assembly

The simplest, the empty network, does identity:

```python
if __name__ == '__main__':
    net = Network()
    assert all(x == net(x) for x in [0, 42, 'cows in trouble'])
    assert all((x == net(x)).all() for x in [np.arange(7), np.arange(3*4*5).reshape(3,4,5)])
```

A stack of maps composes the operations:

```python
if __name__ == '__main__':
    net = Network()
    net.extend(MapLayer(lambda x: x**3, lambda d: 3*d**2))
    assert all(net(x) == x**3 for x in [0, 42, -3.14])
    net.extend(MapLayer(lambda x: 7-x, lambda d: -1))
    assert all(net(x) == 7-x**3 for x in [0, 42, -3.14])

    # It operates on each element of an input vector separately
    assert (net(np.arange(4)) == 7 - np.arange(4) ** 3).all()
```

A composition of affine transformations

_[to do someday]_

### Network Learning

Test simple batch learning of a single affine layer

```python
if __name__ == '__main__':
    from pprint import pprint

    net = Network()
    a = AffineLayer(2,3)
    a.set_state_from_vector(np.arange(9)) # A well-known initial state
    net.extend(a)
    print(f"\nNet has state {net.state_vector()}")

    x = np.array([[0, 1],
                  [2, 3],
                  [4, 5],
                  [6, 7]])

    # The net wraps the layer
    y = a(x)
    assert (net(x) == y).all()

    # Make the training batch.
    # We use a separate affine layer, initialized differently, to determine the ideal
    t_a = AffineLayer(2,3)
    t_a.set_state_from_vector(arangep(9)) # A known different initial state (of primes)
    ideal = t_a(x)

    fact = (x, ideal)
    print(f"fact is:")
    pprint(fact, indent=1)
    print(f"net(x) =\n{net(x)}")

    net.eta = 0.01
    for i in range(10):
        print(f"net.learn([fact]) = {net.learn([fact])}")
    print(f"net(x) =\n{net(x)}")

    # A simple fact yielder. Since it delivers multiple facts in succession,
    # it is a "facts", aka "batch cluster"
    def fact_ory(fact, n):
        for i in range(n):
            yield fact

    def facts_printer(facts):
        # after Network.learn
        for fact in facts:
            print(f"fact: ")
            pprint(fact)
            x, ideal = fact
            print(f"from which we get:\n\tx={x}\n\tideal={ideal}\n")

    #facts_printer(fact_ory(fact, 5))

    #print(f"list(fact_ory(facts[0], 3)) =\n{list(fact_ory(facts[0], 3))}\n")
    print(f"net.learn(fact_ory(fact,10)) = {net.learn(fact_ory(fact,10))}")
    print(f"net(x) =\n{net(x)}")
    for i in range(1000):
        loss = net.learn(fact_ory(fact,10))
        if loss < 1e-7:
            break
    print(f"did {(i+1)*10} more learnings of fact. Now loss is {loss}")
    print(f"net(x) =\n{net(x)}")

    print(f"net.state_vector() = {net.state_vector()}")

    # The network has learned the target transform
    assert closenuf(net(x), fact[1])

if False: # This section of the test is misconceived. Skip it
    # Save prior results and learn again, with different batch clustering
    prev_run_loss = loss
    prev_y = net(x)
    net.set_state_from_vector(np.arange(9)) # A well-known initial state
    print(f"\nReset net to state {net.state_vector()}")

    # Try multiple batches in each call to Network.learn
    def multibatch_fact_ory(fact, n):
        for i in range(n//2):
            yield fact, fact
    facts_printer(multibatch_fact_ory(fact, 5))

    for i in range(1000):
        loss = net.learn(multibatch_fact_ory(fact,10))
        if loss < 1e-7:
            break
    print(f"did {(i+1)*10} learnings of fact. Now loss is {loss}")
    print(f"net(x) =\n{net(x)}")

    # The results should match exactly
    assert loss == prev_run_loss
    assert (net(x) == prev_y).all()
```

### Test Network.losses

```python
if __name__ == '__main__':
    # Make a network. Leave it with the default identity behavior.
    net = Network()

    x = np.array([[0, 1],
                  [2, 3],
                  [4, 5],
                  [6, 7]])
    ideal = net(x)
    facts = [(x, ideal), (x, ideal-np.array([1,-1])), (x, 2*x)]
    assert (net.losses(facts) == [0, 1, 17.5])

    # Add some layers
    net.extend(AffineLayer(2,3)).extend(MapLayer(np.sin, np.cos)).extend(AffineLayer(3,2))
    # Place it in a known state for test repeatability
    net.set_state_from_vector(np.arange(len(net.state_vector())))
    ideal = net(x)
    facts = [(x, ideal), (x, ideal-np.array([1,-1]))]
    #print(net.losses(facts))
    assert (net.losses(facts) == [0, 1])
```

---

# Publishing

To produce an importable `nn.py`:
1. Save this notebook
1. Uncomment the `jupyter nbconvert` line below
1. Execute it.
1. Comment out the convert again
1. Save the notebook again in that form

```python
###!jupyter nbconvert --to script nn.ipynb
```

    [NbConvertApp] Converting notebook nn.ipynb to script
    [NbConvertApp] Writing 27145 bytes to nn.py

```python

```


# $\text{sinc}(x)$ and Its Derivatives
\begin{align}
\text{sinc}(x) &= \begin{cases}
      \frac{x^4}{120}-\frac{x^2}{6}+1 & |x| \leq \epsilon \\
      \frac{\sin(x)}{x} & \text{otherwise}
   \end{cases}\\
\end{align}

```python
import sys

import numpy
from sympy import *
from sympy.plotting import plot

x = Symbol("x")
x0 = Symbol("x_0")
```

```python
def taylor_series(f, n=5):
    dfs = [f]
    for i in range(n):
        dfs.append(dfs[i].diff())
    return sum([
        dfs[i].subs({"x": x0}) / factorial(i) * (x - x0)**i
        for i in range(len(dfs))
    ])
```

```python
sinc = sin(x) / x
display(sinc)
```

$\displaystyle \frac{\sin{\left(x \right)}}{x}$

```python
sinc_ts = taylor_series(sinc)
display(limit(sinc_ts, x0, 0))
```

$\displaystyle \frac{x^{4}}{120} - \frac{x^{2}}{6} + 1$

```python
x1 = 1e-3
print(1.0 - x1**2 / 6)
print(1.0 - x1**2 * (1 / 6 + x1**2 / 120))
x1 = sys.float_info.epsilon
print(N(x1, 50))
print(f"{N(sqrt(x1), 35):e}")
print(f"{N(sqrt(sqrt(x1)), 25):e}")
```

    0.9999998333333333
    0.999999833333325
    2.2204460492503130808472633361816406250000000000000e-16
    1.4901161193847656250000000000000000e-8
    1.220703125000000000000000e-4

## $\frac{d}{dx} \text{sinc}(x)$
\begin{align}
\text{sinc}'(x) &= \begin{cases}
      0 & x = 0 \\
      \frac{x\cos(x) - \sin(x)}{x^2} & \text{otherwise}
   \end{cases}\\
\end{align}

```python
dsinc = simplify(sinc.diff(x))
display(dsinc)
```

$\displaystyle \frac{x \cos{\left(x \right)} - \sin{\left(x \right)}}{x^{2}}$

```python
dsinc_ts = taylor_series(dsinc)
display(limit(dsinc_ts, x0, 0))
```

$\displaystyle - \frac{x^{5}}{840} + \frac{x^{3}}{30} - \frac{x}{3}$

## $\frac{d^2}{dx^2} \text{sinc}(x)$
\begin{align}
\text{sinc}''(x) &= \begin{cases}
      \frac{-1}{3} & x = 0 \\
      \frac{-x^2\sin(x) - 2x\cos(x) + 2\sin(x)}{x^3} & \text{otherwise}
   \end{cases}\\
\end{align}

```python
ddsinc = simplify(dsinc.diff(x))
display(ddsinc)
```

$\displaystyle \frac{- x^{2} \sin{\left(x \right)} - 2 x \cos{\left(x \right)} + 2 \sin{\left(x \right)}}{x^{3}}$

```python
ddsinc_ts = taylor_series(ddsinc)
display(limit(ddsinc_ts, x0, 0))
```

$\displaystyle - \frac{x^{4}}{168} + \frac{x^{2}}{10} - \frac{1}{3}$

## Derivatives of $sinc(\|x\|)$

```python
def norm(x):
    return sqrt(sum(x**2))
```

```python
X = numpy.array(symbols("x y z"))
sinc_normx = sinc.subs({"x": norm(X)})
display(sinc_normx)
```

$\displaystyle \frac{\sin{\left(\sqrt{x^{2} + y^{2} + z^{2}} \right)}}{\sqrt{x^{2} + y^{2} + z^{2}}}$

## Gradient of $\text{sinc}(\|x\|)$
\begin{align}
\nabla \text{sinc}(\|x\|) &= \frac{\text{sinc}'(\|x\|)}{\|x\|}x \\
\frac{\text{sinc}'(x)}{x} &= \begin{cases}
      -\frac{x^4}{840} + \frac{x^2}{30} - \frac{1}{3}& |x| \leq \epsilon\\
      \frac{x\cos(x)-\sin(x)}{x^3} & \text{otherwise}
   \end{cases}
\end{align}
where $\epsilon = 10^{-4}$.

```python
dsinc_normx = numpy.array(sinc_normx.diff(X))
my_dsinc_normx = dsinc.subs({"x": norm(X)}) / norm(X) * X
for i in range(3):
    assert (simplify((dsinc_normx - my_dsinc_normx)[i]) == 0)
```

```python
dsinc_x = dsinc / x
display(dsinc_x)
```

$\displaystyle \frac{x \cos{\left(x \right)} - \sin{\left(x \right)}}{x^{3}}$

```python
dsinc_x_ts = taylor_series(dsinc_x)
display(limit(dsinc_x_ts, x0, 0))
```

$\displaystyle - \frac{x^{4}}{840} + \frac{x^{2}}{30} - \frac{1}{3}$

## Hessian of $\text{sinc}(\|x\|)$
\begin{align}
\nabla^2 \text{sinc}(\|x\|) &= \left(\frac{\text{sinc}''(\|x\|)}{\|x\|^2} - \frac{\text{sinc}'(\|x\|)}{\|x\|^3}\right)xx^T + \frac{\text{sinc}'(\|x\|)}{\|x\|}I \\
\frac{\text{sinc}''(\|x\|)}{\|x\|^2} - \frac{\text{sinc}'(\|x\|)}{\|x\|^3} &= \begin{cases}
      \frac{x^{4}}{7560}-\frac{x^{2}}{210}+\frac{1}{15} & |x| \leq \epsilon \\
      \frac{-x^{2} \sin (x)-3 x \cos (x)+3 \sin (x)}{x^{5}} & \text{otherwise}
   \end{cases}
\end{align}
where $\epsilon = 0.1$.

```python
ddsinc_normx = numpy.array(Matrix(Matrix(dsinc_normx).diff(X)))
my_ddsinc_normx = ((ddsinc.subs({"x": norm(X)}) / norm(X)**2 -
                    dsinc.subs({"x": norm(X)}) / norm(X)**3) *
                   X.reshape(3, 1) @ X.reshape(1, 3) +
                   dsinc.subs({"x": norm(X)}) / norm(X) * numpy.eye(3))
for i in range(3):
    for j in range(3):
        assert (simplify(ddsinc_normx[i, j][0] - my_ddsinc_normx[i, j]) == 0)
```

```python
ddsinc_x = simplify(ddsinc.subs({"x": x}) / x**2 - dsinc.subs({"x": x}) / x**3)
display(ddsinc_x)
```

$\displaystyle \frac{- x^{2} \sin{\left(x \right)} - 3 x \cos{\left(x \right)} + 3 \sin{\left(x \right)}}{x^{5}}$

```python
ddsinc_x_ts = taylor_series(ddsinc_x)
display(limit(ddsinc_x_ts, x0, 0.0))
```

$\displaystyle \frac{x^{4}}{7560} - \frac{x^{2}}{210} + \frac{1}{15}$


# Application of First Monotonicity Principle to the optimization of MRAV
*Created by Aitor Ochotorena (INSA Toulouse), Toulouse, France.*

Based on the differentiability of continuous mathematical functions, in this Notebook we present a guide to **reduce the excess of constraints** in optimization problems.

The standard expression of an optimization problem has the following form:

<math>\begin{align}
&\underset{\mathbf{x}}{\operatorname{minimize}}& & f(\mathbf{x}) \\
&\operatorname{subject\ to}
& &g_i(\mathbf{x}) \leq 0, \quad i = 1, \dots, m \\
&&&h_i(\mathbf{x}) = 0, \quad i = 1, \dots, p,
\end{align}</math>

where $x \in \mathbb{R}^n$ is the optimization variable, the functions $f, g_1, \ldots, g_m$ are convex, and the functions $h_1, \ldots, h_p$ are equality constraints.
In this notation, the function $f$ is the objective function of the problem, and the functions $g_i$ and $h_i$are referred to as the constraint functions.

**Find out which set of inequality constraints can be turned to equality ones has an enormous importance to reduce the complexity of the problem and the calculation time.**

The applicacion of the 'First Monotonicity Principle',
> P. Y. Papalambros, D. J. Wilde, Principles of Optimal Design, Cambridge University Press, 2018.

permits to evaluate if the objective meets the minimum values w.r.t. the considered variable when the constraint acts an equality. We refer to such constraints as *active constraints* and they are identified by studying the monotonicity behaviour of both the objective and the constraint. In a well constrainted objective function, every (strictly) increasing (decreasing) variable is bounded below (above) by at least one active constraint. For a well constrained minimization problem, there exist at least one $x$ that satisfies the optimality conditions:

<math>\begin{align}
$\displaystyle (\frac{\partial{ f}}{\partial{ x_i}})_*+\sum_{j}\mu_j(\frac{\partial{ g_j}}{\partial{ x_i}})_*=0$
\end{align}</math>

where $\mu_j \geq 0$.

In the case in which the sign of a variable in the objective function is
uniquely opposite to the sign of the same variable for a single constraint, this
constraint can be turned to active and as equality. If the sign of the variable
is opposite to that of the objective function in several cosntraints, then no
evident statement can be given and in that case, the constraints will be left
as inequalities.

*Representative plot of an optimization problem with three constraints. Two of them: $g_1$ and $g_2$ are acting as active constraints, since they bound the function objective as equality.*

## 1. Import Sizing Code from a .py code

Here Python reads the sizing code with all equations used for the drone saved in the folder :
``` '.\SizingCode'```. Our file is called: ``` DroneSystem.py```.
Design variables are defined as symbols using the symbolic calculation of Sympy:
**(This part is specific for every sizing code)**

```python
import sys
sys.path.insert(0, 'SizingCode')

from DroneSystem import *
```

## 2. Problem definition

Once the equations are imported from the .py file, we define here the main parameters for the optimization problem: objective ```Obj```, design variables ```Vars```, constraints ```Const``` and bounds ```bounds```. We work in forwards on with symbolic mathematics (SymPy):
**(This part is specific for every sizing code)**

- Objective:

```python
Obj=Mtotal_final
```

- Design Variables:

```python
Vars=[ Mtotal,ND,Tmot,Ktmot,P_esc,V_bat,C_bat,beta, J, D_ratio, Lbra,Dout]
```

- Constraints:

```python
Const=[
    -Tmot_max+Qpro_max ,
    -Tmot_max+Qpro_cl,
    -Tmot+Qpro_hover,
    -V_bat+Umot_max,
    -V_bat+Umot_cl,
    -V_bat+Vesc,
    -V_bat*Imax+Umot_max*Imot_max*Npro/0.95,
    -V_bat*Imax+Umot_cl*Imot_cl*Npro/0.95,
    -P_esc+P_esc_max,
    -P_esc+P_esc_cl,
    -J*n_pro_cl*Dpro+V_cl,
    +J*n_pro_cl*Dpro-V_cl-0.05,
    -NDmax+ND,
    -NDmax+n_pro_cl*Dpro,
    -Lbra+Dpro/2/(math.sin(pi/Narm)),
    (-Sigma_max+Tpro_max*Lbra/(pi*(Dout**4-(D_ratio*Dout)**4)/(32*Dout)))
]
```

- Bounds:

```python
bounds=[(0,100),#M_total
       (0,105000/60*.0254),#ND
       (0.01,10),#Tmot
       (0,1),#Ktmot
       (0,1500),#P_esc
       (0,150),#V_bat
       (0,20*3600),#C_bat
       (0.3,0.6),#beta
       (0,0.5),#J
       (0,0.99),#D_ratio
       (0.01,1),#Lb
       (0.001,0.1),#Dout
       (1,15),#Nred
        ]
```

## 3. Monotonicity algorithm

The next step is to evaluate the monotonicity of the functions. This will be done through the study of the differentiability of the functions. We will follow this procedure: A constraint is passed to ```is_increasing()``` or ```is_decreasing()```, which return a predicate of ```lambda x: x > 0``` or ```lambda x: x < 0``` respectively. This method calls ```compute_min_and_max``` which differentiates the constraint with respect to the desired variable, creates a series of random points defined within the bounds and substitute such values into the derivative of the constraint. If the predicate match the output, this method returns a True

To run the design of experiments satisfactorily, update pyDOE: `pip install --upgrade pyDOE`:

This algorithm is saved under the file `Monotonicity.ipynb` :

**(This part is reusable)**

```python
# Note the python import here
import reuse, sys

# This is the Ipython hook
sys.meta_path.append(reuse.NotebookFinder())
import Monotonicity
```

## 3. Construction of table of Monotonicity

For each constraint and variable we will study the monotonicity behaviour calling the previous methods defined. If the constraint has an increasing behaviour, a ```+``` will be printed, in case where it is decreasing ```-```, and in the case where both increases and decreases, a ```?``` is displayed. The objective will be studied as well.

**(This part is reusable)**

```python
import pandas as pd

M=[["" for x in Vars] for y in Const];
ObjVector=["" for x in Vars];

print('Monotonicity Analysis for the constraints w.r.t. the following variables:')

for Cnumber,C in enumerate(Const): #loop for constraints

    print('Constant %d out of %d' %(Cnumber+1,len(Const)))

    for Anumber,A in enumerate(Vars): #loop for variables
        #print(C,A,bounds,Vars)
        if Monotonicity.is_increasing(C,A,bounds,Vars)=='ZERO':
            M[Cnumber][Anumber]=' '
        elif Monotonicity.is_increasing(C,A,bounds,Vars):
            print('* %s is increasing' %A)
            M[Cnumber][Anumber]='+'
        elif Monotonicity.is_decreasing(C,A,bounds,Vars):
            print('* %s is decreasing' %A)
            M[Cnumber][Anumber]='-'
        else: M[Cnumber][Anumber]='?'
print('\n')
print('Monotonicity Analysis for the objective w.r.t. the following variables:')
for Anumber,A in enumerate(Vars):
    if Monotonicity.is_increasing(Obj,A,bounds,Vars)=='ZERO':
        ObjVector[Anumber]=' '
    elif Monotonicity.is_increasing(Obj,A,bounds,Vars):
        print('* %s is increasing' %A)
        ObjVector[Anumber]='+'
    elif Monotonicity.is_decreasing(Obj,A,bounds,Vars):
        print('* %s is decreasing' %A)
        ObjVector[Anumber]='-'
    else: ObjVector[Anumber]='? '

M.append(ObjVector)
```

    Monotonicity Analysis for the constraints w.r.t. the following variables:
    Constant 1 out of 16
    * Mtotal is increasing
    * ND is decreasing
    * Tmot is decreasing
    * beta is increasing
    Constant 2 out of 16
    * Mtotal is increasing
    * ND is decreasing
    * Tmot is decreasing
    * J is increasing
    Constant 3 out of 16
    * Mtotal is increasing
    * ND is decreasing
    * Tmot is decreasing
    * beta is increasing
    Constant 4 out of 16
    * Tmot is decreasing
    * Ktmot is increasing
    * V_bat is decreasing
    * beta is increasing
    Constant 5 out of 16
    * Tmot is decreasing
    * Ktmot is increasing
    * V_bat is decreasing
    * J is increasing
    Constant 6 out of 16
    * P_esc is increasing
    * V_bat is decreasing
    Constant 7 out of 16
    * V_bat is decreasing
    * C_bat is decreasing
    * beta is increasing
    Constant 8 out of 16
    * V_bat is decreasing
    * C_bat is decreasing
    * J is increasing
    Constant 9 out of 16
    * Mtotal is increasing
    * ND is decreasing
    * Tmot is increasing
    * Ktmot is decreasing
    * P_esc is decreasing
    * V_bat is increasing
    * beta is increasing
    Constant 10 out of 16
    * Mtotal is increasing
    * ND is decreasing
    * Tmot is increasing
    * Ktmot is decreasing
    * P_esc is decreasing
    * V_bat is increasing
    * J is increasing
    Constant 11 out of 16
    * Mtotal is increasing
    * ND is decreasing
    * J is decreasing
    Constant 12 out of 16
    * Mtotal is decreasing
    * ND is increasing
    * J is increasing
    Constant 13 out of 16
    * ND is increasing
    Constant 14 out of 16
    * Mtotal is decreasing
    * ND is increasing
    * J is increasing
    Constant 15 out of 16
    * Mtotal is increasing
    * ND is decreasing
    * beta is decreasing
    * Lbra is decreasing
    Constant 16 out of 16
    * Mtotal is increasing
    * D_ratio is increasing
    * Lbra is increasing
    * Dout is decreasing

    Monotonicity Analysis for the objective w.r.t. the following variables:
    * Tmot is increasing
    * P_esc is increasing
    * V_bat is increasing
    * C_bat is increasing
    * beta is decreasing
    * D_ratio is decreasing
    * Lbra is increasing
    * Dout is increasing

Create from np.array a DataFrame:

```python
import pandas as pd
indexcol=[i for i in range(len(Const))]
indexcol.append('Objective')
pd.DataFrame(M, columns=Vars, index=indexcol)
```

      Mtotal
      ND
      Tmot
      Ktmot
      P_esc
      V_bat
      C_bat
      beta
      J
      D_ratio
      Lbra
      Dout

      0
      +
      -
      -

      +

      1
      +
      -
      -

      ?
      +

      2
      +
      -
      -

      +

      3
      ?
      ?
      -
      +

      -

      +

      4
      ?
      ?
      -
      +

      -

      ?
      +

      5

      +
      -

      6
      ?
      ?
      ?
      ?

      -
      -
      +

      7
      ?
      ?
      ?
      ?

      -
      -
      ?
      +

      8
      +
      -
      +
      -
      -
      +

      +

      9
      +
      -
      +
      -
      -
      +

      ?
      +

      10
      +
      -

      ?
      -

      11
      -
      +

      ?
      +

      12

      +

      13
      -
      +

      ?
      +

      14
      +
      -

      -

      -

      15
      +

      +
      +
      -

      Objective
      ?
      ?
      +

      +
      +
      +
      -

      -
      +
      +

Last point is the comparison of every constraint's variable to objective's oneActive constraints decision:

```python
for index,objvalue in enumerate(ObjVector):
    counter=0;
    for j,constvalue in enumerate([i[index] for i in M]):
       # print(objvalue);
       # print(constvalue)
        if objvalue=='+' and constvalue=='-':
            counter+=1;
            x=j #to save the constraint
        elif objvalue=='-' and constvalue=='+':
            counter+=1;
            x=j; #to save the constraint
    if counter==1:
        print('Const %d w.r.t. %s can be eliminated'%(x,Vars[index]))
```

    Const 15 w.r.t. D_ratio can be eliminated
    Const 14 w.r.t. Lbra can be eliminated
    Const 15 w.r.t. Dout can be eliminated


# Evolutionary Dynamics

We will now consider how Game Theory can be used to study evolutionary processes. The main difference is that we now consider not two player games but game with an **infinite** population. The strategies will make up a dynamic population that changes over time.

## Reproduction

[Video](https://youtu.be/kBhoG3pjyG0?list=PLnC5h3PY-znxMsG0TRYGOyrnEO-QhVwLb)

Consider a simple model of population growth: let $x(t)$ denote the size of the population at time $t$ and let as assume that the rate of growth is $a$ per population size:

$$\frac{dx}{dt}=ax$$

Note that from here on we will refer to this rate as a **fitness**.

The solution of this differential equation is:

$$x(t)=x_0e^{at}\text{ where }x_0=x(0)$$

```python
import sympy as sym
sym.init_printing()
x = sym.Function('x')
t, a = sym.symbols('t, a')
sym.dsolve(sym.Derivative(x(t), t) - a * x(t), x(t))
```

(This is exponential growth.)

We can also use scipy to solve this differential equation numerically (relevant for more complex dynamics):

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

from scipy.integrate import odeint

t = np.linspace(0, 10, 100)  # Obtain 100 time points

def dx(x, t, a):
    """Define the derivate of x"""
    return a * x
```

If $a=10>0$:

```python
a = 10
xs = odeint(func=dx, y0=1, t=t, args=(a,))
plt.plot(xs);
```

If $a=-10<0$:

```python
a = -10
xs = odeint(func=dx, y0=1, t=t, args=(a,))
plt.plot(xs);
```

## Selection

[Video](https://youtu.be/ERbQGLLNGYo?list=PLnC5h3PY-znxMsG0TRYGOyrnEO-QhVwLb)

Reproduction alone is not enough to study evolutionary processes. Let us consider a population made up of two types of individuals:

- $x(t)$ denotes the first type;
- $y(t)$ denotes the second type.

Let us assume the same expressions for the as before:

$$\frac{dx}{dt}=ax\qquad\frac{dy}{dt}=by$$

both these population will increase or decrease independantly so there's not much of interest there **but** if we introduce the following:

$$
\rho(t) = \frac{x(t)}{y(t)}
$$

then $\lim_{t\to\infty}\rho(t)$ indicates which type takes over the population over time.

We have:

$$
\frac{d\rho}{dt} = \frac{\frac{dx}{dt}y -  \frac{dy}{dt}x}{y ^ 2} = \frac{xy(a - b)}{y^2}
$$

which gives:

$$
\frac{d\rho}{dt} = (a-b)\rho
$$

which has solution (this is just the same differential equation as the previous section):

$$
\rho(t) = \rho_0e^{(a-b)t}\text{ where }\rho_0=\rho(0)
$$

note that even if both population grow, but one grows faster than the other (eg $a > b$) then the overall population will grow but one will take over:

```python
def drho(rho, t, a, b):
    """Define the derivate of x"""
    return (a - b) * rho

a, b = 10, 5
rhos = odeint(func=drho, y0=1, t=t, args=(a, b))
plt.plot(rhos);
```

## Selection with constant population size

[Video](https://youtu.be/_bsaV5sq6ZU?list=PLnC5h3PY-znxMsG0TRYGOyrnEO-QhVwLb)

Let us consider the case of $x(t) + y(t)=1$: so the case of a constant population size (choosing a constant of 1 is just a question of scale). For this to be possible, the rates need to reduced:

$$\frac{dx}{dt}=x(a - \phi)\qquad\frac{dy}{dt}=y(b - \phi)$$

because $x(t) + y(t)=1$:

$$\frac{dx}{dt} + \frac{dy}{dt} = 0$$

also:

$$\frac{dx}{dt} + \frac{dy}{dt} = ax + by - \phi(x + y)= ax + by - \phi$$

thus $\phi=ax+by$ (this corresponds to the average of the fitness).

Substituting $y=1-x$ we have:

$$\frac{dx}{dt}=x(a - ax-b(1-x))=x(a(1 - x)-b(1-x))$$

giving:

$$\frac{dx}{dt}=x(a-b)(1-x)$$

We do not need to solve this differential equation. There are two stable points:

- $x=0$: no population of first type: no change
- $x=1$: no population of second type: no change

Also:

- $a=b$: if both types have the same fitness: no change

```python
def dxy(xy, t, a, b):
    """
    Define the derivate of x and y.
    It takes `xy` as a vector
    """
    x, y = xy
    phi = a * x + b * y
    return x * (a - phi), y * (b - phi)

a, b = 10, 5
xys = odeint(func=dxy, y0=[.5, .5], t=t, args=(a, b))
plt.plot(xys);
```

```python
a, b = 10, 5
xys = odeint(func=dxy, y0=[1, 0], t=t, args=(a, b))
plt.plot(xys);
```

```python
a, b = 10, 5
xys = odeint(func=dxy, y0=[0, 1], t=t, args=(a, b))
plt.plot(xys);
```

```python
a, b = 5, 5
xys = odeint(func=dxy, y0=[.5, .5], t=t, args=(a, b))
plt.plot(xys);
```


$$
\renewcommand{\DdQq}[2]{{\mathrm D}_{#1}{\mathrm Q}_{#2}}
\renewcommand{\drondt}{\partial_t}
\renewcommand{\drondx}{\partial_x}
\renewcommand{\drondtt}{\partial_{tt}}
\renewcommand{\drondxx}{\partial_{xx}}
\renewcommand{\drondyy}{\partial_{yy}}
\renewcommand{\dx}{\Delta x}
\renewcommand{\dt}{\Delta t}
\renewcommand{\grandO}{{\mathcal O}}
\renewcommand{\density}[2]{\,f_{#1}^{#2}}
\renewcommand{\fk}[1]{\density{#1}{\vphantom{\star}}}
\renewcommand{\fks}[1]{\density{#1}{\star}}
\renewcommand{\moment}[2]{\,m_{#1}^{#2}}
\renewcommand{\mk}[1]{\moment{#1}{\vphantom{\star}}}
\renewcommand{\mke}[1]{\moment{#1}{e}}
\renewcommand{\mks}[1]{\moment{#1}{\star}}
$$

# Analyze your scheme

Two of the biggest problems encountered when starting to use lattice Bolzmann methods are

- what are the physical equations we're trying to solve?
- how to set the parameters of the scheme (equilibrium values, relaxation parameters,...) so that it is stable and solves what you want?

pylbm tries to give you some ideas to solve them.

For the first one, pylbm can give you the first and second order coefficients of your physical equation (in a next release, it will be possible to have also the third and the fourth order terms). To have better results, it is important to keep SymPy symbols in your scheme as long as possible. Thus, we can see the influence of theses parameters on the physical equations.

For the second one, once you know that you have the good physical equation, pylbm allows to check the linear stability region for these parameters arround a given linearized state. We provide widgets inside a notebook or for a Python script to modify easily these parameters and show the result on the figure interactively.

We believe that these two tools will make it easier for as many people as possible to become familiar with the lattice Boltzmann methods and, in the end, allow them to implement their own schemes.

Let's take a simple example to illustrate how it works.

Assume that you want to solve the advection equation for 1D problem

$$
\begin{aligned}
&\drondt u = c \drondx u, && t>0, \quad x\in(0,1),\\
&u(t=0,x) = u_0(x), && x\in(0,1) \\
&u(t,x=0) = u(t,x=1), && t>0.
\end{aligned}
$$

And you already have a lattice Boltzmann scheme that you want to try: the $\DdQq{1}{3}$ scheme given by

* three velocities $v_0=0$, $v_1=1$, and $v_2=-1$, with associated distribution functions $\fk{0}$, $\fk{1}$, and $\fk{2}$,
* the scheme velocity $\lambda$,
* the three moments
  $$ \mk{0}=\sum_{i=0}^{2} \fk{i}, \quad \mk{1}= \sum_{i=0}^{2} v_i \fk{i}, \quad \mk{2}= \frac{1}{2} \sum_{i=0}^{2} v_i^2 \fk{i},$$

  and their equilibrium values $\mke{0}$, $\mke{1}$, and $\mke{2}$,
* and finaly the two relaxation parameters $s_1$ and $s_2$ lying in $[0,2]$.

We can write this scheme into pylbm as

```python
import sympy as sp

# Symbolic variables definitions
U, X = sp.symbols('u, X')
C, LA, S0, S1 = sp.symbols('c, lambda, s_0, s_1', constants=True)

# The D1Q3 LBM scheme
adv_scheme = {
    'dim': 1,
    'scheme_velocity': LA,
    'schemes': [
        {
            'velocities': list(range(3)),
            'conserved_moments': U,
            'polynomials': [1, X, X**2/2],
            'relaxation_parameters': [0., S0, S1],
            'equilibrium': [U, C*U, C**2*U/2],
        },
    ],
    'parameters': {LA: 1,
                   S0: 1.9,
                   S1: 1.9,
                   C: 1},
}
```

Let's create the scheme and look at the information given by pylbm

```python
import pylbm

scheme = pylbm.Scheme(adv_scheme)
```

```python
print(scheme)
```

    +--------------------+
    | Scheme information |
    +--------------------+
        - spatial dimension: 1
        - number of schemes: 1
        - number of velocities: 3
        - conserved moments: [u]

        +----------+
        | Scheme 0 |
        +----------+
            - velocities
                (0: 0)
                (1: 1)
                (2: -1)

            - polynomials

                ⎡1 ⎤
                ⎢  ⎥
                ⎢X ⎥
                ⎢  ⎥
                ⎢ 2⎥
                ⎢X ⎥
                ⎢──⎥
                ⎣2 ⎦

            - equilibria

                ⎡ u  ⎤
                ⎢    ⎥
                ⎢c⋅u ⎥
                ⎢    ⎥
                ⎢ 2  ⎥
                ⎢c ⋅u⎥
                ⎢────⎥
                ⎣ 2  ⎦

            - relaxation parameters

                ⎡0.0⎤
                ⎢   ⎥
                ⎢s₀ ⎥
                ⎢   ⎥
                ⎣s₁ ⎦

        - moments matrices

            ⎡1  1   1 ⎤
            ⎢         ⎥
            ⎢0  λ   -λ⎥
            ⎢         ⎥
            ⎢    2   2⎥
            ⎢   λ   λ ⎥
            ⎢0  ──  ──⎥
            ⎣   2   2 ⎦

        - inverse of moments matrices

            ⎡        -2 ⎤
            ⎢1   0   ───⎥
            ⎢          2⎥
            ⎢         λ ⎥
            ⎢           ⎥
            ⎢    1   1  ⎥
            ⎢0  ───  ── ⎥
            ⎢   2⋅λ   2 ⎥
            ⎢        λ  ⎥
            ⎢           ⎥
            ⎢   -1   1  ⎥
            ⎢0  ───  ── ⎥
            ⎢   2⋅λ   2 ⎥
            ⎣        λ  ⎦

We can see here that we have described one scheme with three 1D velocities. The moment matrix gives how to find the moments from the distribution functions.

Let's check now if we solve the good physical equations.

```python
pde = pylbm.EquivalentEquation(scheme)
```

```python
print(pde)
```

    +----------------------+
    | Equivalent Equations |
    +----------------------+
        The equation is

        d        d       ∂ ⎛    d    ⎞
        ──(Fx) + ──(U) = ──⎜Bxx⋅──(U)⎟
        dx       dt      ∂x⎝    dx   ⎠

        where

        U = [u]

        Fx = [c⋅u]

        Bxx = [0]

pylbm gives the first and second order terms. In the next release, you will also have access to the third and fourth terms. Our scheme solves the advection equation as exepected.

We can now study the stability of this scheme. Many notions of stability exist and can be used. In pylbm, we focus on a linear notion by computing the eigenvalues of the linear operator corresponding to one time step. The scheme will be considered as stable if all these eigenvalues stay inside the unit circle (as complex values).
This notion is sufficient for linear scheme but just gives partial informations for non-linear scheme.

```python
stab = pylbm.Stability(scheme)

# linearization around a state
uo = 1.

stab.visualize(
    {
        'linearization': {
            U: uo,
        },
        'parameters': {
            LA: {
                'range': [1, 20],
                'init': 1,
                'step': .1
            },
            U: {
                'range': [0, 20],
                'init': uo,
                'step': .1
            },
            C: {
                'range': [0, 20],
                'init': 1,
                'step': .1
            },
            S0: {
                'range': [0, 2],
                'init': 1.9,
                'step': .1
            },
            S1: {
                'range': [0, 2],
                'init': 1.9,
                'step': .1
            },
        },
        'number_of_wave_vectors': 1024,
    }
)

```

    Output()

    FloatSlider(value=1.0, continuous_update=False, description='λ', layout=Layout(width='80%'), max=20.0, min=1.0…

    FloatSlider(value=1.0, continuous_update=False, description='u', layout=Layout(width='80%'), max=20.0)

    FloatSlider(value=1.0, continuous_update=False, description='c', layout=Layout(width='80%'), max=20.0)

    FloatSlider(value=1.9, continuous_update=False, description='s₀', layout=Layout(width='80%'), max=2.0)

    FloatSlider(value=1.9, continuous_update=False, description='s₁', layout=Layout(width='80%'), max=2.0)


# Symbolic Mathematics in Python

There are times when you need to solve a difficult problem symbollically or analytically. If you have ever used Wolfram Alpha, then you have already done this. Sympy is a python library that allows you to do symbolic mathematics in python.

```python
import sympy as sym
```

## 1. Introduction

### Example 1.1

If you try to write the follwing in python by itself, you will get an error telling you x is undefined:
$$x-x$$

```python
x-x
```

$\displaystyle 0$

(The error above is on purpose). Variables in python need to be defined before you can say something specific about them

```python
x=102
x-x
```

    0

If you are trying to show that $x-x=0$ is true for any $x$, the above answer would not be valid. Instead you can use a symbolic expression to show that it is true

**First we define the variable as a symmbolic expression**

```python
x = sym.symbols('x')
```

**Now we can use the variable in a symbolic expression**

```python
x-x
```

    0

### Example 1.2

Sympy can be used to perform algebreic operations (among other things). Consider the following expression: $$(3a-4b)^3$$

We can use symppy to expand the expression algebraically.

**First we need to define the variables as symbolic expressions**

```python
a,b = sym.symbols('a,b')
```

**Side note** Notice that the left hand side of the epression has two variables being defined. Python can define more than one variable at a time:

```python
x1,y1 =10,20
print(x1)
print(y1)
```

    10
    20

**Back to the expression** We can define an expression using the variables $a$ and $b$.

```python
expr = (3*a-4*b)**3
print(expr)
```

    (3*a - 4*b)**3

We can also make it look nicer in our notebook. This doesn't affect the math, but it makes our notebook more readable.

```python
sym.init_printing()
```

```python
expr
```

**Now we expand the function algebreically**

```python
expr.expand()
```

Sympy can also factor the equation

```python
sym.factor(26*a**3-108*a**2*b+144*a*b**2-64*b**3)
```

If you want to copy and paste a result, you print the result.

```python
print(sym.factor(26*a**3-108*a**2*b+144*a*b**2-64*b**3))
```

    2*(a - 2*b)*(13*a**2 - 28*a*b + 16*b**2)

You can also chain together functions

```python
expr.expand().factor()
```

### Exercise 1.1

Show that the following two expressions are true.
$$(2w-3z)(2w+3z)=4w^2-9z^2$$
$$(2w-3z)^2\ne4w^2-9z^2$$

```python
# Your code here
w,z = sym.symbols('w,z')
eq1= (2*w-3*z)*(2*w+3*z)
print (eq1)
eq2= 4*w**2-9*z**2
eq1.expand()
```

    (2*w - 3*z)*(2*w + 3*z)

$\displaystyle 4 w^{2} - 9 z^{2}$

```python
w,z = sym.symbols('w,z')
eq1= (2*w-3*z)**2
eq2= 4*w**2 - 9*z**2
eq1.expand()

```

$\displaystyle 4 w^{2} - 12 w z + 9 z^{2}$

## 2. Solving Equations

Sympy can be used to symbolilically solve equations. As before, you need to define which variables are symbols

### Example 2.1

Use sympy to solve the following equation
$$ax^3+bx^2+cx+d=0$$

```python
# Define the variables
a,b,c,d,x = sym.symbols('a,b,c,d,x')
```

```python
# Define the expression
expr=a*x**3+b*x**2+c*x+d
expr
```

$\displaystyle a x^{3} + b x^{2} + c x + d$

We can use the `solvset` function to solve this equation

```python
solutions=sym.solveset(expr,x)
```

```python
print(solutions)
```

    {-(-3*c/a + b**2/a**2)/(3*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)) - (sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)/3 - b/(3*a), -(-3*c/a + b**2/a**2)/(3*(-1/2 - sqrt(3)*I/2)*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)) - (-1/2 - sqrt(3)*I/2)*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)/3 - b/(3*a), -(-3*c/a + b**2/a**2)/(3*(-1/2 + sqrt(3)*I/2)*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)) - (-1/2 + sqrt(3)*I/2)*(sqrt(-4*(-3*c/a + b**2/a**2)**3 + (27*d/a - 9*b*c/a**2 + 2*b**3/a**3)**2)/2 + 27*d/(2*a) - 9*b*c/(2*a**2) + b**3/a**3)**(1/3)/3 - b/(3*a)}

```python
solutions
```

What if I need help. You can do this with any python function. `function?`

```python
# Run this command to see a help box
sym.solveset?
```

### Exercise 2.1

Use the `solveset` function to solve the following chemical problem.

Phosgene gas, $\text{COCl}_2$, dissociates at high temperatures according to the following equilibrium:

$$ \text{COCl}_2 \rightleftharpoons  \text{CO} + \text{Cl}_2 $$

At $\text{400 C}$, the equilibrium constant $K_c=8.05$.

If you start with a  $\text{0.250 M}$ phosgene sample at $\text{400 C}$, determine the concentrations of all species at equilibrium.

```python
# Your code here
# [CO][Cl]**2 / [COCl2] = 8.05

x = sym.symbols('x')
c= 0.250
eq1= ((x**2)/(0.250-x)) - 8.05
sym.solveset
solutions=sym.solveset(eq1,x)
print(solutions)

print ("equilibrium concentration", 0.250- 0.2426)

```

    {-8.29268379803378, 0.242683798033777}
    equilibrium concentration 0.00739999999999999

Why did you pick your answer?

## 3. Calculus

We can use also Sympy to differentiate and integrate. Let us experiment with differentiating the following expression:

$$x ^ 2 - \cos(x)$$

```python
sym.diff(x ** 2 - sym.cos(x), x)
```

$\displaystyle 2 x + \sin{\left(x \right)}$

Similarly we can integrate:

```python
sym.integrate(x ** 2 - sym.cos(x), x)
```

$\displaystyle \frac{x^{3}}{3} - \sin{\left(x \right)}$

We can also carry out definite integrals:

```python
sym.integrate(x ** 2 - sym.cos(x), (x, 0, 5))
```

$\displaystyle \frac{125}{3} - \sin{\left(5 \right)}$

### Exercise 3.1

Use Sympy to calculate the following:

1. $\frac{d(x ^2 + xy - \ln(y))}{dy}$
1. $\int_0^5 e^{2x}\;dx$

```python
x, y= sym.symbols('x, y')
sym.integrate(x**2+x*y-sym.ln(y),y)

```

$\displaystyle \frac{x y^{2}}{2} + y \left(x^{2} + 1\right) - y \log{\left(y \right)}$

```python
x, y= sym.symbols('x, y')
sym.integrate(sym.E**(2*x),(x, 0, 5))
```

$\displaystyle - \frac{1}{2} + \frac{e^{10}}{2}$

### Exercise 3.2
Solve the following definate integral
$$\int\limits_{ - \infty }^\infty  {\frac{1}{{\sigma \sqrt {2\pi } }}{e^{ - \frac{1}{2}{{\left( {\frac{{x - \mu }}{\sigma }} \right)}^2}}}}$$

Hint, the sympy symbol for infinity is `oo`

```python
# Your code here
import sympy as sym
from sympy import oo
a,x,y= sym.symbols('a, x, y')
sym.integrate((1/(a*(sym.sqrt(2*sym.pi))))*(sym.E**(-0.5*((x-b)/a)**2)),(x, -oo, oo))

```

$\displaystyle \begin{cases} 0.353553390593274 \sqrt{2} \left(2 - \operatorname{erfc}{\left(\frac{0.707106781186548 b}{a} \right)}\right) + 0.353553390593274 \sqrt{2} \operatorname{erfc}{\left(\frac{0.707106781186548 b}{a} \right)} & \text{for}\: \left(\left(2 \left|{\arg{\left(a \right)}}\right| \leq \frac{\pi}{2} \wedge \left|{4 \arg{\left(a \right)} - 2 \arg{\left(b \right)}}\right| < \pi\right) \vee \left(\left|{4 \arg{\left(a \right)} - 2 \arg{\left(b \right)}}\right| \leq \pi \wedge 2 \left|{\arg{\left(a \right)}}\right| < \frac{\pi}{2}\right) \vee \left(\left|{4 \arg{\left(a \right)} - 2 \arg{\left(b \right)}}\right| < \pi \wedge 2 \left|{\arg{\left(a \right)}}\right| < \frac{\pi}{2}\right) \vee 2 \left|{\arg{\left(a \right)}}\right| < \frac{\pi}{2}\right) \wedge \left(\left(2 \left|{\arg{\left(a \right)}}\right| \leq \frac{\pi}{2} \wedge \left|{- 4 \arg{\left(a \right)} + 2 \arg{\left(b \right)} + 2 \pi}\right| < \pi\right) \vee \left(\left|{- 4 \arg{\left(a \right)} + 2 \arg{\left(b \right)} + 2 \pi}\right| \leq \pi \wedge 2 \left|{\arg{\left(a \right)}}\right| < \frac{\pi}{2}\right) \vee \left(\left|{- 4 \arg{\left(a \right)} + 2 \arg{\left(b \right)} + 2 \pi}\right| < \pi \wedge 2 \left|{\arg{\left(a \right)}}\right| < \frac{\pi}{2}\right) \vee 2 \left|{\arg{\left(a \right)}}\right| < \frac{\pi}{2}\right) \\\int\limits_{-\infty}^{\infty} \frac{\sqrt{2} e^{- \frac{0.5 \left(- b + x\right)^{2}}{a^{2}}}}{2 \sqrt{\pi} a}\, dx & \text{otherwise} \end{cases}$

Lookup Gaussian functions: https://en.wikipedia.org/wiki/Gaussian_function
Does your answer maake sense?

## 4. Plotting with Sympy

Finally Sympy can be used to plot functions. Note that this makes use of [matplotlib](http://matplotlib.org/).

Let us plot $x^2$:

```python
expr = x ** 2
p = sym.plot(expr)
```

    <Figure size 640x480 with 1 Axes>

### Exercise 4.1 Plot the following function:

1. $y=x + cos(x)$
1. ${\frac{1}{{ \sqrt {2\pi } }}{e^{ - \frac{x^2}{2}}}}$

```python
# 1.
expr= (x + sym.cos(x))
p = sym.plot (expr)
```

```python
#2
expr= ((1/(sym.sqrt(2*sym.pi)))*sym.E*-((x**2)/2))
p= sym.plot (expr)
```

# Lecture

## L1. Hydrogen Atom

Sympy has built in modules for the eigenfunctions of the hydrogen atom.

```python
import sympy.physics.hydrogen
import numpy as np
```

You can caluclate the eigenvalues ($E$) in Hartrees

`sym.physics.hydrogen.E_nl(n,Z)`

```python
sym.physics.hydrogen.E_nl(1,1)
```

$\displaystyle - \frac{1}{2}$

We can use a loop to print out many energies

```python
for n in range(1,5):
    print(sym.physics.hydrogen.E_nl(n,1))
```

    -1/2
    -1/8
    -1/18
    -1/32

We can plot the hydrogen radial wavefunction (1s orbital)

```python
r=sympy.symbols('r')
sympy.physics.hydrogen.R_nl(1, 0, r, 1)
```

$\displaystyle 2 e^{- r}$

```python
sym.plot(sympy.physics.hydrogen.R_nl(1, 0, r, 1),(r,0,10.50))
```

And the probablity distribution function

```python
r= sympy.symbols('r')
prob_1s=sympy.physics.hydrogen.R_nl(1, 0, r, 1)*sympy.physics.hydrogen.R_nl(1, 0, r, 1)
prob_1s
```

$\displaystyle 4 e^{- 2 r}$

```python
sym.plot(prob_1s,(r,0,10))
```

Plot a 2s orbital

```python
r=sympy.symbols('r')
prob_2s=sympy.physics.hydrogen.R_nl(2, 0, r, 1)*sympy.physics.hydrogen.R_nl(2, 0, r, 1)
prob_2s
```

$\displaystyle \frac{\left(2 - r\right)^{2} e^{- r}}{8}$

```python
sym.plot(prob_2s,(r,0,10))
```

We can change the range to see the node better.

```python
sym.plot(prob_2s,(r,1,8))
```

Notice the node!

### Exercise L1.1

Plot the radial distriubution function for a 2p, 3s, 4s, and 3d orbital.

```python
# 2p orbital
print ('2p orbital')
import sympy.physics.hydrogen
import numpy as np
r= sympy.symbols('r')
prob_2p=sympy.physics.hydrogen.R_nl(2, 1, r, 1)*sympy.physics.hydrogen.R_nl(2, 1, r, 1)
sym.plot(prob_2p,(r,0,10))

#3s orbital
print ('3s orbital')
import sympy.physics.hydrogen
import numpy as np
r= sympy.symbols('r')
prob_3s=sympy.physics.hydrogen.R_nl(3, 0, r, 1)*sympy.physics.hydrogen.R_nl(3, 0, r, 1)
sym.plot(prob_3s,(r,2,25))

#4s orbital
print ('4s orbital')
import sympy.physics.hydrogen
import numpy as np
r= sympy.symbols('r')
prob_4s=sympy.physics.hydrogen.R_nl(4, 0, r, 1)*sympy.physics.hydrogen.R_nl(4, 0, r, 1)
sym.plot(prob_4s,(r,2,35))

#3d orbital
print ('3d orbital')
import sympy.physics.hydrogen
import numpy as np
r= sympy.symbols('r')
prob_3d=sympy.physics.hydrogen.R_nl(3, 2, r, 1)*sympy.physics.hydrogen.R_nl(3, 2, r, 1)
sym.plot(prob_3d,(r,0,14))
```

```python

```


# Installation

1. Sympy is pre-installed in **Anaconda** and **Active Python** distribution.

2. From git

* **Clone** it from the link https://github.com/sympy/sympy.git

```

$ git clone https://github.com/sympy/sympy.git

$ git pull origin master

$ python setupegg.py develop

```

3. Using package installer PIP

```
$ pip install sympy

```

For more information on installation process, visit the official page of [sympy installation](https://docs.sympy.org/latest/install.html)

# Defining Symbols

Given an expression $y = x + 5$ .

To execute the same in the python.

```python
y = x + 5
print(y)
```

```python
x = 6
y = x + 5
print(f'Type of x is {type(x).__name__}')
print(y)
```

```python
from sympy import Symbol

x = Symbol('x')
```

```python
type(x).__name__
```

```python
y = x + 5

y
```

## Different ways of declaring symbols

```python
# Defining a single symbol

from sympy import Symbol

x = Symbol('x')

x
```

```python
# Multiple symbols in one line of code

from sympy import symbols

x , y = symbols('x y')

print(f'y is a type of {type(y).__name__}')
print(f'x is a type of {type(x).__name__}')
```

```python
# importing inbuilt symbols from the sympy library

from sympy.abc import a,b

print(f'a is a type of {type(a).__name__}')
print(f'b is a type of {type(b).__name__}')
```

```python
from sympy import abc

print(dir(abc))
```

# Attention ⚠

```python
from sympy import pi
# from sympy.abc import pi

print(type(pi).__name__)

(pi*pi).evalf()
```

```python
print(abc._clash)
```

## To declare series of variable $ ( x0, x1, x2 ) $

```python
x0, x1, x2 = symbols('x0 x1 x2')

x0
```

```python
x = symbols('x0:30')

x
```

```python
x, y = symbols(('x','y0:10'))
# print(x)
print(y)
```

```python
x = symbols('xa:c')

x
```

## Exercise Questions

1. Generate symbols x8 to x10
2. Create symbols m, n0 to n5, zc to zd  in a single line of code

```python

```

# Algebraic Manipulation

### Evaluating an expression $ x^2+3$

```python
# in sympy

from sympy import Symbol

x = Symbol('x')

exp = x**2+3

exp
```

```python
exp.subs(x,2)
```

$ x = a^2 + b $

```python
exp = a**2 + b

exp.subs({a:2, b:1})
```

To assign more than one variables, Use dictionary

{a:2, b:1}

# Expand algebraic expression

$(a+b)^2 = a^2 + 2ab + b^2$

Using the expand function

```python
from sympy import expand
from sympy.abc import a,b

exp = (a+b)**2

# exp
expand(exp)
```

$ sin(A+B) = sin(A) cos(B) + cos(A) sin(B) $

```python
from sympy import sin
from sympy.abc import A,B

exp = sin(A+B)

expand(exp)
```

```python
from sympy import sin, cos, tan
from sympy.abc import A,B

exp = sin(A+B)

expand(exp, trig=True) # to evaluate trignometry pass hint trig=True
```

## Exercise Questions

1. Calculate the expanded form of $(a+b)^4$
2. Simplify the trigonometric expression $ sin(x) / cos(x) $

```python

```

# Solving Quadratic Equations

$ x^2 - 44x + 123 = 0$

```python
from sympy import solve
from sympy.abc import x,y

exp = x**2 - 44*x + 123

solve(exp)
```

Inequalities

$ x^2 - 44x + 123 < 0$

```python
exp = x**2 -44*x + 123

print(solve(exp < 0))
solve(exp < 0)
```

$ x^2 - 44x + 123 >= 0$

```python
exp = x**2 -44*x + 123

print(solve(exp >= 0))
solve(exp >= 0)
```

# Solving Linear Equations

$x + 2y = 5 $

$x + 3y = 10$

```python
from sympy.abc import x,y
from sympy import solve

e1 = x - 2*y - 5
e2 = x + 3*y -10

solve((e1,e2))
```

# Solving Logical Expressions

We know how to solve basic logic gates in python.

```python
# For Example And Gate

print("      And Gate  ")
print("----------------------")
print("|  a  |  b  | output |")
print("----------------------")
print(f'|  0  |  0  |   { 0 & 0 }    |')
print(f'|  0  |  1  |   { 0 & 1 }    |')
print(f'|  1  |  0  |   { 1 & 0 }    |')
print(f'|  1  |  1  |   { 1 & 1 }    |')
print("----------------------")
```

```python
# For Example XOR Gate

print("      XOR Gate  ")
print("----------------------")
print("|  a  |  b  | output |")
print("----------------------")
print(f'|  0  |  0  |   { 0 ^ 0 }    |')
print(f'|  0  |  1  |   { 0 ^ 1 }    |')
print(f'|  1  |  0  |   { 1 ^ 0 }    |')
print(f'|  1  |  1  |   { 1 ^ 1 }    |')
print("----------------------")
```

$ AND : a & b $

OR : a | b

 NOT : ~a

$ NAND : ~(a & b) $

XOR : a ^ b

## Full Adder

| a | b | c | sum | c_out |
| :--: | :--: | :--: | :--: | :--: |
| 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 1 | 0 |
| 0 | 1 | 0 | 1 | 0 |
| 0 | 1 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 | 0 |
| 1 | 0 | 1 | 0 | 1 |
| 1 | 1 | 0 | 0 | 1 |
| 1 | 1 | 1 | 1 | 1 |

## SUM OF PRODUCTS

```python
from sympy.logic import SOPform # SUM OF PRODUCTS
from sympy.abc import a,b,c

minterms_sum = [
    [0,0,1], [0,1,0], [1,0,0], [1,1,1]
]

minterms_cout = [
    [0,1,1], [1,0,1], [1,1,0], [1,1,1]
]

sum = SOPform([a,b,c],minterms_sum)
cout = SOPform([a,b,c],minterms_cout)

print(f'sum = {sum}')
print(f'C_out = {cout}')
```

```python
from sympy.logic import SOPform  # SUM OF PRODUCTS
from sympy.abc import a,b,c

minterms_sum = [1, 2, 4, 7]

minterms_cout = [3, 5, 6, 7]

sum = SOPform([a,b,c], minterms_sum)
cout = SOPform([a,b,c], minterms_cout)

print(f'sum = {sum}')
print(f'C_out = {cout}')
```

## PRODUCT OF SUMS

```python
from sympy.logic import POSform
from sympy.abc import a,b,c

minterms_sum = [1, 2, 4, 7]

minterms_cout = [3, 5, 6, 7]

sum = POSform([a,b,c],minterms_sum)
cout = POSform([a,b,c],minterms_cout)

print(f'sum = {sum}')
print(f'C_out = {cout}')
```

## Simplifying Logic Expressions

$ (A + B ) (A + \bar B) (\bar A + B) $

```python
from sympy.abc import A,B
from sympy.logic import simplify_logic

exp = (A | B) & (A | ~B) & (~A |B)

simp_exp = simplify_logic(exp)

print(simp_exp)
```

# Plotting graphs

```python
from sympy.abc import x
from sympy.plotting import plot

plot(x**2)
```

### limiting the x axis

```python
# x axis from -5 to 10

plot(x**2, (x,-5,10))
```

## Multiple plots

```python
plot(
(x**2),
(x)
)
```

# ?

#### Sympy doesnot depends on any other library for its basic operations.

#### BUT

#### To get the BEST out of it we require the dependencies.

plotting the graph in an image format, it takes the help of Matplotlib

```python
# Install the matplotlib

```

### Multiple plots with different range of x

```python
plot(
(x**2, (x,-10, 5)),
(x, (x, 5, 5))
)
```

# [SymPy Modules](https://docs.sympy.org/latest/modules/index.html)