```python
import numpy as np
import sympy as sym

import models
import symbolics
```

# Generalized Sexual Selection

```python
x1, x2, x3, x4 = sym.symbols('x1, x2, x3, x4', nonnegative=True, real=True)
T, R, P, S = sym.symbols('T, R, P, S', nonnegative=True, real=True)

M, m = sym.symbols("M, m", nonnegative=True, real=True)

UGA = symbolics.UGA
UgA = symbolics.UgA
```

```python
x = np.array([[x1], [x2], [x3], [1 - x1 - x2 - x3]])
payoff_kernel = np.array([[R, S], [T, P]])
W = models.generalized_sexual_selection(x, UGA, UgA, payoff_kernel, M, m)
x_dot = models.offspring_genotypes_evolution(W, x)
```

```python
x1_star, x2_star, x3_star, x4_star = sym.symbols('x1_star, x2_star, x3_star, x4_star', nonnegative=True, real=True)
UGA_star, UgA_star = sym.symbols("UGA_star, UgA_star", nonnegative=True, real=True)
equilibrium_system = system.subs({UGA(x1 + x3): UGA_star, UgA(x1 + x3): UgA_star, x1: x1_star, x2: x2_star, x3: x3_star})

```

```python
x_dot.shape
```

    (4, 1)

```python

```

```python
system = sym.Matrix(x_dot[:-1, [0]])
F_jac = system.jacobian((x1, x2, x3))
```

```python
system.subs({x1:0, x2: 0, x3:1, m: 0})
```

    Matrix([
    [0],
    [0],
    [0]])

```python
F_jac.subs({x1: 0, x2: 0, x3: 1, m:0})
```

    Matrix([
    [                                                                                -1 + 0.5*(-1.0*M + 2.0*R)/R, nan,                                                                                0],
    [                                                                                0, nan,                                                                                0],
    [0.5*(2.0*M - 4.0*R*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)) + 2.0*S*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)) + 2.0*T*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)))/R + 0.5*(-1.0*M + 4.0*R*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)) - 1.0*R - 2.0*S*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)) - 1.0*T*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)))/R, nan, -1 + 0.5*(-4.0*R*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)) + 2.0*S*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)) + 2.0*T*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)))/R + 0.5*(4.0*R*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)) + 1.0*R - 2.0*S*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)) - 1.0*T*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)))/R]])

```python
UgA_prime_star = sym.symbols("UgA_prime_star", nonnegative=True, real=True)

evaluated_F_jac = (F_jac.subs({x1: 0, x2:0, m:0})
                        .subs({UGA(x3): 1, UgA(x3): 1})
                        .doit()
                        .subs({sym.Derivative(UgA(x3), x3): 1})
                        .subs({x3: 1}))
```

```python
evaluated_F_jac
```

    Matrix([
    [                                                   -1 + 0.5*(-1.0*M + 2.0*R)/R,           0.5*(-0.5*M + 0.5*R)/R,                                                                   0],
    [                                                                             0,      -1 + 0.5*(-0.5*M + 0.5*R)/R,                                                                   0],
    [0.5*(-1.0*M + 3.0*R - 2.0*S - 1.0*T)/R + 0.5*(2.0*M - 4.0*R + 2.0*S + 2.0*T)/R, 1.0*M/R + 0.5*(-0.5*M - 0.5*R)/R, -1 + 0.5*(-4.0*R + 2.0*S + 2.0*T)/R + 0.5*(5.0*R - 2.0*S - 1.0*T)/R]])

```python
eigenvals = evaluated_F_jac.eigenvals()
```

```python
e1, e2, e3 = eigenvals.keys()
```

```python
e1
```

    -(R - T)/(2*R)

```python
e2
```

    -M/(2*R)

```python
e3
```

    -(M + 3*R)/(4*R)

```python
evaluated_jac = sym.zeros(3, 3)

for i in range(3):
    for j in range(3):
        tmp_expr = (F_jac[i, j].subs({x1: 0, x2:0, m:0})
                               .subs({UGA(x1): UGA_star, UgA(x1): UgA_star})
                               .doit()
                               .subs({sym.Derivative(UgA(x3), x3): 1})
                               .subs({x3: 1}))
        if isinstance(tmp_expr, sym.numbers.NaN):
            tmp_expr = (F_jac[i, j].subs({x1:0, x3:1, m:0})
                                   .subs({UGA(x3): UGA_star, UgA(x3): UgA_star})
                                   .doit()
                                   .subs({sym.Derivative(UgA(x3), x3): 1}))
            tmp_expr = sym.limit(tmp_expr, x2, 0)
        evaluated_jac[i, j] = tmp_expr
        print("Done with entry {},{}".format(i, j))
```

    Done with entry 0,0
    Done with entry 0,1
    Done with entry 0,2
    Done with entry 1,0
    Done with entry 1,1
    Done with entry 1,2
    Done with entry 2,0
    Done with entry 2,1
    Done with entry 2,2

```python
evaluated_jac
```

    Matrix([
    [                                                   -1 + 0.5*(-1.0*M + 2.0*R)/R, nan,                                                                   0],
    [                                                                             0, nan,                                                                   0],
    [0.5*(-1.0*M + 3.0*R - 2.0*S - 1.0*T)/R + 0.5*(2.0*M - 4.0*R + 2.0*S + 2.0*T)/R, nan, -1 + 0.5*(-4.0*R + 2.0*S + 2.0*T)/R + 0.5*(5.0*R - 2.0*S - 1.0*T)/R]])

```python
evaluated_jac[:-1, :-1].shape
```

    (3, 3)

```python
eigvals = evaluated_jac[:-1, :-1].eigenvals()
```

```python
e1, e2, e3 = eigvals.keys()
```

```python
e1
```

    -1

```python
e2
```

    -(R - T)/(2*R)

```python
e3
```

    -M/(2*R)

```python
e4
```

    -(M + 3*R)/(4*R)

```python
evaluated_F_jac = (F_jac.subs({x1: 0, x2: 0, x4: 1, m: 0})
      .subs({UGA(x3): 0, UgA(x3): 0})
      .doit()
      .subs({sym.Derivative(UgA(x3)): 1})
      .subs({x3: 0}))

```

```python
eigvals = evaluated_F_jac.eigenvals()
```

```python
e1, e2, e3, e4 = eigvals.keys()
```

```python
e1
```

    -1

```python
e2
```

    -(P - S)/(2*P)

```python
e3
```

    -M/(2*P)

```python
e4
```

    -(M + 3*P)/(4*P)

```python

```

```python
x4_star = 1 - x1_star - x2_star - x3_star
x_star = np.array([x1_star, x2_star, x3_star, x4_star])
N = sym.symbols("N", real=True, positive=True)
F = sym.Matrix(W.sum(axis=2).dot(x_star) - N * x_star).subs({UGA(x1 + x3): UGA_star, UgA(x1 + x3): UgA_star})

```

```python
F.shape
```

    (4, 1)

```python
solution = sym.solve(F[:-1,0], (x1_star, x2_star, x3_star))
```

```python

```

```python
solution = sym.solve_linear_system(F[:-1,0].col_insert(4, sym.zeros(3,1)), symbols=(x1_star, x2_star, x3_star))
```

```python
F
```

    Matrix([
    [                                                                                -N*x1_star + 0.25*UgA_star*x1*(2*UgA_star*(R - m) + 2*(S - m)*(-UgA_star + 1))*(-x1_star - x2_star - x3_star + 1)/(x1 + x3) + x1_star*(1.0*UGA_star*x1*(2*UGA_star*(-M + R) + 2*(-M + S)*(-UGA_star + 1))/(x1 + x3) + 0.5*UGA_star*x3*(2*UGA_star*(-M + R) + 2*(-M + S)*(-UGA_star + 1))/(x1 + x3) + 0.5*x2*(-UGA_star + 1)*(2*UGA_star*(-M + T) + 2*(-M + P)*(-UGA_star + 1))/(-x1 - x3 + 1) + 0.25*(-UGA_star + 1)*(2*UGA_star*(-M + T) + 2*(-M + P)*(-UGA_star + 1))*(-x1 - x2 - x3 + 1)/(-x1 - x3 + 1)) + x2_star*(0.5*UGA_star*x1*(2*UGA_star*(-M + R) + 2*(-M + S)*(-UGA_star + 1))/(x1 + x3) + 0.25*UGA_star*x3*(2*UGA_star*(-M + R) + 2*(-M + S)*(-UGA_star + 1))/(x1 + x3)) + x3_star*(0.5*UgA_star*x1*(2*UgA_star*(R - m) + 2*(S - m)*(-UgA_star + 1))/(x1 + x3) + 0.25*x2*(-UgA_star + 1)*(2*UgA_star*(T - m) + 2*(P - m)*(-UgA_star + 1))/(-x1 - x3 + 1))],
    [                                                       -N*x2_star + x1_star*(0.5*x2*(-UGA_star + 1)*(2*UGA_star*(-M + T) + 2*(-M + P)*(-UGA_star + 1))/(-x1 - x3 + 1) + 0.25*(-UGA_star + 1)*(2*UGA_star*(-M + T) + 2*(-M + P)*(-UGA_star + 1))*(-x1 - x2 - x3 + 1)/(-x1 - x3 + 1)) + 0.25*x2*x3_star*(-UgA_star + 1)*(2*UgA_star*(T - m) + 2*(P - m)*(-UgA_star + 1))/(-x1 - x3 + 1) + x2_star*(0.5*UGA_star*x1*(2*UGA_star*(-M + R) + 2*(-M + S)*(-UGA_star + 1))/(x1 + x3) + 0.25*UGA_star*x3*(2*UGA_star*(-M + R) + 2*(-M + S)*(-UGA_star + 1))/(x1 + x3) + 1.0*x2*(-UGA_star + 1)*(2*UGA_star*(-M + T) + 2*(-M + P)*(-UGA_star + 1))/(-x1 - x3 + 1) + 0.5*(-UGA_star + 1)*(2*UGA_star*(-M + T) + 2*(-M + P)*(-UGA_star + 1))*(-x1 - x2 - x3 + 1)/(-x1 - x3 + 1)) + (0.25*UgA_star*x1*(2*UgA_star*(R - m) + 2*(S - m)*(-UgA_star + 1))/(x1 + x3) + 0.5*x2*(-UgA_star + 1)*(2*UgA_star*(T - m) + 2*(P - m)*(-UgA_star + 1))/(-x1 - x3 + 1))*(-x1_star - x2_star - x3_star + 1)],
    [                                                                                -N*x3_star + 0.25*UGA_star*x2_star*x3*(2*UGA_star*(-M + R) + 2*(-M + S)*(-UGA_star + 1))/(x1 + x3) + x1_star*(0.5*UGA_star*x3*(2*UGA_star*(-M + R) + 2*(-M + S)*(-UGA_star + 1))/(x1 + x3) + 0.25*(-UGA_star + 1)*(2*UGA_star*(-M + T) + 2*(-M + P)*(-UGA_star + 1))*(-x1 - x2 - x3 + 1)/(-x1 - x3 + 1)) + x3_star*(0.5*UgA_star*x1*(2*UgA_star*(R - m) + 2*(S - m)*(-UgA_star + 1))/(x1 + x3) + 1.0*UgA_star*x3*(2*UgA_star*(R - m) + 2*(S - m)*(-UgA_star + 1))/(x1 + x3) + 0.25*x2*(-UgA_star + 1)*(2*UgA_star*(T - m) + 2*(P - m)*(-UgA_star + 1))/(-x1 - x3 + 1) + 0.5*(-UgA_star + 1)*(2*UgA_star*(T - m) + 2*(P - m)*(-UgA_star + 1))*(-x1 - x2 - x3 + 1)/(-x1 - x3 + 1)) + (0.25*UgA_star*x1*(2*UgA_star*(R - m) + 2*(S - m)*(-UgA_star + 1))/(x1 + x3) + 0.5*UgA_star*x3*(2*UgA_star*(R - m) + 2*(S - m)*(-UgA_star + 1))/(x1 + x3))*(-x1_star - x2_star - x3_star + 1)],
    [-N*(-x1_star - x2_star - x3_star + 1) + 0.25*x1_star*(-UGA_star + 1)*(2*UGA_star*(-M + T) + 2*(-M + P)*(-UGA_star + 1))*(-x1 - x2 - x3 + 1)/(-x1 - x3 + 1) + x2_star*(0.25*UGA_star*x3*(2*UGA_star*(-M + R) + 2*(-M + S)*(-UGA_star + 1))/(x1 + x3) + 0.5*(-UGA_star + 1)*(2*UGA_star*(-M + T) + 2*(-M + P)*(-UGA_star + 1))*(-x1 - x2 - x3 + 1)/(-x1 - x3 + 1)) + x3_star*(0.25*x2*(-UgA_star + 1)*(2*UgA_star*(T - m) + 2*(P - m)*(-UgA_star + 1))/(-x1 - x3 + 1) + 0.5*(-UgA_star + 1)*(2*UgA_star*(T - m) + 2*(P - m)*(-UgA_star + 1))*(-x1 - x2 - x3 + 1)/(-x1 - x3 + 1)) + (-x1_star - x2_star - x3_star + 1)*(0.25*UgA_star*x1*(2*UgA_star*(R - m) + 2*(S - m)*(-UgA_star + 1))/(x1 + x3) + 0.5*UgA_star*x3*(2*UgA_star*(R - m) + 2*(S - m)*(-UgA_star + 1))/(x1 + x3) + 0.5*x2*(-UgA_star + 1)*(2*UgA_star*(T - m) + 2*(P - m)*(-UgA_star + 1))/(-x1 - x3 + 1) + 1.0*(-UgA_star + 1)*(2*UgA_star*(T - m) + 2*(P - m)*(-UgA_star + 1))*(-x1 - x2 - x3 + 1)/(-x1 - x3 + 1))]])

