# QuTiP development notebook for testing liouvillian construction

Copyright (C) 2011 and later, Paul D. Nation & Robert J. Johansson

```
%pylab inline
```

```
from qutip import *
import time
```

```
def system_ops2(N):
    N1 = N
    N2 = N + 1
    a1 = tensor(rand_dm(N1, density=0.75), identity(N2))
    a2 = tensor(identity(N1), rand_dm(N2, density=0.75))
    H = a1.dag() * a1 + a2.dag() * a2
    c_ops = [sqrt(0.01) * a1, sqrt(0.025) * a2]

    return H, c_ops
```

```
def system_ops(N):
    N1 = N
    N2 = N + 1
    N3 = N + 2
    a1 = tensor(rand_dm(N1, density=0.75), identity(N2), identity(N3))
    a2 = tensor(identity(N1), rand_dm(N2, density=0.75), identity(N3))
    a3 = tensor(identity(N1), identity(N2), rand_dm(N3, density=0.75))
    H = a1.dag() * a1 + a2.dag() * a2 + a3.dag() * a3
    c_ops = [sqrt(0.01) * a1, sqrt(0.025) * a2, sqrt(0.05) * a3]

    return H, c_ops
```

```
H, c_ops = system_ops(3)
```

```
L1 = liouvillian(H, c_ops)
```

```
L1
```

\begin{equation}\text{Quantum object: dims = [[[3, 4, 5], [3, 4, 5]], [[3, 4, 5], [3, 4, 5]]], shape = [3600, 3600], type = oper, isHerm = False}\\[1em]\begin{pmatrix}-0.00256494099689 & (-0.00307415314864-0.076080799489j) & (0.033366093512-0.0528309427102j) & (-0.0183708833457-0.0205756760023j) & (-0.00298280679784-0.0383390031161j) & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\(0.00254464103746-0.0758148452381j) & (-0.00284982536638-0.00392547434544j) & (0.00306075496276-0.0682200814748j) & (-0.021925618026-0.0498224321009j) & (0.00816725018561-0.0359836649834j) & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\(-0.0335618884238-0.0535207987983j) & (-0.00308318703801-0.0675039760736j) & (-0.00293737657568+0.0620860250016j) & (-0.0145142057428-0.0247217855076j) & (-0.0043980322777-0.0191890586456j) & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\(0.0174252222192-0.0206108256213j) & (0.0229221808928-0.0498631170457j) & (0.0148379106412-0.0243359017603j) & (-0.00371603521964+0.0987181802235j) & (0.013748778866-0.0125933410448j) & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\(0.00318001184024-0.0381833481246j) & (-0.00834056050947-0.0360124453046j) & (0.00407535300662-0.0192301831298j) & (-0.0134902859676-0.0131358387474j) & (-0.00300305511706+0.1002020077j) & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\vdots & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots & \vdots & \vdots\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & (-0.00283214206424-0.1002020077j) & (-0.00406593321521-0.0759574524111j) & (0.0326185894166-0.0535481147571j) & (-0.0183963772296-0.0203147465414j) & (-0.00362325817036-0.0382450034502j)\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & (0.00155286097089-0.075938192316j) & (-0.00300367311903-0.104127482046j) & (0.00203777869023-0.0680493642967j) & (-0.0229844163148-0.049494504321j) & (0.00767358994043-0.0361176499589j)\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & (-0.0343093925193-0.0528036267514j) & (-0.00410616331054-0.0676746932517j) & (-0.00203637880258-0.0381159826987j) & (-0.0149846653195-0.0243819678752j) & (-0.00459160097588-0.0191372492238j)\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & (0.0173997283353-0.0208717550822j) & (0.021863382604-0.0501910448256j) & (0.0143674510645-0.0246757193927j) & (-0.00178652069194-0.00148382747683j) & (0.0134751119728-0.0129646575603j)\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & (0.00253956046772-0.0382773477904j) & (-0.00883422075465-0.0358784603291j) & (0.00388178430845-0.0192819925516j) & (-0.0137639528608-0.0127645222319j) & -0.00148379365523\\\end{pmatrix}\end{equation}

```
L2 = liouvillian_fast(H, c_ops)
```

```
L2
```

\begin{equation}\text{Quantum object: dims = [[[3, 4, 5], [3, 4, 5]], [[3, 4, 5], [3, 4, 5]]], shape = [3600, 3600], type = oper, isHerm = True}\\[1em]\begin{pmatrix}-0.00256494099689 & (-0.00307415314864-0.076080799489j) & (0.033366093512-0.0528309427102j) & (-0.0183708833457-0.0205756760023j) & (-0.00298280679784-0.0383390031161j) & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\(0.00254464103746-0.0758148452381j) & (-0.00284982536638-0.00392547434544j) & (0.00306075496276-0.0682200814748j) & (-0.021925618026-0.0498224321009j) & (0.00816725018561-0.0359836649834j) & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\(-0.0335618884238-0.0535207987983j) & (-0.00308318703801-0.0675039760736j) & (-0.00293737657568+0.0620860250016j) & (-0.0145142057428-0.0247217855076j) & (-0.0043980322777-0.0191890586456j) & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\(0.0174252222192-0.0206108256213j) & (0.0229221808928-0.0498631170457j) & (0.0148379106412-0.0243359017603j) & (-0.00371603521964+0.0987181802235j) & (0.013748778866-0.0125933410448j) & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\(0.00318001184024-0.0381833481246j) & (-0.00834056050947-0.0360124453046j) & (0.00407535300662-0.0192301831298j) & (-0.0134902859676-0.0131358387474j) & (-0.00300305511706+0.1002020077j) & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\vdots & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots & \vdots & \vdots\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & (-0.00283214206424-0.1002020077j) & (-0.00406593321521-0.0759574524111j) & (0.0326185894166-0.0535481147571j) & (-0.0183963772296-0.0203147465414j) & (-0.00362325817036-0.0382450034502j)\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & (0.00155286097089-0.075938192316j) & (-0.00300367311903-0.104127482046j) & (0.00203777869023-0.0680493642967j) & (-0.0229844163148-0.049494504321j) & (0.00767358994043-0.0361176499589j)\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & (-0.0343093925193-0.0528036267514j) & (-0.00410616331054-0.0676746932517j) & (-0.00203637880258-0.0381159826987j) & (-0.0149846653195-0.0243819678752j) & (-0.00459160097588-0.0191372492238j)\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & (0.0173997283353-0.0208717550822j) & (0.021863382604-0.0501910448256j) & (0.0143674510645-0.0246757193927j) & (-0.00178652069194-0.00148382747683j) & (0.0134751119728-0.0129646575603j)\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & (0.00253956046772-0.0382773477904j) & (-0.00883422075465-0.0358784603291j) & (0.00388178430845-0.0192819925516j) & (-0.0137639528608-0.0127645222319j) & -0.00148379365523\\\end{pmatrix}\end{equation}

```
timeit liouvillian(H, c_ops)
```

    1 loops, best of 3: 496 ms per loop

```
timeit liouvillian_fast(H, c_ops)
```

    10 loops, best of 3: 41.5 ms per loop

### Full inspection of small system

```
N1 = 2
N2 = 2

a1 = tensor(rand_dm(N1, density=1.0), identity(N2))
a2 = tensor(identity(N1), rand_dm(N2, density=1.0))

H = a1.dag() * a1 + a2.dag() * a2

c_ops = [sqrt(1.0) * a1, sqrt(2.0) * a2]
```

```
N1 = 2

a1 = rand_dm(N1, density=1.0)

H = a1.dag() * a1

c_ops = [sqrt(1.0) * a1]
```

```
L1 = liouvillian(H, c_ops)

L1
```

\begin{equation}\text{Quantum object: dims = [[[2], [2]], [[2], [2]]], shape = [4, 4], type = oper, isHerm = False}\\[1em]\begin{pmatrix}-0.144765690223 & (0.344985612913-0.161228510818j) & (0.344985612913+0.161228510818j) & 0.144765690223\\(-0.357041058975-0.132406147982j) & (-0.148136870154-0.0821118740651j) & (-0.101655033798-0.103069681134j) & (0.357041058975+0.132406147982j)\\(-0.357041058975+0.132406147982j) & (-0.101655033798+0.103069681134j) & (-0.148136870154+0.0821118740651j) & (0.357041058975-0.132406147982j)\\0.144765690223 & (-0.344985612913+0.161228510818j) & (-0.344985612913-0.161228510818j) & -0.144765690223\\\end{pmatrix}\end{equation}

```
L2 = liouvillian_fast(H, c_ops)

L2
```

\begin{equation}\text{Quantum object: dims = [[[2], [2]], [[2], [2]]], shape = [4, 4], type = oper, isHerm = True}\\[1em]\begin{pmatrix}-0.144765690223 & (0.344985612913-0.161228510818j) & (0.344985612913+0.161228510818j) & 0.144765690223\\(-0.357041058975-0.132406147982j) & (-0.148136870154-0.0821118740651j) & (-0.101655033798-0.103069681134j) & (0.357041058975+0.132406147982j)\\(-0.357041058975+0.132406147982j) & (-0.101655033798+0.103069681134j) & (-0.148136870154+0.0821118740651j) & (0.357041058975-0.132406147982j)\\0.144765690223 & (-0.344985612913+0.161228510818j) & (-0.344985612913-0.161228510818j) & -0.144765690223\\\end{pmatrix}\end{equation}

```
L1-L2
```

\begin{equation}\text{Quantum object: dims = [[[2], [2]], [[2], [2]]], shape = [4, 4], type = oper, isHerm = False}\\[1em]\begin{pmatrix}0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0\\\end{pmatrix}\end{equation}

### Benchmark vs system size

```
def benchmark_liouvillian(N_vec):

    res1 = zeros(shape(N_vec))
    res2 = zeros(shape(N_vec))

    for idx, N in enumerate(N_vec):
        H, c_ops = system_ops(N)

        t0 = time.time()
        L = liouvillian(H, c_ops)
        t1 = time.time()
        res1[idx] = t1-t0

        t0 = time.time()
        L = liouvillian_fast(H, c_ops)
        t1 = time.time()
        res2[idx] = t1-t0

    return res1, res2
```

```
N_vec = array([2,3,4,5,6,7,8])
```

```
t0 = time.time()
res1, res2 = benchmark_liouvillian(N_vec)
t1 = time.time()

t1-t0
```

    651.1269519329071

```
fig, axes = subplots(1, 2, figsize=(14, 6))

axes[0].plot(N_vec, res1, lw=2, label='liouvillian')
axes[0].plot(N_vec, res2, lw=2, label='liouvillian_fast')
axes[0].legend(loc=0)

axes[1].plot(N_vec, res1 / res2, lw=2, label='speedup')
axes[1].legend(loc=0)
```

### Test different combinations of preconstructed liouvillians

```
H, c_ops = system_ops2(3)
```

#### Standard

```
liouvillian(H, c_ops) - liouvillian_fast(H, c_ops)
```

\begin{equation}\text{Quantum object: dims = [[[3, 4], [3, 4]], [[3, 4], [3, 4]]], shape = [144, 144], type = oper, isHerm = False}\\[1em]\begin{pmatrix}0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\vdots & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots & \vdots & \vdots\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\end{pmatrix}\end{equation}

#### Without collapse operators

```
liouvillian(H, []) - liouvillian_fast(H, [])
```

\begin{equation}\text{Quantum object: dims = [[[3, 4], [3, 4]], [[3, 4], [3, 4]]], shape = [144, 144], type = oper, isHerm = False}\\[1em]\begin{pmatrix}0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\vdots & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots & \vdots & \vdots\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\end{pmatrix}\end{equation}

#### Without hamiltonian

```
liouvillian(None, c_ops) - liouvillian_fast(None, c_ops)
```

\begin{equation}\text{Quantum object: dims = [[[3, 4], [3, 4]], [[3, 4], [3, 4]]], shape = [144, 144], type = super, isHerm = False}\\[1em]\begin{pmatrix}0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\vdots & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots & \vdots & \vdots\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\end{pmatrix}\end{equation}

#### With preconstructed liouvillian instead of collapse operators

```
L_c_ops = liouvillian(None, c_ops)
```

```
liouvillian(H, [L_c_ops]) - liouvillian_fast(H, [L_c_ops])
```

\begin{equation}\text{Quantum object: dims = [[[3, 4], [3, 4]], [[3, 4], [3, 4]]], shape = [144, 144], type = oper, isHerm = False}\\[1em]\begin{pmatrix}0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\vdots & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots & \vdots & \vdots\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\end{pmatrix}\end{equation}

```
liouvillian(None, [L_c_ops]) - liouvillian_fast(None, [L_c_ops])
```

\begin{equation}\text{Quantum object: dims = [[[3, 4], [3, 4]], [[3, 4], [3, 4]]], shape = [144, 144], type = super, isHerm = False}\\[1em]\begin{pmatrix}0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\vdots & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots & \vdots & \vdots\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\end{pmatrix}\end{equation}

#### With preconstructed liouvillian instead of hamiltonian

```
L_H = liouvillian_fast(H, [])
```

```
liouvillian(H, []) - liouvillian_fast(L_H, [])
```

\begin{equation}\text{Quantum object: dims = [[[3, 4], [3, 4]], [[3, 4], [3, 4]]], shape = [144, 144], type = oper, isHerm = False}\\[1em]\begin{pmatrix}0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\vdots & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots & \vdots & \vdots\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\end{pmatrix}\end{equation}

```
liouvillian(H, c_ops) - liouvillian_fast(L_H, c_ops)
```

\begin{equation}\text{Quantum object: dims = [[[3, 4], [3, 4]], [[3, 4], [3, 4]]], shape = [144, 144], type = oper, isHerm = False}\\[1em]\begin{pmatrix}0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\vdots & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots & \vdots & \vdots\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\end{pmatrix}\end{equation}

```
liouvillian(H, c_ops) - liouvillian_fast(L_H, [L_c_ops])
```

\begin{equation}\text{Quantum object: dims = [[[3, 4], [3, 4]], [[3, 4], [3, 4]]], shape = [144, 144], type = oper, isHerm = False}\\[1em]\begin{pmatrix}0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\vdots & \vdots & \vdots & \vdots & \vdots & \ddots & \vdots & \vdots & \vdots & \vdots & \vdots\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\0.0 & 0.0 & 0.0 & 0.0 & 0.0 & \cdots & 0.0 & 0.0 & 0.0 & 0.0 & 0.0\\\end{pmatrix}\end{equation}

### Software versions

```
from qutip.ipynbtools import version_table; version_table()
```

Software	Version

Cython	0.16

SciPy	0.10.1

QuTiP	2.3.0.dev-25303b6

Python	2.7.3 (default, Sep 26 2012, 21:51:14)
[GCC 4.7.2]

IPython	0.13

OS	posix [linux2]

Numpy	1.7.0.dev-3f45eaa

matplotlib	1.3.x

Tue Mar  5 15:26:54 2013


```python
import sympy as sm
```

```python
c1, c2, c3, c4, r, z, p = sm.symbols(['c1', 'c2', 'c3', 'c4', 'r', 'z', 'p'], real=True, positive=True)
```

```python
mu = sm.sqrt(1 - r**2)
Ir = 1 - c1 * (1 - mu ** sm.Rational(1/2)) - c2 * (1 - mu) - c3 * (1 - mu ** sm.Rational(3/2)) - c4 * (1 - mu ** 2)
```

```python
inside = sm.integrate(Ir * 2 * r, (r, z-p, z+p))
sm.simplify(inside)
```

$\displaystyle - \frac{4 c_{1} \sqrt[4]{1 - \left(- p + z\right)^{2}} \left(p - z\right)^{2}}{5} + \frac{4 c_{1} \sqrt[4]{1 - \left(- p + z\right)^{2}}}{5} + \frac{4 c_{1} \sqrt[4]{1 - \left(p + z\right)^{2}} \left(p + z\right)^{2}}{5} - \frac{4 c_{1} \sqrt[4]{1 - \left(p + z\right)^{2}}}{5} + c_{1} \left(p - z\right)^{2} - c_{1} \left(p + z\right)^{2} - \frac{2 c_{2} \sqrt{1 - \left(- p + z\right)^{2}} \left(p - z\right)^{2}}{3} + \frac{2 c_{2} \sqrt{1 - \left(- p + z\right)^{2}}}{3} + \frac{2 c_{2} \sqrt{1 - \left(p + z\right)^{2}} \left(p + z\right)^{2}}{3} - \frac{2 c_{2} \sqrt{1 - \left(p + z\right)^{2}}}{3} + c_{2} \left(p - z\right)^{2} - c_{2} \left(p + z\right)^{2} - \frac{4 c_{3} \left(1 - \left(- p + z\right)^{2}\right)^{\frac{3}{4}} \left(p - z\right)^{2}}{7} + \frac{4 c_{3} \left(1 - \left(- p + z\right)^{2}\right)^{\frac{3}{4}}}{7} + \frac{4 c_{3} \left(1 - \left(p + z\right)^{2}\right)^{\frac{3}{4}} \left(p + z\right)^{2}}{7} - \frac{4 c_{3} \left(1 - \left(p + z\right)^{2}\right)^{\frac{3}{4}}}{7} + c_{3} \left(p - z\right)^{2} - c_{3} \left(p + z\right)^{2} + \frac{c_{4} \left(p - z\right)^{4}}{2} - \frac{c_{4} \left(p + z\right)^{4}}{2} - \left(p - z\right)^{2} + \left(p + z\right)^{2}$

```python
result = sm.simplify(sm.collect(sm.collect(sm.simplify(sm.expand(inside)), -p**2 - 2 * p * z - z**2 + 1),-p**2 + 2 * p * z - z**2 + 1))
```

```python
alpha, beta = sm.symbols(['alpha', 'beta'], real=True)
```

```python
result.subs(z, 0)
```

$\displaystyle 0$

```python
result.subs(-p**2 - 2 * p * z - z**2 + 1, alpha).subs(-p**2 + 2 * p * z - z**2 + 1, beta)
```

$\displaystyle - \frac{4 \alpha^{\frac{7}{4}} c_{3}}{7} - \frac{4 \alpha^{\frac{5}{4}} c_{1}}{5} - \frac{2 \alpha^{\frac{3}{2}} c_{2}}{3} + \frac{4 \beta^{\frac{7}{4}} c_{3}}{7} + \frac{4 \beta^{\frac{5}{4}} c_{1}}{5} + \frac{2 \beta^{\frac{3}{2}} c_{2}}{3} - 4 c_{1} p z - 4 c_{2} p z - 4 c_{3} p z - 4 c_{4} p^{3} z - 4 c_{4} p z^{3} + 4 p z$

```python
print(_)
```

    -4*alpha**(7/4)*c3/7 - 4*alpha**(5/4)*c1/5 - 2*alpha**(3/2)*c2/3 + 4*beta**(7/4)*c3/7 + 4*beta**(5/4)*c1/5 + 2*beta**(3/2)*c2/3 - 4*c1*p*z - 4*c2*p*z - 4*c3*p*z - 4*c4*p**3*z - 4*c4*p*z**3 + 4*p*z

```python
inside = sm.integrate(Ir * 2 * r, (r, z-p, 1))
sm.simplify(inside)
```

$\displaystyle - \frac{4 c_{1} \sqrt[4]{1 - \left(- p + z\right)^{2}} \left(p - z\right)^{2}}{5} + \frac{4 c_{1} \sqrt[4]{1 - \left(- p + z\right)^{2}}}{5} + c_{1} \left(p - z\right)^{2} - c_{1} - \frac{2 c_{2} \sqrt{1 - \left(- p + z\right)^{2}} \left(p - z\right)^{2}}{3} + \frac{2 c_{2} \sqrt{1 - \left(- p + z\right)^{2}}}{3} + c_{2} \left(p - z\right)^{2} - c_{2} - \frac{4 c_{3} \left(1 - \left(- p + z\right)^{2}\right)^{\frac{3}{4}} \left(p - z\right)^{2}}{7} + \frac{4 c_{3} \left(1 - \left(- p + z\right)^{2}\right)^{\frac{3}{4}}}{7} + c_{3} \left(p - z\right)^{2} - c_{3} + \frac{c_{4} \left(p - z\right)^{4}}{2} - \frac{c_{4}}{2} - \left(p - z\right)^{2} + 1$

```python
result = sm.simplify(sm.collect(sm.collect(sm.simplify(sm.expand(inside)), -p**2 - 2 * p * z - z**2 + 1),-p**2 + 2 * p * z - z**2 + 1))
```

```python
result
```

$\displaystyle c_{1} p^{2} - 2 c_{1} p z + c_{1} z^{2} + \frac{4 c_{1} \left(- p^{2} + 2 p z - z^{2} + 1\right)^{\frac{5}{4}}}{5} - c_{1} + c_{2} p^{2} - 2 c_{2} p z + c_{2} z^{2} + \frac{2 c_{2} \left(- p^{2} + 2 p z - z^{2} + 1\right)^{\frac{3}{2}}}{3} - c_{2} + c_{3} p^{2} - 2 c_{3} p z + c_{3} z^{2} + \frac{4 c_{3} \left(- p^{2} + 2 p z - z^{2} + 1\right)^{\frac{7}{4}}}{7} - c_{3} + \frac{c_{4} p^{4}}{2} - 2 c_{4} p^{3} z + 3 c_{4} p^{2} z^{2} - 2 c_{4} p z^{3} + \frac{c_{4} z^{4}}{2} - \frac{c_{4}}{2} - p^{2} + 2 p z - z^{2} + 1$

```python
result.subs(-p**2 - 2 * p * z - z**2 + 1, alpha).subs(-p**2 + 2 * p * z - z**2 + 1, beta)
```

$\displaystyle \frac{4 \beta^{\frac{7}{4}} c_{3}}{7} + \frac{4 \beta^{\frac{5}{4}} c_{1}}{5} + \frac{2 \beta^{\frac{3}{2}} c_{2}}{3} + \beta + c_{1} p^{2} - 2 c_{1} p z + c_{1} z^{2} - c_{1} + c_{2} p^{2} - 2 c_{2} p z + c_{2} z^{2} - c_{2} + c_{3} p^{2} - 2 c_{3} p z + c_{3} z^{2} - c_{3} + \frac{c_{4} p^{4}}{2} - 2 c_{4} p^{3} z + 3 c_{4} p^{2} z^{2} - 2 c_{4} p z^{3} + \frac{c_{4} z^{4}}{2} - \frac{c_{4}}{2}$

```python
import sympy as sm
r, a, e, w, i = sm.symbols(['r', 'a', 'e', 'w', 'i'], real=True, positive=True)
x, y = sm.symbols(['x', 'y'], real=True)
v = y / (x - a*e)
sol = sm.atan(v)
```

```python
f = sol
g = sm.sqrt(x**2+y**2) - (a*(1-e**2)/(1+e*sm.cos(f)))*sm.sqrt(1-(sm.sin(w+f)**2)*sm.sin(i)**2)
```

```python
sm.expand(g)
```

$\displaystyle \frac{a e^{2} \sqrt{- \sin^{2}{\left(i \right)} \sin^{2}{\left(w + \operatorname{atan}{\left(\frac{y}{- a e + x} \right)} \right)} + 1}}{\frac{e}{\sqrt{\frac{y^{2}}{a^{2} e^{2} - 2 a e x + x^{2}} + 1}} + 1} - \frac{a \sqrt{- \sin^{2}{\left(i \right)} \sin^{2}{\left(w + \operatorname{atan}{\left(\frac{y}{- a e + x} \right)} \right)} + 1}}{\frac{e}{\sqrt{\frac{y^{2}}{a^{2} e^{2} - 2 a e x + x^{2}} + 1}} + 1} + \sqrt{x^{2} + y^{2}}$

```python

```


```python
%matplotlib inline
```

# 592B, Class 3.1 (09/17). Fourier series basis functions and coefficients