## Tests!
All of the following limits should exactly 0!

```python
sym.limit(symbolics.equation_motion_GA_share(x1, 0, 0, UGA, UgA, T, R, P, S), x1, 1)
```

    0

```python
sym.limit(symbolics.equation_motion_GA_share(x1, 0, 0, UGA, UgA, T, R, P, S), x1, 0)
```

    0

```python
sym.limit(symbolics.equation_motion_Ga_share(x1, 0, 0, UGA, UgA, T, R, P, S), x1, 1)
```

    0

```python
sym.limit(symbolics.equation_motion_Ga_share(x1, 0, 0, UGA, UgA, T, R, P, S), x1, 0)
```

    0

```python
sym.limit(symbolics.equation_motion_gA_share(x1, 0, 0, UGA, UgA, T, R, P, S), x1, 1)
```

    0

```python
sym.limit(symbolics.equation_motion_gA_share(x1, 0, 0, UGA, UgA, T, R, P, S), x1, 0)
```

    0

```python
sym.limit(symbolics.equation_motion_ga_share(x1, 0, 0, UGA, UgA, T, R, P, S), x1, 1)
```

    0

```python
sym.limit(symbolics.equation_motion_ga_share(x1, 0, 0, UGA, UgA, T, R, P, S), x1, 0)
```

    0

The derivatives should sum to zero...

```python
(symbolics.equation_motion_GA_share(x1, x2, x3, UGA, UgA, T, R, P, S) +
 symbolics.equation_motion_Ga_share(x1, x2, x3, UGA, UgA, T, R, P, S) +
 symbolics.equation_motion_gA_share(x1, x2, x3, UGA, UgA, T, R, P, S) +
 symbolics.equation_motion_ga_share(x1, x2, x3, UGA, UgA, T, R, P, S)).simplify()
```

    0

```python
sym.solve()
```

```python
sol, = sym.solve(symbolics.equation_motion_GA_share(x1, x2, x3, UGA, UgA, T, R, P, S), (x1, x2, x3), implicit=True)
```

```python
sol.keys()
```

    dict_keys([x2])

```python
sol[x2]
```

    (2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))*(P*x1**2*(-UGA(x1 + x3) + 1.0)**2/((-x1 - x3 + 1.0)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) + P*x1*x3*(-UGA(x1 + x3) + 1.0)**2/((-x1 - x3 + 1.0)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - P*x1*(-UGA(x1 + x3) + 1.0)**2/((-x1 - x3 + 1.0)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - 4.0*R*x1**2*UGA(x1 + x3)**2/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) + R*x1**2*UgA(x1 + x3)**2/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - 2.0*R*x1*x3*UGA(x1 + x3)**2/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - R*x1*x3*UgA(x1 + x3)**2/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - R*x1*UgA(x1 + x3)**2/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) + 4.0*S*x1**2*UGA(x1 + x3)**2/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - 4.0*S*x1**2*UGA(x1 + x3)/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - S*x1**2*UgA(x1 + x3)**2/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) + S*x1**2*UgA(x1 + x3)/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) + 2.0*S*x1*x3*UGA(x1 + x3)**2/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - 2.0*S*x1*x3*UGA(x1 + x3)/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) + S*x1*x3*UgA(x1 + x3)**2/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - S*x1*x3*UgA(x1 + x3)/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) + S*x1*UgA(x1 + x3)**2/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - S*x1*UgA(x1 + x3)/((x1 + x3)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - T*x1**2*UGA(x1 + x3)**2/((-x1 - x3 + 1.0)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) + T*x1**2*UGA(x1 + x3)/((-x1 - x3 + 1.0)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - T*x1*x3*UGA(x1 + x3)**2/((-x1 - x3 + 1.0)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) + T*x1*x3*UGA(x1 + x3)/((-x1 - x3 + 1.0)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) + T*x1*UGA(x1 + x3)**2/((-x1 - x3 + 1.0)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) - T*x1*UGA(x1 + x3)/((-x1 - x3 + 1.0)*(2.0*P*((x1 + x2)*(-UGA(x1 + x3) + 1.0)**2 + (-UgA(x1 + x3) + 1.0)**2*(-x1 - x2 + 1.0)) + 2.0*R*((x1 + x2)*UGA(x1 + x3)**2 + (-x1 - x2 + 1.0)*UgA(x1 + x3)**2) + (2.0*S + 2.0*T)*((x1 + x2)*(-UGA(x1 + x3) + 1.0)*UGA(x1 + x3) + (-UgA(x1 + x3) + 1.0)*(-x1 - x2 + 1.0)*UgA(x1 + x3)))) + 2.0*x1)/(P*x1*(-UGA(x1 + x3) + 1.0)**2/(-x1 - x3 + 1.0) + P*x3*(-UgA(x1 + x3) + 1.0)**2/(-x1 - x3 + 1.0) + 2.0*R*x1*UGA(x1 + x3)**2/(x1 + x3) - R*x1*UgA(x1 + x3)**2/(x1 + x3) + R*x3*UGA(x1 + x3)**2/(x1 + x3) - 2.0*S*x1*UGA(x1 + x3)**2/(x1 + x3) + 2.0*S*x1*UGA(x1 + x3)/(x1 + x3) + S*x1*UgA(x1 + x3)**2/(x1 + x3) - S*x1*UgA(x1 + x3)/(x1 + x3) - S*x3*UGA(x1 + x3)**2/(x1 + x3) + S*x3*UGA(x1 + x3)/(x1 + x3) - T*x1*UGA(x1 + x3)**2/(-x1 - x3 + 1.0) + T*x1*UGA(x1 + x3)/(-x1 - x3 + 1.0) - T*x3*UgA(x1 + x3)**2/(-x1 - x3 + 1.0) + T*x3*UgA(x1 + x3)/(-x1 - x3 + 1.0))

```python
sol, = sym.solve(equation_motion_Ga_share(x1, x2, x3, UGA, UgA, T, R, P, S), (x1, x2, x3), implicit=True)
```

```python
sol.keys()
```

    dict_keys([x3])

# Stability analysis

```python
rhs = sym.Matrix([symbolics.equation_motion_GA_share(x1, x2, x3, UGA, UgA, T, R, P, S),
                  symbolics.equation_motion_Ga_share(x1, x2, x3, UGA, UgA, T, R, P, S),
                  symbolics.equation_motion_gA_share(x1, x2, x3, UGA, UgA, T, R, P, S)])
```

```python
rhs_jac = rhs.jacobian([x1, x2, x3])
```

```python
evaluated_jac = (rhs_jac.subs({x1: 0, x2:0, m:0})
                        .subs({UGA(x3): 1, UgA(x3): 1})
                        .doit()
                        .subs({sym.Derivative(UgA(x3), x3): 1})
                        .subs({x3: 1}))
```

```python
eigenvals = evaluated_jac.eigenvals()
```

```python
e1, e2, e3 = eigenvals.keys()
```

```python
e1
```

    -3/4

```python
e2
```

    -(R - T)/(2*R)

```python
e3
```

    0

## GA corner equilibrium

```python
evaluated_jac = sym.zeros(3, 3)

for i in range(3):
    for j in range(3):
        tmp_expr = rhs_jac[i, j].subs({x1: 1, x2: 0, x3: 0})
        if isinstance(tmp_expr, sym.numbers.NaN):
            tmp_expr = sym.limit(rhs_jac[i, j].subs({x2: 0, x3: 0}), x1, 1)
        evaluated_jac[i, j] = tmp_expr
```

```python
evaluated_jac.eigenvals()
```

    {-(R - T*Subs(Derivative(UGA(_xi_1), _xi_1), (_xi_1,), (1,)))/(2*R): 1,
     -(3*R - T*Subs(Derivative(UGA(_xi_1), _xi_1), (_xi_1,), (1,)))/(4*R): 1,
     0: 1}

Necessary conditions for stability are that both non-zero eigenvalues are strictly negative.  We can derive the following necessary conditions...

\begin{align}
    U_{GA}'(1) < \frac{3 R}{T}
\end{align}

...and...

\begin{align}
    U_{GA}'(1) < \frac{R}{T} < 1
\end{align}

...note that the second condition implies the first.

Note that, since one of the above eigenvalues is zero, the above conditions are actually necessary and *sufficient* for the stability of a mixed $\gamma$ pure $\alpha=A$ equilibrium.

## Ga corner equilbrium

```python
evaluated_jac = sym.zeros(3, 3)

for i in range(3):
    for j in range(3):
        tmp_expr = rhs_jac[i, j].subs({x1: 0, x2: 1, x3: 0})
        if isinstance(tmp_expr, sym.numbers.NaN):
            tmp_expr = rhs_jac[i, j].subs({x1: 0, x2: 1}).simplify()
            numer, denom = sym.fraction(tmp_expr)
            evaluated_jac[i, j] = numer.diff(x3).subs({x3: 0}) / denom.diff(x3).subs({x3: 0})
        else:
            evaluated_jac[i, j] = tmp_expr
```

```python
evaluated_jac.eigenvals()
```

    {-(P - S*Subs(Derivative(UGA(_xi_1), _xi_1), (_xi_1,), (0,)))/(2*P): 1,
     -(3*P - S*Subs(Derivative(UGA(_xi_1), _xi_1), (_xi_1,), (0,)))/(4*P): 1,
     0: 1}

Necessary conditions for stability are that both non-zero eigenvalues are strictly negative.  We can derive the following necessary conditions...

\begin{align}
    U'_{GA}(0) < \frac{P}{S}
\end{align}

...and...

\begin{align}
    U'_{GA}(0) < \frac{3P}{S}
\end{align}

...note that the first inequality above implies the second.

Note that, since one of the above eigenvalues is zero, the above conditions are actually necessary and *sufficient* for the stability of a mixed $\gamma$ pure $\alpha=a$ equilibrium. This is an example of neutral stability.

## gA corner equilbrium

```python
evaluated_jac = sym.zeros(3, 3)

for i in range(3):
    for j in range(3):
        tmp_expr = rhs_jac[i, j].subs({x1: 0, x2: 0, x3: 1})
        if isinstance(tmp_expr, sym.numbers.NaN):
            tmp_expr = sym.limit(rhs_jac[i, j].subs({x1: 0, x2: 0}), x3, 1)
        evaluated_jac[i, j] = tmp_expr
```

```python
evaluated_jac.eigenvals()
```

    {-(R - T*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)))/(2*R): 1,
     -(3*R - T*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (1,)))/(4*R): 1,
     0: 1}

Necessary conditions for stability are that both non-zero eigenvalues are strictly negative. This implies the following set of conditions...

\begin{align}
    U_{gA}'(1) < \frac{3 R}{T}
\end{align}

\begin{align}
    U_{gA}'(1) < \frac{R}{T} < 1
\end{align}

...note that this second condition implies the first. Again, presence of zero eigenvalue indicates that the above conditions are necessary and sufficient for stability of a mixed $\gamma$ pure $A$ equilibrium.

## ga corner equilbrium

```python
evaluated_jac = sym.zeros(3, 3)

for i in range(3):
    for j in range(3):
        tmp_expr = rhs_jac[i, j].subs({x1: 0, x2: 0, x3: 0})
        if isinstance(tmp_expr, sym.numbers.NaN):
            tmp_expr = rhs_jac[i, j].subs({x1: 0, x2: 0}).simplify()
            numer, denom = sym.fraction(tmp_expr)
            evaluated_jac[i, j] = numer.diff(x3).subs({x3: 0}) / denom.diff(x3).subs({x3: 0})
        else:
            evaluated_jac[i, j] = tmp_expr
```

```python
evaluated_jac.eigenvals()
```

    {-(P - S*Subs(Derivative(UgA(_xi_1), _xi_1), (_xi_1,), (0,)))/(2*P): 1,
     -(3*P - S*Subs(Derivative(UgA(x3), x3), (x3,), (0,)))/(4*P): 1,
     0: 1}

Necessary conditions for stability are that both non-zero eigenvalues are strictly negative.  We can derive the following necessary conditions...

\begin{align}
    U'_{gA}(0) < \frac{P}{S}
\end{align}

...and...

\begin{align}
    U'_{gA}(0) < \frac{3P}{S}
\end{align}

...note that the first inequality above implies the second.

Note that, since one of the above eigenvalues is zero, the above conditions are actually necessary and *sufficient* for the stability of a mixed $\gamma$ pure $\alpha=a$ equilibrium.

```python

```


2019年度
線形代数　I　講座資料

資料番号　No.2-2

### [2-2]ベクトルの外積

### 　外積　（ベクトル積）
$ \boldsymbol{ a }$と$ \boldsymbol{ b } $の 外積は

$ \boldsymbol{ a } \times\boldsymbol{ b } $
で表す

外積の大きさは、
$ |\boldsymbol{a} \times \boldsymbol{b}|= |\boldsymbol{a}||\boldsymbol{b}|sin\theta$

ただし、なす角$ \theta $は$0^\circ \leqq \theta \leqq 180^\circ $の範囲である

$\boldsymbol{a}=O$ または,$\boldsymbol{b}=O$のとき、$ \boldsymbol{a} \times \boldsymbol{b}=O$

外積の方向について
$ \boldsymbol{ a } \times\boldsymbol{ b } $のとき、

ベクトル $ \boldsymbol{ a }$を、ベクトル $ \boldsymbol{ b}$に重なるように回転したときに、その回転で右ねじが進む方向とする。
$ \boldsymbol{ a } \times\boldsymbol{ b } $は$ \boldsymbol{ a }$と $ \boldsymbol{ b}$の両方ともに直角である

 fig-2-2-1.png
 fig-2-2-2.png

外積の性質　$k$は実数

(1) $ |\boldsymbol{a} \times \boldsymbol{b}|= |\boldsymbol{a}||\boldsymbol{b}|sin\theta$

(2) $\boldsymbol{a} \times (\boldsymbol{b}+\boldsymbol{c})=  \boldsymbol{a} \times\boldsymbol{b}+ \boldsymbol{a} \times \boldsymbol{c}  $

　 $(\boldsymbol{a}+ \boldsymbol{b}) \times \boldsymbol{c}=  \boldsymbol{a} \times\boldsymbol{c}+ \boldsymbol{b} \times \boldsymbol{c}  $

(3)$ (k \boldsymbol{a})\times\boldsymbol{b} = k(\boldsymbol{a} \times \boldsymbol{b}) =  \boldsymbol{a} \times (k\boldsymbol{b})$

(4) $\boldsymbol{a} \times \boldsymbol{a}=O$　（ゼロベクトル）

(5) $\boldsymbol{a} \times \boldsymbol{b}=-\boldsymbol{b} \times \boldsymbol{a}$

(6)  $(\boldsymbol{a} \times \boldsymbol{b})\cdot\boldsymbol{a}=0$ （数のゼロ）

  $(\boldsymbol{a} \times \boldsymbol{b})\cdot\boldsymbol{b}=0$ （数のゼロ）

基本ベクトルの外積

$ \boldsymbol{ i } \times \boldsymbol{ j } =\boldsymbol{ k}$

$ \boldsymbol{ j } \times \boldsymbol{ k } =\boldsymbol{ i}$

成分による外積の計算方法

$ \boldsymbol{ a }= (a_1 , a_2, a_3)、 \boldsymbol{b }= (b_1 , b_2, b_3) $とすると

 $\boldsymbol{a} \times \boldsymbol{b}= ( a_2b_3 -a_3b_2 , a_3b_1 -a_1b_3 ,a_1b_2 -a_2b_1 )  $

 となる

$\begin{array}{cccccc}  a_1 & a_2 & a_3 &a_1 & a_2 & a_3 \\ b_1 & b_2 & b_3 &b_1 & b_2 & b_3\\     &a_2b_3 -a_3b_2 & a_3b_1 -a_1b_3 &a_1b_2 -a_2b_1  & &  \end{array}　$

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

```python
import numpy as np
from matplotlib import pyplot
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
```

```python
# 問1.13 (1)　　 Sympy による数式処理の計算
a ,b,ab = symbols('a b ab ')

# 外積の関数を定義
def crossfn(a,b):
    ab = Matrix([ a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0] ])  # 外積の計算
    return ab

# ベクトルを定義する
a = Matrix([6, 2, 0])
b = Matrix([4,-3, 0])

#ab = crossfn(a,b) # 外積の計算
#ab
ab = a.cross(b)  # 外積の計算
ab
```

$$\left[\begin{matrix}0\\0\\-26\end{matrix}\right]$$

```python
ab.norm()     # 外積の大きさ
```

```python
# 問1.13 (1)　 numpy での数値計算

v1 = np.array((6, 2, 0))
v2 = np.array((4,-3 ,0))

     #内積
     # numpy.dot(v1,v2)    #=> 0
#外積
numpy.cross(v1,v2)  #=>[0 0 1]

```

    array([  0,   0, -26])

```python
# 問1.14 (1)　 Sympy による数式処理の計算
a ,b,c ,ab = symbols('a b c ab ')

# 外積の計算の関数を定義する
def crossfn(a,b):
    ab = Matrix([ a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0] ])
    return ab

a = Matrix([0,1,1])
b = Matrix([1,0,1])
c = Matrix([1,1,0])

#ab = crossfn(a,b) # 外積の計算
#ab

ab = a.cross(b)             # 外積の計算
ab
```

$$\left[\begin{matrix}1\\1\\-1\end{matrix}\right]$$

```python

# 問1.14 (1)　 Sympy による数式処理の計算
a ,b,c ,ab = symbols('a b c ab ')
# 外積の関数を定義
def crossfn(a,b):
    ab = Matrix([ a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0] ])  # 外積の計算
    return ab

a = Matrix([0,1,1])
b = Matrix([1,0,1])
c = Matrix([1,1,0])

ab = a  # aとｂの入れ替え
a = b
b = ab

#ab = crossfn(a,b)
#ab

ab = a.cross(b)             # 外積の計算
ab
```

$$\left[\begin{matrix}-1\\-1\\1\end{matrix}\right]$$

```python
# 問1.14 (2)　 Sympy による数式処理の計算
a ,b,c ,ab = symbols('a b c ab ')
# 外積の関数を定義
def crossfn(a,b):
    ab = Matrix([ a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0] ])  # 外積の計算
    return ab

a = Matrix([0,1,1])
b = Matrix([1,0,1])
c = Matrix([1,1,0])

#crossfn(crossfn(a,b),c)

(a.cross(b)).cross(c)
```

$$\left[\begin{matrix}1\\-1\\0\end{matrix}\right]$$

```python
#crossfn(a,crossfn(b,c))
a.cross(b.cross(c))
```

$$\left[\begin{matrix}0\\-1\\1\end{matrix}\right]$$

```python
# 問1.14 (1)　 numpy での数値計算　　（別解）

a = np.array((0,1,1))
b = np.array((1,0,1))
c = np.array((1,1,0))

     #内積
     # numpy.dot(v1,v2)    #=> 0
#外積
np.cross(a,b)  #=>[0 0 1]

```

    array([ 1,  1, -1])

```python
numpy.cross(b,a)  #=>[0 0 1]
```

    array([-1, -1,  1])

```python
# 問1.14 (2)
np.cross(np.cross(a,b),c)
```

    array([ 1, -1,  0])

```python
np.cross(a,np.cross(b,c))
```

    array([ 0, -1,  1])

```python
# 例題　１．７ 　　Sympyによる解法

a ,b,c,ab,ac,aba = symbols('a b c ab ac aba')
# 外積の関数を定義
def crossfn(a,b):
    ab = Matrix([ a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0] ])  # 外積の計算
    return ab

a = Matrix([-1, -1, 0])
b = Matrix([1,  3, 4])
c = Matrix([3,  1,4])
ab = b-a
ac = c-a
# aba = crossfn(ab,ac)# 外積の計算
#aba
ab.cross(ac)
```

$$\left[\begin{matrix}8\\8\\-12\end{matrix}\right]$$

```python
aba.norm()
```

```python
aba.norm()/2
```

```python
# 例題　１．７ 　 numpy での数値計算　（別解）

a = np.array((-1,-1,0))
b = np.array((1,3,4))
c = np.array((3,1,4))

ab = b-a
ac = c-a
numpy.cross(ab,ac)
```

    array([  8,   8, -12])

```python
al = np.linalg.norm(numpy.cross(ab,ac))
al/2
```

```python
# 問１．６ (1)　Sympy による数式処理の計算
A ,B,C,AB,AC,a,b,c,ab,ac,aba,d = symbols('A B C AB AC a b c ab ac aba d')

# 外積の関数を定義
def crossfn(a,b):
    ab = Matrix([ a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0] ])  # 外積の計算
    return ab

A = Matrix([a, 0, 0])
B = Matrix([0, b, 0])
C = Matrix([0, 0, c])

AB = B-A
AC = C-A

#aba =crossfn(AB,AC)# 外積の計算
#aba

aba = AB.cross(AC)
aba
```

$$\left[\begin{matrix}b c\\a c\\a b\end{matrix}\right]$$

```python
# 問１．６ (2)　大きさを求める

aba.norm()/2
```