```python
import numpy as np
import matplotlib.pyplot as plt
import scipy.io.wavfile as wavfile

from ipywidgets import interactive
from IPython.display import Audio, display
```

## Adding up sine waves: towards Fourier series

Let's take the two sinusoidal signals we worked on last week and in your homework and try adding them up.

```python
fs = 44100 # define the sampling rate, f_s = 44.1 kHz

t_start = 0 # We start sampling at t = 0s
t_stop =  1 # We stop sampling at t = 1s

ns = (t_stop - t_start) * fs + 1

x = np.linspace(t_start, t_stop, ns)

f1 = 440 # frequency of y_1(t)
f2 = 220 # frequency of y_2(t)

y1 = np.sin(2*np.pi*f1*x)
y2 = np.sin(2*np.pi*f2*x)

y1_plus_y2 = y1+y2

```

```python
plt.figure("Adding up sines") # Create a new figure

plt.xlim(0,0.01)
plt.plot(x , y1, "-g", label="y1") # plot (x,y1) as a green line
plt.plot(x , y2, "-b", label="y2") # plot (x,y2) as a blue line
plt.plot(x , y1_plus_y2, "-r", label="y1+y2") # plot (x,y2) as a blue line

#plt.stem(x,y1, 'r', )
plt.legend(loc="upper right")
plt.xlabel('Time (s)')
plt.ylabel('Amplitude (dB)')
plt.title("Adding up sines")

```

You should play with setting other parameters! Remember, you can listen to your generated sinusoids too, using `Audio`.

Below is some code that creates an interactive "widget" for both plotting and playing two sine waves and their superposition (sum).