```python
# 問１．６ (3)　Sympy による数式処理の計算

A ,B,C,AB,AC,a,b,c,ab,ac,aba ,d= symbols('A B C AB AC a b c ab ac aba ,d')

def crossfn(a,b):
    ab = Matrix([ a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0] ])  # 外積の計算
    return ab

A = Matrix([a, 0, 0])
B = Matrix([0, b, 0])
C = Matrix([0, 0, c])
AB = B-A
AC = C-A
# aba =crossfn(AB,AC)# 外積の計算
aba = AB.cross(AC)

aba.norm()/2
#d = aba.T * A / aba.norm()
d = aba.dot(A) / aba.norm()
d
```


Complex Laplacian Eigenmodes
---

Find the highest spatial corelation values achieved by the best performing eigenmodes for each canonical network:

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# spectrome imports
from spectrome.brain import Brain
from spectrome.utils import functions, path
from spectrome.forward import eigenmode, runforward
```

```python
# Some house keeping:
data_dir = "../data"

# Define frequency range of interest
fmin = 2  # 2Hz - 45Hz signal range, filter for this with hbp
fmax = 45
fvec = np.linspace(fmin, fmax, 40)

# Load Pablo's Yeo 2017 canonical network maps
fc_dk = np.load("../data/com_dk.npy", allow_pickle=True).item()
fc_dk_normalized = pd.read_csv("../data/DK_dictionary_normalized.csv").set_index(
    "Unnamed: 0"
)

# Define variables for analysis:
alpha_vec = np.linspace(
    0.5, 4.5, 17
)  # np.linspace(0.5,5,10) # coupling strength values we are going to explore

#alpha_vec = np.linspace(0.1, 1.5, 15)

k_vec = np.linspace(0, 100, 11)  # wave numbers we are going to explore
num_fc = 7  # 7 canonical networks
num_emode = 86  # number of eigenmodes, we are using 86 region DK atlas

default_k = 20  # default wave number
default_alpha = 0.1  # default alpha

# define list of canonical network names and re-order the dictionary using these names:
fc_names = [
    "Limbic",
    "Default",
    "Visual",
    "Fronto \n parietal",
    "Somato \n motor",
    "Dorsal \n Attention",
    "Ventral \n Attention",
]
fc_dk_normalized = fc_dk_normalized.reindex(
    [
        "Limbic",
        "Default",
        "Visual",
        "Frontoparietal",
        "Somatomotor",
        "Dorsal_Attention",
        "Ventral_Attention",
    ]
).fillna(0)

# turbo color map
turbo = functions.create_turbo_colormap()
```

Compute Spearman correlation values:

```python
# pre-allocate an array for spearman R of best performing eigenmodes:
params_bestr = np.zeros((len(alpha_vec), len(k_vec), num_fc))

# Create brain object from spectrome with HCP connectome:
hcp_brain = Brain.Brain()
hcp_brain.add_connectome(data_dir)
hcp_brain.reorder_connectome(hcp_brain.connectome, hcp_brain.distance_matrix)
hcp_brain.bi_symmetric_c()
hcp_brain.reduce_extreme_dir()

# for each network, scan through alpha and k values, compute all eigenmode's spearman R
# then select the best performing eigenmode's spearman R
for i in np.arange(0, num_fc):
    print('Computing for {} network'.format(fc_dk_normalized.index[i]))
    for a_ind in np.arange(0, len(alpha_vec)):
        for k_ind in np.arange(0, len(k_vec)):
            # get eigenmodes of complex laplacian:
            hcp_brain.decompose_complex_laplacian(alpha = alpha_vec[a_ind], k = k_vec[k_ind])
            # compute spearman correlation
            spearman_eig = eigenmode.get_correlation_df(
                hcp_brain.norm_eigenmodes, fc_dk_normalized.iloc[[i]], method = 'spearman'
            )
            params_bestr[a_ind, k_ind, i] = np.max(spearman_eig.values)
```

    Computing for Limbic network
    Computing for Default network
    Computing for Visual network
    Computing for Frontoparietal network
    Computing for Somatomotor network
    Computing for Dorsal_Attention network
    Computing for Ventral_Attention network

Visualize in heatmap:

```python
dynamic_range = [0.30, 0.70]
k_ticks = 11
k_labels = np.linspace(0, 100, 11).astype(int)
a_ticks = 3
a_labels = np.linspace(0.5, 4.5, 3)

with plt.style.context("seaborn-paper"):
    corr_fig, corr_ax = plt.subplots(1,7, figsize = (8,5), sharey=True)
    for i, ax in enumerate(corr_fig.axes):
        im = ax.imshow(np.transpose(params_bestr[:,:,i]), vmin = 0, vmax = 1, cmap = turbo, aspect = 'auto')
        ax.yaxis.set_major_locator(plt.LinearLocator(numticks = k_ticks))
        ax.xaxis.tick_top()
        ax.set_yticklabels(k_labels)
        ax.xaxis.set_major_locator(plt.LinearLocator(numticks = a_ticks))
        ax.set_xticklabels(a_labels)
        im.set_clim(dynamic_range)
        if i < 3:
            ax.set_title(fc_names[i], y=-0.08, fontsize=8, weight="bold")
        else:
            ax.set_title(fc_names[i], y=-0.12, fontsize=8, weight="bold")

    plt.suptitle('Coupling Strength', fontsize = 12, y = 1.02)
    cbar_ax = corr_fig.add_axes([1, 0.15, 0.03, 0.7])
    cb = corr_fig.colorbar(im, cax=cbar_ax, extend="both")
    corr_fig.text(-0.008, 0.35, 'Wave Number', rotation='vertical', fontsize=12)
    #corr_fig.add_subplot(1, 1, 1, frameon=False)
    #plt.tick_params(labelcolor="none", top="off", bottom="off", left="off", right="off")
    #plt.grid(False)
    #plt.ylabel('Wave Number', fontsize = 12)

    plt.tight_layout()
    plt.savefig('../figures/supp/param_bestr.png', dpi = 300, bbox_inches = 'tight')
```

Note - global coupling doesn't affect the best performing eigenmode but may change which eigenmode is the best performing eigenmode as well as the other eigenmodes.

Split the wave number parameter into oscillatory frequency and signal transmission velocity since wave number $k$ is defined as $k = \frac{2 \pi f}{\nu}$. Then perform the same exploratory exercise as above:

```python
# define parameter ranges:
freq_vec = np.linspace(2, 47, 46)
nu_vec = np.linspace(1, 20, 21)

# define plotting visuals
dynamic_range = [0.3, 0.7]
f_ticks = 6
f_labels = np.linspace(2, 47, 6).astype(int)
nu_ticks = 3
nu_labels = np.linspace(0.5, 20, 3).astype(int)

#pre-allocate array for results
k_bestr = np.zeros((len(freq_vec), len(nu_vec), num_fc))

# compute spearman Rs:
for i in np.arange(0, num_fc):
    print('Computing for {} network'.format(fc_dk_normalized.index[i]))
    for f_ind in np.arange(0, len(freq_vec)):
        for v_ind in np.arange(0, len(nu_vec)):
            # get eigenmodes of complex laplacian:
            hcp_brain.decompose_complex_laplacian(alpha = default_alpha, k = None, f = freq_vec[f_ind], speed = nu_vec[v_ind])
            # compute spearman correlation
            spearman_eig = eigenmode.get_correlation_df(
                hcp_brain.norm_eigenmodes, fc_dk_normalized.iloc[[i]], method = 'spearman'
            )
            k_bestr[f_ind, v_ind, i] = np.max(spearman_eig.values)

```

```python
# Plot as above:
with plt.style.context("seaborn-paper"):
    k_fig, k_ax = plt.subplots(1,7, figsize = (8,4),sharey=True)
    for i, ax in enumerate(k_fig.axes):
        im = ax.imshow(k_bestr[:,:,i], vmin = 0, vmax = 1, cmap = turbo, aspect = 'auto')
        ax.yaxis.set_major_locator(plt.LinearLocator(numticks = f_ticks))
        ax.xaxis.tick_top()
        ax.set_yticklabels(f_labels)
        ax.xaxis.set_major_locator(plt.LinearLocator(numticks = nu_ticks))
        ax.set_xticklabels(nu_labels)
        im.set_clim(dynamic_range)
        if i < 3:
            ax.set_title(fc_names[i], y=-0.08, fontsize=8, weight="bold")
        else:
            ax.set_title(fc_names[i], y=-0.12, fontsize=8, weight="bold")

    plt.suptitle('Transmission Velocity (m/s)', fontsize = 12, y = 1.02)
    k_fig.text(-0.008, 0.35, 'Frequency (Hz)', rotation='vertical', fontsize=12)
    #cbar_ax = k_fig.add_axes([1, 0.15, 0.03, 0.7])
    #cb = k_fig.colorbar(im, cax=cbar_ax, extend="both")
    #k_fig.add_subplot(1, 1, 1, frameon=False)
    #plt.tick_params(labelcolor="none", top="off", bottom="off", left="off", right="off")
    #plt.grid(False)
    #plt.ylabel('Frequency (Hz)', fontsize = 12)

    plt.tight_layout()
    plt.savefig('../figures/supp/k_bestr.png', dpi = 300, bbox_inches = 'tight')
```

Entropy
---

We want to see the entropy for all the Spearman correlation values computed with each parameter combination. The information entropy metric is defined as:

\begin{equation}
    S = - \sum_{i} \pmb{P}_i log \pmb{P}_i
\end{equation}

Entropy is generally viewed as the uncertainty when making a prediction based on available information. In the case of high entropy, we know the information we have is highly diverse and it is difficult to make any predictions. So we want to look for parameter values providing low entropy, or high fidelity.

```python
from scipy.stats import entropy

params_entropy = np.zeros((len(alpha_vec), len(k_vec), num_fc))

# using the same parameter values as above
for i in np.arange(0, num_fc):
    print('Computing for {} network'.format(fc_dk_normalized.index[i]))
    for a_ind in np.arange(0, len(alpha_vec)):
        for k_ind in np.arange(0, len(k_vec)):
            # get eigenmodes of complex laplacian:
            hcp_brain.decompose_complex_laplacian(alpha = alpha_vec[a_ind], k = k_vec[k_ind])
            # compute spearman correlation
            spearman_eig = eigenmode.get_correlation_df(
                hcp_brain.norm_eigenmodes, fc_dk_normalized.iloc[[i]], method = 'spearman'
            )

            # Create distribution of eigenmode spearman Rs with consistent bins, and turn into probability distribution
            prob_dist, _ = np.histogram(spearman_eig.values.astype(np.float64), bins = 40, range = (-0.5, 0.8), density = True)

            #print(np.squeeze(np.asarray(spearman_eig.values)))
            params_entropy[a_ind, k_ind, i] = entropy(prob_dist, base = 2)
```

    Computing for Limbic network
    Computing for Default network
    Computing for Visual network
    Computing for Frontoparietal network
    Computing for Somatomotor network
    Computing for Dorsal_Attention network
    Computing for Ventral_Attention network

```python
#entropy_range = [3.3, 3.7]

# plot like before:
with plt.style.context("seaborn-paper"):
    corr_fig, corr_ax = plt.subplots(1,7, figsize = (8,4))
    for i, ax in enumerate(corr_fig.axes):
        im = ax.imshow(np.transpose(params_entropy[:,:,i]), cmap = turbo, aspect = 'auto')
        ax.yaxis.set_major_locator(plt.LinearLocator(numticks = k_ticks))
        ax.xaxis.tick_top()
        ax.set_yticklabels(k_labels)
        ax.xaxis.set_major_locator(plt.LinearLocator(numticks = a_ticks))
        ax.set_xticklabels(a_labels)
        #im.set_clim(entropy_range)
        if i < 3:
            ax.set_title(fc_names[i], y=-0.08, fontsize=8, weight="bold")
        else:
            ax.set_title(fc_names[i], y=-0.12, fontsize=8, weight="bold")

    plt.suptitle('Coupling Strength', fontsize = 12, y = 1)
    cbar_ax = corr_fig.add_axes([1, 0.15, 0.03, 0.7])
    cb = corr_fig.colorbar(im, cax=cbar_ax, extend="both")
    corr_fig.add_subplot(1, 1, 1, frameon=False)
    plt.tick_params(labelcolor="none", top="off", bottom="off", left="off", right="off")
    plt.grid(False)
    plt.ylabel('Wave Number', fontsize = 12)

    plt.tight_layout()
```


Table of Contents

1  28-June-2019

1.1  Diagonalization

1.2  Symmetric matrices

2  1-July-2019

2.1  Singular value decomposition (SVD)

3  2-July-2019

3.1  SVD to DMD

4  4-July-2019

4.1  Least squares

4.2  Reducing dimensionality with SVD

5  5-July-2019

5.1  DMD: Overview, Definition, and Algorithm

6  Exercises

# 28-June-2019

## Diagonalization

Convert the unknown problem to known problem

We do this because it converts

$N \times N$ problem to $N$ dimensional

So a dimensionality reduction method

- History of least squares
Gauss invented it and Legendre also made contributions

- Example from dynamical system
Lyapunov exponent are singular values

- Necessary and sufficient condition: need $N$ independent eigenvectors

- How do we get the eigenvalue problem?
\begin{equation}
A = \begin{pmatrix}
a & b \\
c & d
\end{pmatrix} \; \qquad \text{Known (given)}
\end{equation}

\begin{equation}
\Lambda = \begin{pmatrix}
\lambda_1 & 0 \\
0 & \lambda_2
\end{pmatrix} \; \qquad \text{desire}
\end{equation}

A general square linear system
$$A x = b$$

Assume an invertible matrix $P$ exists

\begin{align}
A P & = P \Lambda \\
P^{-1}AP & = \Lambda \\
A & = P \Lambda P^{-1}
\end{align}

For a $2 \times 2$ matrix $A P = P \Lambda$ gives

\begin{equation}
\begin{pmatrix}
a e_1^1 + b e_1^2  & a e_2^1 + b e_2^2 \\
c e_1^1 + d e_1^2  & c e_2^1 + d e_2^2
\end{pmatrix} =
\begin{pmatrix}
\lambda_1 e_1^1  & \lambda_2 e_2^1 \\
\lambda_1 e_1^2  & \lambda_2 e_2^2
\end{pmatrix}
\end{equation}

This results in $A e_i = \lambda_i e_i$.

\begin{equation}
\underbrace{\underbrace{M}_{m \times n} \underbrace{M^T}_{n \times m} }_{m \times m} = U \Sigma V^T V \Sigma^T U^T = U \Sigma \Sigma^T U^T
\end{equation}

\begin{equation}
\underbrace{\underbrace{M^T}_{n \times m} \underbrace{M}_{m \times n} }_{n \times n} = V \Sigma^T U^T U \Sigma V^T = V \Sigma^T \Sigma V^T
\end{equation}

$MM^T$ and $M^TM$ are positive semi-definite symmetric matrices.

## Symmetric matrices

- Orthogonal matrix is symmetric, when it is real

- Orthogonal matrix is unitary, when it is complex

- Prove the eigenvalues of real, symmetric matrix is real

- Prove the eigenvectors corresponding to distinct eigenvalues are orthogonal

Positive definite symmetric matrix, $M$, which is $N \times N$

\begin{equation}
U^T M U > 0\, , \qquad \forall \; U \in \mathbb{R}^N
\end{equation}

Eigenvalue of $2 \times 2$ matrix $A$ is

\begin{equation}
\lambda_{1,2} = \frac{\text{tr} (A)}{2} \pm \frac{1}{2}\sqrt{\text{tr}(A)^2 - 4 \text{det}(A) }
\end{equation}

Conditions for the eigenvalues to be non-zero and distinct.

__Theorem__ Prove eigenvalues of real, symmetric matrix is real.

Sketch of the proof: $$A e = \lambda e$$ and $$A e^* = \lambda^* e^*$$

\begin{equation}
\lambda (e^*)^T e = (e^*)^T A e = ((e^*)^T A) e = (A^Te^*)^T e = (A e^*)^T e = (\lambda^* e^*)^T e = \lambda^* (e^*)^T e
\end{equation}

# 1-July-2019

## Singular value decomposition (SVD)

Positive semi-definite matix satisfies

1. All diagonal elements are positive
2. All eigenvalues are positive

\begin{equation}
A \in \mathbb{R}^N \times \mathbb{R}^N
\end{equation}

Symmetric
$$ A = A^T $$

Orthogonal
$$A^{-1} = A^T$$

__Rank-Nullity theorem__

Matrices $\underbrace{M}_{m \times n}$

\begin{align}
rank(M) + nullity (M) & = n \\
dim(I_m M) + dim(ker M) & = n
\end{align}

Domain of $\underbrace{M}_{m \times n}$ has dimension

$n$ is the dimension of the domain then it is also the column space of the $M$

__Transpose of a matrix__ (Non-square matrices)

"Swap rows with columns" $A_{ij} = A_{ji}$

$A$ and $A^T$ have the ``same eigenvalue``. (strictly true for square matrices)

$AA^T$ and $A^TA$ is symmetric

Note they are different, $AA^T$ is a $N \times N$ matrix and $A^TA$ is $M \times M$.

For non-square matrices $M_{m \times n}$, the singular value decomposition is

\begin{equation}
\underbrace{M}_{m \times n} = \underbrace{U}_{m \times m} \qquad \underbrace{\Sigma}_{m \times n} \qquad \underbrace{V^T}_{n \times n}
\end{equation}

1. Columns of $U$ are the eigenvectors of $MM^T$
2. Columns of $V$ are the eigenvectors of $M^TM$

Non-zero elements of $\Sigma$ are the square roots of the non-zero eigenvalues of $M^TM$ or $MM^T$

\begin{equation}
M = U \Sigma V^T, \qquad \underbrace{\Sigma}_{m \times n}  = \underbrace{U^T}_{m \times m} \qquad \underbrace{M}_{m \times n} \qquad \underbrace{V}_{n \times n}
\end{equation}

\begin{equation}
\Sigma = diag (\sigma_1, \ldots, \sigma_p), \qquad p = \min(m,n)
\end{equation}

$\sigma_1 \geqslant \sigma_2 \geqslant \ldots \sigma_p > 0$ are the __singular values__ of M

If $r$ is the rank of $M$, then $M$ has exactly $r$ positive singular values

__Exercise__

1. Singular value decomposition of $M = [1, 2]$.

# 2-July-2019

## SVD to DMD

- Four things to remember:

\begin{equation}
\underbrace{M}_{m \times n} = \underbrace{U}_{m \times m} \qquad \underbrace{\Sigma}_{m \times n} \qquad \underbrace{V^T}_{n \times n}
\end{equation}

\begin{equation}
\underbrace{\Sigma}_{m \times n}  = \underbrace{U^T}_{m \times m} \qquad \underbrace{M}_{m \times n} \qquad \underbrace{V}_{n \times n}
\end{equation}

Two equations involving $M^TM$ and $MM^T$

__Algorithm to compute SVD__

1. Compute the non-zero eigenvalues
$\sigma_1^2 \geqslant \sigma_2^2 \geqslant \ldots \sigma_p^2 > 0, \qquad p = \min(m,n)$
corresponding eigenvectors of $M^TM$

If $p < n$ the other eigenvalues are zero

The corresponding eigenvalues

\begin{equation}
\left[ v_1, \ldots, v_p, v_{p+1}, \ldots, v_n \right]
\end{equation}

the p columns make up $V$

2. From the form of the SVD we have

$$M V = U \Sigma$$

when $\sigma_i > 0$ (that is for $1 \leqslant i \leqslant p$)

$$ \frac{1}{\sigma_i} M V_i = U_i$$

Extend the set $u_1, \ldots, u_p$ to an orthonormal basis

$$ u_1, \ldots, u_p, \ldots, u_m$$ then

\begin{equation}
U = \left[ u_1, \ldots, u_p, u_{p+1}, \ldots, u_m \right]
\end{equation}

- Example:

\begin{equation}
\underbrace{M}_{2 \times 4} = \underbrace{U}_{2 \times 2} \qquad \underbrace{\Sigma}_{2 \times 4} \qquad \underbrace{V^T}_{4 \times 4}
\end{equation}

Let
\begin{equation}
M = \begin{pmatrix}
1 & 0 & 1 & 0 \\
0 & 1 & 0 & 1
\end{pmatrix}
\end{equation}

\begin{equation}
\underbrace{M}_{2 \times 4} \underbrace{M^T}_{4 \times 2} = \begin{pmatrix}
2 & 0 \\
0 & 2
\end{pmatrix}
\end{equation}

\begin{equation}
\underbrace{M^T}_{4 \times 2} \underbrace{M}_{2 \times 4} = \begin{pmatrix}
1 & 0 & 1 & 0 \\
0 & 1 & 0 & 1 \\
1 & 0 & 1 & 0 \\
0 & 1 & 0 & 1
\end{pmatrix}
\end{equation}

Note: both matrices above have rank 2 and eigenvalues $2,2,0,0$. ``check this claim``

\begin{equation}
\Sigma = \begin{pmatrix}
\sqrt{2} & 0 & 0 & 0 \\
0 & \sqrt{2} & 0 & 0
\end{pmatrix}
\end{equation}

Also,
\begin{equation}
U = \begin{pmatrix}
1 & 0 \\
0 & 1
\end{pmatrix}
\end{equation}

We can calculate the matrix $V$ as

\begin{equation}
\underbrace{M^T}_{4 \times 2} \underbrace{U}_{2 \times 2} \underbrace{\Sigma}_{2 \times 4} = \underbrace{V}_{4 \times 4}
\end{equation}

```python
import numpy as np
from numpy.linalg import svd, inv, eig

M = [[1,0,1,0],[0,1,0,1]]
[U, S, V] = svd(M, full_matrices = True)
# S = np.diagflat(S) # because numpy returns S as 1D array

print(np.size(M,0))

print(np.shape(M))
print(U)

print(S)

print(V)

```

    2
    (2, 4)
    [[1. 0.]
     [0. 1.]]
    [1.41421356 1.41421356]
    [[ 0.70710678  0.          0.70710678  0.        ]
     [ 0.          0.70710678  0.          0.70710678]
     [-0.70710678  0.          0.70710678  0.        ]
     [ 0.         -0.70710678  0.          0.70710678]]

```python
# check
Sigma = np.zeros((2,4))
Sigma[0,0] = S[0]
Sigma[1,1] = S[1]

print(np.shape(Sigma))

np.dot(np.dot(U,Sigma),V)
# print(np.shape(temp))
print(np.dot(np.dot(U,Sigma),V))