You need to import these libraries, as we did at the beginning of the notebook:
```python
from ipywidgets import interactive
from IPython.display import Audio, display
```
The code is inspired by this [beat frequencies demo](https://ipywidgets.readthedocs.io/en/stable/examples/Beat%20Frequencies.html).

```python
def plot_play_summed_sines(f1 = 440, f2 = 880, t_start = 0, t_stop = 2, fs = 44100, xlim_max = 0.01):
    x = np.linspace(t_start, t_stop, fs * (t_stop - t_start))
    y1 = np.sin(2*np.pi*f1*x)
    y2 = np.sin(2*np.pi*f2*x)

    plt.xlim(t_start,xlim_max)
    plt.plot(x , y1, "-g", label="y1")
    plt.plot(x , y2, "-b", label="y2")
    plt.plot(x , y1 + y2, "-r", label="y1+y2")
    plt.legend(loc="upper right")
    plt.xlabel('Time (s)')
    plt.ylabel('Amplitude (dB)')
    plt.title("Adding up sines")

    display(Audio(data=y1, rate=fs))
    display(Audio(data=y2, rate=fs))
    display(Audio(data=y1+y2, rate=fs))
```

```python
v = interactive(plot_play_summed_sines, f1=(100,900), f2=(100,900), t_start = (0,0), t_stop = (0,5))
display(v)
```

    interactive(children=(IntSlider(value=440, description='f1', max=900, min=100), IntSlider(value=880, descripti…

## A first look at Fourier series

Recall from last week that a standard definition of a sinusoidal signal (i.e., a sine wave) is given as function of time $t$:

$$y(t) = A \sin (\omega t + \phi) $$

where:
- $A$ is the amplitude
- $\omega$ is the angular frequency, n.b., $\omega = 2\pi f$ (where $f$ is the frequency in cycles per second (Hertz or Hz))
- $\phi$ is the phase shift

One standard definition of a Fourier series is:

\begin{equation}
      f(t) = a_0 + \displaystyle\sum\limits_{n=1}^N \left(a_n\cos(2\pi nt) + b_n\sin(2\pi nt)\right)
\end{equation}

where

$n \in \mathbb{Z}$, and $a_0 \ldots a_N$ and $b_0 \ldots b_N$ are called **Fourier coefficients**. Note that these are amplitude values for each component sinusoidal function in the sum: they provide weights for the individual components in the sum. We'll see later that these determine the amplitude of peaks in a spectrum of $f(t)$.

***Discussion: Why is there no $b_0$ in the formula?***

Let's see what each of the individual $a_n\cos(2\pi nt) + b_n\sin(2\pi nt)$ terms look like, for $a_n = b_n = 1$ for all $n$, for $n <6$, otherwise 0. And let's include the $a_0$ term by starting at $N=0$.

```python
# Inspired by https://matplotlib.org/examples/pylab_examples/subplots_demo.html Three subplots sharing both x/y axes

t_start = 0; t_stop = 1; fs = 1000
x = np.linspace(t_start, t_stop, fs * (t_stop - t_start))
y = [np.sin(2*np.pi*n*x) + np.cos(2*np.pi*n*x) for n in np.arange(0,6)]

f_fourier_series, (ax1, ax2, ax3, ax4, ax5, ax6) = plt.subplots(6, sharex=True, sharey=True)
ax1.plot(x , y[0], "-", label="n = 0")
ax1.set_title('Fourier series up to N = 5, a_n = b_n = 1')
ax2.plot(x , y[1], "-", label="n = 1")
ax3.plot(x , y[2], "-", label="n = 2")
ax4.plot(x , y[3], "-", label="n = 3")
ax5.plot(x , y[4], "-", label="n = 4")
ax6.plot(x , y[5], "-", label="n = 5")

plt.xlabel('Time (s)')
plt.ylabel('Amplitude (dB)')

# Fine-tune figure; make subplots close to each other and hide x ticks for
# all but bottom plot.
f_fourier_series.subplots_adjust(hspace=0)
plt.setp([a.get_xticklabels() for a in f_fourier_series.axes[:-1]], visible=False)

```

These are the first 6 terms in a **set of basis functions**, in particular, the **Fourier basis functions**. Each term, for some given $N$, is a basis function. In the limit, as $N \rightarrow \infty$ we can approximate any function with this set of basis functions. And each of the basis functions are **orthogonal** to one another. For intuition, we'll review vector spaces and orthogonal vectors.

Below, we "multiply" one basis function by another to illustrate orthogonality: the integral of the product of any two of the basis functions is 0.

```python
t_start = 0; t_stop = 3; fs = 1000
x = np.linspace(t_start, t_stop, fs * (t_stop - t_start))
y = [np.sin(2*np.pi*f*x) + np.cos(2*np.pi*f*x) for f in np.arange(0,6)]

f_ortho, (ax1, ax2, ax3, ax4, ax5, ax6) = plt.subplots(6, sharex=True, sharey=True)
ax1.plot(x , y[1]*y[2], "-", label="n = 0")
ax1.plot((t_start, t_stop), (0, 0), 'r-')
ax1.set_title('Fourier series up to N = 5, a_n = b_n = 1')
ax2.plot(x , y[1]*y[3], "-", label="n = 1")
ax2.plot((t_start, t_stop), (0, 0), 'r-')
ax3.plot(x , y[2]*y[3], "-", label="n = 2")
ax3.plot((t_start, t_stop), (0, 0), 'r-')
ax4.plot(x , y[3]*y[4], "-", label="n = 3")
ax4.plot((t_start, t_stop), (0, 0), 'r-')
ax5.plot(x , y[4]*y[5], "-", label="n = 4")
ax5.plot((t_start, t_stop), (0, 0), 'r-')
ax6.plot(x , y[5]*y[5], "-", label="n = 5")
ax6.plot((t_start, t_stop), (0, 0), 'r-')

plt.xlabel('Time (s)')
plt.ylabel('Amplitude (dB)')

# Fine-tune figure; make subplots close to each other and hide x ticks for
# all but bottom plot.
f_ortho.subplots_adjust(hspace=0.5)
plt.setp([a.get_xticklabels() for a in f_ortho.axes[:-1]], visible=False)

```

In your homework, you'll explore building up a complex wave as a Fourier series.

## Determining Fourier coefficients

We'll go over more about determining Fourier coefficients next week, and you'll learn more about them when you read *Who is Fourier? Ch. 1-3*. But for now, I'll just bequeath you formulae for computing these: they involve integration. If you need a review of integration, *Who is Fourier?* is excellent for providing geometric intuition---read it!

\begin{equation}
\label{eq:a0}
a_0 = \frac{1}{T}\int_0^T f(t)dt
\end{equation}

\begin{equation}
\label{eq:an}
a_n = \frac{2}{T}\int_0^T f(t)\cos 2\pi ntdt
\end{equation}

\begin{equation}
\label{eq:bn}
b_n = \frac{2}{T}\int_0^T f(t)\sin 2\pi ntdt
\end{equation}

where $T$ is the period of $f(t)$.

It turns out that there is a way to do symbolic math in python, using the `sympy` library.

Let's examine a Fourier series up to $N=2$ with the following coefficient values:

\begin{align}
a_0 = 8\\
a_1 = 3\\
b_1 = 2\\
a_2 = 2\\
b_2 = 4\\
\end{align}

Knowing that those are the coefficient values, we'll now try to compute $a_0$ using the formula above, and see if we get 8.

```python
# http://www.scipy-lectures.org/packages/sympy.html#integration
import sympy as sym # note: you may need to run conda install sympy in your terminal

x = sym.Symbol('x')
sym.integrate(8*sym.cos(2*sym.pi*0*x) + 0*sym.sin(2*sym.pi*0*x)
              + 3*sym.cos(2*sym.pi*1*x) + 2*sym.sin(2*sym.pi*1*x)
              + 2*sym.cos(2*sym.pi*2*x) + 4*sym.sin(2*sym.pi*2*x),
              (x, 0, 1))
```

$\displaystyle 8$

OK now let's try calculating *b2*.

```python
x = sym.Symbol('x')
2* sym.integrate(sym.sin(2*sym.pi*2*x) * (8*sym.cos(2*sym.pi*0*x) + 0*sym.sin(2*sym.pi*0*x)
              + 3*sym.cos(2*sym.pi*1*x) + 2*sym.sin(2*sym.pi*1*x)
              + 2*sym.cos(2*sym.pi*2*x) + 4*sym.sin(2*sym.pi*2*x)),
              (x, 0, 1))

```

$\displaystyle 4$

```python

```

Now let's working on finding the Fourier coefficients of a square wave.

```python
import scipy.signal as signal
t = np.linspace(0, 1, 500, endpoint=False)
plt.plot(t, signal.square(2 * np.pi * 1 * t))
plt.ylim(-2, 2)

#square = signal.square(2 * np.pi * 1 * t)
#print(square)

```

```python

```


# Music machine learning - Neural networks

### Author: Philippe Esling (esling@ircam.fr)

In this course we will cover
1. A [quick introduction](#intro) on the code that we will adress and auxiliary functions
2. A simple implementation for a [single neuron](#single)
3. An implementation of [regression](#implem) using scikit-learn
4. Some [common good practices](#practices) in machine learning

# Introducing neural networks

In this tutorial, we will cover a more advanced classification algorithm through the use of *neural networks*. The tutorial starts by performing a simple **single neuron** discrimination of two random distributions. Then, we will study the typical **XOR problem** by using a more advanced 2-layer **perceptron**. Finally, we generalize the use of neural networks in order to perform classification on a given set of audio files.

We are going to use relatively _low-level_ libraries to perform the first exercises (implementing your own neurons)

```python
import time
import numpy as np
%matplotlib inline
from matplotlib import pyplot as plt
```

To simplify your work, we provide a first set of functions that provides simple plotting functionnalities (from the `helper_plot.py` file)

  |**File**|*Explanation*|
  |-------:|:---------|
  |`plot_boundary`|Plots the decision boundary of a single neuron with 2-dimensional inputs|
  |`plot_patterns`|Plots (bi-dimensionnal) input patterns|

```python
from helper_plot import plot_boundary
from helper_plot import plot_patterns
```

Remember that a single neuron is only able to learn _linearly separable_ problems. To produce such classes of problems, we provide a script that draw a set of random 2-dimensional points, then choose a random line in this space that will act as the linear frontier between 2 classes (hence defining a linear 2-class problem). The variables that will be used by your code are the following.

```Matlab
desired       % classes of the patterns
inputs        % 2 x n final matrix of random input patterns
weights       % 2 x 1 vector of neuron weights
bias          % 1 x 1 vector of bias
```

You can execute the code below to see our simple classification problem. (Note that running the same cell multiple times produces a different starting dataset).

```python
# Number of points to generate
nPats = 30 + np.floor(np.random.rand() * 30);
# Generate 2-dimensional random points
patterns = np.random.rand(2, int(nPats)) * 2 - 1;
# Slope of separating line
slope = np.log(np.random.rand() * 10);
yint = np.random.rand() * 2 - 1;
# Create the indexes for a two-class problem
desired = (patterns[1,:] - patterns[0,:] * slope - yint > 0) * 1;
# Plot the corresponding pattern
fig = plot_patterns(patterns,desired);
```

## Single neuron

For the first parts of the tutorial, we will perform the simplest classification model possible in a neural network setting, a single neuron. We briefly recall here that; given an input vector $ \mathbf{x} \in \mathbb{R}^{n} $, a single neuron computes the function

$$
\begin{equation}
\hat{y}=\phi\left(\sum_{i = 1}^{n}w_{i}.x_{i} + b\right)
\label{eq1}
\end{equation}
$$

with $ \mathbf{w} \in \mathbb{R}^{n} $ a weight vector, $ b $ a bias and $ \phi\left(\cdot\right) $ an *activation function*. Therefore, if we consider the *threshold* activation function ($ \phi_{0}\left(x\right)=1 $ if $ x \geq 0$), a single neuron simply performs an *affine transform* and then a *linear* discrimination of the space. A network will be composed of _layers_ of these neurons, which produce successive computations

Geometrically, a single neuron computes an hyperplane that separates the space. In order to learn, we have to adjust the weights and know "how much wrong we are". To do so, we consider that we know the desired output $ d $ of a system for a given example $ \mathbf{x} $ (eg. a predicted value for a regression system, a class value for a classification system). Therefore, we define the loss function $ \mathcal{L}_{\mathcal{D}} $ over a whole dataset as

$$
\begin{equation}
\mathcal{L}_{\mathcal{D}}=\sum_{j=1}^{k_{\mathcal{D}}}\left\Vert y_{j}-\hat{y}_{j}\right\Vert ^{2}
\label{eq2}
\end{equation}
$$

In order to know how to change the weights based on the value of the errors, we need to now "how to change it to make it better". Therefore, we should compute the sets of derivatives of the error given each parameter

$$
\begin{equation}
\Delta\bar{\mathbf{w}}=\left(\frac{\delta\mathcal{L}}}{\delta w_{1}},\ldots,\frac{\delta\mathcal{L}}}{\delta w_{n}}\right)
\label{eq3}
\end{equation}
$$

***

**Exercise**

  1. Perform the derivatives of the output given a single neuron
  2. Perform the derivatives for the bias as well

***

### Training your own neuron

We will start by training a single neuron to learn how to perform this discrimination with a linear problem (so that a single neuron is enough to solve it).

```python
# Inputs to use
inputs = patterns;
# Initialize the weights
weights = np.random.randn(1, 2);
bias = np.random.randn(1, 1)
# Learning rate
eta = 0.05;
# Weight decay
lambdaW = 0.1
```

Now you need to update the following code loop to ensure that your neuron learns to separate between the classes
***

**Exercise**
  1. Update the loop so that it computes the forward propagation error
  2. Update the loop to perform learning (based on back-propagation)
  3. Run the learning procedure, which should produce a result similar to that displayed on the website
  4. Perform multiple re-runs by **tweaking the hyperparameters** (learning rate, weight decay)
  5. What observations can you make on the learning process?
  6. (Optional) Change the input patterns, and confirm your observations.
  6. (Optional) Incorporate the bias in the weights to obtain a **vectorized** code.

***

```python
# Plot the corresponding pattern
fig = plot_patterns(patterns,desired);
plt.draw()
# Update loop
for i in range(50):

    ######################
    # YOUR CODE GOES HERE
    ######################

    print('%2d.  weights = %f, %f, %f' % (i, bias[0, 0], weights[0, 0], weights[0, 1]))
    plot_boundary(np.concatenate((bias, weights), axis=1), i, '--', fig)
    plt.draw()
    time.sleep(0.2)
plot_boundary(np.concatenate((bias, weights), axis=1), i, '-', fig);
```

### 2-layer XOR problem

In most cases, classification problems are far from being linear. Therefore, we need more advanced methods to be able to compute non-linear class boundaries. The advantage of neural networks is that the same principle can be applied in a *layer-wise* fashion. This allows to further discriminate the space in sub-regions (as seen in the course). We will try to implement the 2-layer *perceptron* that can provide a solution to the infamous XOR problem. The idea is now to have the output of the first neurons to be connected to a set of other neurons. Therefore, if we take back our previous formulation, we have the same output for the first neuron(s) $\hat{y}$, that we will now term as $y^{(1)}$. Then, we feed these outputs to a second layer of neurons, which gives

$$
\begin{equation}
y^{(2)}=\phi\left(\sum_{i = 1}^{n}w_{i}.y^{(1)}_{i} + b\right)
\label{eq6}
\end{equation}
$$

Finally, we will rely on the same loss $\mathcal{L_{D}}$ as in the previous exercise, but the outputs used are $y_2$ instead of $y$. As in the previous case, we now need to compute the derivatives of the weights and biases for several layers . However, you should see that some form of generalization might be possible for any number of layer.

***

**Exercise**
  1. Perform the derivatives for the last layer specifically
  2. Define a generalized derivative for any previous layer

***

We can construct the prototypical set of XOR values by using the following code (note that this is the most simple case, but still this is typically a problem that cannot be solved by a _linear classifier_

```python
patterns = np.array([[-1, -1],[-1,  1],[1, -1],[1,  1]]).transpose() # Input patterns
desired = np.array([0, 1, 1, 0])                       # Corresponding classes
# Initialize based on their sizes
nInputs = patterns.shape[0]
nOutputs = 1
nPat = patterns.shape[1]
# First plot the patterns
fig = plot_patterns(patterns, desired);
```

The variables that will be used by your code are the following.

```Matlab
patterns          % 2 x n matrix of random points
desired           % classes of the patterns
inputs1           % 3 x n final matrix of inputs (accounting for bias)
nHiddens          % Number of hidden units
learnRate         % Learning rate parameter
momentum          % Momentum parameter (bonus)
weights1          % 1st layer weights
weights2          % 2nd layer weights
TSS_Limit         % Sum-squared error limit
```

```python
nHiddens = 2           # Number of hidden units
learnRate = 0.001       # Learning rate parameter
momentum = 0.1         # Momentum parameter
# Overall input patterns
inputs = patterns
TSS_Limit = 0.02                                           # Sum-squared error limit
# Learning rate
eta = 0.001
```

***

**Exercise**

  1. Update the forward propagation and error computation (compared to desired).
  2. Update the back-propagation part to learn the weights of both layers.
  3. Run the learning, which should produce a result similar to that displayed below.
  4. Perform multiple re-runs of the learning procedure (re-launching with different initializations)
  5. What observations can you make on the learning process?
  6. What happens if you initialize all weights to zeros?
  7. (Optional) Implement the *sparsity* constraint in your neural network.
  7. (Optional) Implement the *weight decay* constraint in your network.
  7. (Optional) Add the *momentum* to the learning procedure.

*For optional questions, please look after the first code box for more information*

***

```python
# Weights of first and second layer
weights1 = (np.random.randn(nHiddens, nInputs) - 0.5)      # 1st layer weights
weights2 = (np.random.randn(nOutputs, nHiddens) - 0.5)     # 2nd layer weights
bias1 = (np.random.randn(nHiddens, 1) - 0.5)               # 1st layer biases
bias2 = (np.random.randn(nOutputs, 1) - 0.5)               # 2nd layer biases
# First plot the patterns
fig = plot_patterns(patterns, desired);
# Iterate for a fixed number of iterations
for epoch in range(200):

    ######################
    # YOUR CODE GOES HERE
    ######################

    #print('Epoch %3d:  Error = %f' % (epoch, TSS));
    #if TSS < TSS_Limit:
    #    break
    if (epoch - 1 % 20)==0:
        plot_boundary(np.concatenate((bias2, weights2), axis=1), epoch, '--', fig)

plot_boundary(np.concatenate((bias1, weights1), axis=1), epoch, '-', fig)
```

**Optional questions**

2. *Weight decay* constraint

As nothing constrains the weights in the network, we can note that usually all weights vector given a multiplicative factor might be equivalent, which can stall the learning (and lead to exploding weights). The *weight decay* allows to regularize the learning by penalizing weights with a too wide amplitude. The idea is to add this constraint as a term to the final loss (which leads to an indirect "pressure" on the learning process. Therefore, the final loss will be defined as

$$
\begin{equation}
\mathcal{L}_{final}=\mathcal{L_D} + \lambda \sum_{l} \sum_{i} \sum_{j} \left( W_{ij}^{l} \right)^{2}
\end{equation}
$$

where the parameter $\lambda$ controls the relative importance of the two terms.

3. *Momentum* in learning

Usually, in complex problems, the gradient can be very noisy and, therefore, the learning might oscillate widely. In order to reduce this problem, we can *smooth* the different gradient updates by retaining the values of the gradient at each iteration and then performing an update based on the latest gradient $\delta_{i}^{t}$ and the gradient at the previous iteration $\delta_{i}^{t-1}$. Therefore, a gradient update is applied as

$$
\begin{equation}
\delta_{final}^{t} = \delta_{i}^{t} + m.\delta_{i}^{t-1}
\end{equation}
$$

with $m$ the momentum parameter, which control the amount of gradient smoothing.

## 3-layer audio classification

Finally, we will attack a complete audio classification problem and try to perform neural network learning on a set of audio files. The data structure will be the same as the one used for parts 1 and 2. As discussed during the courses, even though a 2-layer neural network can provide non-linear boundaries, it can not perform "holes" inside those regions. In order to obtain an improved classification, we will now rely on a 3-layer neural network. The modification to the code of section 3.2 should be minimal, as the back-propagation will be similar for the new layer as one of the two others. We do not develop the math here as it is simply a re-application of the previous rules with an additional layer (which derivatives you should have generalized in the previous exercise).

However, up until now, we only performed *binary classification* problems, but this time we need to obtain a decision rule for multiple classes. Therefore, we cannot rely on simply computing the distance between desired patterns and the obtained binary value. The idea here is to rely on the *softmax regression*, by considering classes as a vector of probabilities. The desired answers will therefore be considered as a set of *probabilities*, where the desired class is $1$ and the others are $0$ (called *one-hot* representation). Then, the cost function will rely on the softmax formulation

$$
\begin{equation}
\mathcal{L_D}(\theta) = - \frac{1}{m} \left[ \sum_{i=1}^{m} \sum_{j=1}^{k} 1\left\{y^{(i)} = j\right\} log \frac{e^{\theta_{j}^{T} x^{(i)}}}{\sum_{l=1}^{k} e^{ \theta_{l}^{T} x^{(i)} }}  \right]
\end{equation}
$$

Therefore, we compute the output of the softmax by taking

$$
\begin{equation}
p(y^{(i)} = j | x^{(i)}; \theta) = \frac{e^{\theta_{j}^{T} x^{(i)}}}{\sum_{l=1}^{k} e^{ \theta_{l}^{T} x^{(i)}} }
\end{equation}
$$

By taking derivatives, we can show that the gradient of the softmax layer is

$$
\begin{equation}
\nabla_{\theta_{j}} \mathcal{L_D}(\theta) = - \frac{1}{m} \sum_{i=1}^{m}{ \left[ x^{(i)} \left( 1\{ y^{(i)} = j\}  - p(y^{(i)} = j \mid x^{(i)}, \theta) \right) \right]}
\end{equation}
$$

### Sweet activation functions

As discussed in the course, the interest of stacking layers is that there is an _activation function_, which allows non-linear interactions between the dimensions (and avoids to only compute a single huge affine transform). Although the `sigmoid` function has been historically the most used, there has been some large developments since. Notably the `ReLU` (Rectified Linear Unit) is one of the major difference in modern networks (we will see more about that in a later course)

```python
# Function for computing the Sigmoid activation
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
def dsigmoid(a):
    return a * (1.0 - a)
# Function for computing the ReLU activation
def relu(x):
    return np.maximum(0, x)
def drelu(x):
    return 1 / (1 + np.exp(-x))
# Function for computing the Tanh activation
def tanh(x):
    return np.tanh(x);
def dtanh(x):
    return np.cosh(x) ^ -2
```

Here, we plot some simple examples of what these activation functions look like. You can try to rely on these functions in your previous training code and witness the differences in training.

```python
from helper_plot import prep_plots, finalize_plots
funcs = [('sigmoid',sigmoid,'red'), ('tanh',tanh,'orange'), ('relu',relu,'yellow')]
axes = prep_plots([funcs[0][0], funcs[1][0], funcs[2][0]], fig_size=(18,5), fig_num=1)
x_plot = np.linspace(-5,5,100)
np.maximum(0, x_plot)
for f in range(3):
    axes[f].plot(x_plot, funcs[f][1](x_plot), color=funcs[f][2], linewidth=4, label=funcs[f][0])
finalize_plots(axes, fig_title="Activation functions")
```

## Coding the whole network from scratch

You should now have all the tools necessary to apply neural networks from scratch to a more complex problem. In the following exercise, we simply removed any guideline code, and you need to code all the procedure for training a NN and **apply it to audio data**. You will use the spectral features discussed in the previous exercise as an input.

***

**Exercise**

  1. Based on the previous neural network, upgrade the code to a 3-layer neural network
  2. Implement the *softmax regression* on top of your 3-layer network
  3. Use the provided code to perform classification on a pre-defined set of features
  4. As previously, change the set of features to assess their different accuracies
  5. Evaluate the neural network accuracy for all features combinations
  6. What happens if the learning rate is too large ? What is this phenomenon ?
  7. (Optional) Perform a more advanced visualization of the learning process.

***

```python
# Iterate for a fixed number of iterations
for epoch in range(500):

    ######################
    # YOUR CODE GOES HERE
    ######################

    print('Epoch %3d:  Error = %f\n' % epoch, TSS);
    if TSS < TSS_Limit:
        break
plo_boundary(np.concatenate(bias1, weights1), epoch, '-');
```

## Using Pytorch to enjoy life

Up to now, we have been writing every operations by ourselves (in order to better understand the mathematics behind NN). However, there exists of course some simplifying libraries that provide large simplifications to this question.

One of the most powerful and complete library of this sort is `Pytorch`, which has been developed for several years (even prior to the recent boom of deep learning). `Pytorch` provides a large set of pre-coded layers, but also **computational graphs** and **autograd**, which are very powerful paradigms allowing to define complex operators and automatically taking derivatives.

### Defining our network

When building neural networks we frequently think of arranging the computation into layers, some of which have learnable parameters which will be optimized during learning. In `PyTorch`, the `nn` package provides higher-level abstractions over raw computational graphs that are useful for building neural networks. The `nn` package defines a set of `Modules`, which are roughly equivalent to neural network layers. A `Module` receives input `Tensors` and computes output `Tensors`, but may also hold internal state such as `Tensors` containing learnable parameters. The nn package also defines a set of useful loss functions that are commonly used when training neural networks.

In the following example, we use the `nn` package to show how easy it is to instantiate our previous three-layers network

```python
import torch
# Define the input dimensions
in_size = 1000
# Number of neurons in a layer
hidden_size = 100
# Output (target) dimension
output_size = 10
# Use the nn package to define our model and loss function.
model = torch.nn.Sequential(
    torch.nn.Linear(in_size, hidden_size),
    torch.nn.ReLU(),
    torch.nn.Linear(hidden_size, hidden_size),
    torch.nn.ReLU(),
    torch.nn.Linear(hidden_size, output_size),
    torch.nn.Softmax(dim=1)
)
```

### Optimizing the network

Up to this point we have updated the weights of our models by manually performing the gradient descent algorithm (changing the parameters vectors). Although this is not a huge burden for simple optimization algorithms like stochastic gradient descent, in practice we often train neural networks using more sophisticated optimizers like AdaGrad, RMSProp or Adam (that we will see later in this course)

The `optim` package in PyTorch abstracts the idea of an optimization algorithm and provides implementations of commonly used optimization algorithms, and greatly simplfies the training loop associated with training a neural network.

For the sake of presentation we will use random inputs $\mathbf{x}$ that should be matched with random outputs $\mathbf{y}$

```python
batch_size = 64
# Create random Tensors to hold inputs and outputs
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
```

In the following example we optimize the model using the Adam algorithm provided by the `optim` package, based on a `MSE` loss.

```python
# Learning rate
learning_rate = 1e-4
# Loss function that we will use
loss_fn = torch.nn.MSELoss(reduction='sum')
# Optimizer to fit the weights of the network
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for t in range(500):
    # Forward pass: compute predicted y by passing x to the model.
    y_pred = model(x)
    # Compute the loss.
    loss = loss_fn(y_pred, y)
    # Before the backward pass, zero all of the network gradients
    optimizer.zero_grad()
    # Backward pass: compute gradient of the loss with respect to parameters
    loss.backward()
    # Calling the step function to update the parameters
    optimizer.step()
```

## Using Pytorch to classify audio

Now that we know the main components of `Pytorch` to define and optimize networks, your assignement is to define a complete classification problem from audio data, by relying on this toolbox

***

**Exercise**

  1. Use `Pytorch` to define a model for audio classification
  2. Import the audio features dataset and check that your model produces an output
  3. Write the optimization loop (think carefully about the _loss function_
  4. As previously, change the set of features to assess their different accuracies
  5. Think of how you could use more complex features (time series, audio, STFT) to classify your data

***

```python

######################
# YOUR CODE GOES HERE
######################

```


# Physics 256
## The Poisson Equation

\begin{equation}
\nabla \cdot \vec{E} = \frac{\rho}{\varepsilon_0}
\end{equation}

```python
import style
style._set_css_style('../include/bootstrap.css')
```

## Last Time

### [Notebook Link: 24 Relaxation Methods](./23_Relaxation_Methods.ipynb)

- Relaxation methods including:
 - Jacobi method
 - Guass-Seidel method
- Computing the electric field

## Today

- Simultaneous over relaxation (SOR)
- Application to the Poisson equation (finite charge density)

## Setting up the Notebook

```python
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
plt.style.use('../include/notebook.mplstyle');
%config InlineBackend.figure_format = 'svg'
colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
```

## Jacobi and Gauss-Seidel Methods

Last time, we derived iterative relaxation methods to solve the Laplace equation in two dimensions:

\begin{equation}
V(i,j) = \frac{1}{4} \left[V(i+1,j) + V(i-1,j) + V(i,j+1) + V(i,j-1)  \right].
\end{equation}

Both had poor $\mathrm{O}(N^2)$ scaling and have limited applicability for fine grids.  The main advance of Gauss-Seidel was to update the potential *in place* using new values as they become available.  We can extend this idea to take a *larger* relaxation step.

## Simultaneous Over Relaxtion (SOR)

At each grid point $(i,j)$ we defined

\begin{equation}
\Delta V(i,j) = V_{\rm new} (i,j) - V_{\rm old}(i,j)
\end{equation}

where $V_{\rm new}$ is computed with the expression above. The Gauss-Seidel method can then be written as:

\begin{equation}
V_{\rm new}(i,j) = V_{\rm old}(i,j) + \alpha \Delta V(i,j)
\end{equation}

where $\alpha = 1$.  However, we can arbitrarily increase the value of $\alpha$, in particular:

\begin{align}
\alpha = 1 \quad & : \quad \rm {Gauss-Seidel} \newline
1 < \alpha < 2 \quad & : \quad \rm {SOR} \newline
\alpha \ge 2 \quad & : \quad \rm {Unstable}.
\end{align}

The optimal value of $1 < \alpha < 2$ to use depends on the particular proglem.  For elliptical PDEs with Dirichlet boundary condtions it can be estimated as:

\begin{equation}
\alpha \approx \frac{2}{1+\pi/L}.
\end{equation}

### Coding

Starting from the expression above, it is convenient to write:
\begin{align}
V_{\rm new}(i,j) &= V_{\rm old}(i,j) + \alpha \Delta V(i,j) \newline &= \frac{\alpha}{4} \left[V(i+1,j) + V(i-1,j) + V(i,j+1) + V(i,j-1)  \right] + (1-\alpha)V(i,j) .
\end{align}

Let's code it up.

```python
def SOR_update(V,α,mask):
    '''Update the potential according to the Simultaneous-Over-Relaxation method.'''

    # here we use a mask which tells us which elements should be updated
    ΔV = 0.0
    for i in range(mask.shape[0]):
        for j in range(mask.shape[1]):
            if mask[i,j]:
                V_old = V[i,j]
                V[i,j] = (α/4)*(V[i-1,j] + V[i+1,j] + V[i,j+1] + V[i,j-1]) + (1-α)*V_old
                ΔV += abs(V_old-V[i,j])

    return ΔV
```

## Box inside a Box

Find the potential everywhere the nested box shown below.  We assume that the inner box spans -1/5 ... 1/5 in the $x$ and $y$ directions.

```python
fig,ax = plt.subplots(1,1, figsize=(7,7))
ax.axis('off')
ax.plot((-1,-1),(-1,1),'k', lw=15)
ax.plot((1,1),(-1,1),'k', lw=15)
ax.plot((-1,1),(1,1),'k', lw=15)
ax.plot((-1,1),(-1,-1),'k', lw=15)

ax.plot((-0.2,-0.2),(-0.2,0.2),'r', lw=10)
ax.plot((0.2,0.2),(-0.2,0.2),'b', lw=10)
ax.plot((-0.2,0.2),(0.2,0.2),'r', lw=10)
ax.plot((-0.2,0.2),(-0.2,-0.2),'r', lw=10)

cV = [0,0,0,0]
for i,loc in enumerate([(-1.2,0),(0,-1.2),(1.2,0),(0,1.05)]):
    ax.text(*loc,r'$V=%d$'%cV[i], fontsize=20, ha='center')

ax.text(0,0.28,r'$V=1$', fontsize=20, ha='center')
ax.text(0.5,0.0,r'$V=-1$', fontsize=20, va ='center', ha='center')

ax.set_aspect('equal')
```

###  Initialze the potential

```python
def init(N):
    '''Initialize the potential with boundary conditions.'''

    # the potential
    V  = np.zeros([N,N])

    # the mask
    mask = np.ones([N,N], dtype=int)

    # set the boundary conditions
    box_min = int(N/2) - int(0.5*N/5)
    box_max = int(N/2) + int(0.5*N/5)

    V[box_min:box_max,box_min] = 1
    V[box_min:box_max,box_max] = -1
    V[box_min,box_min:box_max] = 1
    V[box_max,box_min:box_max] = 1

    # set the mask
    mask[box_min:box_max,box_min] = 0
    mask[box_min:box_max,box_max] = 0
    mask[box_min,box_min:box_max] = 0
    mask[box_max,box_min:box_max] = 0
    mask[:,0] = 0
    mask[:,-1] = 0
    mask[0,:] = 0
    mask[-1,:] = 0

    return V,mask
```

###  Iterate until convergence

```python
from matplotlib import animation,rc
from IPython.display import HTML
rc('animation', html='jshtml')

# the number of lattice points in the x and y direction
N = 51

# the tolerance for convergence
ϵ = 1.0E-5

# the over-relaxation parameter
α = 1.5

# initialize
V,mask = init(N)

# Plot the initial configuration
fig = plt.figure(figsize=(7,4))
plt.xlabel('x')
plt.ylabel('y')
ims = [[plt.imshow(V, cmap=plt.get_cmap('bwr'), animated=True)]]
cb = plt.colorbar(label='Electric Potential', pad=0.1)

# Iterate until convergence
n = 0
ΔV = 1.0

while ΔV/(N*N) > ϵ:
    ΔV = SOR_update(V,α,mask)

    # plot the intermediate results
    if n < 200:
        ims.append([plt.imshow(V, extent=[-1,1,-1,1], cmap=plt.get_cmap('bwr'), animated=True)])

    if n > 1000:
        break
    n+= 1

print("ΔV = %8.5E in %d steps" % ((ΔV/N**2),n))
# animate the final result
anim = animation.ArtistAnimation(fig, ims, interval=10, blit=False, repeat_delay=1, repeat=True)
```

```python
anim
```

## The Poisson Equation

So far we have used relaxation methods to solve the Laplace equation, i.e. we find the potential in the absence of any charges.  When we have some fixed charge density, we must solve the Poisson equation:

\begin{equation}
\nabla^2 V = \frac{\rho(x,y,z)}{\varepsilon_0}
\end{equation}

where $\rho(x,y,z)$ is the charge density and $\varepsilon_0$ is the permittivity of free space.  Performing the same discretization procedure as before and taking an isotropic grid: $\Delta x = \Delta y = \Delta z = \Delta$ we find:

\begin{equation}
V(i,j,k) = \frac{1}{6} \left[V(i+1,j,k) + V(i-1,j,k) + V(i,j+1,k) + V(i,j-1,k) +V(i,j,k+1) + V(i,j,k-1) \right] + \frac{\rho(i,j,k)\Delta^2}{6\varepsilon_0}.
\end{equation}

The choice of boundary conditions can be a bit tricky in these problems.

 Team Programming challenge

Find the potential due to a single charge at the origin with $q = \Delta \varepsilon_0$ thus $\rho(0,0,0) = q/\Delta^3 =  \varepsilon_0 /\Delta^2$.

Hint:  Carefully consider your boundary conditions.  What should the potential be for $r\gg 1$?

```python
def init_poisson(N,Δ):
    '''Initialize the potential with boundary conditions.'''

    # midpoint
    mid = int(N/2)

    # potential, charge density and mask
    V  = np.zeros([N,N,N],dtype=float)
    ρ = np.zeros_like(V)
    mask = np.ones([N,N,N],dtype=int)

    ###
    # INITIALIZE MASK
    ###

    ###
    # INITIALIZE ρ
    ###

    return V,ρ,mask

```

```python
def SOR_update_3D(V,ρ,α,Δ,mask):
    '''Update the potential according to the Simultaneous-Over-Relaxation method.'''

    # here we use a mask which tells us which elements should be updated
    ΔV = 0.0
    for i in range(mask.shape[0]):
        for j in range(mask.shape[1]):
            for k in range(mask.shape[2]):
                if mask[i,j,k]:
                    V_old = V[i,j,k]
                    ###
                    # INSERT CODE HERE
                    ###

                    ΔV += abs(V_old-V[i,j,k])
    return ΔV
```

```python
# the number of lattice points in each direction
N = 11

# box and grid size
L = 1.0
Δ = L/N

# the tolerance for convergence
ϵ = 1.0E-5

# the over-relaxation parameter
α = 1.5

# initialize
V,ρ,mask = init_poisson(N,Δ)

# Iterate until convergence
ΔV = 100.0
while ΔV/(N**3) > ϵ:
    pass
    ###
    # INSERT CODE HERE
    ###

print("ΔV = %8.5E in %d steps" % ((ΔV/N**3),n))

# plot the final result as a contour plot
X,Y = np.meshgrid(np.linspace(-L/2,L/2,N), np.linspace(-L/2,L/2,N))
cs = plt.contourf(X,Y,np.transpose(V[:,:,int(N/2)]),10,cmap=plt.get_cmap('bwr'))
cb = plt.colorbar(label='Electric Potential', pad=0.1)
plt.xlabel('x')
plt.ylabel('y')
plt.axes().set_aspect('equal')
```

```python

```


```python
# Método para resolver las energías y eigenfunciones de un sistema cuántico numéricamente por Teoría de Pertubaciones
# Modelado Molecular 2
# By: José Manuel Casillas Martín     22-oct-2017
import numpy as np
from sympy import *
from sympy.physics.qho_1d import E_n, psi_n
from sympy.physics.hydrogen import E_nl, R_nl
from sympy import init_printing; init_printing(use_latex = 'mathjax')
from scipy import integrate
from scipy.constants import hbar, m_e, m_p, e
from mpmath import spherharm
from numpy import inf, array
import numpy as np
import matplotlib.pyplot as plt
import traitlets
from IPython.display import display
from ipywidgets import Layout, Box, Text, Dropdown, Label, IntRangeSlider, IntSlider, RadioButtons
```

Teoría de Perturbaciones

Consiste en resolver un sistema perturbado(se conoce la solución al no perturbado), y donde el interés es conocer la contribución de la parte perturbada $H'$ al nuevo sistema total.

$$ H = H^{0}+H'$$

La resolución adecuada del problema, depende en gran parte, de una correcta elección de $H'$.

```python
form_item_layout = Layout(display='flex',flex_flow='row',justify_content='space-between')

PType=Dropdown(options=['Particle in a one-dimensional box', 'Harmonic oscilator', 'Hydrogen atom (Helium correction)'])
Pert=Text()
Rang=IntRangeSlider(min=0, max=20, step=1, disabled=False, continuous_update=False, orientation='horizontal',\
                    readout=True, readout_format='d')
M=Text()
Correc=Dropdown(options=['1', '2'])
hbarra=Dropdown(options=[1, 1.0545718e-34])

form_items = [
    Box([Label(value='Problem'),PType], layout=form_item_layout),
    Box([Label(value='Perturbation'),Pert], layout=form_item_layout),
    Box([Label(value='Correction order'),Correc], layout=form_item_layout),
    Box([Label(value='n Range'),Rang], layout=form_item_layout),
    Box([Label(value='Mass'),M], layout=form_item_layout),
    Box([Label(value='Hbar'),hbarra], layout=form_item_layout),]

form = Box(form_items, layout=Layout(display='flex',flex_flow='column',border='solid 2px',align_items='stretch',width='40%'))
form
```

En esta caja interactiva llena los datos del problema que deseas resolver.

# Nota 1:
Es recomendable usar unidades atómicas de Hartree para eficientar los cálculos. 1 u.a. (energía)= 27.211eV.

# Nota 2:
Para la partícula en una caja unidimensional es recomendable que n sea mayor o igual a 1.

## Nota 3:
Para la correción a la energía del átomo de Helio sólo es necesario seleccionar el problema, automáticamente se calcula la correción a primer orden y no se corrigen las funciones de onda.

```python
Problem=PType.value
form_item_layout = Layout(display='flex',flex_flow='row',justify_content='space-between')

L=Text()
W=Text()
atomic_number=RadioButtons(options=['1 (Show Hydrogen energies)','2 (Correct Helium first energy)'],disabled=False)

if Problem=='Particle in a one-dimensional box':
    form_items = [Box([Label(value='Large of box'),L], layout=form_item_layout)]

if Problem=='Harmonic oscilator':
    form_items = [Box([Label(value='Angular Fr'),W], layout=form_item_layout)]

if Problem=='Hydrogen atom (Helium correction)':
    form_items = [Box([Label(value='Atomic number'),atomic_number], layout=form_item_layout)]

form = Box(form_items, layout=Layout(display='flex',flex_flow='column',border='solid 2px',align_items='stretch',width='40%'))
form
```

```python

```

```python
# Variables que se utilizarán
# x=variable de integracion, l=largo del pozo, m=masa del electrón, w=frecuencia angular
# n=número cuántico principal, Z=Número atómico, q=número cuántico angular(l)
var('x theta phi')
var('r1 r2', real=True)
var('l m hbar w n Z', positive=True, real=True)

# Perturbación
if Pert.value!='':
    H_p=sympify(Pert.value)
    h_p=eval(Pert.value)
else:
    H_p=0
    h_p=0

# Constantes
h=hbarra.value
a0=5.2917721067e-11
if M.value!='':
    mass=float(eval(M.value))
else:
    mass=1

# Energías y funciones que se desea corregir
n_inf=min(Rang.value)
n_sup=max(Rang.value)

if Problem=='Particle in a one-dimensional box':
    if L.value=='':
        large=1
    else:
        large=float(eval(L.value))
    omega=0
    # Energías del pozo de potencial infinito
    k=n*pi/l
    En=hbar**2*k**2/(2*m)

    # Funciones de onda del pozo de potencial infinito
    Psin=sqrt(2/l)*sin(n*pi*x/l)

    # Límites del pozo definido de 0 a l para sympy
    li_sympy=0
    ls_sympy=l
    # Mismo limites para scipy
    li_scipy=0
    ls_scipy=large

if Problem=='Harmonic oscilator':
    large=0
    if W.value=='':
        omega=1
    else:
        omega=float(eval(W.value))
    # Energías del oscilador armónico cuántico
    En=E_n(n,w)

    # Funciones de onda del oscilador armónico cuántico
    Psin=psi_n(n,x,m,w)

     # Límites del pozo definido de -oo a oo para sympy
    li_sympy=-oo
    ls_sympy=oo
    # Límites del pozo definido de -oo a oo para scipy
    li_scipy=-inf
    ls_scipy=inf

if Problem=='Hydrogen atom (Helium correction)':
    if atomic_number.value=='1 (Show Hydrogen energies)':
        z=1
    if atomic_number.value=='2 (Correct Helium first energy)':
        z=2
    large=0
    omega=0

    # Energías del átomo hidrogenoide
    En=z*E_nl(n,z)

    # Funciones de onda del átomo de hidrógeno
    # Número cuántico l=0
    q=0 # La variable l ya esta siendo utilizada para el largo de la caja por ello se sustituyo por q
    Psin=(R_nl(n,q,r1,z)*R_nl(n,q,r2,z))

     # Límites del átomo de hidrógeno de 0 a oo para sympy
    li_sympy=0
    ls_sympy=oo
    # Límites del átomo de hidrógeno de 0 a oo para scipy
    li_scipy=0
    ls_scipy=inf
```

Para sistemas no degenerados, la corrección a la energía a primer orden se calcula como

$$ E_{n}^{(1)} = \int\psi_{n}^{(0)*} H' \psi_{n}^{(0)}d\tau$$

** Tarea 1 : Programar esta ecuación si conoces $H^{0}$ y sus soluciones. **

```python
def correcion_1st_order_Energy(E_n,Psi_n,H_p,li,ls):
    E1_n=Integral(Psi_n*(H_p)*Psi_n,(x,li,ls)).doit()
    return(E_n+E1_n)
```

```python
# Correción de la energía a primer orden
E=[]
Eev=[]
Ec1=[]
if Problem=='Particle in a one-dimensional box' or Problem=='Harmonic oscilator':
    for i in range(n_inf,n_sup+1):
        E.append(En.subs({n:i}))
        Eev.append(E[i-n_inf].subs({m:mass, l:large, hbar:h}).evalf())
        Ec1.append(correcion_1st_order_Energy(En.subs({n:i}),Psin.subs({n:i}),H_p,li_sympy,ls_sympy))
if Problem=='Hydrogen atom (Helium correction)':
    for i in range(n_inf,n_sup+1):
        E.append(En.subs({n:i}))
        Eev.append(E[i-n_inf])
    if z==2:
        integral_1=Integral(Integral((16*z**6*r1*r2**2*exp(-2*z*(r1+r2))),(r2,0,r1)),(r1,0,oo)).doit()
        integral_2=Integral(Integral((16*z**6*r1**2*r2*exp(-2*z*(r1+r2))),(r2,r1,oo)),(r1,0,oo)).doit()
        integral_total=(integral_1+integral_2)
        Ec1.append(E[0]+integral_total)
```

Y la corrección a la función de onda, también a primer orden, se obtiene como:

 $$ \psi_{n}^{(1)} = \sum_{m\neq n} \frac{\langle\psi_{m}^{(0)} | H' | \psi_{n}^{(0)} \rangle}{E_{n}^{(0)} - E_{m}^{(0)}} \psi_{m}^{(0)}$$

**Tarea 2: Programar esta ecuación si conoces $H^{0}$ y sus soluciones. **

```python
# Correción de las funciones a primer orden
if Pert.value!='':
    if Problem=='Particle in a one-dimensional box' or Problem=='Harmonic oscilator':
        Psi_c=[]
        integrals=np.zeros((n_sup+1,n_sup+1))
        for i in range(n_inf,n_sup+1):
            a=0
            for j in range(n_inf,n_sup+1):
                if i!=j:
                    integ= lambda x: eval(str(Psin.subs({n:j})*(h_p)*Psin.subs({n:i}))).subs({m:mass,l:large,w:omega,hbar:h})
                    integrals[i,j]=integrate.quad(integ,li_scipy,ls_scipy)[0]
                    cte=integrals[i,j]/(En.subs({n:i,m:mass,l:large})-En.subs({n:j,m:mass,l:large})).evalf()
                    a=a+cte*Psin.subs({n:j})
            Psi_c.append(Psin.subs({n:i})+a)
```

**Tarea 3: Investigue las soluciones a segundo orden y también programe las soluciones. **

Y la corrección a la energía a segundo orden, se obtiene como:

 $$  E_{n}^{(2)} = \sum_{m\neq n} \frac{|\langle\psi_{m}^{(0)} | H' | \psi_{n}^{(0)} \rangle|^{2}}{E_{n}^{(0)} - E_{m}^{(0)}} $$

```python
# Correción a la energía a segundo orden
if Pert.value!='':
    if Problem=='Particle in a one-dimensional box' or Problem=='Harmonic oscilator':
        if Correc.value=='2':
            Ec2=[]
            for i in range(n_inf,n_sup+1):
                a=0
                for j in range(n_inf,n_sup+1):
                    if i!=j:
                        cte=((integrals[i,j])**2)/(En.subs({n:i,m:mass,l:large,hbar:h})-En.subs({n:j,m:mass,l:large,hbar:h})).evalf()
                        a=a+cte
                Ec2.append(Ec1[i-n_inf]+a)
```

**A continuación se muestran algunos de los resultados al problema resuelto**

Las energías sin perturbación son:

```python
E
```

$$\left [ \frac{\pi^{2} \hbar^{2}}{2 l^{2} m}, \quad \frac{2 \pi^{2} \hbar^{2}}{l^{2} m}, \quad \frac{9 \pi^{2} \hbar^{2}}{2 l^{2} m}\right ]$$

La correción a primer orden de las energías son:

```python
Ec1
```

$$\left [ \frac{\pi^{2} \hbar^{2}}{2 l^{2} m} + \frac{1}{l} \left(- \frac{5 l^{3}}{\pi^{2}} + \frac{10 l^{3}}{3} - 5 l l^{2} + \frac{5 l}{2} l^{2}\right), \quad \frac{2 \pi^{2} \hbar^{2}}{l^{2} m} - \frac{5 l l}{4 \pi^{2}} + \frac{1}{l} \left(- \frac{5 l^{3}}{4 \pi^{2}} + \frac{10 l^{3}}{3} - 5 l l^{2} + \frac{5 l l^{2}}{4 \pi^{2}} + \frac{5 l}{2} l^{2}\right), \quad \frac{9 \pi^{2} \hbar^{2}}{2 l^{2} m} + \frac{1}{l} \left(- \frac{5 l^{3}}{9 \pi^{2}} + \frac{10 l^{3}}{3} - 5 l l^{2} + \frac{5 l}{2} l^{2}\right)\right ]$$

Si seleccionaste en los parámetros iniciales una correción a segundo orden entonces...

Las correciones a la energía a segundo orden son:

```python
Ec2
```

$$\left [ \frac{\pi^{2} \hbar^{2}}{2 l^{2} m} - 0.00365681767955573 + \frac{1}{l} \left(- \frac{5 l^{3}}{\pi^{2}} + \frac{10 l^{3}}{3} - 5 l l^{2} + \frac{5 l}{2} l^{2}\right), \quad \frac{2 \pi^{2} \hbar^{2}}{l^{2} m} - \frac{5 l l}{4 \pi^{2}} - 4.42245128165018 \cdot 10^{-34} + \frac{1}{l} \left(- \frac{5 l^{3}}{4 \pi^{2}} + \frac{10 l^{3}}{3} - 5 l l^{2} + \frac{5 l l^{2}}{4 \pi^{2}} + \frac{5 l}{2} l^{2}\right), \quad \frac{9 \pi^{2} \hbar^{2}}{2 l^{2} m} + 0.00365681767955573 + \frac{1}{l} \left(- \frac{5 l^{3}}{9 \pi^{2}} + \frac{10 l^{3}}{3} - 5 l l^{2} + \frac{5 l}{2} l^{2}\right)\right ]$$

Ahora vamos con la función de onda $(\psi)$

```python
form_item_layout = Layout(
    display='flex',
    flex_flow='row',
    justify_content='space-between')

Graph=IntSlider(min=n_inf, max=n_sup, step=1, disabled=False, continuous_update=False, orientation='horizontal',\
                readout=True, readout_format='d')

form_items = [
    Box([Label(value='What function do you want to see?'),
         Graph], layout=form_item_layout)]

form = Box(form_items, layout=Layout(
    display='flex',
    flex_flow='column',
    border='solid 2px',
    align_items='stretch',
    width='40%'))
form
```

La función de onda original es:

```python
Psin.subs({n:Graph.value})
```

$$\frac{\sqrt{2}}{\sqrt{l}} \sin{\left (\frac{2 \pi}{l} x \right )}$$

La correción a primer orden a la función de onda (utilizando todas las funciones en el rango seleccionado) es:

```python
Psi_c[Graph.value-n_inf]
```

$$\frac{\sqrt{2}}{\sqrt{l}} \sin{\left (\frac{2 \pi}{l} x \right )} + \frac{1.11621501604059 \cdot 10^{-18} \sqrt{2}}{\hbar^{2} \sqrt{l}} \sin{\left (\frac{\pi x}{l} \right )} - \frac{4.321004688208 \cdot 10^{-18} \sqrt{2}}{\hbar^{2} \sqrt{l}} \sin{\left (\frac{3 \pi}{l} x \right )}$$

Vamos a graficarlas para verlas mejor...

La función de onda original es:

```python
if Problem=='Particle in a one-dimensional box':
    plot(eval(str(Psin)).subs({n:Graph.value,m:mass,l:large,w:omega,hbar:h}),xlim=(li_scipy,ls_scipy),\
         title='$\psi_{%d}$'%Graph.value)
if Problem=='Harmonic oscilator':
    plot(eval(str(Psin)).subs({n:Graph.value,m:mass,l:large,w:omega,hbar:h}),xlim=(-10*h/(mass*omega),10*h/(mass*omega)),\
         title='$\psi_{%d}$'%Graph.value)
if Problem=='Hydrogen atom (Helium correction)':
    print('Densidad de probabilidad para un electrón')
    plot(eval(str((4*pi*x**2*R_nl(Graph.value,q,x,z)**2))),xlim=(0,10),ylim=(0,20/Graph.value), title='$\psi_{%ds}$'%Graph.value)
    print('Tome en cuenta que debido a la dificultad para seleccionar los límites de la gráfica se muestran bien los primeros\n\
3 estados. A partir de ahí visualizar la gráfica se complica.')
```

La corrección a la función de onda es:

```python
if Problem=='Particle in a one-dimensional box':
    if Pert.value!='':
        plot(eval(str(Psi_c[Graph.value-n_inf])).subs({n:Graph.value,m:mass,l:large,w:omega,hbar:h}),\
             xlim=(li_scipy,ls_scipy),title='$\psi_{%d}$'%Graph.value)
    if Pert.value=='':
        print('No se ingreso ninguna perturbación')
if Problem=='Harmonic oscilator':
    if Pert.value!='':
        plot(eval(str(Psi_c[Graph.value-n_inf])).subs({n:Graph.value,m:mass,l:large,w:omega,hbar:h}),\
             xlim=(-10*h/(mass*omega),10*h/(mass*omega)),title='$\psi_{%d}$' %Graph.value)
    if Pert.value=='':
        print('No se ingreso ninguna perturbación')
if Problem=='Hydrogen atom (Helium correction)':
    print('Este programa no corrige las fucniones de un átomo hidrogenoide')
```

**Tarea 4.  Resolver el átomo de helio aplicando los programas anteriores.**

Para el cálculo a las correciones del átomo de Helio se tomó en cuenta lo siguiente...

La función de onda del átomo de Helio puede ser representada como:

$$ \psi_{nlm} = \psi(r1)_{nlm} \psi(r2)_{nlm}$$

Donde, para el estado fundamental:

$$ \psi(r_{1}.r_{2})_{100} = \frac{Z^{3}}{\pi a_{0}^{3}} e^{\frac{-Z}{a_{0}}(r_{1}+r_{2})}$$

Y la perturbación sería el término de repulsión entre los dos electrones, es decir:

$$ H'= \frac{e^{2}}{r_{12}}=\frac{e^{2}}{|r_{1}-r_{2}|}$$

Finalmente la correción a primer orden de la energía sería:

$$ E^{(1)}= \langle\psi_{n}^{(0)} | H' | \psi_{n}^{(0)} \rangle =\frac{Z^{6}e^{2}}{\pi^{2} a_{0}^{6}} \int_{0}^{2\pi}\int_{0}^{2\pi}\int_{0}^{\pi}\int_{0}^{\pi}\int_{0}^{\infty}\int_{0}^{\infty} \frac{e^{\frac{-2Z}{a_{0}}(r_{1}+r_{2})}}{r_{12}} r_{1}^{2}r_{2}^{2}sen{\theta_{1}}sen{\theta_{2}} dr_{2} dr_{1} d\theta_{2} d\theta_{1} d\phi_{2} d\phi_{1}$$

Se utiliza una expansión del termino de repulsión con los armónicos esféricos y se integra la parte angular. Una vez hecho eso, la integral queda expresada de la siguiente manera:

$$ E^{(1)}= \frac{16Z^{6}e^{2}}{a_{0}^{6}} \left[\int_{0}^{\infty} r_{1}^{2} e^{\frac{-2Z}{a_{0}}r_{1}} \left(\int_{0}^{r_{1}} \frac{r_{2}^{2}}{r_{1}} e^{\frac{-2Z}{a_{0}}r_{2}} dr_{2}+\int_{r_{1}}^{\infty}r_{2} e^{\frac{-2Z}{a_{0}}r_{2}}dr_{2}\right) dr_{1} \right]$$

Esta fue la integral que se programó para hacer la correción a la energía del Helio

**Tarea 5: Método variacional-perturbativo. **

Este método nos permite estimar de forma precisa $E^{(2)}$ y correcciones perturbativas de la energía de órdenes más elevados para el estado fundamental del sistema, sin evaluar sumas infinitas. Ver ecuación 9.38 del libro.

$$ \langle u | H^{0} - E_{g}^{(0)} | u \rangle + \langle u | H' - E_{g}^{(1)} | \psi_{g}^{(0)} \rangle +\langle\psi_{g}^{(0)} | H' - E_{g}^{(1)} | u \rangle \geq E_{g}^{(2)} $$

Donde:

u, es cualquier función que se comporte bien y satisfaga condiciones de frontera.

Por lo general, u tiene un parámetro variacional que permite minimizar el lado izquierdo de la ecuación para estimar $E_{g}^{(2)} $. La función u resulta ser una aproximación a $\psi_{g}^{(1)}$, por lo que se puede utilizar está misma función para estimar $E_{g}^{(3)}$ y seguir haciendo iteraciones para hacer correciones de orden superior tanto a la energía como a la función de onda.

Es necesario meter parámetros de optimización a las funciones de prueba para que tenga sentido intentar programar esto. Esto nos limita a usar Sympy para resolver la integral simbólicamente. Entonces, estamos limitados a las capacidades de esta librería para resolver las integrales y a la capacidad de nuestro procesador.

A continuación se muestra un código que, aprovechando los datos introducidos anteriormente, encuentra por medio del método variacional-perturbativo la correción a segundo orden de la energía y a primer orden de la función de onda.

```python
form_item_layout = Layout(display='flex',flex_flow='row',justify_content='space-between')

F_prueba=Text()
Ran_n=IntSlider(min=n_inf, max=n_sup, step=1, disabled=False, continuous_update=False, orientation='horizontal',\
                    readout=True, readout_format='d')
Correcc=RadioButtons(options=[2],disabled=False)

form_items = [
    Box([Label(value='Test function'),F_prueba], layout=form_item_layout),
    Box([Label(value='Correction order'),Correcc], layout=form_item_layout),
    Box([Label(value='Function to correct'),Ran_n], layout=form_item_layout),]

form = Box(form_items, layout=Layout(display='flex',flex_flow='column',border='solid 2px',align_items='stretch',width='40%'))
form
```

Para la función de prueba se espera que el usuario introduzca una función que sea cuadrado-integrable y que sea compatible en las fronteras con el tipo de problema que se esta solucionando. Además, puede intruducir una constante de optimización (utilice "c")

```python
# Variables que se utilizarán
# c y d=constantes de optimización
var('c')#, real=True)

u=eval(F_prueba.value)
order=Correcc.value
n_n=Ran_n.value

if Problem=='Particle in a one-dimensional box':
    V=0

if Problem=='Harmonic oscilator':
    V=(m/2)*w**2*x**2

if Problem=='Particle in a one-dimensional box' or Problem=='Harmonic oscilator':
    integrando_a=(u)*((-hbar**2)/(2*m)*diff(u,x,2))+(u)*V*(u)-(u)*En.subs({n:n_n})*(u)
    integrando_b=(u)*h_p*(Psin.subs({n:n_n}))-(u)*(Ec1[n_n-n_inf]-En.subs({n:n_n}))*(Psin.subs({n:n_n}))
    integral_a=Integral(eval(str(integrando_a)),(x,li_sympy,ls_sympy)).doit()
    integral_b=Integral(eval(str(integrando_b)),(x,li_sympy,ls_sympy)).doit()
    integral_T=integral_a+2*integral_b
    f_opt=diff(integral_T,c,1)
    c2=solve(f_opt,c)
    E_c2=Ec1[n_n-n_inf]+integral_T.subs({c:c2[0]})
    Psi_c1=(Psin.subs({n:n_n})+u.subs({c:c2[0]}))
```

Se considero que para fines de esta tarea que:

 $$\langle u | H' - E_{g}^{(1)} | \psi_{g}^{(0)} \rangle =\langle\psi_{g}^{(0)} | H' - E_{g}^{(1)} | u \rangle$$

Lo cual se cumple cuando $H'$ es una función sin operadores diferenciales, y además, u y $\psi_{g}^{(0)}$ son funciones reales.

Las correciones a la energía son:

```python
E[n_n-n_inf]
```

$$\frac{\pi^{2} \hbar^{2}}{2 l^{2} m}$$

```python
Ec1[n_n-n_inf]
```

$$\frac{\pi^{2} \hbar^{2}}{2 l^{2} m} + \frac{1}{l} \left(- \frac{5 l^{3}}{\pi^{2}} + \frac{10 l^{3}}{3} - 5 l l^{2} + \frac{5 l}{2} l^{2}\right)$$

```python
E_c2
```

$$\frac{\pi^{2} \hbar^{2}}{2 l^{2} m} - \frac{l^{3}}{6 m} \left(\frac{3.05840621137506 l^{3}}{\hbar^{2}} m^{2} + \frac{1.52920310568753 \pi^{2}}{\hbar^{2}} l^{3} m^{2}\right) + \frac{1}{l} \left(- \frac{5 l^{3}}{\pi^{2}} + \frac{10 l^{3}}{3} - 5 l l^{2} + \frac{5 l}{2} l^{2}\right) + \frac{0.764601552843765 m}{\hbar^{2}} l^{6} + \frac{0.22938046585313 m}{\hbar^{2}} \pi^{2} l^{6} - \frac{24.7321904059267 l}{\hbar^{2}} \sqrt{2} m \left(- \frac{24 l^{5}}{\pi^{5}} + \frac{5 l^{5}}{2 \pi^{3}}\right) + \frac{24.7321904059267 l}{\hbar^{2}} \sqrt{2} m \left(- \frac{5 l^{5}}{2 \pi^{3}} + \frac{24 l^{5}}{\pi^{5}}\right) + \frac{197.857523247414 \sqrt{2} l^{3.0} m}{\pi^{3} \hbar^{2}} \left(- \frac{l^{3}}{4 \pi^{2}} + \frac{l^{3}}{24}\right)$$

Las correción a la función de onda:

```python
Psin.subs({n:n_n})
```

$$\frac{\sqrt{2}}{\sqrt{l}} \sin{\left (\frac{\pi x}{l} \right )}$$

```python
Psi_c1
```

$$\frac{\sqrt{2}}{\sqrt{l}} \sin{\left (\frac{\pi x}{l} \right )} - \frac{1.23660952029633 x}{\hbar^{2}} l^{\frac{3}{2}} m \left(l - x\right)$$

Veamos ahora las gráficas de estas funciones...

La función de onda original es:

```python
if Problem=='Particle in a one-dimensional box':
    plot(eval(str(Psin)).subs({n:n_n,m:mass,l:large,w:omega,hbar:h}),xlim=(li_scipy,ls_scipy),\
         title='$\psi_{%d}$'%n_n)
if Problem=='Harmonic oscilator':
    plot(eval(str(Psin)).subs({n:n_n,m:mass,l:large,w:omega,hbar:h}),xlim=(-10*h/(mass*omega),10*h/(mass*omega)),\
         title='$\psi_{%d}$'%n_n)
```

Su correción por medio del método variacional-perturbativo es:

```python
if Problem=='Particle in a one-dimensional box':
    if Pert.value!='':
        plot(Psi_c1.subs({m:mass,l:large,w:omega,hbar:h}),\
             xlim=(li_scipy,ls_scipy),ylim=(-1.5,1.5),title='$\psi_{%d}$'%n_n)
    if Pert.value=='':
        print('No se ingreso ninguna perturbación')
if Problem=='Harmonic oscilator':
    if Pert.value!='':
        plot(Psi_c1.subs({m:mass,l:large,w:omega,hbar:h}),\
             xlim=(-10*h/(mass*omega),10*h/(mass*omega)),title='$\psi_{%d}$' %n_n)
```

**Tarea 6. Revisar sección 9.7. **

Inicialmente a mano, y en segunda instancia favor de intentar programar sección del problema, i.e. integral de Coulomb  e integral de intercambio.

Para comenzar a solucionar este problema, es necesario ser capaz de escribir el termino de repulsión que aparece en la fuerza coulombiana ($\frac {1} {r_{12} }$) en términos de los armónicos esféricos de la siguiente manera:

$$ \frac {1} {r_{12} } = \sum_{l=0}^{\infty} \sum_{m=-l}^{l} \frac{4\pi}{2l+1} \frac{r_{<}^{l}}{r_{>}^{l+1}}  \left[Y_{l}^{m}(\theta_{1},\phi_{1})  \right]^{*} Y_{l}^{m}(\theta_{2},\phi_{2})  $$

Se tiene el problema de que para programar esta ecuación los armónicos esféricos no están programados de forma simbólica, así como tampoco estan programados los polinomios asociados de Legendre.

Se podría intentar programar las integral de intercambio y de Coulomb de forma numérica o intentar aprovechar la ortogonalidad de los armónicos esféricos, que al integrar aparecen los términos $\delta_{l,l'}$ y $\delta_{m,m'}$, por lo que la mayoría de las integrales se hacen $0$.

Aún cuando se lograra programar todo esto, hace falta verificar que se seleccionaron correctamente los límites de integración radiales para que el término $\frac{r_{<}^{l}}{r_{>}^{l+1}}$ tenga sentido.

```python
form_item_layout = Layout(display='flex',flex_flow='row',justify_content='space-between')

T1=RadioButtons(options=['1s','2s','2p','3s'],disabled=False)
T2=RadioButtons(options=['1s','2s','2p','3s'],disabled=False)

form_items = [
    Box([Label(value='Type electron 1'),T1], layout=form_item_layout),
    Box([Label(value='Type electron 2'),T2], layout=form_item_layout)]

form = Box(form_items, layout=Layout(display='flex',flex_flow='column',border='solid 2px',align_items='stretch',width='40%'))
form
```

```python
# Se puede intentar programar aprovechando la ortogonalidad de los armónicos esféricos para evitar las 4 integrales angulares
# y solo integrar sobre las coordenadas radiales
var('rmenor rmayor')
var('lq',integrer=True, positive=True)
Type1=T1.value
Type2=T2.value
n_a=2

if Type1=='1s':
        n1=1
        l1=0
if Type1=='2s':
        n1=2
        l1=0
if Type1=='2p':
        n1=2
        l1=1
if Type1=='3s':
        n1=3
        l1=0

if Type2=='1s':
        n2=1
        l2=0
if Type2=='2s':
        n2=2
        l2=0
if Type2=='2p':
        n2=2
        l2=1
if Type2=='3s':
        n2=3
        l2=0

r12_inv=1/(2*lq+1)*rmenor**lq/rmayor**(lq+1)

if n1>n2:
    n_m=n1
else:
    n_m=n2

for n in range(1,n_m+1):
    a=[]
    for nn in range(1,n_m+1):
        b=[]
        for o in range(n_m):
            c=[]
            if o<nn:
                for m in range(-o,o+1):
                    c.append(r12_inv.subs({lq:o}))
                b.append(c)
        a.append(b)
# Después de esto tengo una lista que para utilizarla debo usar a[n-1][l][m+l]

Psi1=R_nl(n1,l1,r1,n_a)
Psi2=R_nl(n2,l2,r2,n_a)
Psi_t=Psi1*Psi2

Integral_Coulumb1=0
Integral_Coulumb2=0
Integral_Intercambio1=0
Integral_Intercambio2=0

if l1==l2:
    for m in range(-l1,l1+1):
    Integral_Coulumb1=Integral_Coulumb1+Integral(Integral(r1**2*r2**2*Psi1**2*Psi2**2*\
                            a[n1-1][l1][l1].subs({rmayor:r1,rmenor:r2,lq:0}),(r2,0,r1)),(r1,0,oo)).doit()
    Integral_Coulumb2=Integral_Coulumb2+Integral(Integral(r1**2*r2**2*Psi1**2*Psi2**2*\
                            a[n1-1][l1][l1].subs({rmayor:r2,rmenor:r1,lq:0}),(r2,r1,oo)),(r1,0,oo)).doit()
    Integral_CoulumbT=Integral_Coulumb1+Integral_Coulumb2
else:
     Integral_CoulumbT=0

if l1!=l2:
    Integral_Intercambio1=Integral(Integral(r1**2*r2**2*Psi1*Psi2*Psi1.subs({r1:r2})*Psi2.subs({r2:r1})*\
                                            a[n1-1][l1][l1].subs({rmayor:r1,rmenor:r2,lq:0}),(r2,0,r1)),(r1,0,oo)).doit()
    Integral_Intercambio2=Integral(Integral(r1**2*r2**2*Psi1*Psi2*Psi1.subs({r1:r2})*Psi2.subs({r2:r1})*\
                                            a[n1-1][l1][l1].subs({rmayor:r2,rmenor:r1,lq:0}),(r2,r1,oo)),(r1,0,oo)).doit()
    Integral_IntercambioT=Integral_Intercambio1+Integral_Intercambio2
else:
    Integral_IntercambioT=0
```

```python
# Programa funciona para 1s

# Encontrar la manera de hacer matrices nxlxm que puedan ser llenadas con variables para posteriormente revisar
# como encontrar las integrales aprovechando ortogonalidad de los armónicos esféricos.

# Una opción para la matriz puedes hacer una lista y hacer reshape cada ciertas iteraciones en el ciclo for
```

Falta verificar la ortogonalidad de los armónicos esféricos y como aprovecharla para hacer únicamente las integrales radiales pero aún no esta listo.

```python

```