```

    (2, 4)
    [[1. 0. 1. 0.]
     [0. 1. 0. 1.]]

# 4-July-2019

\begin{equation}
\underbrace{M}_{m \times n} = \underbrace{U}_{m \times m} \underbrace{\Sigma}_{m \times n} \underbrace{V^T}_{n \times n}
\end{equation}

domain $\mathbb{R}^n$ and range $\mathbb{R}^m$

rank nullity theorem

\begin{equation}
rank(\underbrace{M}_{m \times n}) + dim (ker \underbrace{M}_{m \times n}) = n
\end{equation}

__Example__:

\begin{equation}
M = \begin{pmatrix}
1 & 0 & 1 & 0 \\
0 & 1 & 0 & 1
\end{pmatrix}
\end{equation}

here, $dim(ker \underbrace{M}_{2 \times 4}) = 2$ and hence $M$ is non-invertible

__Moore-Penrose Pseudoinverse__

\begin{equation}
\underbrace{M}_{m \times n} = \underbrace{U}_{m \times m} \underbrace{\Sigma}_{m \times n} \underbrace{V^T}_{n \times n}
\end{equation}

The inverse of $M$ defined as $M^+$

\begin{equation}
\underbrace{M^+}_{n \times m} = \underbrace{V}_{n \times n} \underbrace{\Sigma^+}_{n \times m} \underbrace{U^T}_{m \times m}
\end{equation}

$\Sigma^+$: take the reciprocal of each non-zero singular value, then take the transpose.

__Missing show $M^TM$ is close to identity__

__Example__:

Compute $\Sigma^+$ and check the product $\Sigma \Sigma^+$ for the matrix M given by

\begin{equation}
M = \begin{pmatrix}
1 & 0 & 1 & 0 \\
0 & 1 & 0 & 1
\end{pmatrix}
\end{equation}

```python

Mplus = np.linalg.pinv(M)
print(Mplus)

[vplus,sigmaplus,uplus] = np.linalg.svd(Mplus)

print(vplus)

print(sigmaplus)

print(uplus)

```

## Least squares

Solve $M x = b$ and the pseudoinverse

\begin{equation}
\underbrace{M}_{m \times n} \underbrace{x}_{n \times 1} = \underbrace{b}_{m \times 1}
\end{equation}

- Always a non-trivial kernel of $m$ and $n$ are unequal

Statement (without proof)

__Exercise__

\begin{equation}
A = \begin{pmatrix}
0 & 1\\
0 & 0
\end{pmatrix}
\end{equation}

Calculate the kernel and image of this linear map.

\begin{equation}
\hat{x}_{LS} = M^+ b
\end{equation}

this vector minimizes $|| M x - b ||^2$

$|| \cdot ||$ is the 2-norm (square root of the sum of the squares)

Construct an example where $\hat{x}$ solves $M x = b$, where $M$ is the same as above and choose $b$ to solve for $\hat{x}$.

## Reducing dimensionality with SVD

__Truncated SVD__

Take the $k$ largest singular values, $0 < k < n$ and their corresponding eigenvectors.

\begin{equation}
\underbrace{M}_{m \times n} \approx \underbrace{U_t}_{m \times k} \underbrace{\Sigma}_{k \times k} \underbrace{V_t^T}_{k \times n} = M_t
\end{equation}

__Reduced SVD__

keep all non-zero singular values

\begin{equation}
M_r = U_r \Sigma_r V_r^T
\end{equation}

- Compute both for the matrix $M$

__DMD__: Dynamic mode reduction

``Always be mindful of the intended use and expected outcome``

- Potential use of DMD

    * Diagnostics

    * State estimation and future state prediction

    * Control

# 5-July-2019

## DMD: Overview, Definition, and Algorithm

__Data__

Data from a dynamical system

\begin{equation}
\dot{x} = f(x, t, \mu), \qquad x \in \mathbb{R}^n
\end{equation}

or discrete dynamical system $x_{k+1} = g(x_k)$

DMD constructs a proxy local(linear) dynamical system that approximates the dynamics

\begin{equation}
\frac{dx}{dt} = \mathcal{A} x
\end{equation}

where $\mathcal{A}$

or, for discrete time evolution $A = e^{\mathcal{A}t}$

Collection of trajectories $x_1, \ldots, x_m$ where $x_i \in \mathbb{R}^n$.

DMD produces a matrix $A$ such that

\begin{equation}
|| x_{k+1} - A x_k ||
\end{equation}

is minimized for $k = 1, \ldots, (m-1)$.

$x_1, \ldots, x_m$ are evaluated at a specific time, $\delta t$

"$m$ snapshots of the dynamical system"

Arrange the $m$ snapshots into 2 data matrices

\begin{equation}
\underbrace{X}_{n \times (m-1)} = \begin{pmatrix}
\vert & \ldots & \vert \\
x_1 & \ldots & x_{m-1} \\
\vert & \ldots & \vert
\end{pmatrix}
\end{equation}

\begin{equation}
\underbrace{X^\prime}_{n \times (m-1)} \approx \begin{pmatrix}
\vert & \ldots & \vert \\
x_2 & \ldots & x_m \\
\vert & \ldots & \vert
\end{pmatrix}
\end{equation}

such that $$X^\prime = A X$$ and this works because
\begin{equation}
\begin{pmatrix}
\vert & \ldots & \vert \\
x_2 & \ldots & x_m \\
\vert & \ldots & \vert
\end{pmatrix} =
\begin{pmatrix}
\vert & \ldots & \vert \\
Ax_1 & \ldots & Ax_{m-1} \\
\vert & \ldots & \vert
\end{pmatrix}
\end{equation}

Traditional DMD applications have $n >> m$

We try to find the best fit $A$ to this system

\begin{equation}
A \approx X^\prime X^+
\end{equation}

where $X^+$ is the pseudo-inverse. This solution minimizes

\begin{equation}
|| X^\prime - A X ||_F
\end{equation}

where $||\cdot||_F$ is the Frobenius norm

\begin{equation}
||X||_F = \sqrt{\Sigma_{j=1}^n \Sigma_{k=1}^m X^2_{jk}}
\end{equation}

__DMD Definition__

Suppose we have a dynamical system

\begin{equation}
\frac{dx}{dt} = f(x, t; \mu)
\end{equation}

and two sets of data $X,X^\prime$ such that

\begin{equation}
x_k^\prime = \mathcal{F}(x_k)
\end{equation}

where $\mathcal{F}(\cdot)$ is the evolution map of

\begin{equation}
\frac{dx}{dt} = F(x,t;\mu)
\end{equation}

for time $\delta t$

DMD computes the leading eigendecomposition of the best fit linear operator $A$ relating the data

\begin{equation}
X^\prime \approx A X, \qquad A = X^\prime X^+
\end{equation}

where $X^+$

The DMD modes, also called dynamic modes, are the eigenvectors of $A$ and each DMD mode corresponds to the particular eigenvalue of $A$.

# Exercises

1. Prove that symmetric matrices have $N$ independent eigenvectors.

```python

```


# Reinforcement Learning for Ion Trap Quantum Computers

This exercise is a short extension of the **Ion Trap Reinforcement Learning Environment** where we are going to employ a Projective Simulation (PS) agent to use short laser pulse sequences mapping an initially unentangled state $|000\rangle$ onto a GHZ-like state:

\begin{align}
|\mathrm{GHZ}\rangle = \frac{1}{\sqrt{d}}\sum_{i=0}^{d-1}|iii\rangle.\nonumber
\end{align}

We will consider three qutrits, i.e., $d=3$ for simplicity but you may choose to extend this at your own leisure.

More formally, we do not want to find GHZ states exactly but those states which are maximally entangled. We consider $n$ $d$-level states to be maximally entangled if they have a *Schmidt rank vector* (SRV) of $(d,...,d)$ where the $i$th entry is the rank of the reduced density matrix $\rho_i=\mathrm{tr}_{\bar{i}}(\rho)$ where $\bar{i}$ is the complement of $\{i\}$ in $\{1,...,n\}$.

Luckily, you don't really have to take care of this since this is already the default settings of the environment which we are going to load now:

```python
from ion_trap import IonTrapEnv
```

That was easy. According to the docs in the `init` method, the class allows the following kwargs:

* `num_ions` (int): The number of ions. Defaults to 3.
* `dim` (int): The local (odd) dimension of an ion. Defaults to 3.
* `goal` (list): List of SRVs that are rewarded. Defaults to `[[3,3,3]]`.
* `phases` (dict): The phases defining the laser gate set. Defaults to `{'pulse_angles': [np.pi/2], 'pulse_phases': [0, np.pi/2, np.pi/6], 'ms_phases': [-np.pi/2]}`
* `max_steps` (int): The maximum number of allowed time steps. Defaults to 10.

If you want to change anything you need to provide kwargs in form of a `dict` with the desired arguments as follows `IonTrapEnv(**{ 'max_steps': 20 })`.
Indeed, let us submit a small change. Since this is just supposed to be a small scale test, let us reduce the number of allowed phases and therefore, the number of possible actions.

```python
import numpy as np
KWARGS = {'phases': {'pulse_angles': [np.pi/2], 'pulse_phases': [np.pi/2], 'ms_phases': [-np.pi/2]}}
env = IonTrapEnv(**KWARGS)
```

Next, we need to get the reinforcement learning agent that is to learn some pulse sequences. We have a simple PS agent for you in store:

```python
from ps import PSAgent
```

For the args of this class the docs say the following:

* `num_actions` (int): The number of available actions.
* `glow` (float, optional): The glow (or eta) parameter. Defaults to 0.1
* `damp` (float, optional): The damping (or gamma) parameter. Defaults to 0.
* `softmax` (float, optional): The softmax (or beta) parameter. Defaults to 0.1.

We don't know the number of actions at this point, but possibly want to keep all the other default parameters. Let's ask the environment how many actions there are and initialize the agent accordingly.

```python
num_actions = env.num_actions

agent = PSAgent(num_actions)
```

Fantastic, we have everything ready for a first run. Let's do that. The interaction between an environment and an agent is standardized through the [*openAI* `gym`](https://github.com/openai/gym) environments. In terms of code, we can imagine the interaction to go as follows,

Indeed, every reinforcement learning environment should provide at least two methods:

* `reset()`: Resets the environment to its initial state. *Returns* the initial observation.
* `step(action)`: Performs an action (given by an action index) on the environment. *Returns* the new observation, an associated reward and a bool value `done` which indicates whether a terminal state has been reached.

The agent on the other hand, supports the following two main methods:

* `predict(observation)`: Given an observation, the agent predicts an action. *Returns* an action index.
* `learn(reward)`: Uses the current reward to update internal network.

Knowing that the `IonTrapEnv` has been built according to this standard and the agent features the two methods above, we can start coding the interaction between agent and environment:

```python
# data set for performance evaluation
DATA_STEPS = []
# maximum number of episodes
NUM_EPISODES = 5000

for i in range(NUM_EPISODES):
    # initial observation from environment
    observation = env.reset()
    #bool: whether or not the environment has finished the episode
    done = False
    #int: the current time step in this episode
    num_steps = 0
    action_seq = []
    while not done:
        # increment counter
        num_steps += 1
        # predict action
        action = agent.predict(observation)
        action_seq.append(action)
        # perform action on environment and receive observation and reward
        observation, reward, done = env.step(action)
        # learn from reward
        agent.train(reward)

        # gather statistics
        if done:
            DATA_STEPS.append(num_steps)

print(action_seq)

```

    [0, 1, 5, 3, 0]

And this is all the code that is needed to have an agent interact with our environment! In `DATA_STEPS` we have gathered the data that keeps track of the length of pulse sequences that generate GHZ-like states. We can use `matplotlib` to visualize the performance of the agent over time:

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

x_axis = np.arange(len(DATA_STEPS))

plt.plot(x_axis, DATA_STEPS)
plt.ylabel('Length of pulse sequence')
plt.xlabel('Episode')

```

We have witnessed an agent learning! The agent was able to push the gate sequences down to 5 laser pulses consisting of two Molmer-Sorensen gates and three single-ion laser pules.
Note that this is of course not conclusive because it is a single agent. Nevertheless, it has obviously learned and we can expect future agents to fare similarly. **Good work!**

Reinforcement Learning for Ion Traps.ipynb


```python
%matplotlib inline

import numpy as np
import os
import pandas as pd
import scipy.stats
#from torch_models.torch_autoencoder import TorchAutoencoder
from utils import devdf_generator
import utils
import vsm
%load_ext autoreload
%autoreload 2
```

```python
from vsm import observed_over_expected, pmi

VSM_HOME = os.path.join('data', 'vsmdata')
DATA_HOME = os.path.join('data', 'wordrelatedness')
dev = pd.read_csv(os.path.join(DATA_HOME, "cs224u-wordrelatedness-dev.csv"))
highest = devdf_generator(dev, scoring='highest')

giga5 = pd.read_csv(
    os.path.join(VSM_HOME, 'giga_window5-scaled.csv.gz'), index_col=0)

def run_ppmi_lsa_pipeline(count_df, k):
    #reweights count matrix with PPMI
    ppmi_df = pmi(count_df)

    #reduce dimensions to k
    lsa_df = vsm.lsa(ppmi_df, k=k)

    #evaluate matrices and return rho value
    return lsa_df
```

    All problematic word pairs are removed

```python
lsa_pmi_df = run_ppmi_lsa_pipeline(giga5, 200)
```

```python
lsa_pmi_df
```

      0
      1
      2
      3
      4
      5
      6
      7
      8
      9
      ...
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199

      ):
      -18.711409
      4.900579
      -11.949006
      7.619705
      -4.803741
      0.295553
      -1.690303
      0.776002
      -8.204364
      -0.222223
      ...
      -0.695254
      0.532318
      1.208098
      1.119802
      -1.381378
      -0.335691
      -0.087262
      1.132048
      -0.913122
      -0.084406

      );
      -44.039976
      4.653525
      -29.982383
      10.874609
      -7.397520
      0.708079
      -8.166617
      3.459671
      0.291242
      -1.563390
      ...
      -1.607194
      -0.252435
      1.892477
      -0.429981
      -0.026948
      1.738859
      1.683467
      0.567581
      0.046754
      -0.580528

      ..
      -6.628508
      3.921621
      -6.371132
      3.384212
      -1.776474
      -0.531172
      -1.983503
      0.544106
      0.006197
      -0.688598
      ...
      -0.647599
      -0.158286
      0.780035
      0.448969
      -1.396239
      1.890057
      -0.105619
      0.654488
      0.364068
      -0.559338

      ...
      -10.811513
      6.491693
      -0.398072
      5.652977
      4.717426
      -2.477134
      3.875756
      -1.596096
      -2.386200
      -0.312380
      ...
      -2.274872
      -0.379487
      1.220273
      -0.551056
      -1.374651
      1.891362
      0.082035
      0.199472
      0.512574
      -0.015002

      :(
      -10.097933
      6.200493
      -1.056482
      3.904959
      -1.104757
      -0.080628
      -0.228672
      0.676866
      -2.202600
      0.297533
      ...
      0.789521
      0.285525
      2.067676
      0.045864
      1.884015
      -0.388984
      -0.676349
      -0.125118
      -1.458238
      -1.315703

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      zero
      -9.027550
      4.891728
      3.786590
      -0.120374
      -2.815991
      -3.505473
      2.257091
      1.860360
      -3.169254
      -0.794824
      ...
      -0.275107
      -0.496205
      -1.076901
      -1.569307
      0.975762
      1.162182
      0.672893
      0.650175
      0.819465
      0.784140

      zinc
      -14.663524
      -0.477127
      3.943060
      -6.055657
      -6.342210
      -4.004544
      -3.800908
      1.155025
      -0.814718
      -1.958009
      ...
      0.756889
      1.668791
      0.408496
      0.466616
      1.076727
      1.716668
      0.728453
      0.688041
      0.450873
      -0.703006

      zombie
      -18.307259
      9.332290
      -2.659564
      4.959793
      8.941554
      1.407367
      0.476790
      -0.579876
      0.983676
      -4.546603
      ...
      -1.182854
      1.717256
      0.148734
      -2.962060
      -0.995720
      -0.192644
      -0.703326
      -2.301400
      -1.911398
      -2.079608

      zone
      -7.055234
      4.008188
      0.172956
      -2.166350
      -4.881651
      -2.365222
      4.729490
      3.258752
      -1.518766
      1.428573
      ...
      1.007383
      -0.314858
      -0.476889
      -0.927420
      0.963988
      0.797876
      0.377941
      0.462486
      -2.145414
      -0.633081

      zoo
      -14.514810
      7.306112
      -6.026169
      -1.506014
      -3.894146
      4.562071
      7.279045
      1.326965
      6.625558
      -0.546725
      ...
      1.295017
      -1.869767
      1.684031
      -0.035928
      0.711830
      -0.827298
      0.677804
      1.255523
      0.670681
      1.620571

6000 rows × 200 columns

# Word Embeddings: Encoding Lexical Semantics

Word embeddings are dense vectors of real numbers, one per word in your
vocabulary. In NLP, it is almost always the case that your features are
words! But how should you represent a word in a computer? You could
store its ascii character representation, but that only tells you what
the word *is*, it doesn't say much about what it *means* (you might be
able to derive its part of speech from its affixes, or properties from
its capitalization, but not much). Even more, in what sense could you
combine these representations? We often want dense outputs from our
neural networks, where the inputs are $|V|$ dimensional, where
$V$ is our vocabulary, but often the outputs are only a few
dimensional (if we are only predicting a handful of labels, for
instance). How do we get from a massive dimensional space to a smaller
dimensional space?

How about instead of ascii representations, we use a one-hot encoding?
That is, we represent the word $w$ by

\begin{align}\overbrace{\left[ 0, 0, \dots, 1, \dots, 0, 0 \right]}^\text{|V| elements}\end{align}

where the 1 is in a location unique to $w$. Any other word will
have a 1 in some other location, and a 0 everywhere else.

There is an enormous drawback to this representation, besides just how
huge it is. It basically treats all words as independent entities with
no relation to each other. What we really want is some notion of
*similarity* between words. Why? Let's see an example.

Suppose we are building a language model. Suppose we have seen the
sentences

* The mathematician ran to the store.
* The physicist ran to the store.
* The mathematician solved the open problem.

in our training data. Now suppose we get a new sentence never before
seen in our training data:

* The physicist solved the open problem.

Our language model might do OK on this sentence, but wouldn't it be much
better if we could use the following two facts:

* We have seen  mathematician and physicist in the same role in a sentence. Somehow they
  have a semantic relation.
* We have seen mathematician in the same role  in this new unseen sentence
  as we are now seeing physicist.

and then infer that physicist is actually a good fit in the new unseen
sentence? This is what we mean by a notion of similarity: we mean
*semantic similarity*, not simply having similar orthographic
representations. It is a technique to combat the sparsity of linguistic
data, by connecting the dots between what we have seen and what we
haven't. This example of course relies on a fundamental linguistic
assumption: that words appearing in similar contexts are related to each
other semantically. This is called the `distributional
hypothesis <https://en.wikipedia.org/wiki/Distributional_semantics>`__.

## Getting Dense Word Embeddings

How can we solve this problem? That is, how could we actually encode
semantic similarity in words? Maybe we think up some semantic
attributes. For example, we see that both mathematicians and physicists
can run, so maybe we give these words a high score for the "is able to
run" semantic attribute. Think of some other attributes, and imagine
what you might score some common words on those attributes.

If each attribute is a dimension, then we might give each word a vector,
like this:

\begin{align}q_\text{mathematician} = \left[ \overbrace{2.3}^\text{can run},
   \overbrace{9.4}^\text{likes coffee}, \overbrace{-5.5}^\text{majored in Physics}, \dots \right]\end{align}

\begin{align}q_\text{physicist} = \left[ \overbrace{2.5}^\text{can run},
   \overbrace{9.1}^\text{likes coffee}, \overbrace{6.4}^\text{majored in Physics}, \dots \right]\end{align}

Then we can get a measure of similarity between these words by doing:

\begin{align}\text{Similarity}(\text{physicist}, \text{mathematician}) = q_\text{physicist} \cdot q_\text{mathematician}\end{align}

Although it is more common to normalize by the lengths:

\begin{align}\text{Similarity}(\text{physicist}, \text{mathematician}) = \frac{q_\text{physicist} \cdot q_\text{mathematician}}
   {\| q_\text{physicist} \| \| q_\text{mathematician} \|} = \cos (\phi)\end{align}

Where $\phi$ is the angle between the two vectors. That way,
extremely similar words (words whose embeddings point in the same
direction) will have similarity 1. Extremely dissimilar words should
have similarity -1.

You can think of the sparse one-hot vectors from the beginning of this
section as a special case of these new vectors we have defined, where
each word basically has similarity 0, and we gave each word some unique
semantic attribute. These new vectors are *dense*, which is to say their
entries are (typically) non-zero.

But these new vectors are a big pain: you could think of thousands of
different semantic attributes that might be relevant to determining
similarity, and how on earth would you set the values of the different
attributes? Central to the idea of deep learning is that the neural
network learns representations of the features, rather than requiring
the programmer to design them herself. So why not just let the word
embeddings be parameters in our model, and then be updated during
training? This is exactly what we will do. We will have some *latent
semantic attributes* that the network can, in principle, learn. Note
that the word embeddings will probably not be interpretable. That is,
although with our hand-crafted vectors above we can see that
mathematicians and physicists are similar in that they both like coffee,
if we allow a neural network to learn the embeddings and see that both
mathematicians and physicists have a large value in the second
dimension, it is not clear what that means. They are similar in some
latent semantic dimension, but this probably has no interpretation to
us.

In summary, **word embeddings are a representation of the *semantics* of
a word, efficiently encoding semantic information that might be relevant
to the task at hand**. You can embed other things too: part of speech
tags, parse trees, anything! The idea of feature embeddings is central
to the field.

## Word Embeddings in Pytorch

Before we get to a worked example and an exercise, a few quick notes
about how to use embeddings in Pytorch and in deep learning programming
in general. Similar to how we defined a unique index for each word when
making one-hot vectors, we also need to define an index for each word
when using embeddings. These will be keys into a lookup table. That is,
embeddings are stored as a $|V| \times D$ matrix, where $D$
is the dimensionality of the embeddings, such that the word assigned
index $i$ has its embedding stored in the $i$'th row of the
matrix. In all of my code, the mapping from words to indices is a
dictionary named word\_to\_ix.

The module that allows you to use embeddings is torch.nn.Embedding,
which takes two arguments: the vocabulary size, and the dimensionality
of the embeddings.

To index into this table, you must use torch.LongTensor (since the
indices are integers, not floats).

```python
# Author: Robert Guthrie

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)
```

    <torch._C.Generator at 0x7f1029950e30>

```python
dev = torch.device(
    "cuda") if torch.cuda.is_available() else torch.device("cpu")
dev
```

    device(type='cuda')

```python
word_to_ix = {"hello": 0, "world": 1}
embeds = nn.Embedding(2, 5, device=dev)# 2 words in vocab, 5 dimensional embeddings
print(embeds)

lookup_tensor = torch.tensor([word_to_ix["hello"]], dtype=torch.long, device=dev)
print(lookup_tensor)

hello_embed = embeds(lookup_tensor)
print(hello_embed)
```

    Embedding(2, 5)
    tensor([0], device='cuda:0')
    tensor([[-0.2963,  2.6764, -0.1408, -0.8441,  0.2905]], device='cuda:0',
           grad_fn=<EmbeddingBackward>)

## An Example: N-Gram Language Modeling

Recall that in an n-gram language model, given a sequence of words
$w$, we want to compute

\begin{align}P(w_i | w_{i-1}, w_{i-2}, \dots, w_{i-n+1} )\end{align}

Where $w_i$ is the ith word of the sequence.

In this example, we will compute the loss function on some training
examples and update the parameters with backpropagation.

```python
scores = {}

for x in range(100,410,10):
    giga5_pmi_lsa = TorchAutoencoder(max_iter=1500, hidden_dim=x, eta=0.01).fit(lsa_pmi_df)
    df, rho = vsm.word_relatedness_evaluation(highest, giga5_pmi_lsa)
    scores[x] = np.round(rho, 4)
    print(f'Score captured for value {x}')
```

```python
CONTEXT_SIZE = 2
EMBEDDING_DIM = 10
# We will use Shakespeare Sonnet 2
test_sentence = """When forty winters shall besiege thy brow,
And dig deep trenches in thy beauty's field,
Thy youth's proud livery so gazed on now,
Will be a totter'd weed of small worth held:
Then being asked, where all thy beauty lies,
Where all the treasure of thy lusty days;
To say, within thine own deep sunken eyes,
Were an all-eating shame, and thriftless praise.
How much more praise deserv'd thy beauty's use,
If thou couldst answer 'This fair child of mine
Shall sum my count, and make my old excuse,'
Proving his beauty by succession thine!
This were to be new made when thou art old,
And see thy blood warm when thou feel'st it cold.""".split()
# we should tokenize the input, but we will ignore that for now
# build a list of tuples.  Each tuple is ([ word_i-2, word_i-1 ], target word)
trigrams = [([test_sentence[i], test_sentence[i + 1]], test_sentence[i + 2])
            for i in range(len(test_sentence) - 2)]
# print the first 3, just so you can see what they look like
print(trigrams[:3])

vocab = set(test_sentence)
word_to_ix = {word: i for i, word in enumerate(vocab)}

class NGramLanguageModeler(nn.Module):

    def __init__(self, vocab_size, embedding_dim, context_size):
        super(NGramLanguageModeler, self).__init__()
        self.embeddings = nn.Embedding(vocab_size, embedding_dim)
        self.linear1 = nn.Linear(context_size * embedding_dim, 128)
        self.linear2 = nn.Linear(128, vocab_size)

    def forward(self, inputs):
        embeds = self.embeddings(inputs).view((1, -1))
        out = F.relu(self.linear1(embeds))
        out = self.linear2(out)
        log_probs = F.log_softmax(out, dim=1)
        return log_probs
```

    [(['When', 'forty'], 'winters'), (['forty', 'winters'], 'shall'), (['winters', 'shall'], 'besiege')]

```python
losses = []
loss_function = nn.NLLLoss()
model = NGramLanguageModeler(len(vocab), EMBEDDING_DIM, CONTEXT_SIZE)
optimizer = optim.SGD(model.parameters(), lr=0.001)

for epoch in range(10):
    total_loss = 0
    for context, target in trigrams:

        # Step 1. Prepare the inputs to be passed to the model (i.e, turn the words
        # into integer indices and wrap them in tensors)
        context_idxs = torch.tensor([word_to_ix[w] for w in context], dtype=torch.long)

        # Step 2. Recall that torch *accumulates* gradients. Before passing in a
        # new instance, you need to zero out the gradients from the old
        # instance
        model.zero_grad()

        # Step 3. Run the forward pass, getting log probabilities over next
        # words
        log_probs = model(context_idxs)

        # Step 4. Compute your loss function. (Again, Torch wants the target
        # word wrapped in a tensor)
        loss = loss_function(log_probs, torch.tensor([word_to_ix[target]], dtype=torch.long))

        # Step 5. Do the backward pass and update the gradient
        loss.backward()
        optimizer.step()

        # Get the Python number from a 1-element Tensor by calling tensor.item()
        total_loss += loss.item()
    losses.append(total_loss)
print(losses)  # The loss decreased every iteration over the training data!

# To get the embedding of a particular word, e.g. "beauty"
print(model.embeddings.weight[word_to_ix["beauty"]])
```

    [521.0087082386017, 518.5399315357208, 516.0894110202789, 513.6576066017151, 511.24383211135864, 508.8462567329407, 506.4648950099945, 504.0981340408325, 501.74385833740234, 499.4007124900818]
    tensor([-0.5097,  1.2628, -0.2391,  1.7533, -1.3143,  0.1718, -0.0881, -0.4224,
            -1.2166, -0.8238], grad_fn=<SelectBackward>)

## Exercise: Computing Word Embeddings: Continuous Bag-of-Words

The Continuous Bag-of-Words model (CBOW) is frequently used in NLP deep
learning. It is a model that tries to predict words given the context of
a few words before and a few words after the target word. This is
distinct from language modeling, since CBOW is not sequential and does
not have to be probabilistic. Typically, CBOW is used to quickly train
word embeddings, and these embeddings are used to initialize the
embeddings of some more complicated model. Usually, this is referred to
as *pretraining embeddings*. It almost always helps performance a couple
of percent.

The CBOW model is as follows. Given a target word $w_i$ and an
$N$ context window on each side, $w_{i-1}, \dots, w_{i-N}$
and $w_{i+1}, \dots, w_{i+N}$, referring to all context words
collectively as $C$, CBOW tries to minimize

\begin{align}-\log p(w_i | C) = -\log \text{Softmax}(A(\sum_{w \in C} q_w) + b)\end{align}

where $q_w$ is the embedding for word $w$.

Implement this model in Pytorch by filling in the class below. Some
tips:

* Think about which parameters you need to define.
* Make sure you know what shape each operation expects. Use .view() if you need to
  reshape.

```python
CONTEXT_SIZE = 2  # 2 words to the left, 2 to the right
raw_text = """We are about to study the idea of a computational process.
Computational processes are abstract beings that inhabit computers.
As they evolve, processes manipulate other abstract things called data.
The evolution of a process is directed by a pattern of rules
called a program. People create programs to direct processes. In effect,
we conjure the spirits of the computer with our spells.""".split()

# By deriving a set from `raw_text`, we deduplicate the array
vocab = set(raw_text)
vocab_size = len(vocab)

word_to_ix = {word: i for i, word in enumerate(vocab)}
data = []
for i in range(2, len(raw_text) - 2):
    context = [raw_text[i - 2], raw_text[i - 1],
               raw_text[i + 1], raw_text[i + 2]]
    target = raw_text[i]
    data.append((context, target))
print(data[:5])

class CBOW(nn.Module):

    def __init__(self):
        pass

    def forward(self, inputs):
        pass

# create your model and train.  here are some functions to help you make
# the data ready for use by your module

def make_context_vector(context, word_to_ix):
    idxs = [word_to_ix[w] for w in context]
    return torch.tensor(idxs, dtype=torch.long)

make_context_vector(data[0][0], word_to_ix)  # example
```


Copyright **Paolo Raiteri**, January 2022

# Langmuir isotherm virtual lab

The Langmuir isotherm is one of the simplest models that can be used to describe the adsorption of molecules on surfaces, either in the gas phase or in solutions.
It is based on 5 key assumptions:
1. The surface is flat
2. The adsorbate is immobile on the surface
3. All adsorption sites are equivalent
4. There are no interactions between adsorbate molecules on adjacent sites
5. One one molecule can adsorb in a site (monolayer coverage)

The fundamental equation of the Langmuir adsorption isotherm can be derived using either thermodynamic or kinetic arguments.
Here we will follow the thermodynamic route.
The surface adsorption process can be regarded as an equilibrium problem, where the adsorbate molecules, $A$, on the surface are in dynamic equilibrium with those in solution

\begin{equation}
A + S \leftrightharpoons SA \tag{1}
\end{equation}

where $A$ are the free molecules in solution (or in the gas phase), $S$ are the available adsorption sites and $SA$ are the filled adsorption sites.
The equilibrium constant for this chemical reaction is

\begin{equation}
K = \frac{[SA]}{[A][S]} \tag{2}
\end{equation}

Although, the concentrations of free/occupied adsorption sites are somewhat ill-defined quantities, it is easy to see how their "concentration" would be related to the surface coverage.
If we define the coverage, $\theta$ as the fraction of occupied surface sites,

\begin{equation}
[SA] \propto \theta \tag{3}
\end{equation}
\begin{equation}
[S]  \propto (1-\theta) \tag{4}
\end{equation}
\begin{equation}
[A] = c_{sol} \tag{5}
\end{equation}

where we have introduced a slight change of notation by calling the $c_{sol}$ the equilibrium concentration of the adsorbate in solution. We can then rewrite the equilibrium constant as

\begin{equation}
K_L = \frac{\theta}{(1-\theta)c_{sol}} \tag{6}
\end{equation}

where $K_L$ is the Langmuir constant, which contains all the unknown proportionality constant that connect the coverage with the "concentrations" that are in the definition of the equilibrium constant. This equation can then be rewritten to obtain the famous Langmuir isotherm equation

\begin{equation}
\theta = \frac{K_Lc_{sol}}{1+K_Lc_{sol}} \tag{7}
\end{equation}

where $\theta$ is the fraction of adsorption sites that are occupied, $K_L$ is the Langmuir equilibrium constant, and $c_{sol}$ is the equilibrium concentration of the adsorbate in solution.
Because it is not possible to directly measure the fraction of occupied surface sites, a more practical version of that equation is

\begin{equation}
c_{surf} = \frac{QK_Lc_{sol}}{1+K_Lc_{sol}} \tag{8}
\end{equation}

where $c_{surf}$ is the concentration of the adsorbate that is on the surface, _i.e._ out of the solution, and the new parameter $Q$ corresponds to the _monolayer_ coverage, _i.e._ the maximum concentration of molecules that can adsorb on the substrate. The linear form of the above equation, which uses the inverse of the concentrtions, is more convenient for the fitting;

\begin{equation}
\frac{1}{c_{surf}} = \frac{1}{QK_L} \frac{1}{c_{sol}} + \frac{1}{Q}  \tag{9}
\end{equation}

The name _isotherm_ stems from the fact that the experiments are performed at constant temperature and in principle both the Langmuir equilibrium constant, $K_L$, and the _monolayer_ coverage, $Q$, can have a temperature dependence.
Similarly to normal chemical reactions, by performing a series of experiments at different conditions it is possible to determine the enthalpy and entropy of the adsorption process using the van't Hoff equation.

In the virtual laboratory below, you will be looking at the adsorption of the dye Acid Blue 158 on chitin in water. Perform a series of experiments at different conditions to determine the enthalpy of adsorption of the dye on the substrate. The molar mass of Acid Blue 158 is 584.91 g/mol.

### Instructions
* Select the temperature and amount on water for your experiments
* Select an appropriate minimum and maximum amounts of acid Blue 158 to use in the experiments; this has to cover a large enough range to allow for a proper fit of the curve.
* Select how many experiments you want to perform ($N$)

<p style="text-align: center;"> Figure 1: Schematic representation of the adsorption virtual experiment. </p>

Each _experiment_ consists of adding a certain amount of dye, $c_{tot}$, to the chosen volume of DI water with  a fixed amount of chitin and measuring the concentration of the dye that is left in solution at equilibrium, $c_{sol}$.

\begin{equation}
c_{tot}=c_{surf}+c_{sol} \tag{10}
\end{equation}

When you click the *Perform experiment* button, you will obtain $N$ observations at the selected temperature, where the amount of dye that is added to the chosen amount of water is varied between the minimum and maximum values that you have chosen, in equally spaced intervals.

Every time you click *Perform experiment* $N$ new observations will be generated and appended to the output file.

If you click *Reset experiments* all observations will be deleted.
This will be useful to generate a clean set of data after you have done a few tests to find what is an appropriate range for the amount of dye to use in the virtual experiment.

You can download all the observations in CSV format and import them directly into excel, or you can use a jupyter notebook to read and analyse the data, and produce the figures for the report.

### Questions to be answered in the lab report
1. What is the Langmuir constant at a minimum of 4 different temperatures?
2. What is the monolayer coverage at those temperatures?
3. What are the enthalpy and entropy of adsorption?
4. How do your result compare with experimental values?

## Launch virtual experiment
- [Adsorption isotherms virtual lab](virtualExperiment.ipynb)

```python

```